第1章

Size: px
Start display at page:

Download "第1章"

Transcription

1 21 世纪高职高专规划教材 计算机系列 数据结构概论 尹绍宏董卿霞苑春苗 编著 清华大学出版社 北京交通大学出版社 北京

2 内容简介 本书详细地介绍了各种类型的数据结构, 以及查找和排序的方法 对每一种数据结构, 主要讲述其基本概念, 各种存储结构, 以及不同存储结构下的各种操作的实现, 并用 C 语言对其算法进行实现 对查找和排序的各种不同方法除讲述其方法外, 还给出了用 C 语言实现的算法程序, 并对不同的算法给出了定性的分析和比较 本书既注重理论又注重实践, 并配有大量的习题和实习题, 内容丰富 概念清楚 通俗易懂, 既可用于教学, 又便于读者自学 本书可以作为大专院校计算机应用及相关专业的教材, 也可以供从事计算机技术与应用工作的科技人员使用 版权所有, 翻印必究 本书封面贴有清华大学出版社激光防伪标签, 无标签者不得销售 图书在版编目 (CIP) 数据数据结构概论 / 尹绍宏, 董卿霞, 苑春苗编著. 北京 : 清华大学出版社 ; 北京交通大学出版社, (21 世纪高职高专规划教材 计算机系列 ) ISBN Ⅰ. 数 Ⅱ.1 尹 2 董 3 苑 Ⅲ. 数据结构 高等学校 : 技术学校 教材 Ⅳ.TP 中国版本图书馆 CIP 数据核字 (2004) 第 号 责任编辑 : 韩乐 特邀编辑 : 朱宇 出版者 : 清华大学出版社 邮编 : 电话 : 北京交通大学出版社 邮编 : 电话 : , 印刷者 : 北京瑞达方舟印务有限公司 发行者 : 新华书店总店北京发行所 开 本 : 印张 :15 字数 :374 千字 版 次 :2004 年 5 月第 1 版 2004 年 5 月第 1 次印刷 书 号 :ISBN / TP 110 印 数 :0001~5000 册 定价 :21.00 元

3 出版说明 高职高专教育是我国高等教育的重要组成部分, 它的根本任务是培养生产 建设 管理和服务第一线需要的德 智 体 美全面发展的高等技术应用型专门人才, 所培养的学生在掌握必要的基础理论和专业知识的基础上, 应重点掌握从事本专业领域实际工作的基本知识和职业技能, 因而与其对应的教材也必须有自己的体系和特色 为了适应我国高职高专教育发展及其对教学改革和教材建设的需要, 在教育部的指导下, 我们在全国范围内组织并成立了 21 世纪高职高专教育教材研究与编审委员会 ( 以下简称 教材研究与编审委员会 ) 教材研究与编审委员会 的成员单位皆为教学改革成效较大 办学特色鲜明 办学实力强的高等专科学校 高等职业学校 成人高等学校及高等院校主办的二级职业技术学院, 其中一些学校是国家重点建设的示范性职业技术学院 为了保证规划教材的出版质量, 教材研究与编审委员会 在全国范围内选聘 21 世纪高职高专规划教材编审委员会 ( 以下简称 教材编审委员会 ) 成员和征集教材, 并要求 教材编审委员会 成员和规划教材的编著者必须是从事高职高专教学第一线的优秀教师或生产第一线的专家 教材编审委员会 组织各专业的专家 教授对所征集的教材进行评选, 对列选教材进行审定 目前, 教材研究与编审委员会 计划用 2~3 年的时间出版各类高职高专教材 200 种, 范围覆盖计算机应用 电子电气 财会与管理 商务英语等专业的主要课程 此次规划教材全部按教育部制定的 高职高专教育基础课程教学基本要求 编写, 其中部分教材是教育部 新世纪高职高专教育人才培养模式和教学内容体系改革与建设项目计划 的研究成果 此次规划教材编写按照突出应用性 实践性和针对性的原则编写并重组系列课程教材结构, 力求反映高职高专课程和教学内容体系改革方向 ; 反映当前教学的新内容, 突出基础理论知识的应用和实践技能的培养 ; 适应 实践的要求和岗位的需要, 不依照 学科 体系, 即贴近岗位群, 淡化学科 ; 在兼顾理论和实践内容的同时, 避免 全 而 深 的面面俱到, 基础理论以应用为目的, 以必要 够用为度 ; 尽量体现新知识 新技术 新工艺 新方法, 以利于学生综合素质的形成和科学思维方式与创新能力的培养 此外, 为了使规划教材更具广泛性 科学性 先进性和代表性, 我们希望全国从事高职高专教育的院校能够积极加入到 教材研究与编审委员会 中来, 推荐 教材编审委员会 成员和有特色 有创新的教材 同时, 希望将教学实践中的意见与建议及时反馈给我们, 以便对已出版的教材不断修订 完善, 不断提高教材质量, 完善教材体系, 为社会奉献更多更新的与高职高专教育配套的高质量教材 此次所有规划教材由全国重点大学出版社 清华大学出版社与北京交通大学出版社联合出版 适合于各类高等专科学校 高等职业学校 成人高等学校及高等院校主办的二级职业技术学院使用 21 世纪高职高专教育教材研究与编审委员会 2004 年 3 月

4 前 言 随着计算机技术的不断发展, 计算机在各个领域都得到了广泛的应用 但在应用过程中, 都会涉及数据的组织与程序的编写等问题, 都会用到各种各样的数据结构, 特别是对非数值型数据的表示, 各种操作算法的实现, 都离不开数据结构课程的内容 因此, 数据结构一直是各高等院校计算机专业教学内容中的一门主要的专业基础课程 本书是在作者多年教学经验的基础上编写而成的 在编写过程中, 从内容和结构入手, 进行了精心的设计 在内容选择方面, 全面地反映了数据结构各个方面的内容, 突出了概念 方法和应用, 既注重基本原理的介绍, 又注重实践能力的培养 ; 在结构的安排方面, 以逻辑结构为主要线索, 对每一种数据结构都是按照基本概念和定义, 顺序存储结构的表示及各基本操作的实现和链式存储结构的表示及各基本操作的实现, 以及应用这几个方面详细地进行介绍 这样便于对各种结构的学习, 掌握各种结构的特点及它们之间的关系 书中还配有大量的例题 习题和实习题供学生练习, 以加深对各知识点的理解 书中的算法全部采用 C 语言描述, 所给出的程序均已在计算机上运行通过, 并给出了程序的运行结果, 以便读者了解算法的实质和基本思想 全书分为 10 章, 第 1 章介绍了数据 数据结构 抽象数据类型及算法的概念和性能分析等基本概念 ; 第 2 章至第 6 章主要讨论了各种线性结构及其应用, 包括线性表 栈 队列 串 二维数组和广义表 ; 第 7 章和第 8 章讨论了非线性结构及其应用, 重点讨论了树和二叉树 图的基本概念及其应用 ; 第 9 章讨论了各种内部排序的方法, 以及每种排序方法的性能 特点和适用场合 ; 第 10 章讨论了不同存储结构下的各种查找方法及不同查找方法的性能分析 在每一章的后面都附有习题和实习题, 可以检验读者的学习效果, 培养程序设计能力 王炜 杨清永 高海明 王宇鑫 苏丽华 赵金英 黎剑兵等也参加了程序的调试及部分文档的整理工作 由于作者水平有限, 书中难免会有错漏和疏忽, 殷切希望广大读者予以批评指正 编者 2004 年 4 月

5 21 世纪高职高专规划教材 计算机系列编审委员会成员名单 主任委员李兰友 边奠英 副主任委员 周学毛 崔世钢 王学彬 丁桂芝 赵 伟 韩瑞功 汪志达 委 员 ( 按姓名笔画排序 ) 马辉 万志平 万振凯 王永平 王建明 尤晓 丰继林 尹绍宏 左文忠 叶 华 叶伟 付晓光 付慧生 冯平安 江 中 佟立本 刘 炜 刘建民 刘 晶 曲建民 孙培民 邢素萍 华铨平 吕新平 陈小东 陈月波 李长明 李 可 李志奎 李 琳 李源生 李群明 李静东 邱希春 沈才梁 宋维堂 汪 繁 张文明 张权范 张宝忠 张家超 张 琦 金忠伟 林长春 林文信 罗春红 苗长云 竺士蒙 周智仁 孟德欣 柏万里 宫国顺 柳 炜 钮 静 胡敬佩 姚策 赵英杰 高福成 贾建军 徐建俊 殷兆麟 唐 健 黄 斌 章春军 曹豫莪 程琪 韩广峰 韩其睿 韩 劼 裘旭光 童爱红 谢 婷 曾瑶辉 管致锦 熊锡义 潘玫玫 薛永三 操静涛 鞠洪尧

6 目 录 第 1 章绪论 基本概念和术语 发展历程 算法和算法描述 概念和特性 算法设计要求 算法描述 算法的性能分析 时间复杂度 空间复杂度 9 小结 9 习题 9 实习 10 第 2 章线性表 概念和定义 概念 定义 顺序存储结构 顺序表的存储表示 顺序表的基本操作的实现 链式存储结构 单链表的存储表示 单链表基本操作的实现 循环链表的表示和基本操作的实现 双向链表的表示和基本操作的实现 应用举例 顺序表 单链表 26 小结 28 习题 29 实习 31 第 3 章栈 概念和定义 32 I

7 3.2 顺序存储表示 顺序栈的存储表示 顺序栈基本操作的实现 链式存储结构 链栈的存储表示 链栈基本操作的实现 应用举例 38 小结 42 习题 42 实习 44 第 4 章队列 概念和定义 顺序存储结构 顺序队列的存储表示 顺序队列基本操作的实现 循环队列 链式存储结构 链队列的存储表示 链队列基本操作的实现 应用举例 53 小结 54 习题 55 实习 56 第 5 章串 概念和定义 顺序存储结构 定长顺序串的存储表示及操作的实现 堆存储表示及操作的实现 块链存储表示 应用举例 65 小结 67 习题 67 实习 68 第 6 章二维数组和广义表 二维数组概念和定义 二维数组的顺序存储结构 矩阵的压缩存储 概念 特殊矩阵的压缩存储 71 II

8 6.3.3 稀疏矩阵的顺序存储表示和基本操作的实现 稀疏矩阵的链式存储表示和基本操作的实现 广义表的概念和定义 广义表的操作和链式存储结构 82 小结 85 习题 86 实习 88 第 7 章树与二叉树 树的概念 定义 表示方法 基本概念和常用术语 二叉树 概念和定义 性质 存储结构 遍历 二叉树的线索化 树和森林 树的存储结构 树和森林的遍历 树 森林与二叉树的转换 哈夫曼树 概念和定义 哈夫曼树的构造 哈夫曼编码的实现 112 小结 115 习题 115 实习 116 第 8 章图 图的概念 定义 基本概念和常用术语 存储结构 邻接矩阵表示及各操作的实现 邻接表的表示及各操作的实现 图的遍历 深度优先搜索 广度优先搜索 134 III

9 8.4 生成树和最小生成树 生成树的概念和分类 最小生成树的概念和实现方法 AOV 网及其应用 概念 拓扑排序 AOE 网及其应用 概念 关键路径 最短路径 任意源点到其余各点的最短路径 任意两点间的最短路径 159 小结 160 习题 160 实习 162 第 9 章排序 概念及分类 插入排序 直接插入排序 折半插入排序 路插入排序 希尔排序 交换排序 冒泡排序 快速排序 选择排序 简单选择排序 树型选择排序 堆排序 K- 路归并排序 基数排序 内部排序方法的比较 时间性能 空间性能 稳定性 排序方法的选择 193 小结 193 习题 194 实习 195 IV

10 第 10 章查找 概念 顺序存储结构查找 顺序查找 折半查找 分块查找 树存储结构查找 二叉排序树 B- 树 哈希表查找 基本概念 哈希函数的构造方法 解决冲突的方法 查找方法 218 小结 221 习题 221 实习 222 习题答案 223 参考文献 225 V

11 第 1 章绪 论 本章要点 : 数据结构的基本概念和术语数据结构的发展过程及其所处地位算法和算法的描述算法的性能分析 1.1 基本概念和术语 数据结构是计算机专业的专业基础课之一, 是一门十分重要的核心课程 计算机的所有系统软件和应用软件都需要用到各种类型的数据结构 要想编写出一个好的程序, 仅仅学习计算机语言是不够的, 必须扎实地掌握数据结构的基本知识和基本技能 同时, 它也是学习计算机专业其他课程, 如操作系统 数据库原理与应用和软件工程等所必需的基础知识 掌握好数据结构方面的知识, 对于增强我们解决实际问题的能力将会有很大帮助 事实上, 一个好的程序无非是选择了一个合理的数据结构和一个好的算法, 而好的算法的选择在很大程度上取决于描述实际问题的数据结构 所以, 学好数据结构课程是进一步提高程序设计水平的关键之一 随着计算机应用领域的不断扩大, 非数值计算问题已跃居主导地位, 简单的数据类型已远远不能满足需要, 各数据元素之间的复杂联系不再是普通数学方程式所能表达的了 因此简单地说, 数据结构就是研究非数值计算的程序设计问题中, 计算机的操作对象, 以及它们之间的关系和操作等的学科 下面将对一些在以后章节中使用的基本概念和术语加以定义和解释 1. 数据数据是人们利用文字符号 数字符号及其他规定的符号, 对现实世界的事物及其活动所做的抽象描述 例如, 文字 数字和符号都是数据 2. 数据元素数据元素是数据的基本单位 例如, 学生是一个数据元素 有些时候, 一个数据元素可以由若干个数据项组成, 数据项是具有独立含义的数据最小单位, 称为字段或域 例如, 作为数据元素的学生由学号 姓名 性别和班级等数据项组成 3. 数据对象数据对象是性质相同的数据元素的集合, 是数据的一个子集 例如, 整数数据对象是集

12 2 数据结构概论 合 0,±1,±2,, 字符数据对象是集合 A, B,, Z 4. 数据结构数据结构是指数据及其相互之间的联系, 可以看做是相互间存在一种或多种特定关系的数据元素的集合 在任何问题中, 数据元素之间都不会是孤立的, 在它们之间都存在着这样或那样的关系, 这种数据元素之间的关系称为结构 根据数据元素间的不同特性关系, 通常有下面 4 类基本结构 (1) 集合结构集合结构的数据元素之间的关系是 属于同一个集合, 它是元素间关系最为松散的一种结构 如图 1-1(a) 所示 (2) 线性结构线性结构的数据元素之间存在一对一的关系, 如图 1-1(b) 所示 (3) 树型结构树型结构的数据元素之间存在一对多的关系, 如图 1-1(c) 所示 (4) 图状结构图状结构的数据元素之间存在多对多的关系, 图状结构又称网状结构, 如图 1-1(d) 所示 图 1-1 基本数据结构图 5. 数据类型数据类型是与数据结构密切相关的一个概念, 容易引起混淆 数据类型最早出现在高级语言中, 是对数据的取值范围, 每一数据的结构, 以及允许执行的操作的一种描述, 它刻画了程序中操作对象的特性 每一种程序语言都定义有自己的数据类型, 在用程序语言编写的程序中, 每个变量 常量或表达式都有一个它所属的确定的数据类型 所谓数据类型, 是一个值的集合, 以及在这些值上定义的一组操作的总称 数据类型可以分为简单类型和结构类型两种 简单类型中的每个数据 ( 即简单数据 ) 都是无法再分割的整体, 如一个整数 实数 字符 指针 枚举量等都是无法再分割的整体, 所以它们所属的类型均为简单类型 结构类型由简单类型按照一定的规则构造而成, 并且结构类型中可以包含结构类型, 所以一种结构类型中的数据 ( 即结构数据 ) 可以分解为若干个简单数据或结构数据, 每个结构数据仍可再分 例如,C 语言中的数组是一种结构类型, 它

13 第 1 章绪论 3 是由固定个数的同一类型的数据顺序排列而成 结构体也是一种结构类型, 它是由固定个数 的不同类型的数据顺序排列而成 数据类型也可以被定义为一种数据结构和能够对该数据结构进行的操作的集合 对于简 单类型, 其数据结构就是相应取值范围内的所有数据, 每一个数据值是不可分的独立整体, 因而数据值内部也就无结构可言 对于结构类型, 其数据结构就是相应元素的集合 6. 抽象数据类型 抽象数据类型 (Abstract Data Type,ADT) 是指一个数学模型, 以及定义在该模型上的 一组操作 抽象数据类型的定义仅取决于它的一组逻辑特性, 而与其在计算机内部的表示方 式及实现无关, 即它独立于具体实现 一个 ADT 定义可描述为 ADT 抽象数据类型名 数据对象 : 数据对象的定义 /* 对数据元素的说明 */ 数据关系 : 数据关系的定义 /* 对数据元素之间逻辑关系的描述 */ 基本操作 : 基本操作的定义 /* 对该抽象数据类型允许进行的操作的描述 */ 操作 1 /* 对本操作的初始条件 参数的类型 操作的功能和操作结果的描述 */ 操作 2 ADT 抽象数据类型名 抽象数据类型可以看做是描述问题的模型, 其优点是将数据和操作封装在一起, 使用户 程序只能通过在 ADT 里定义的某些操作来访问其中的数据, 从而实现了信息隐蔽 数据结构包括数据的逻辑结构和物理结构 数据的逻辑结构可以看做是从具体问题中抽 象出来的数学模型, 与数据的存储无关, 是独立于计算机的 数据元素之间的逻辑关系称为 数据的逻辑结构 我们研究数据结构的目的是为了在计算机中实现对它的操作, 为此还需要 研究如何在计算机中表示一个数据结构 数据结构在计算机中的表示称为数据的物理结构, 又称为数据的存储结构 它研究的是数据结构在计算机中的实现方法, 包括数据元素及其关 系在计算机中的表示 数据的逻辑结构有线性结构和非线性结构两大类 线性结构的逻辑特征是, 若结构是非空集, 则有且只有一个开始结点和一个终端结点, 并且除第一个结点之外, 每个结点都有且只有一个直接前驱, 除最后一个结点之外, 每个结 点都有且只有一个直接后继 线性表就是典型的线性结构 非线性结构的逻辑特征是, 一个结点可能有多个直接前驱和多个直接后继 树和图的结 构就是典型的非线性结构 数据的存储结构可以采用顺序存储 链式存储 索引存储和散列存储 4 种方法 顺序存储方法是把逻辑上相邻的结点存储在物理位置上相邻的存储单元中, 结点间的逻 辑关系由存储单元的邻接关系来体现 由此得到的存储表示称为顺序存储结构 顺序存储结 构一般借助于程序设计语言的数组来描述 链式存储方法不要求逻辑上相邻的结点在物理位置上亦相邻, 其结点间的逻辑关系是由 附加的指针域表示的 由此得到的存储表示称为链式存储结构 链式存储结构一般借助于程 序设计语言的指针类型来描述

14 4 数据结构概论 索引存储方法通常是在存储结点信息的同时建立一个索引表, 索引表中的每一项称为索引项, 它一般包括关键字和地址两项内容, 关键字是唯一能标识一个结点的数据项 建立索引表时, 可以是每个结点在索引表中都有一个索引项, 也可以是一组结点在索引表中只对应于一个索引项 索引存储方法便于检索操作 散列存储方法的基本思想是根据结点的关键字直接计算出该结点的存储地址 上述 4 种基本的存储方法, 既可以单独使用, 也可以混合起来对数据进行存储使用 对于同一种逻辑结构, 采用不同的存储方法, 可以得到不同的存储结构, 应根据具体要求而定 1.2 发展历程 数据结构作为一门独立的课程是从 1968 年才开始设立的 在此之前, 它的某些内容曾在其他课程中有所阐述 1968 年, 在美国一些大学的计算机系的教学计划中, 虽然把数据结构规定为一门课程, 但对课程的范围仍没有明确的规定 当时, 数据结构几乎与图论, 特别是与表 树的理论为同义语 随后, 数据结构的概念被扩充到包括网络 集合代数论 关系等方面, 从而变成了现在称之为离散结构的内容 由于数据必须在计算机中进行处理, 因此, 不仅需要考虑数据本身的数学性质, 而且还必须考虑数据的存储结构, 这就进一步扩大了数据结构的内容 近年来, 随着数据库系统的不断发展, 在数据结构课程中又增加了文件管理 ( 特别是大型文件的组织等 ) 的内容 1968 年, 美国唐 欧 克努特教授开创了数据结构的最初体系, 他所著的 计算机程序设计技巧 是第一本较系统地阐述数据的逻辑结构和存储结构及其操作的著作 从 20 世纪 60 年代末到 70 年代初, 出现了大型程序, 软件也相对独立, 结构化程序设计成为程序设计方法学的主要内容, 人们就越来越重视数据结构, 认为程序设计的实质是对确定的问题选择一种好的结构, 再加上设计一种好的算法 从 70 年代中期到 80 年代初, 各种版本的数据结构著作相继出现 目前在我国, 数据结构也已经不仅仅是计算机专业的教学计划中的核心课程之一, 而是其他非计算机专业的主要选修课程之一 数据结构在计算机科学中是一门综合性的专业基础课 数据结构的研究不仅涉及计算机硬件 ( 特别是编码理论 存储装置和存取方法等 ) 的研究范围, 而且与计算机软件的研究有着更密切的关系, 无论是编译原理还是操作系统, 都涉及数据元素在存储器中的分配问题 在研究信息检索时也必须考虑如何组织数据, 以便数据元素更为方便地查找和存取 因此, 可以认为数据结构是介于数学 计算机硬件和计算机软件之间的一门核心课程 在计算机科学中, 数据结构不仅是一般程序设计 ( 特别是非数值计算的程序设计 ) 的基础, 而且是设计和实现编译程序 操作系统 数据库系统及其他系统程序和大型应用程序的重要基础 今后, 数据结构还将继续发展, 一方面, 向各专门领域中特殊问题的数据结构的研究方向发展, 如多维图形数据结构等 ; 另一方面, 从抽象数据类型的观点讨论数据结构, 这一发展方向已成为新的趋势, 越来越为人们所重视

15 第 1 章绪论 算法和算法描述 计算机软件的最终结果都是以程序形式表现的, 数据结构的各种操作都是以算法形式描述的 数据结构 算法和程序是密不可分的, 它们之间的关系可以表示为数据结构 + 算法 = 程序 概念和特性数据元素之间具有逻辑关系和物理关系 与此对应, 有逻辑结构上的操作功能和具体存储结构上的操作实现 我们把具体存储结构上的操作实现方法称为算法 确切地说, 算法是对特定问题求解步骤的一种描述, 它是指令的有限序列, 其中每一条指令表示计算机的一个或多个操作 一个算法具有以下 5 个重要特性 有穷性 一个算法必须总是 ( 对任何合法的输入值 ) 在执行有穷步之后结束, 且每一步都可以在有穷时间内完成 确定性 算法中的每一条指令都必须具有确切的含义, 不会产生二义性 可行性 算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现 输入 一个算法有零个或多个输入 这些输入取自于某个选定的对象的集合 输出 一个算法有一个或多个输出 这些输出是与输入有某些特定关系的量 没有输出的算法是没有意义的 算法与程序不同, 程序可以不满足上述的有穷特性 例如, 一个操作系统 ( 如 DOS 或 Windows) 在用户未操作之前一直处于 等待 的循环中, 直到出现新的用户操作为止 但一般的程序则不能出现这种情况 另外, 程序中的指令必须是机器可执行的, 而算法中的指令则无此限制 算法代表了对问题的解, 程序则是算法在计算机上特定的实现 一个算法若用程序设计语言来描述, 它就是一个程序 在计算机科学的研究中, 算法与数据结构是相辅相成的 解决某一特定类型问题的算法可以选定不同的数据结构, 而选择是否恰当将直接影响算法的效率 算法设计要求算法与数据结构的优劣直接相关 设计一个好的算法通常需要满足以下要求 1. 正确性一个正确的算法是指在合法的数据输入下, 要求算法能在有限的运行时间内得出正确的结果 这是最重要也是最基本的准则 2. 可使用性要求算法能够很方便地使用 该特性称为用户友好性 3. 可读性算法最主要的目的是阅读与交流, 并将它转换成可实现的程序在计算机中执行 算法应当是可读的, 即可读性好 在保证算法正确的前提下, 应该强调算法的可读性 为了达到这个要求, 算法的逻辑必须是清晰的 简单的和结构化的

16 6 数据结构概论 4. 健壮性要求算法具有很好的容错性, 即能够对错误的情况进行处理, 对不合理的数据进行检查, 并做出适当的反应或进行处理, 不会经常出现异常中断或死机现象 5. 效率算法的效率主要指算法执行时计算机资源的消耗, 包括存储单元和运行时间的开销 前者称为算法的空间复杂度, 后者称为算法的时间复杂度 对于同一个问题, 在相同的数据规模下, 如果有多个算法可以使用, 则执行时间短的算法效率最高, 所需存储空间少的算法较好 通常, 两者是矛盾的一对, 节约算法的执行时间, 往往以牺牲更多的空间作为代价 ; 而为了节省空间可能需要耗费更多的计算时间, 因此我们只能根据具体情况有所侧重 若程序使用较少, 则力求算法简单易懂 ; 对于需要反复多次使用的程序, 应尽可能选用快速的算法 ; 若所求问题的数据量较大, 机器的存储空间又较小, 则在设计算法时应主要考虑如何节省空间 算法描述算法的描述有多种方式, 可以用自然语言 计算机程序语言或其他语言来说明, 但要求该说明必须能够精确地描述计算过程 一般地, 描述算法最恰当的语言是介于自然语言和程序语言之间的伪语言, 它的控制结构比较容易理解和表达 在这里, 考虑到易于上机验证算法和提高读者的实际程序设计能力, 本书中我们采用 C 语言描述算法 C 语言是一种高效 灵活和精炼的高级程序设计语言, 优点是类型丰富 语句简捷, 编写的程序结构化程度高 可读性强 常用的用于描述算法的 C 语言语句如下 (1) 输入语句 scanf(< 格式控制字符串 >,< 输入项表 >); (2) 输出语句 printf(< 格式控制字符串 >,< 输出项表 >); (3) 赋值语句变量名 = 表达式 ; (4) 条件语句 if(< 条件 >)< 语句 >; 或者 if(< 条件 >)< 语句 1> else < 语句 2>; (5) 循环语句 while(< 表达式 >)< 循环体语句 >; 或者 do

17 第 1 章绪论 7 < 循环体语句 >; while(< 表达式 >); 或者 for(< 赋初值表达式 1>;< 条件表达式 2>;< 步长表达式 3>) < 循环体语句 >; (6) 返回语句 return(< 返回表达式 >); (7) 定义函数语句 < 函数返回值类型 >< 函数名 >(< 类型名 >< 形参 1>,< 类型名 >< 形参 2>, ); < 说明部分 >; < 函数语句部分 >; (8) 调用函数语句 < 函数名 >(< 实参 1>,< 实参 2>, ); 1.4 算法的性能分析 在一个算法设计好之后, 还需要对其进行分析, 以确定该算法的优劣 一个算法的质量优劣将直接影响到算法乃至程序的效率 算法性能分析的目的在于尽可能地选择一个合适的算法 对一个算法进行评价的性能指标主要是算法的时间复杂度和空间复杂度 时间复杂度时间复杂度又称计算复杂度, 是一个算法运行时间的相对量度 算法的运行时间是指在计算机上从开始到结束运行所花费的时间, 应该是算法中每条语句的执行时间之和 而每条语句的执行时间是该语句的执行次数 ( 也称为频度 ) 与该语句执行一次所需时间的乘积 因为每条语句执行一次所需时间取决于机器本身的硬件和软件环境, 与算法无关, 所以我们假设每条语句执行一次所需的时间都是单位时间 这样, 一个算法的时间耗费就是该算法中所有语句的频度之和 显然, 在一个算法中, 进行基本操作的总的次数越少, 其运行时间也就相对地越少 ; 次数越多, 其运行时间也就相对地越多 所以, 通常用算法中所包含的简单操作次数来描述算法的时间复杂度, 以此衡量一个算法的运行时间性能 一般情况下, 算法中基本操作重复执行的次数是问题规模 n 的某个函数, 用 T(n) 表示 若有某个辅助函数 f(n), 使得当 n 趋近于无穷大时,T(n)/f(n) 的极限值为不等于零的常数, 则称 f(n) 是 T(n) 的同数量级函数 记作 T(n) = O(f(n)) 称 O(f(n)) 为算法的渐进时间复杂度, 简称时间复杂度 式中的 f(n) 一般取算法中频度最大的语句频度 下面举例说明如何计算算法的时间复杂度

18 8 数据结构概论 例 1-1 将 a 和 b 变量的内容互换 t=a; a=b; b=t; 上面 3 条语句的频度都是 1, 该程序的执行时间是一个与问题规模 n 无关的常数, 因此, 算法的时间复杂度为常数阶, 记作 T(n)=O(1) 事实上, 只要算法的执行时间不随着问题规模 n 的增加而增长, 即使算法中有上千条语句, 其执行时间也只不过是一个较大的常数, 此时, 算法的时间复杂度也是 O(1) 例 1-2 求 1~n 所有自然数的和 主要语句为 1 sum=0; 2 for(i=1;i<=n;i++) 3 sum=sum+i; 一般情况下, 对步进循环语句只需考虑循环体中语句的执行次数, 而忽略该语句中步长 加 1 终值判断控制转移等成分 因此, 上面程序段中频度最大的语句是 3, 其频度 f(n)=n, 所以该程序段的时间复杂度为 T(n)=O(n), 称为线性阶 例 1-3 分析下面程序段的时间复杂度 1 a=0; 2 b=0; 3 for(i=1;i<=n;i++) 4 a++; 5 for(j=1;j<=n;j++) 6 for(k=1;k<=n;k++) 7 b++; 根据前面的分析可知, 上面程序段中频度最大的语句是 7, 其频度 f(n)=n 2, 所以该程序 段的时间复杂度为 T(n)=O(n 2 ), 称为平方阶 由此可见, 当有若干个循环语句时, 算法的时间 复杂度是由嵌套层数最多的循环语句中最内层语句的频度 f(n) 决定的 例 1-4 分析如下程序段的时间复杂度 1 a=0; 2 for(i=1;i<=n;i++) 3 for(j=1;j<=i;j++) 4 for(k=1;k<=j;k++) 5 a++; 显然, 上面程序段中频度最大的语句是 5, 在程序中, 内循环的执行次数虽然与问题的 规模 n 没有直接的关系, 但却与外层循环变量的取值有关, 而最外层循环的次数直接与 n 有 关, 因此可以从内层循环向外层分析语句 5 的执行次数, 通过计算得出该程序段的时间复杂 度为 T(n)=O(n 3 ), 称为立方阶 算法的时间复杂度 ( 用 f(n) 表示 ) 采用数量级的形式表示后, 将对求一个算法的 f(n) 带来

19 第 1 章绪论 9 很大方便 这时, 只需要分析影响一个算法时间复杂度的主要部分即可, 不必对每一步都进行详细分析 算法的时间复杂度按数量级递增的次序排列, 依次为常数阶 O(1), 对数阶 O(lb n), 线性阶 O(n), 线性对数阶 O(n lb n), 平方阶 O(n 2 ), 立方阶 O(n 3 ),,k 次方阶 O(n k ) 指数阶 O(2 n ) 和 n 的阶乘阶 O(n!) 等形式 随着问题规模 n 的不断增大, 时间复杂度不断增大, 算法的执行效率不断降低, 也即数量级越高, 则算法的效率越低 空间复杂度与时间复杂度类似, 一个算法的空间复杂度是指该算法在计算机内执行时所需存储空间的度量 它也是问题规模 n 的函数, 记作 S(n) = O(f(n)) 一个算法在计算机存储器上所占用的存储空间应该包括 : 存储算法本身所占用的空间, 算法的输入 输出数据所占用的空间和算法在运行过程中临时占用的空间三个方面 存储算法本身所占用的空间与算法的长度成正比, 要想减少这方面的存储空间, 就必须编写出较精炼的算法 输入 输出数据所占用的空间是由求解的问题所决定的, 不随算法的不同而改变 但算法在运行过程中临时占用的存储空间是因算法的不同而不同的 在这里, 我们一般讨论的是除正常占用内存单元以外的辅助存储单元的开销 算法的空间复杂度的计算方法与时间复杂度的计算方法类似, 不再赘述 算法的时间复杂度和空间复杂度合称为算法的复杂度 小结 本章介绍了数据结构的基本概念和常用术语, 数据结构的地位及作用, 算法 算法的描述及算法性能的分析 基本学习要点如下 (1) 掌握数据结构的定义, 数据的逻辑结构 存储结构的概念及相互关系 数据结构和数据类型的区别及联系 (2) 重点掌握数据结构的类型 ( 即线性结构 树型结构和图状结构 ), 以及各种结构之间的区别 (3) 掌握算法的定义 特性及使用 C 语言进行算法描述 (4) 重点掌握算法的时间复杂度分析 习题 1. 是描述客观事物的数 字符及所有能输入到计算机中并被计算机程序加工处理的符号的集合 A. 数据 B. 数据元素 C. 数据项 D. 数据对象 2. 是数据的基本单位, 即数据集合中的个体 A. 数据 B. 数据元素 C. 数据项 D. 数据对象 3. 由记录所组成的线性表为

20 10 数据结构概论 A. 数据元素 B. 数据项 C. 数据对象 D. 文件 4. 被计算机加工的数据元素不是孤立无关的, 它们彼此之间一般存在着某种联系 通常将数据元素间的这种联系关系称为 A. 规则 B. 集合 C. 结构 D. 运算 5. 一个完整的算法应该具有 5 个基本特性, 但其中的 特性是可有可无的 A. 输入 B. 输出 C. 有穷性 D. 确定性 6. 算法的计算量称 A. 现实性 B. 复杂度 C. 难度 D. 效率 7. 与程序运行时间有关的因素主要有以下 4 个方面, 其中与算法关系密切的是 A. 问题的规模 B. 机器代码质量的优劣 C. 机器的执行速度 D. 语句的执行次数 8. 算法分析的目的是 A. 研究算法的输入与输出之间的关系 B. 找出数据结构的合理性 C. 分析算法的效率以求改进算法 D. 分析算法的可读性和可移植性 9. 是数据的基本单位, 是数据的最小单位 10. 逻辑结构是指数据, 而存储结构是指 11. 评价一个算法的优劣应该从算法的正确性 和可读性等几方面进行 12. 一个完整的算法应该具有 和 5 个特性 13. 通常说数据结构是一个二元组 (D,R), 其中的 D,R 分别代表什么? 14. 数据的逻辑结构与数据的存储结构有何联系? 15. 算法与程序有何异同及联系? 16. 什么是算法的时间复杂度? 什么是算法的空间复杂度? 举例说明之 17. 一个算法是正确的是指什么意思? 实习 1. 实验目的掌握算法设计的原则 设计要求及算法性能分析的方法, 重点掌握算法的时间复杂度和空间复杂度的分析方法 2. 实验内容 (1) 任意设计一个算法, 使其算法的时间复杂度分别为 O(1),O(n) 和 O(n 2 ) (2) 随机产生 n 个整数, 选择不同的排序方法分别设计算法, 然后分别讨论各种不同算法的时间复杂度, 讨论各自的性能及优缺点

21 第 2 章线性表 本章要点 : 线性表顺序表单链表循环链表双向链表 2.1 概念和定义 线性结构具有如下特点 在数据元素的非空有限集合中, (1) 存在惟一的一个没有前驱的首元素 (2) 存在惟一的一个没有后继的尾元素 (3) 除首元素和尾元素之外, 集合中的每个数据元素均有惟一的直接前驱和惟一的直接后继 概念线性表是一种最简单的数据结构, 一般用于描述数据元素之间某种简单的先后次序关系 简言之, 线性表就是 n 个类型相同的数据元素的有限序列 例如, 英文字母表 (A,B,, Z) 就是一个简单的线性表, 表中的每一个英文字母就是一个数据元素 当然, 在不同情况下数据元素可以表示不同的具体含义 例如, 某班学生的数据结构成绩表 ( 表 2-1) 就是一个较复杂的线性表 这种情况下, 线性表中的每个数据元素为一个记录, 包括学号 姓名 籍贯 成绩 备注 5 个数据项 表 2-1 学生成绩表 学 号 姓 名 籍 贯 成 绩 备 注 2001 高洁 天津市 董玉鸣 河北邢台 赵斌 天津市 高海明 天津市 75 Μ Μ Μ Μ Μ

22 12 数据结构概论 定义综上所述, 可将线性表定义如下 线性表 (linear list) 是具有 n(n 0) 个类型相同的数据元素的序列, 记作 (a 1,a 2,, a n ) 其中,a i (1 i n) 称为表中的第 i 个数据元素, 在同一个线性表中, 每个元素 a i 具有相同的数据类型 数据元素个数 n 称为线性表的长度,n=0 时, 线性表为空表 线性表满足线性结构特点, 除首元素 a 1 外, 每个元素 a i 有且仅有一个被称为其直接前驱的结点 a i 1 ( 简称前驱 ), 除了最后一个元素 a n 外, 每个元素 a i 有且仅有一个被称为其直接后继的结点 a i+1 ( 简称后继 ) 线性表的抽象数据类型定义如下 ADT Linear List 数据元素零个或多个元素的有限序列 数据关系线性表中元素是线性关系 基本操作 1 InitList(L) 操作前提 L 为未初始化线性表 操作结果将 L 初始化为空表 2 int IsEmpty(L) 操作前提线性表 L 已经存在 操作结果如果 L 为空表则返回 TRUE, 否则返回 FALSE 3 ClearList(L) 操作前提线性表 L 已经存在 操作结果将表 L 置为空表 4 int ListLength(L) 操作前提线性表 L 已经存在 操作结果如果 L 为空表则返回零, 否则返回表中的元素个数 5 ListIn(L,i,e) 操作前提线性表 L 已经存在,e 为合法元素且 1 i ListLength(L)+1 操作结果在 L 中第 i 个位置插入新的元素 e,l 的长度加 1 6 Ele_Type ListDel(L,i,&e) 操作前提表 L 已经存在且非空,1 i ListLength(L) 操作结果删除 L 中的第 i 个元素, 并用 e 返回其值,L 的长度减 1 ADT List 2.2 顺序存储结构 表结构的存储方式有多种, 其中最常用的是顺序存储和链式存储两种

23 第 2 章线性表 顺序表的存储表示线性表的顺序存储指按数据元素在表中的次序依次存储, 使线性表中在逻辑结构上相邻的数据元素存储在相邻的物理单元中 采用顺序存储结构的线性表通常称为顺序表 采用顺序存储结构可以实现对线性表中数据元素的随机存取 因为数据元素类型相同, 因此每个元素所占单元数相同 假设每个元素占 k 个存储单元, 首元素地址为 loc(a 1 ), 则第 i 个元素的地址 loc(a i ) 可用如下公式算出 : loc(a i )=loc(a 1 )+(i 1)k 图 2-1 示意了线性表的顺序存储结构 图 2-1 顺序存储示意图 用 C 语言描述时, 顺序存储结构通常借助于一维数组表示 例如, 定义存储一个班级 ( 人 数 50) 的学生数据结构课程的成绩表 ( 一名学生对应于一个数据元素 ), 用结构类型数组 stu[n](n 50) 存放, 算法描述如下 struct Ele_Type int num; /* 学号域 */ char name[20]; /* 姓名域 */ int score; /* 成绩域 */ ; struct Ele_Type stu[50]= 2001,"Gao Jie",92,2002,"Dong Yuming", 88, 2003,"Zhao Bin",55, 2004,"Gao Haiming",75; 注意 C 语言中数组下标从 0 开始编号, 而顺序表中的元素是从 1 开始编号 因此, 数组下 标为 i 的元素, 对应于线性表中第 i+1 个数据元素 顺序表的基本操作的实现如上所述, 用公式很容易实现对线性表中第 i 个元素的随机存取, 下面着重讨论如何在线性表的顺序存储结构上实现线性表的初始化 插入和删除操作 1. 初始化操作顺序表的初始化就是为顺序表分配一个预定义容量的数组空间, 并将线性表的当前长度

24 14 数据结构概论 设为 0 算法实现如下 算法 2-1 线性表初始化 typedef struct Ele_Type elem[50]; int length; seqlist; struct seqlist *InitList() seqlist L; L.elem=(Ele_Type*)malloc(sizeof(Ele_Type)); if(!l.elem) printf("fail"); L.length=0; return(&l); 2. 插入操作 线性表的插入操作是指在线性表的第 i 1 个数据元素和第 i 个数据元素之间插入一个新的 数据元素 x, 使长度为 n 的线性表 a 1 a 2 a i 1 a i a n 变成长度为 n+1 的线性表 a 1 a 2 a i 1 x a i a n 可以分两种情况讨论之 (1) 当 i=n+1 时, 指在线性表的表尾插入结点, 所以只需直接插入, 不影响其他元素 (2) 当 i n 时, 必须先将原表中第 i,i+1,,n 共 (n i)+1 个元素, 依次移到第 i+1, i+2,,n+1 的位置上, 空出第 i 个位置后, 插入新结点 x 算法实现如下 算法 2-2 线性表插入函数 int ListIn(seqlist *L,int i,ele_type x) /* 在顺序表 L 中第 i 个元素之前插入元素 x*/ int k; if((i<1) (i>max+1)) printf("i 值不合法 \n"); return ERROR;

25 第 2 章线性表 15 if(l->length>=max) printf(" 表已满无法插入 \n"); return ERROR; for(k=l->length-1;k>=i-1;k--) L->elem[k+1]=L->elem[k]; L->elem[i-1].num=x.num; StrCopy(L->elem[i-1].name,x.name); L->elem[i-1].score=x.score; L->length++; return OK; 从算法 2-2 可以看出, 当 i = L->length+1 时,for 循环语句的终值大于初值, 循环体内的语句不执行, 不需要移动元素, 直接在表尾插入新元素 x 即可 ; 当 i = 1 时, 循环体执行 n 次, 即需要移动 n 个元素后才能插入新元素 x 由此可见, 移动元素的个数取决于插入的位置 3. 删除操作线性表的删除操作是指将表中的第 i(1 i n) 个元素删除, 使长度为 n 的线性表 a 1 a 2 a i 1 a i a i+1 a n 变成长度为 n 1 的线性表 a 1 a 2 a i 1 a i+1 a n 下面分两种情况讨论 1) 当 i=n 时, 删除表尾元素, 可以直接删除, 不影响其他元素 2) 当 i<n 时, 需将表中第 i+1,i+2,,n 共 (n i) 个元素依次移到第 i,i+1,,n 1 的位置上, 从而 覆盖 第 i 个元素 算法实现如下 算法 2-3 顺序表删除运算 int ListDel(seqlist *L, int i) /* 在顺序表 L 中删除第 i 个元素 */ int k; if((i<1) (i>max)) printf("i 值不合法 \n"); return ERROR; for (k=i;k<=l->length-1;k++)

26 16 数据结构概论 L->elem[k-1]=L->elem[k]; L->length--; return OK; 与插入操作类似, 在顺序表上实现删除操作也必须移动元素 当 i=n 时, 循环体内的语 句不执行, 因此不需要移动元素, 仅将表长减 1 即可 ; 当 i<n 时, 需要移动 n i 个元素 可 见, 删除操作中移动元素的个数也与 i 有关 下面举一个简单的例子说明线性表基本操作的应用 例如, 我们对前面的顺序表 stu[50] 执行下面的操作 : 先在顺序表的第 3 位置后面插入元 素 x=2008, Wang Xiao, 61, 然后再删除顺序表的第 2 个元素 程序如下 #define OK 1 #define ERROR 0 main() int i; seqlist *L; Ele_Type stu[50]= 2001,"Gao Jie",92, 2002,"Dong Yuming",88, 2003,"Zhao Bin",55, 2004,"Gao Haiming",75; Ele_Type x=2008, "Wang Xiao", 61; L=(seqlist*)malloc(sizeof(seqlist)); L->length=4; for(i=0;i<l->length;i++) L->elem[i]=stu[i]; ListIn(L,3,x); printf("inserted seqlist is:\n"); for(i=0;i<l->length;i++) printf("%6d %s %5d\n",L->elem[i].num, L->elem[i].name, L->elem [i].score); ListDel(L,2); printf("deleted seqlist is:\n"); for(i=0;i<l->length;i++) printf("%6d %s %5d\n",L->elem[i].num, L->elem[i].name, L->elem [i].score);

27 第 2 章线性表 17 free(l); 运行结果如下 2.3 链式存储结构 从前面的描述可以看出, 对顺序表的插入和删除操作, 算法的大部分时间花费在移动元素上 为了克服顺序表的这一缺点, 可以采用链式存储结构来存储线性表 单链表的存储表示顺序表存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻, 因此便于随机存取表中的任一元素 而链式存储结构则是用任意的存储单元存储线性表的数据元素 这些存储单元可以分布在内存的任何位置, 这就要求每个数据元素 a i 除了存储其本身的信息外, 还需存储一个指示其直接后继的信息 这两部分信息组成的存储映像称为结点 (node), 如图 2-2 所示 链表是由这种含有两个域的结点的指针域将线性表的 n 个结点按其逻辑顺序链接在一起的 如果链表的每个结点只有一个指针域, 则称此链表为单链表 每个链表都用一个指针变量 (head) 指向链表的第一个结点,head 就是链表的表头结点指针, 称为头指针 一般将头指针为 head 的链表称为链表 head 链表中的最后一个结点的指针域不再指向任何结点, 故置为空 (NULL) 图 2-3 给出简单的单链表的逻辑结构示例 图 2-2 单链表的结点结构 图 2-3 单链表的逻辑结构示例 有时, 我们在单链表的第一个结点之前附设一个头结点 头结点的数据域可以存储关于线性表的附加信息, 比如线性表的长度, 也可以什么都不储存 头结点的指针域存储指向第一个结点的地址 ( 即第一个结点的存储位置 ), 此时, 单链表的头指针指向头结点 若线性表为空表, 则头结点的指针域为空 如图 2-4 所示 图 2-4 带头结点的单链表图示

28 18 数据结构概论 链式存储结构类型定义为 typedef struct linkednode /* 定义链表的结点类型 */ Ele_Type data; struct linkednode *next; Node,*Link_List; 结点类型中的 data 域可以是用户自定义的数据类型, 这样, 我们就可以用类型名 Link_List 定义一个链表 例如, Link_List L; 则 L 是一个结构指针, 它指向单链表的第一个结点 若单链表为空表, 则 L=NULL, 对于带 头结点的单链表为 L->next=NULL 单链表基本操作的实现 像顺序表一样, 对单链表的操作也有创建 插入 删除等几种 下面我们将详细讨论这 些基本操作的实现 1. 建立链表 所谓建立链表, 是指从无到有地建立起一个链表, 即一个一个地输入各结点数据, 并建 立起前后相连的关系 算法思想是, 设置 3 个指针变量 :L,p 1,p 2, 它们都指向结点类型数据, 让 p 1 指向新开 的结点,p 2 指向链表中最后一个结点, 把 p 1 所指的结点连接在 p 2 所指的结点后面, 用 p 2 - > next=p 1 来实现 算法实现如下 算法 2-4 链表的建立 #define NULL 0 #define LEN sizeof(struct linkednode) struct linkednode /* 定义链表的结点类型 */ char data; struct linkednode *next; ; Link_List create() Link_List L; Node *p1, *p2; char data; L=(Node*)malloc(LEN); /* 生成头结点 */ p2=l; while((data=getchar())!='\n') p1=(node*)malloc(len);

29 第 2 章线性表 19 p1->data=data; p2->next=p1; p2=p1; p2->next=null; return L; 2. 插入操作 假设要在线性表的两个数据元素 a 和 b 之间插入一个数据元素 x,p 为指向结点 a 的指针 插入过程如图 2-5 所示, 图中实线表示操作前的指针, 虚线表示操作后的指针, 下同 图 2-5 插入过程示意 如果 s 为指向结点 x 的指针, 则上述过程可用语句描述为 s->next=p->next; p->next=s; 一般化言之, 在带头结点的单链表 L 中的第 i 个位置插入一个数据元素 x, 应先在单链表 中找到第 i 1 个结点, 由指针 p 指示, 然后申请一个新的结点并由指针 s 指示, 该结点的数据 域为 x, 这样就相当于在 p 和 p->next 之间插入 s 指示的结点, 过程如图 2-6 所示 算法实现如下 算法 2-5 单链表的插入操作 图 2-6 带头结点的单链表插入过程示意 int ListIn(Link_List L,int i,char x) /* 在链表 L 的第 i 个位置插入值为 x 的结点 */ Node *p, *s; int k; p=l; k=0;

30 20 数据结构概论 while(p!=null && k<i-1) p=p->next; k++; if(p==null k!=i-1) printf(" 插入位置不存在!\n"); return ERROR; s=(node*)malloc(sizeof(node)); s->data=x; s->next=p->next; p->next=s; return OK; 3. 删除操作 在单链表中删除结点 b 时, 只需将结点 a 的后继改为结点 c 即可, 如图 2-7 所示 实现语句为 p->next=p->next->next 图 2-7 删除结点示意 如果要在带头结点的单链表 L 中删除第 i 个结点, 应先要找到第 i 1 个结点并由指针 p 指示, 然后将第 i 个结点的后继改为 p 的后继, 即删除了第 i 个结点 算法实现如下 算法 2-6 单链表的删除操作 int ListDel(Link_List L, int i) /* 删除链表 L 的第 i 个结点 */ Node * p, *r; int k; p=l; k=0; while(p->next!=null && k<i-1) p=p->next; k++; if(p->next==null k!=i-1)

31 第 2 章线性表 21 printf(" 删除结点位置 i 不合法!\n"); return ERROR; r=p->next; p->next=p->next->next; free(r); return OK; 将以上建立 插入 删除的函数组织在一个程序中, 用 main 函数作为主调函数, 可以写 出以下 main 函数 为便于观察结果, 我们先给出输出链表的函数 print() void print(link_list L) /* 输出链表 L 所有结点的数据域 */ Node * p; p=l->next; while(p!=null) printf("%c",p->data); p=p->next; printf("\n"); #include <conio.h> #include <stdio.h> #define ERROR 0 #define OK 1 #define NULL 0 main() Link_List L=NULL; char x; int del_num,in_num; clrscr(); printf(" 请输入链表结点 :\n"); L=create(); printf(" 请输入插入的结点值及插入位置 :\n"); scanf("%c %d",&x,&in_num); ListIn(L,in_num,x); printf(" 插入后的链表为 :"); print(l); printf(" 请输入删除结点位置 :"); scanf("%d",&del_num); ListDel(L, del_num); printf(" 删除后的链表为 :\n");

32 22 数据结构概论 print(l); 运行结果如下 循环链表的表示和基本操作的实现顾名思义, 循环链表是一个首尾相接的单链表, 即将单链表的最后一个结点的指针域指向表头结点 循环链表可以带 ( 也可以不带 ) 表头结点 图 2-8 给出了带表头结点的循环链表 图 2-8 循环链表带头结点的循环链表的各种操作的实现算法与带头结点的单链表的实现算法类似, 不同之处在于算法中循环条件不是 p!=null 或 p->next!=null, 而是 p!=l 或 p->next!=l 这里不再赘述 有时为了便于某操作简单, 可以在循环单链表中设置尾指针 rear 例如, 要找循环单链表的开始结点 a 1 和最后一个结点 a n, 在设有头指针的单链表中, 时间复杂度分别为 O(1) 和 O(n), 而在带有尾指针的单链表中只需用 rear->next->next 和 rear 即可确定位置, 时间复杂度都是 O(1) 因此, 采用尾指针表示循环链表更实用 带尾指针的循环链表, 如图 2-9 所示 图 2-9 带尾指针的循环单链表 双向链表的表示和基本操作的实现在单链表中, 搜索一个结点的后继结点非常方便, 只要该结点的指针域不为零, 就可以通过指针域找到该结点的后继结点 但要搜索一个指定结点的前驱结点却很难实现, 必须从链头开始, 沿指针域顺序检测, 直到某一结点的后继结点为该指定结点, 则该结点即为指定结点的前驱结点 为了能方便地找到一个结点的前驱结点, 我们可以想像如果每个结点都有两个指针域, 一个指向其前驱结点, 一个指向其后继结点, 这样形成的链表中就有两个不同方向的链, 我

33 第 2 章线性表 23 们称之为双向链表 (double linked list) 双向链表的结点结构如图 2-10 所示 双向链表的结构定义如下 typedef struct Dnode Ele_Type data; Struct Dnode *prior, *next; Dnode, *DoubleList; 在双向链表中, 因为每个结点都有两个指针域, 所以在双向链表中进行插入结点和删除 结点操作需修改的指针比在单链表中的操作更多 下面具体讨论在双向链表中如何实现插入 和删除操作 1. 插入操作 在双向链表第 i 个结点之前插入一个新结点 ( 数据域为 钱 ), 则指针的变化情况如图 2-11 所示 图 2-10 双向链表的结点结构 图 2-11 在双向链表中插入一个结点 实现语句如下 /* 找到恰当的插入位置后, 以指针 p 指向第 i 个结点, 指针 s 指向新结点 */ 1 s->prior=p->prior; 2 p->prior->next=s; 3 s->next=p; 4 p->prior=s; 2. 删除操作 删除双向链表中第 i 个结点, 则指针的变化情况如图 2-12 所示 实现语句如下 /* 找到要删除的结点后, 以指针 p 指示 */ *e=p->data; p->prior->next=p->next; 图 2-12 在双向链表中删除一个结点

34 24 数据结构概论 p->next->prior=p->prior; free(p); 与单链表类似, 双向链表也可以带头指针, 使双向链表的某些操作更加简便 同时, 双 向链表也可以有循环表, 即双向循环链表, 如图 2-13 所示 图 2-13 双向循环链表 2.4 应用举例 顺序表 例 2-1 顺序表的合并 设有两个顺序表 LA 和 LB, 均为非递减有序排列, 编写将它们合并成一个新的顺序表 LC 的算法, 要求 LC 也是非递减有序排列, 允许 LC 中有相同元素 算法思想设 LC 开始为一个空表, 算法要求实现将表 LA 和表 LB 中的元素按非递减顺 序依次插入表 LC 中, 这就需要设置两个指针 i 和 j, 分别指向表 LA 和表 LB 中的元素, 当 LA->elem[i]>LB->elem[j] 时, 将 LB->elem[j] 插入表 LC 中, 然后 j++; 反之, 将 LA->elem[i] 插入表 LC 中, 并使 i++ 如此循环, 直到表 LA 或 LB 中的元素全部扫描完毕 再将未被扫描 到的表中剩余元素插入表 LC 中 算法实现 #include "stdio.h" #define ERROR 0 #define Max 50 typedef struct char elem[max]; int length; seqlist; void mergeseqlist(seqlist LA,seqlist LB,seqlist *LC) int i,j,k; i=0;j=0;k=0; while(i<la.length&&j<lb.length) if(la.elem[i]<=lb.elem[j]) LC->elem[k]=LA.elem[i];

35 第 2 章线性表 25 i++; k++; else LC->elem[k]=LB.elem[j]; j++; k++; while(i<la.length) LC->elem[k]=LA.elem[i]; i++; k++; while(j<lb.length) LC->elem[k]=LB.elem[j]; j++; k++; LC->length=LA.length+LB.length; main() seqlist LA,LB,*LC; int i=0; char c; printf(" 输入顺序表 LA:"); while((c=getchar())!='\n') LA.elem[i]=c; i++; LA.length=i; i=0; printf(" 输入顺序表 LB:"); while((c=getchar())!='\n') LB.elem[i]=c; i++; LB.length=i; mergeseqlist(la,lb,lc); printf(" 合并后的顺序表 LC 为 :\n"); for(i=0;i<lc->length;i++) c=lc->elem[i];

36 26 数据结构概论 putchar(c); 运行结果如下 单链表 例 2-2 编写一算法, 计算带头结点的单链表的长度 算法思想可以设置一个计数器 i 和一个指针 p,p 开始指向单链表的第一个结点, 然后让指针 p 依次指向其后继结点,p 每移动一次,i 值增 1, 直到 p=null 算法实现 int ListCount(Link_List L) Node *p; int i=0; p=l->next; while(p!=null) p=p->next; i++; return i; #include "stdio.h" #include "conio.h" #define ERROR 0 #define OK 1 #define NULL 0 main() Link_List LL=NULL; int i; printf(" 请输入链表 LL:\n"); LL=create(); i=listcount(ll); printf(" 链表 LL 的长度为 :%d\n",i);

37 第 2 章线性表 27 运行结果如下 例 2-3 设线性表 LA=(a 1,a 2,,a m ),LB=(b 1,b 2,,b n ), 编写一个按下列规 则合并表 LA, 表 LB 为线性表 LC 的算法, 即使得 LC=(a 1,b 1,a 2,b 2,,a m,b m,b m+1,,b n ) m n 或者 LC=(a 1,b 1,a 2,b 2,,a n,b n,a n+1,,a m ) n m 线性表 LA,LB 和 LC 均以单链表作为存储结构, 且表 LC 利用表 LA 和表 LB 中的结点空间构 成 算法思想以表 LA 的头结点作为表 LC 的头结点, 自 a 1 和 b 1 起交替将表 LA 和表 LB 中 的结点连接到表 LC 中, 这就需要有一个指针 ( 假设为 pc) 指向表 LC 中当前最后一个结点, pa 和 pb 分别指向表 LA 和表 LB 中当前尚未连接到表 LC 中 ( 剩余部分 ) 的第一个结点, 表 LC 中每次要增加一个结点, 则 pc->next 或者指向 pa 所指向的结点或者指向 pb 所指向的结点, 连接完表 LA 和表 LB 中的所有结点后, 释放表 LB 的头结点 算法实现 Link_List listcon(link_list LA, Link_List LB) Link_List LC; Node *pa, *pb, *pc; LC=LA; pa=la->next; pb=lb->next; pc=lc; while(pa!=null && pb!=null) pc->next=pa; pa=pa->next; pc=pc->next; pc->next=pb; pb=pb->next; pc=pc->next; if(pa==null) pc->next=pb; if(pb==null) pc->next=pa; free(lb); LB=NULL; return LC; #include "stdio.h" #include "conio.h"

38 28 数据结构概论 #define ERROR 0 #define OK 1 #define NULL 0 main() Link_List LA=NULL; Link_List LC=NULL; Link_List LB=NULL; clrscr(); printf(" 输入单链表 LA 的各结点 :\n"); LA=create(); printf(" 输入单链表 LB 的各结点 :\n"); LB=create(); LC=listcon(LA, LB); printf(" 合并后的单链表 LC 为 :\n"); print(lc); 运行结果如下 小结 本章主要介绍了线性表的概念和线性表的两种存储方式 线性表是 n 个类型相同的数据元素的有限序列 线性表满足线性结构的特点 线性表有两种常用的存储方式 顺序存储方式和链式存储方式 采用顺序存储的线性表称为顺序表, 其存储结构特点是线性表在逻辑结构上相邻的数据元素存储在相邻的物理单元中 对顺序表可实现的操作有查找 插入 删除等 线性表采用顺序存储的优点是, 可以方便地随机存取表中的任一元素 无需为表示结点间的逻辑关系而增加额外的存储空间 其缺点是, 插入和删除操作不方便 除了表尾位置, 在表中其他位置进行插入或删除操作都需要移动大量的数据元素, 执行效率较低 由于顺序表要求占用连续的存储空间, 存储分配只能预先进行静态分配 因此, 当表长变化较大时, 难以确定合适的存储规模 若按可能达到的最大长度预先分配表空间, 又可能造成一部分空间因长期得不到利用而浪费 ; 若事先对表长估计不足, 则插入操作时可能超过预先分配的空间而产生溢出

39 第 2 章线性表 29 为了克服顺序表的缺点, 可以考虑线性表的另一种存储方式 链式存储方式 采用链式存储结构的线性表称为链表 链表是用一组任意的存储单元 ( 可以是连续的, 也可以是不连续的 ) 来存放线性表的结点, 这样链表中结点的逻辑顺序与物理顺序不一定相同 对链表可实现的操作有查找 插入 删除等 线性表采用链式存储的优点是, 链表的存储空间是动态分配的, 只要内存尚有空间, 就不会产生溢出 便于插入和删除操作, 在链表中的任何位置上进行插入和删除操作都只需修改指针, 节省时间 链式存储的缺点是, 链表中的每个结点都要设置数据域和指针域, 从存储密度来讲是不经济的 在链表中查找一个结点, 必须从头指针起遍历该结点前面所有结点才能找到 通过比较可以看出, 线性表在选择存储方式时可以遵循以下原则 当线性表的长度变化较大, 难以估计其存储规模时, 采用动态链表作为存储结构较好 当线性表的长度变化不大, 易于事先确定其大小时, 为节省空间, 宜采用顺序表作为存储结构 若线性表的操作主要是查找, 很少进行插入和删除操作时, 宜采用顺序表作存储结构 对于频繁进行插入和删除操作的线性表, 宜采用链表作为存储结构 若表的插入和删除主要在首 尾两端, 则宜采用尾指针表示的单循环链表 习题 1. 若长度为 n 的线性表采用顺序存储结构, 在其第 i 个位置插入一个新元素的算法的时 间复杂度为 (1 i n+1) A. O (0) B.O(1) C.O(n) D.O(n 2 ) 2. 若一个线性表采用顺序存储方式, 第 1 个元素的存储地址是 100, 每个元素的长度为 2, 则第 5 个元素的存储地址是 A.110 B.108 C.100 D 在某链表中最常用的操作是在最后一个结点之后插入一个结点和删除最后一个结点, 则采用 存储方式最节省运算时间 A. 单链表 B. 双链表 C. 单循环链表 D. 带头结点的双循环链表 4. 在长度为 n 的顺序表的第 i(1 i n+1) 个位置上插入一个元素, 元素的移动次数为 A.n i+1 B.n i C.i D.i 1 5. 对于只在表的首 尾两端进行插入操作的线性表, 宜采用的存储结构为 A. 顺序表 B. 用头指针表示的单循环链表 C. 用尾指针表示的单循环链表 D. 单链表 6. 链表不具有的特点是 A. 可随机访问任一元素 B. 插入和删除时不需移动元素 C. 不必事先估计存储空间 D. 所需空间与线性表长度成正比

40 30 数据结构概论 7. 若线性表最常用的操作是存取第 i 个元素及其前驱的值, 则采用 存储方式节省时间 A. 单链表 B. 双链表 C. 单循环链表 D. 顺序表 8. 在一个长度为 n 采用顺序方式存储的线性表中, 要删除第 i 个元素 (1 i n) 时, 需向前移动 个元素 9. 在一个长度为 n 采用顺序方式存储的线性表中, 要在第 i 个元素 (1 i n) 之前插入一个元素时, 需向后移动 个元素 10. 在双向链表中, 每个结点有两个指针域, 一个指向, 另一个指向 11. 顺序表中, 逻辑上相邻的元素 一定相邻 单链表中, 逻辑上相邻的元素的物理位置 相邻 12. 在什么情况下用链表比顺序表好? 用图示法说明向单向线性链表插入结点的过程 13. 有一学生成绩单, 画出用链式存储结构时的成绩单数据的存储映像 14. 试描述头指针 头结点 开始结点的区别, 并说明头指针和头结点的作用 15. 何时选用顺序表 何时选用链表作为线性表的存储结构为宜? 16. 在顺序表中插入和删除一个结点需平均移动多少个结点? 具体的移动次数取决于哪两个因素? 17. 为什么在单循环链表中设置尾指针比设置头指针更好? 在单链表 双链表和单循环链表中, 若仅知道指针 p 指向某结点, 不知道头指针, 能否将结点 *p 从相应的链表中删去? 若可以, 其时间复杂度为多少? 18. 用 C 语言实现单向线性链表 编写存储结构的定义及基本算法 19. 设长度为 n 的线性表用顺序存储方式存储在数组 a[m](0 n m) 中 试编写一算法, 将线性表逆序 ( 即元素次序与原次序相反 ), 并计算算法中移动元素的次数 20. 已知两个有序表 A=(a 1,a 2,,a n ) 和 B=(b 1,b 2,,b m ), 分别用顺序法存储在数组 a[n] 和 b[m] 中 按下述要求分别写出算法, 将表 A 和 B 合并成一个有序表 C=(c 1,c 2,, c n+m ) 表 C 顺序存储在数组 c[m+n] 中 (1) 完全合并 ( 允许有重复元素 ) ( 2 ) 若表 A 和 B 均无重复元素, 但表 A 与 B 可能存在重复元素, 要求合并后表 C 无重复元素 ( 相等的元素只保留一个 ) 21. 已知一个线性表中的元素值为非递减有序排列, 编写一个函数删除线性表中多余的值相同的元素 22. 编写一个函数从一个给定的线性表中删除元素值在 x 到 y(x y) 之间的所有元素, 要求以较高的效率实现 23. 已知一个循环单链表如下图所示, 编写一个函数将所有箭头方向取反

41 第 2 章线性表 31 实习 1. 实验目的加深理解线性表链式存储的含义, 掌握链表的基本操作, 在此基础上学会用链表解决遇到的问题 2. 实验内容 (1) 假设有两个按元素值递减有序排列的线性表 A 和 B, 均以单链表作为存储结构, 试编写算法将表 A 和表 B 归并成一个按元素值递减有序 ( 允许表中含有值相同的元素 ) 排列的线性表 C, 并要求利用原表的结点空间构造表 C (2) 将若干城市的信息存入一个带头结点的单链表, 结点中的城市信息包括城市名 城市的位置坐标 要求 : 1 给定一个城市名, 返回其位置坐标 ; 2 给定一个位置坐标 P 和一个距离 D, 返回所有与 P 的距离小于等于 D 的城市

42 第 3 章栈 本章要点 : 栈 顺序栈 链式栈 3.1 概念和定义 栈 (stack) 是一种操作受限的线性表, 它的用途很广泛, 例如汇编处理程序中的句法识别和表达式计算就是基于栈实现的 栈还经常用于函数调用时的参数传递和函数值的返回方面 与线性表不同的是, 对于线性表的操作允许在表的任何位置进行, 而栈是只允许在表的末端进行插入和删除的顺序表 允许插入和删除的一端称为栈顶 (top), 栈顶的当前位置是动态变化的, 由栈顶指针指示 不允许插入和删除的一端称为栈底 (bottom) 如果栈中没有任何元素, 则称为空栈 根据以上定义, 最后一个进栈的元素总是在先进栈的元素的上面而成为新的栈顶, 而在出栈时, 栈顶元素是最先被弹出的 这就是说, 最后进栈的元素总是最先出栈, 因此栈又称为后进先出 (LIFO) 的线性表 生活中洗碗的例子可以用来比喻栈, 最先洗的碗放在下面, 后洗的放在上面, 用时总是先用上面的, 也就是后洗的先用到 图 3-1 给出了栈的示意图 图 3-1 栈除了在栈顶进行插入和删除外, 栈的基本操作还包括栈的初始化 判空及取栈顶元素等 下面给出栈的抽象数据类型的定义 ADT Stack

43 第 3 章栈 33 数据对象零个或多个元素的有限序列 数据关系数据元素之间是线性关系, 元素按后进先出原则进 出栈 基本操作 1 InitStack(&S) 操作结果构造一个空栈 2 ClearStack(&S) 操作前提栈 S 已经存在 操作结果将栈置成空栈 3 int IsEmpty(S) 操作前提栈 S 已经存在 操作结果如果栈中元素个数等于 0, 则返回 TRUE; 否则返回 FALSE 4 int IsFull(S) 操作前提栈 S 已经存在 操作结果如果栈中元素个数等于 Stack_Size 1; 则返回 TRUE, 否则返回 FALSE 5 StaEle_Type Pop(S, x) 操作前提栈 S 已经存在且非空 操作结果弹出栈顶元素, 并用 x 返回其值 6 StaEle_Type GetTop(S, x) 操作前提栈 S 已经存在且非空 操作结果取栈顶元素, 但不改变栈顶位置 7 viod Push(S, x) 操作前提栈 S 已经存在且栈未满 操作结果在栈 S 的顶部插入元素 x ADT Stack 3.2 顺序存储表示 栈在计算机中的存储主要有两种方式 : 顺序方式和链式方式, 下面分别介绍这两种方式 并讨论各自的优缺点 顺序栈的存储表示 栈以顺序方式存储就称为顺序栈 顺序栈就是利用一组地址连续的存储单元依次存放自 栈底到栈顶的数据元素, 同时附设指针 top 指示栈顶元素在顺序栈中的位置, 通常以 top= 1 表示栈空 如果用 C 语言描述, 顺序栈定义如下 typedef struct StaEle_Type elem[stack_size]; int top;

44 34 数据结构概论 SeqStack; 顺序栈基本操作的实现对顺序栈可实现的基本操作有栈的初始化 判栈空 判栈满 元素入栈 元素出栈 取栈顶元素等 下面着重讨论元素的入栈和出栈操作 1. 入栈操作入栈即在栈未满的情况下, 先让栈顶指针 top 加 1 后, 元素 x 进栈 入栈操作要求先判断栈是否已满, 如果栈满, 再有新元素入栈, 将发生栈溢出, 程序就应转入出错处理 图 3-2 给出了顺序栈的入栈过程 算法实现如下 算法 3-1 顺序栈的入栈操作 图 3-2 顺序栈的入栈 #define TRUE 1 #define Stack_Size 50 int Push(SeqStack *S, StaEle_Type x) if(s->top==stack_size-1) printf(" 栈已满无法进栈 "); S->top++; S->elem[S->top]=x; return(true); 2. 出栈操作 出栈是在栈不空的情况下, 栈顶元素出栈, 栈顶指针 top 减 1 出栈时, 应先判断栈是否 空 图 3-3 给出了顺序栈的出栈过程 算法实现如下 图 3-3 顺序栈的出栈过程

45 第 3 章栈 35 算法 3-2 顺序栈的出栈操作 #define TRUE 1 #define FALSE 0 int Pop(SeqStack *S,StaEle_Type *x) if(s->top==-1) printf(" 栈空无法出栈 "); return(false); else *x=s->elem[s->top]; S->top--; return(true); 可以看出, 算法 3-1 和算法 3-2 都不带循环语句, 其时间复杂度与栈深无关, 都为 O(1), 可见入栈和出栈效率是极高的 下面的程序就是运用前面顺序栈的入栈 出栈算法将用户输入的字符依次入栈, 然后再 按出栈顺序输出 为简便起见, 程序中的 StaEle_Type 用 char 类型 #include "stdio.h" main() SeqStack *ST; char c1,*c2; ST->top=-1; printf(" 请输入入栈字符 :\n"); while((c1=getchar())!='\n') Push(ST,c1); printf(" 输出出栈字符为 :\n"); do Pop(ST,c2); putchar(*c2); while(st->top!=-1); 运行结果如下

46 36 数据结构概论 3.3 链式存储结构 采用链式方式表示一个栈, 便于结点的插入和删除 在程序中同时使用多个栈的情况下, 用链接表示不仅能够提高效率, 还可以达到共享存储空间的目的 链栈的存储表示我们采用带头结点的单链表表示一个栈, 则表头指针就作为栈顶指针, 如图 3-4 所示 图 3-4 链式栈 由图 3-4 可知, 链式栈的栈顶在链表的表头 若 top->next=null, 则表示栈空 采用链 式栈不需要预先估计栈的最大容量, 只要系统有空间, 就可以进行入栈操作 链栈的基本操 作与单链表的基本操作类似 用 C 语言描述, 链式栈的结构可定义如下 typedef struct Node StaEle_Type data; struct Node *next; LstNode; typedef LstNode *LStack; 链栈基本操作的实现对于链栈, 可以实现栈的初始化 判栈空 入栈 出栈等操作 下面着重讨论链栈中如何实现入栈和出栈操作 1. 入栈操作链栈的入栈操作类似于在带头结点的单链表表头插入一个结点, 链栈中的指针变化如图 3-5 所示 算法实现如下 算法 3-3 链栈的入栈操作 图 3-5 链式栈的入栈过程 LStack Push(LStack top,char x) LstNode *temp; temp=(lstnode *)malloc(sizeof(lstnode)); if(temp==null)

47 第 3 章栈 37 printf(" 申请空间失败 "); temp->data=x; temp->next=top->next; top->next=temp; return(top); 2. 出栈操作 出栈算法比较简单, 只要栈顶指针不为 NULL, 就可以安全出栈 出栈过程类似于在头 指针的单链表中删除第一个结点, 其指针变化情况如图 3-6 所示 算法实现如下 算法 3-4 链栈的出栈操作 char Pop(LStack top) char x; LstNode *temp; if(top->next==null) printf(" 栈空无法出栈 "); temp=top->next; x=temp->data; top->next=temp->next; free(temp); return(x); 图 3-6 链栈的出栈过程 下面的程序调用上述入栈和出栈算法实现将用户从键盘输入的字符入栈, 然后再依次将 栈中字符出栈并显示输出 #include "stdio.h" main() LStack LT; char c; LT->next=NULL; printf(" 请输入入栈字符 :\n"); while((c=getchar())!='\n')

48 38 数据结构概论 LT=Push(LT,c); printf(" 输出出栈字符为 :\n"); do c=pop(lt); putchar(c); while(lt->next!=null); 运行结果如下 3.4 应用举例 在程序设计中, 处理嵌套结构和递归结构时常常用到栈, 因为处理步骤遵循的是 后进先出 原则 1. 数制转换要把十进制整数 N 转换为 d 进制数一般常用 除基取余 法 具体做法是, 将十进制整数及此间产生的商不断除以非十进数的基, 直到商为 0 为止, 记下每一次相除所得的余数, 按照 从后往前 的顺序, 将各余数作为 k n 1,k n 2,,k 0, 从而构成相应的 d 进制数 例如,1348D=2504Q, 基应取 8, 具体转换过程如下 除以基 8 商余数 k i k k k k 3 如果用程序实现对任意一个非负十进制整数, 打印输出与其等值的八进制数 由于上述 计算过程是从低位 k 0 到高位 k n 1, 顺序产生八进制数的各个数位, 而打印输出习惯从高位到 低位进行, 恰好与计算过程相反 因此, 用栈来暂存计算过程中得到的八进制数各位, 然后 以出栈顺序打印输出, 即可得到正确的八进制数 程序实现如下 #define FALSE 0 #define TRUE 1 #define Stack_Size 50 typedef struct int elem[stack_size]; int top;

49 第 3 章栈 39 SeqStack; main() int N; SeqStack *S; int Q; S->top=-1; printf("\n 输入任一十进制数 N:"); scanf("%d",&n); while(n>0) Q=N%8; Push(S,Q); N=N/8; printf(" 转换所得八进制数为 :"); while(s->top!=-1) Pop(S,&Q); printf("%d",q); 运行结果如下 2. 简单表达式求值算法用高级语言编写的源程序, 可能会有各种各样的表达式, 如算术表达式 关系表达式 逻辑表达式等 在编译时就需要将表达式翻译成一条条机器语言指令, 翻译过程中除了考虑用户书写的语法错误外, 还要考虑各运算符的优先级 括号及函数调用等 我们下面只考虑如何翻译简单算术表达式 假设表达式中不含括号 函数和一目运算符, 需要考虑的只有求解顺序, 假设表达式中出现 5 种算术运算 加 减 乘 除和乘幂, 运算符分别为 + * / 和, 各运算符的优先级见表 3-1 表 3-1 操作符优先级表 * / + # 算式 a + xy k c/b 对应的表达式为 a + x * y k c/b 按上面运算符的优先级别, 上述表达式的计算顺序为 (1) 计算 t 1 = y k;

50 40 数据结构概论 (2) 计算 t 2 = x * t 1 ; (3) 计算 t 3 = a + t 2 ; (4) 计算 t 4 = c/b; (5) 计算 t 5 = t 3 t 4 结果值为 t 5 如果用一维数组 A 存储上述表达式, 则数组格式如图 3-7 所示 a + x * y k c / b # 图 3-7 数组 A 要设计上述表达式的求值算法, 应顺序扫描数组 A, 但当扫描到 a + 和 x 3 个元素后, 按运算符优先规则, 还不能作 a+x 运算 为了便于向前追溯已经扫描过但尚未处理的运算符和运算对象, 算法中设置了两个栈 操作数栈 s 和运算符栈 f 扫描过程中的入栈情况如图 3-8 所示 图 3-8 表达式 a+x*y k c/b 的计算步骤开始, 先将 a 和 x 依次进栈 s, 将 + 进栈 f, 继续查看下一个元素, 如果下一个符号 ( 必定是运算符或结束符 ) 的优先级不比前面的运算符高, 就从栈中取出来运算 a+x; 反之, 若下一个运算符是 *,/ 或, 就不能作 a+x 运算, 还要继续扫描下一个元素, 直到运算符, 前面存入栈的部分能够计算, 于是开始出栈 综上所述, 得出下面的算法 算法 3-5 简单表达式的求值算法 顺序扫描存储表达式的数组元素, 1) 遇到操作数, 则使其进入操作数栈 s, 继续扫描下一个元素 2) 遇到运算符 Δ, 则进入下面的循环 1 若运算符栈 f 不空, 则使 Δ 与运算符栈 f 的栈顶元素 比较优先级 ; 2 若 Δ 的优先级高于 的优先级, 转步骤 3), 否则进行下一步 ; 3 若 Δ 的优先级不高于 的优先级, 使操作数 s 退两栈, 所退出的操作数依次记作 x 2 和 x 1, 使运算符栈 s 退一栈, 退出的运算符为, 作一次运算 x 3 = x 1 x 2, 并使 x 3 进入操作数栈 s 4 重复步骤 1 至 3, 直到 Δ 的优先级高于运算符栈 f 的栈顶元素 的优先级, 或栈 f 已空 3) 若 Δ 是结束符 #, 则算法结束 此时, 操作数栈 s 中只有一个元素, 即计算结果 否则 Δ 进入运算符栈 f, 继续扫描数组的下一个元素, 返回步骤 1)

51 第 3 章栈 41 算法实现如下 int ExEvaluation char ch; InitStack(&s); InitStack(&f); Push(&f, # ); printf("\n 请输入一个表达式 ( 以 # 结束 ):"); ch=getchar; while(ch!= # GetTop(f)!= # ) if(!in(ch,ops)) int temp; temp=ch- 0 ; ch=getchar(); while(!in(ch,ops)) temp=temp*10+ch- 0 ; ch=getchar(); Push(&s,temp); else switch(compare(gettop(f),ch)) case < :Push(&f,ch); ch=getchar();break; case = :Pop(&f,&x); ch=getchar();break; case > :Pop(&f,&op); Pop(&s,&b); Pop(&s,&a); v=execute(a,op,b); Push(&s,v); break; v=gettop(s); return(v); 该算法中的 a 和 b 是两个操作数,OPS 为运算符集合

52 42 数据结构概论 小结 本章介绍了一种重要的数据结构栈 后进先出 (LIFO) 的线性表, 并讨论了栈的两种存储方式 顺序存储方式和链式存储方式 栈以顺序方式存储就称为顺序栈 顺序栈是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素 采用顺序方式存储栈的优点是存储结构简单, 内存利用率较高且操作容易实现 ; 缺点是必须预先估计栈的最大容量, 以免入栈时产生栈溢出 链栈是指用链表作为存储结构实现的栈, 采用链栈的优点是不必预先估计栈的最大容量, 只要系统有空间, 链栈就不会溢出 ; 缺点是每个结点需有数据和指针两个域, 内存利用率较低, 且入栈和出栈操作需修改链表指针, 操作比较麻烦 一般情况下, 栈的最大容量容易估计时, 采用顺序存储比较好 ; 栈的最大容量不容易估计时, 采用链栈较合适 习题 1. 栈结构通常采用的两种存储结构是 A. 线性存储结构和链表存储结构 B. 散列方式和索引方式 C. 链表存储结构和数组 D. 线性存储结构和非线性存储结构 2. 如果元素进栈的次序依次为 a,b,c,d, 则可能的出栈顺序为 A.a,b,c,d B.a,c,b,d C.d,c,b,a D.b,d,c,a 3. 栈的插入和删除操作在 进行 A. 栈顶 B. 栈底 C. 任意位置 D. 指定位置 4. 当利用大小为 N 的数组顺序存储一个栈时, 假定用 top==n 表示栈空, 则向这个栈插 入一个元素时, 首先应执行 语句修改 top 指针 A.top++; B.top--; C.top=0; D.top; 5. 栈又称为 的线性表 6. 栈在计算机中主要有两种存储方式 : 和 7. 设 S[1 Maxsize] 为一个顺序存储的栈, 变量 top 指示栈顶位置, 栈为空的条件是, 栈为满的条件是 8. 向栈中压入元素的操作为 9. 在一个顺序栈中, 若栈顶指针等于 1, 则为 ; 若栈顶指针等于 Stack_Size 1, 则为 10. 从一个栈删除元素时, 首先取出, 然后再前移一位 11. 在一个链栈中, 若栈顶指针等于 NULL 则为 12. 向一个链栈插入一个新结点时, 首先把栈顶指针的值赋给, 然后再把 新结点的存储位置赋给 13. 从一个链栈中删除一个结点时, 需要把栈顶结点 的值赋给 14. 向一个栈顶指针为 HS 的链栈中插入一个新结点 *p 时, 应执行 和 操

53 第 3 章栈 43 作 15. 从一个栈顶指针为 HS 的非空链栈中删除结点并不需要返回栈顶结点的值和回收结 点时, 应执行 操作 16. 简述栈和线性表的差别 17. 假定有 4 个元素 A,B,C,D, 按所列次序进栈, 试写出所有可能的出栈列 注意, 每一个元素入栈后都允许出栈, 如 ACDB 就是一种出栈序列 18. 在图 3-1 所示的栈结构中, (1) 若编号为 1,2,3 的 3 节车厢按不同的入 出栈次序穿过栈, 可以得到哪几种不同 次序的排列 排列 (2) 若编号为 1,2,3,4 的 4 节车厢按不同的入 出栈次序穿过栈, 可以得到哪几种 19. 按四则运算和幂运算 ( ) 优先关系的惯例, 仿照本章图 3-8 的格式, 画出对下列 算术表达式 求值时操作数栈和运算符栈的变化过程 A B * C/D + E F 20. 假设一个算术表达式中包含圆括号 方括号和花括号, 编写一个判断表达式中括号 是否正确配对的函数 correct(exp, tag), 其中,exp 为字符串类型的变量, 表示被判别的表达式, tag 为布尔型变量 21. 在一个数组空间 stack[stack_size] 中可以同时存放两个顺序栈, 栈底分别在数组的两 端, 当第一个栈的栈顶指针 top1 等于 1 时栈 1 为空, 当第二个栈的栈顶指针 top2 等于 Stack_Size 时栈 2 为空 两个栈均向中间增长, 当向栈 1 插入元素时, 使 top1 加 1 得到新的 栈顶位置, 当向栈 2 插入元素时, 则使 top2 减 1 才能够得到新的栈顶位置 当 top1 等于 top2 1 或者 top2 等于 top1+1 时, 存储空间用完, 无法再向任一栈插入元素 用于双栈操作的顺序存 储类型可定义如下 struct BothStack ElemType stack[stack_size]; int top1,top2; ; 双栈操作的抽象数据类型可定义如下 DAT BStack 数据元素采用顺序结构存储的双栈, 其存储类型为 BothStack 基本操作 void InitStack(BothStack& BS,int k); /* 初始化栈 当 k=1 或 2 时对应置栈 1 或栈 2 为空,k=3 时置两个栈均为空 */ void ClearStack(BothStack& BS,int k) /* 清除栈 当 k=1 或 2 时对应栈 1 或栈 2 被清除,k=3 时两个栈均被清除 */ int StackEmpty(BothStack& BS,int k) /* 判断栈是否为空 当 k=1 或 2 时判断对应的栈 1 或栈是否为空,k=3 时判断两个栈是否同时为空 若判断结果为空则返回 1, 否则返回 0*/ ElemType Peek(BothStack& BS,int k)

54 44 数据结构概论 /* 取栈顶元素 当 k=1 或 2 时对应返回栈 1 或栈 2 的栈顶元素 */ void Push(BothStack& BS,int k,const ElemType& item) /* 入栈 当 k=1 或 2 时对应向栈 1 或栈 2 的顶端压入元素 item*/ BStack (1) 试写出上述抽象数据类型中每一种操作的算法 (2) 假定上述所有操作的算法已存储于头文件 bstack.h 中, 试编写一个程序, 让计算机 随机产生出 100 以内的 20 个正整数并输出, 同时把它们中的偶数依次存入第一个栈中, 奇数 依次存入第二个栈中, 接着按照存入每个栈中元素的次序分别输出每个栈中的所有元素 ( 此 操作不改变栈的状态 ), 然后按照后进先出的原则输出每个栈中的所有元素 实习 1. 实验目的掌握栈的先进后出的特性, 在遇到实际问题时会运用栈结构进行解决 2. 实验内容 (1) 写出 前置波兰表达式 求值算法 ( 只用一个栈 ) ( 2 ) 写出 后置波兰表达式 求值算法

55 第 4 章队 列 本章要点 : 队列顺序队列循环队列链队列 4.1 概念和定义 队列 (queue) 是一种操作受限的线性表, 它是只允许在一端进行插入, 而在另一端进行删除的表结构 允许插入的一端称为队尾 (rear), 允许删除的一端称为队头 (front) 通常, 用头 尾两指针 ( 比如变量 front 和 rear) 分别指向队头元素和队尾元素 队结构好似一条通道, 如图 4-1, 结点从一端进入, 从另一端退出 因此, 队结构是遵循先进先出 (FIFO) 原则的, 所以队又称为先进先出表 图 4-1 队结构示意图在程序设计中, 队结构的应用很广 在操作系统中, 常常有多个进程同时需要占用同一资源 ( 比如 CPU), 于是进程调度程序负责将它们排好队, 依次调度执行 下面给出队的抽象数据类型定义 ADT Queue 数据对象零个或多个元素的有限序列 数据关系数据元素之间是线性关系, 元素按先进先出原则进 出队列 基本操作 1 InitQueue(&Q) 操作结果设置一个空队列 2 int IsEmpty(Q) 操作前提队 Q 已经存在 操作结果若队空, 返回 TRUE, 否则返回 FALSE

56 46 数据结构概论 3 int EnQueue(&Q, x) 操作前提队 Q 已存在且不满 操作结果在队 Q 的队尾插入 x, 操作成功, 返回值 TRUE, 否则返回值 FALSE 4 QEle_Type DelQueue(&Q, &x) 操作前提队 Q 已存在且不空 操作结果使队 Q 的队头元素出队, 并用 x 带回其值 5 QEle_Type GetHead (&Q, &x) 操作前提队 Q 已存在且不空 操作结果取队头元素, 并用 x 带回其值 6 ClearQueue(&Q) 操作前提队 Q 已存在 操作结果将队 Q 置为空队列 ADT Queue 4.2 顺序存储结构 与线性表和栈类似, 队列也有两种存储表示 顺序表示和链式表示 顺序队列的存储表示 队列的顺序存储是指用一组地址连续的存储单元依次存放从队头到队尾的元素 因为对 队的插入和删除操作是在队的两端进行, 因此用两个指针 front 和 rear 分别指示队头和队尾元 素在数组中的位置 设一维数组 q[m] 用于存储一个队, 变量 front 和 rear 分别是队的头 尾指针, 队空时 front=rear, 参见图 4-2(a) 顺序队列的类型定义如下 #define TRUE 1 #define FALSE 0 #define m 50 typedef struct QEle_Type elem[m]; int front; int rear; SeqQueue; 顺序队列基本操作的实现 1. 初始化操作该操作完成设置一个空队列 算法实现如下 void InitQueue(SeqQueue *q)

57 第 4 章队列 47 q->front=0; q->rear=0; 2. 入队操作 对于顺序队列, 当元素 x 要入队时, 在 rear 不等于 m 的条件下, 直接将 x 送入尾指针 rear 所指的单元, 然后尾指针加 1, 如图 4-2(b) 所示 算法实现如下 3. 出队操作 int EnQueue(SeqQueue *q, int x) if(q->rear==m) printf(" 队列已满 "); return(false); else q->elem[q->rear]=x; q->rear++; return(true); 在队列非空的情况下, 元素要出队时, 只需修改队头指针, 参见图 4-2(c) 算法实现如下 图 4-2 队列的基本操作 int DeQueue(SeqQueue *q,int x) if(q->rear==q->front) printf(" 队空 "); return(false); else

58 48 数据结构概论 x=q->elem[q->front]; q->front++; return x; 下面的例子把以上顺序队列的操作综合起来 程序实现初始化一个队列, 将用户输入的 不等于 0 的数字入队, 然后按出队顺序打印所有数字 #define TRUE 1 #define FALSE 0 #define m 50 typedef struct int elem[m]; int front; int rear; SeqQueue; main() SeqQueue *Q; int x; InitQueue(Q); printf(" 输入入队的数字 :\n"); scanf("%d",&x); while(x!=0) EnQueue(Q,x); scanf("%d",&x); printf(" 出队队列为 :\n"); while(x=dequeue(q,x)) printf("%2d",x); printf("\n"); 运行结果如下

59 第 4 章队列 循环队列 在非空顺序队列中, 当 rear=m 时, 认为队满, 但此时不一定是真的队满, 因为随着部分 元素出队, 数组前面会出现一些空的单元, 如图 4-3 所示 图 4-3 假溢出队列由于入队只能在队尾进行, 因此前面的空单元无法被利用, 我们称这种现象为假溢出, 假溢出将造成内存空间的浪费 为了解决假溢出现象, 我们可以将存储顺序队列的数组看成一个环状的空间, 即队列最后一个单元的后继为第一个单元, 这样当尾指针已经移到数组末端 (rear=m 1), 下一步可移向数组前部 (0 下标处 ), 元素再进队时, 就可以占用数组前端的单元, 这种循环使用数组存储单元的队列称为循环队列, 或环型队列 (cyclic queue) 结构 为了形象地表示循环队列的工作原理, 将数组 q 画成圆环形状, 如图 4-4 所示,q[0] 便接在 q[m 1] 之后 图 4-4 循环队列循环队列就是一种顺序队列, 其类型定义与顺序队列的类型定义相同, 这里不再给出 现在考虑如何设置判断队空和队满的条件 程序开始时, 队空, 指针 front=rear=0 随着入队和出队操作的交替进行, 队头 队尾指针也相继沿圆周顺时针向前移动 假设在某一段时间内只有元素出队, 没有元素入队, 指针 rear 不动, 但指针 front 随着元素的出队一步步地靠近 rear 当 front=rear 时, 队空, 这时只能允许元素入队, 不能出队 如图 4-4(a) 所示 反之, 如果在一段时间内只有元素入队, 没有元素出队, 指针 front 不动,rear 也会靠近 front, 当 rear=front 时, 数组的 m 个单元全部占满 ( 队满 ), 这是不能入队, 只能出队 由此可见, 条件 front=rear 既表示队空, 又表示队满 因此, 两指针是否相等不能作为测试队空或队满的条件, 需另选条件测试 一种测试方法是少用一个元素空间, 当尾指针所指向的空单元的后继是队头元素所在的单元时, 停止入队 这样, 队尾指针永远追不上队头指针, 所以队满时不会有 front=rear, 即可用条件 front=rear 来判断队空, 条件 (rear+1)%m=front 来判断队满 另一种测试方法是增加一个 当前队长度 变量 ( 比如 n), 用 n 的值作为队空或队满的

60 50 数据结构概论 测试条件, 每当元素入队或出队时, 除修改队尾或队头指针外, 还要修改 n 的值 法 下面给出的算法中的循环队列都是少用一个元素空间来区分队列空与队列满 1. 入队算法 int EnQueue(SeqQueue *q, QEle_Type x) if((q->rear+1)%m==q->front) printf(" 队列已满 "); return(false); else q->elem[q->rear]=x; q->rear=(q->rear+1)%m; return(true); 2. 出队算法 int DeQueue(SeqQueue *q, QEle_Type *x) if(q->rear==q->front) printf(" 队空 "); return(false); else *x=q->elem[q->front]; q->front=(q->front+1)%m; return(true); 如果采用另外一种处理假溢出问题的方法, 读者可以自己写出循环队列的入队和出队算 4.3 链式存储结构 与栈相同, 队列也可以采用链式存储 用于存储一个队列的链表使用两个指针,front 指向头结点,rear 指向尾结点 链队列的存储表示为了便于操作, 采用带头结点的链表结构存储队列, 如图 4-5 所示 开始时,front 和 rear

61 第 4 章队列 51 同时指向头结点, 表示空的链队列 图 4-5 链队列 链队列定义如下 typedef struct Node QEle_Type data; struct Node next; LinkQueueNode; typedef struct LinkQueueNode *front; LinkQueueNode *rear; LinkQueue; 链队列基本操作的实现 下面给出链队列基本操作的算法实现 1. 入队算法 int EnQueue(LinkQueue *q,qele_type x) LinkQueueNode *p; p=(linkqueuenode *)malloc(sizeof(linkqueuenode)); if(p) p->data=x; p->next=null; q->rear->next=p; q->rear=p; return(true); else return(false); 2. 出队算法 int Del_Q(LinkQueue *q,qele_type *x) LinkQueueNode *p;

62 52 数据结构概论 if(q->front==q->rear) /* 链队列空 */ return(false); p=q->front->next; q->front->next=p->next; if(q->rear==p) q->rear=q->front; *x=p->data; free(p); return(true); 下面程序是对链式队列算法的应用 #define NULL 0 #define TRUE 1 #define FALSE 0 typedef struct Node char data; struct Node *next; LinkQueueNode; typedef struct LinkQueueNode *front; LinkQueueNode *rear; LinkQueue; #include "stdio.h" main() LinkQueue *Q; char x; printf(" 输入入队的字符序列 :\n"); while((x=getchar())!='\n') EnQueue(Q,x); printf(" 出队字符序列为 :\n"); while(del_q(q,&x)) putchar(x); printf("\n"); 运行结果如下

63 第 4 章队列 应用举例 打印二项展开式 (a+b) i 的系数 将二项式 (a+b) i 展开, 其系数构成杨辉三角形, 如图 4-6 所示 现在要求一算法, 打印展 开式系数的前 n 行 1 i= 图 4-6 杨辉三角形 从三角形的形状可以看出, 每行两端的数据都为 1, 除第 0 行外, 第 i 行的数据 ( 两端除 外 ) 都是其两 肩头 数据之和 所以, 第 i 行的数据可以由第 i 1 行的数据生成, 我们可以 利用循环队列实现杨辉三角形的打印 在循环队列中依次存放第 i 1 行的数据, 然后逐个输 出并打印, 同时生成第 i 行的数据并入队 具体实现程序如下 #define TRUE 1 #define FALSE 0 #define m 50 typedef struct int elem[m]; int front; int rear; SeqQueue; void YANGHUI(int n) int i, j, t, s=0; int pos=0; SeqQueue q; InitQueue(&q); EnQueue(&q, 1); EnQueue(&q, 1); /* 打印 n=0 时的系数 1*/ printf(""); for(pos=0; pos<=2*n; pos++) printf(" ");

64 54 数据结构概论 printf("1"); if(n==0) return; for(i=1; i<=n; i++) printf(""); /* 设定输出的位置 */ for(pos=0; pos<=2*n-i; pos++) printf(" "); EnQueue(&q, 0); for(j=1; j<=i+2; j++) DeQueue(&q, &t); EnQueue(&q, s+t); s=t; if(j!=i+2) printf("%d", s); main() YANGHUI(6); 运行结果如下 小结 本章介绍了另一种操作受限的线性表 队列 队列的插入操作只能在队尾一端进行, 而删除操作只允许在队头一端进行 先进队的元素必然先出队, 所以队列又称为先进先出 (FIFO) 表 队列常用的存储方式也有两种 顺序存储方式和链式存储方式 队列的顺序存储是指用一组地址连续的存储单元依次存放从队头到队尾的元素, 采用顺序队列容易出现假溢出的问题, 即队列前面有空的单元, 但因为尾指针 rear=m, 而插入操作又只能在队尾进行, 不允许新的元素入队, 造成内存空间浪费 循环队列也是一种顺序队列, 它是把顺序队列看成环状, 规定最后一个单元的后继为第一个单元, 采用循环队列可以充分利用队列空间, 解决假溢出问题, 但循环队列的判队空或

65 第 4 章队列 55 队满的条件较难实现 本章讨论了两种方法, 一种方法是少用一个存储空间, 这样队满的判断条件是 (rear+1)%m=front, 队空的判断条件是 front=rear 另一种方法是增加一个 当前队长度 变量 ( 比如 n), n 值表示当前队中的元素个数,n=0 表示队空,n=m 表示队满 链队列是用链表表示的队列, 由于链表中的每个结点至少需设置两个域 ( 指针域和数据域 ), 因此链队列的存储密度比顺序队列的存储密度低 习题 1. 一个队列的入队顺序是 1,2,3,4, 则队列的输出顺序为 A.4,3,2,1 C.1,2,3,4 B.1,2,4,3 D.1,4,3,2 2. 判定一个循环队列 QU( 最多元素为 m 0 ) 为空的条件是 A.QU->front==QU->rear B.QU->front!=QU->rear C.QU->front== (QU->rear+1)%m 0 D.QU->front!=(QU->rear+1)%m 0 3. 循环队列采用数组 A[0, m 1] 存放其元素值, 已知其头 尾指针分别是 front 和 rear, 则当前队列中的元素个数是 A.(rear front+m)%m C.rear front 1 B.rear front+1 D.rear front 4. 在一个链队中, 假设 f 和 r 分别为队头和队尾指针, 则插入 s 所指结点的运算是 A.f->next=s; f=s C.s->next=r; r=s B.r->next=s; r=s D.s->next=f; f=s 5. 循环队列中数组的下标范围是 1~n, 头 尾指针分别为 f 和 r, 则其元素个数为 A.r f B.r f+1 C.(r f)%(n+1) D.(r f+n)%n 6. 从一个顺序队列删除元素时, 首先需要 A. 前移一个队头指针 B. 后移一位队头指针 C. 取出队头指针所指位置上的元素 D. 取出队尾指针所指位置上的元素 7. 假定一个顺序队列的队头和队尾指针分别为 f 和 r, 则判断队空指针的条件为 A.f+1==r B.r+1==f C.f==0 D.f==r 8. 假定一个链队的队头和队尾指针分别为 front 和 rear, 则判断队空的条件为 A.front==rear B.front!=NULL C.rear!=NULL D.front==NULL 9. 队列的特性是 10. 队列的插入操作在进行, 删除操作在进行 11. 在一个循环队列中, 队头指针指向队头元素的 12. 在一个循环队列中删除一个元素时, 其操作是 13. 在具有 n 个单元的循环队列中, 队满时共有个元素 14. 在一个顺序队列 Q 中, 判断队空的条件为, 判断队满的条件为 15. 在一个链队中, 若队头指针与队尾指针的值相同, 则表示该队为或

66 56 数据结构概论 该队 16. 假定 front 和 rear 分别为一个链队的队头和队尾指针, 则该链队中只有一个结点的条件为 或者 17. 假定用于顺序存储一个队列的数组的长度为 n, 队头和队尾指针分别为 front 和 rear, 写出求此队列长度 ( 即所含元素个数 ) 的公式 18. 何谓队列的上溢现象? 它的解决方法有哪些? 19. 假设用一个循环单链表表示队列 ( 称为循环链队列 ), 该队列只设一个队尾指针 rear, 不设队头指针, 编写函数实现如下功能 (1) 向循环链队列中插入一个值为 x 的结点 (2) 从循环链队列中删除一个结点 20. 如果用一个循环数组 qu[0,m 0 1] 表示队列时, 该队列只有一个头指针 front, 不设队尾指针 rear, 而改用计数器 count 用以记录队列中结点的个数 编写实现队列 5 种基本运算的算法 21. 假定在一个链接队列中只设置队尾指针, 不设置队头指针, 并且让队尾结点的指针域指向队头结点 ( 称此为循环链队 ), 试分别写出在循环链队上进行插入和删除的算法 实习 1. 实验目的掌握队列 先进先出 的特性, 加深理解环形队列的含义, 并学会用队列结构解决实际问题 2. 实验内容 (1) 编写向顺序分配的环形队列 QU[0,m 0 1] 中插入一个结点的函数 enqueue 和从该队列中取出一个结点的 dequeue 函数 (2) 编写一个程序求解迷宫问题 迷宫是一个如下图所示的 m 行 n 列的 0/1 矩阵, 其中 0 表示无障碍,1 表示有障碍 设入口为 (1,1), 出口为 (m,n), 每次移动只能从一个无障碍的单元移到其周围 8 个方向上任一无障碍的单元, 编制程序给出一条通过迷宫的路径或报告一个 无法通过 的信息

67 第 5 章串 本章要点 : 定长顺序串 堆串 块链串 5.1 概念和定义 串 (string) 是字符串的简称, 是有穷的字符序列 串通常可记为 s='a 1 a 2 a n ' n 0 其中,s 是串名, 可以是串变量名, 也可以是串常量名 用单引号或双引号括起来的字符序列称为串值, 其中 a i 是串中的字符 (0<i n),n 是串中的字符个数, 也称为串的长度, 串长度不包括引号和串结束符 '\0' n=0 时的串称为空串, 除串结束符外, 空串不包括任何其他字符, 以后我们用符号 表示空串 需要注意区分的是空串和空格串 (blank string), 空格串是由一个或多个称为空格的特殊字符组成的串, 其长度是串中空格字符的个数 串中任意个连续的字符组成的子序列称为该串的子串 相应地, 包含子串的串称为主串 例如,s='students',p='den', 则 p 是 s 的子串, 它是在 s 中从第 4 个字符开始, 连续取 3 个字符组成的串 一般称子串的第 1 个字符在串中的位置为子串在串中的位置, 所以 p 在 s 中的位置为 4 空串是任意串的子串, 任意串又是它自身的子串 除自身以外, 一个串的其他子串都是它的真子串 串的逻辑结构和线性表极为相似, 只是串的数据对象为约定的字符集 但是对串的操作与线性表却有很大区别 线性表多以单个元素作为操作对象, 比如对线性表的插入 删除操作等, 而对串的操作, 多数情况下以串的整体作为操作对象, 比如, 两个串的连接, 复制一个字符串等 还有一些操作是字符串所特有的, 如求一个子串在串中的位置, 在一个串中提取子串等 串的抽象数据类型定义如下 ADT String 数据对象 n 个字符序列,n 0 数据关系字符之间为线性关系 基本操作

68 58 数据结构概论 1 StrLength(s) 操作前提串 s 存在 操作结果求出串 s 的长度 2 StrAssign(s1, s2) 操作前提 s 1 是一个串变量,s 2 或者是一个串常量, 或者是一个串变量 ( 通常 s 2 是一个串常量时称为串赋值, 是一个串变量时称为串复制 ) 操作结果将 s 2 的串值赋值给 s 1,s 1 原来的值被覆盖 3 StrConcat (s1, s2, s) 或 StrConcat (s1, s2) 操作前提串 s 1, s 2 存在 操作结果两个串的连接就是将一个串的串值紧接着放在另一个串的后面, 连接成一个串 前者产生新串 s, 而串 s 1 和 s 2 不改变 ; 后者是在串 s 1 的后面连接串 s 2 的串值, 串 s 1 改变, 串 s 2 不改变 例如,s 1 = 'he',s 2 = 'bei', 前者的操作结果是 s='hebei', 后者的操作结果是 s 1 ='hebei' 4 SubStr(s, i, len) 操作前提串 s 存在,1 i StrLength(s),0 len StrLength(s) i+1 操作结果返回从串 s 的第 i 个字符开始的长度为 len 的子串,len=0 得到的是空串 例如,SubStr('abcdefghi', 3, 4)= 'cdef ' 5 StrComp(s1, s2) 操作前提串 s 1, s 2 存在 操作结果若 s 1 ==s 2, 操作返回值为 0; 若 s 1 <s 2, 返回值 <0; 若 s 1 >s 2, 返回值 > 0 6 StrIndex(s, t) 操作前提串 s, t 存在 操作结果找子串 t 在主串 s 中首次出现的位置 若 t s, 则操作返回 t 在 s 中首次出现的位置 ; 否则, 返回值为 1 例如,StrIndex('abcdebda', 'bc')=2 StrIndex('abcdebda', 'ba')= 1 7 StrInsert(s, i, t) 操作前提串 s, t 存在,1 i StrLength(s)+1 操作结果将串 t 插入串 s 的第 i 个字符位置上,s 的串值发生改变 8 StrDelete(s, i, len) 操作前提串 s 存在,1 i StrLength(s),0 len StrLength(s) i+1 操作结果删除串 s 中从第 i 个字符开始的长度为 len 的子串,s 的串值改变 9 StrRep(s, t, r) 操作前提串 s, t, r 存在,t 不为空 操作结果用串 r 替换串 s 中出现的所有与串 t 相等的不重叠的子串,s 的串值改变 ADT String

69 第 5 章串 顺序存储结构 串有 3 种机内表示方法, 下面先介绍串的顺序存储表示 定长顺序串的存储表示及操作的实现 与线性表的顺序存储结构类似, 用一组地址连续的存储单元存储串值的字符序列, 可以 用定长数组如下描述定长顺序串 #define Max 255 typedef struct char ch[max]; int len; SeqString; 关于串的基本运算, 基本上在 C 语言中已经学过, 主要包括 : 求串长 StrLen(char *s); 串复制 StrCpy(char *to, char *from); 串连接 StrCat(char *to, char *from); 串比较 CharCmp(char *s1, char *s2); 字符定位 StrChr(char *s, char c) 等 下面给出定长顺序串各种操作的实现 1. 串清空操作 define TRUE 1 define FALSE 0 int StrClear(SeqString *s) s->len=0; return(true); 2. 串插入操作 在串 s 中序号为 i 的字符之前插入串 t #define Max 50 #define FALSE 0 #define TRUE 1 int StrInsert(SeqString *s,int i,seqstring t) int j; if(i<0 i>s->len) printf(" 插入位置不合理 ");

70 60 数据结构概论 return(false); if(s->len+t.len<=max) for(j=s->len+t.len-1;j>=t.len+i;j--) s->ch[j]=s->ch[j-t.len]; for(j=0;j<t.len;j++) s->ch[j+i]=t.ch[j]; s->len=s->len+t.len; else if(i+t.len<=max) for(j=max-1;j>t.len+i-1;j--) s->ch[j]=s->ch[j-t.len]; for(j=0;j<t.len;j++) s->ch[j+i]=t.ch[j]; s->len=max; else for(j=0;j<max-i;j++) s->ch[j+i]=t.ch[j]; s->len=max; return(true); 3. 串删除操作 在串 s 中删除从序号 i 开始的 j 个字符 int StrDelete(SeqString *s,int i,int j) int k; if(i<0 i>(s->len-j)) printf(" 删除位置不合理 "); return(false); for(k=i+j;k<s->len;k++) s->ch[k-j]=s->ch[k]; s->len=s->len-j; return(true); 例如, 下面的程序运用前面的算法实现将顺序串 s 2 插入顺序串 s 1 中, 然后再删除 s 2 main()

71 第 5 章串 61 SeqString *s1,s2; int i,j; s1->len=12; s2.len=4; printf(" 输入顺序串 s1:\n"); gets(s1->ch); printf(" 输入顺序串 s2:\n"); gets(s2.ch); printf(" 输入插入位置 :"); scanf("%d",&i); StrInsert(s1,i,s2); printf(" 插入后的顺序串为 :\n"); puts(s1->ch); printf(" 输入删除位置及删除串的长度 :"); scanf("%d,%d",&i,&j); StrDelete(s1,i,j); printf(" 删除后的顺序串为 :\n"); puts(s1->ch); 运行结果如下 堆存储表示及操作的实现 堆存储表示的特点是仍以一组地址连续的存储单元存放串值字符序列, 但堆存储空间是 在程序执行过程中动态分配而得到的 在 C 语言中, 利用函数 malloc() 为每一个新产生的串 分配一块实际串长所需的存储空间, 若分配成功, 则返回一个指向起始地址的指针, 作为串 的基址 堆串定义如下 typedef struct char *ch; int len; HString; 其中,ch 域指示串的起始地址,len 域表示串的长度 这种存储结构表示的串操作仍是基于字符序列的复制进行的, 下面讨论堆存储表示的串

72 62 数据结构概论 的各种操作的实现 1. 串复制操作 复制操作的算法思想是, 将串 s 复制到串 t 中, 当串 s 不空时, 首先为串 t 分配与串 s 长 度相等的存储空间, 然后将串 s 的值复制到串 t 中 算法实现如下 int StrCopy(HString *t, HString s) int i; t->ch=(char *)malloc(s.len); if(t->ch==null) return(false); for(i=0;i<s.len;i++) t->ch[i]=s.ch[i]; t->len=s.len; return(true); 2. 串插入操作 要实现在串 s 中插入串 t, 算法思想是为串 s 重新分配大小等于串 s 和串 t 长度之和的存 储空间, 然后进行串值复制 算法实现如下 int StrInsert(HString *s,int i,hstring t) int k; char *temp; if(i<0 i>s->len+t.len) printf(" 插入位置不合理 "); return(false); temp=(char *)malloc(s->len+t.len); if(temp==null) return(false); for(k=0;k<i;k++) temp[k]=s->ch[k]; for(k=0;k<t.len;k++) temp[k+i]=t.ch[k]; for(k=i;k<s->len;k++) temp[k+t.len]=s->ch[k]; s->len=s->len+t.len; free(s->ch); s->ch=temp; return(true);

73 第 5 章串 串常量赋值操作 int StrAssign(HString *s1,char *s2) /* 将一个字符型数组 s 2 中的字符串送入堆 store 中,free 是自由区的指针 */ int i=0,len; while(s2[i]!='\0') i++; len=i; if(len) s1->ch=(char *)malloc(len); if(s1->ch==null) return(false); for(i=0;i<len;i++) s1->ch[i]=s2[i]; else s1->ch=null; s1->len=len; return(true); 4. 求子串操作 int StrSub(HString *t,hstring s,int i,int len) /* 将串 s 中第 i 个字符开始的长度为 len 的子串送到一个新串 t 中 */ int k; if(i<0 len<0 len>s.len-i+1) printf(" 删除位置不合理 \n"); return FALSE; else t->ch=(char *)malloc(len); if(t->ch==null) return(false); t->len=len; for(k=0;k<len;k++) t->ch[k]=s.ch[k+i]; return(true); 5. 串连接操作 HString *StrConcat(s1,s2,s)

74 64 数据结构概论 HString s1,s2; HString *s; HString t; StrCopy(s,s1); StrCopy(&t,s2); s->len=s1.len+s2.len; 下面的程序是对以上堆串操作的应用 #include "stdio.h" #define TRUE 1 #define FALSE 0 #define NULL 0 main() int i,len; HString *s1,*sub,s2; s2.ch="how are you?"; s2.len=12; 运行结果如下 StrCopy(s1,s2); printf(" 输出复制串 s1:"); puts(s1->ch); printf(" 请输入子串位置及长度 :\n"); scanf("%d,%d",&i,&len); StrSub(Sub,s2,i,len); printf(" 子串为 :"); for(i=0;i<sub->len;i++) putchar(sub->ch[i]); printf("\n"); printf(" 输入插入位置 :"); scanf("%d",&i); StrInsert(s1,i,s2); printf(" 插入后的堆串 s1 为 :\n"); for(i=0;i<s1->len;i++) putchar(s1->ch[i]);

75 第 5 章串 块链存储表示 串也可以采用链式存储法 但因串结构的特殊性 结构中的每一个数据元素是一个字符, 则用链表存储串值时, 需要考虑 结点大小 问题, 因为每一个结点可以存放一个字符, 也可以存放多个字符 ( 如图 5-1), 这时结点大小的选择就显得非常重要, 它影响着串处理的效率 图 5-1 串值的链式存储方式为了便于操作, 串的块链表除了附设头指针外, 再增加一个尾指针指示链表中的最后一个结点 块链结构定义如下 #define Block_Size typedef struct Block char ch[block_size]; struct Block *next; Block; typedef struct Block *head; Block *tail; BString; 当结点大小为 1 时, 运算处理方便, 然而存储占用最大, 当每个结点有多个字符时, 此时插入 删除操作比较复杂, 需要考虑结点的分拆和合并, 这里不再详细介绍 5.4 应用举例 例 5-1 输入一行字符, 统计其中有多少个单词, 单词之间用空格分隔开 程序如下 #include "stdio.h" typedef struct char ch[70]; int len;

76 66 数据结构概论 SeqString; main() SeqString ss; int i,num=0,word=0; char c; printf(" 请输入字符串 :\n"); gets(ss.ch); for(i=0;(c=ss.ch[i])!='\0';i++) if(c==' ') word=0; else if(word==0) word=1; num++; printf(" 该行字符串中共有 %d 个单词 \n",num); 运行结果如下 例 5-2 文本编辑 文本编辑程序是利用计算机进行文字加工的基本软件工具, 实现对文本文件的插入 删除等修改操作 为了编辑方便, 可以用换页符和换行符把文本划分为若干页, 每页有若干行 如果把文本看成一个文本串, 那么页就是它的子串, 行又是页的子串 比如有下面一段源程序 main() int a,b,sum; a=123;b=456; sum=a+b; printf("sum is %d\n",sum); 输入内存后如图 5-2 所示, 文本编辑程序中设置页指针 行指针和字符指针分别指示当前操作的页 行和字符

77 第 5 章串 67 m a i n ( ) i n t a, b, s u m ; a = ; b = ; s u m = a + b ; p r i n t f ( s u m i s % d \ n, s u m ) ; 图 5-2 文本格式示例 表 5-1 图 5-2 所示文本串的行表 行 号 起始地址 长 度 如果在某行内插入或删除若干字符, 则要修改行表中该行的长度 ; 如果要插入或删除一行, 就要涉及行表的插入或删除 关于文本编辑中的基本操作的具体算法, 这里不再给出, 感兴趣的话读者可以自己编写 小结 本章介绍了一种非数值数据 字符串, 简称串 串是有穷字符序列, 串可以采用顺序存储, 也可以采用链式存储 用一组地址连续的存储单元存放串值字符序列称为定长顺序串, 这种存储结构中实现串操作的原操作为字符序列的复制 对定长顺序串可以实现的操作很多, 本章中我们给出了串清空 串插入 串删除等操作的具体实现 堆存储表示的串是以一组地址连续的存储单元存放串值字符序列, 但其存储空间是在程序执行过程中动态分配而得 这种存储结构表示的串操作仍是基于字符序列的复制进行的 本章中我们给出了堆串的串复制 串插入 串常量赋值 求子串 串连接等操作的实现 串采用链式存储结构时, 由于串的特殊性, 在具体实现时需考虑结点大小问题 如果结点大小不为 1, 则对串进行插入 删除等操作时较难实现 习题 1. 串是 A. 不少于一个字母的序列 B. 任意个字母的序列 C. 不少于一个字符的序列 D. 有限个字符的序列 2. 为查找某一特定单词在文本中出现的位置, 可应用的串运算是

78 68 数据结构概论 A. 插入 B. 删除 C. 串连接 D. 子串定位 3. 串是一种特殊的线性表, 其特殊性体现在 A. 可以顺序存储 B. 数据元素是一个字符 C. 可以链接存储 D. 数据元素可以是多个字符 4. 已知函数 SubStr(s, i, j) 的功能是返回串 s 中从第 i 个字符开始长度为 j 的子串, 函数 StrCopy(s, t) 的功能为复制串 t 到 s 若字符串 s='sciencestudy', 则调用函数 StrCopy(p, Sub (s, 1, 7)) 后得到 A.p='SCIENCE' B.p='STUDY' C.s='SCIENCE' D.s='STUDY' 5. 串的两种最基本的存储方式是 6. 空串是, 其长度为 7. 空格串是, 其长度为 8. 若某串的长度小于一个常数, 则采用 存储方式最节省空间 9. 简述空串和空格串 ( 或称空格字符串 ) 的区别 10. 编写算法, 求得所有包含在串 s 中而不包含在串 t 中的字符 (s 中重复的字符只选一 个 ) 构成的新串 r, 以及 r 中每个字符在 s 中第一次出现的位置 11. 假设用块链结构表示串 试编写将串 s 插入到串 t 中某个字符之后的算法 ( 若串 t 中 不存在此字符, 则将串 s 连接在串 t 的末尾 ) 12. 若 x,y 是两个采用顺序结构存储的串, 编写一个比较两个串是否相等的函数 13. 若采用顺序结构存储串, 编写一个算法求串 s 中出现的第一个最长重复子串的下标 和长度 14. 若 s 是采用链表存储的串, 编写一个算法将其中的所有 c 替换成字符 'd' 15. 若 x,y 是两个采用链表结构存储的串, 编写一个算法找出 x 中第一个不在 y 中出现 的字符 实习 1. 实验目的掌握串存储结构的特点, 学会用串的基本操作解决实际问题 2. 实验内容 (1) 设计一个文学研究辅助程序, 统计小说中特定单词出现的频率和位置 (2) 编写一个算法, 将正文串 a[m] 中出现的模式串 p[n 1 ] 全部换成另一个串 q[n 2 ]

79 第 6 章二维数组和广义表 本章要点 : 二维数组压缩存储稀疏矩阵广义表 6.1 二维数组概念和定义 关于数组 (array) 的概念在学习程序设计语言时已经接触过, 我们把数组看做是存储于一个连续存储空间中的相同数据类型的数据元素的集合, 记作 A=(a 1,a 2,,a n ) 所以, 数组也是一种线性数据结构 如果我们把一维数组看成一个线性表, 那二维数组就可以看成是线性表的线性表, 也就是数组中元素的数据类型是定长的线性表 如图 6-1 所示, 二维数组可以用 m 行 n 列的矩阵表示, 记作 A m n, A m n a11 a12 L a1n a21 a22 L a2n = M M M am1 am2 a L mn 其中,a j =(a 1j,a 2j,,a mj )(1 j n) 称为数组的列矢量,a i =(a i1,a i2,,a in )(1 i m) 称为数组的行矢量 数组 A m n 共有 m n 个数组元素, 每一个元素 a[i][j](1 i m,1 j n) 同时处于第 i 个行矢量和第 j 个列矢量之中, 它在行的方向和列的方向各有一个直接前驱 a[i][j 1] 和 a[i 1][j], 各有一个直接后继 a[i][j+1] 和 a[i+1][j]( 如果有的话 ), 因此某一数组元素在数组中的位置需 由下标的二元组 [i][j] 惟一确定 二维以上的数组称为多维数组 多维数组实际上也是用一维数组实现的 n 维数组 a[m 1 ][m 2 ] [m n ] 中, 共有 m 1 m 2 m n 个数组元素, 每一个数组元素 a[i 1 ][i 2 ] [i n ](1 i 1 m 1,1 i 2 m 2,,1 i n m n ) 处于 n 个矢量之中, 其位置由下标的 n 元组 [i 1 ][i 2 ] [i n ] 惟 一确定

80 70 数据结构概论 6.2 二维数组的顺序存储结构 在实现数组的存储时, 通常是按各个数组元素的排列顺序, 顺次存放在一个连续的存储区域中 这种情况下, 由下标确定元素存储地址 ( 称为对元素的 存取 ) 非常简单, 从下标到存储地址的变换可由公式计算得出 变换公式取决于二维数组是按行存储还是按列存储, 如图 6-1 所示 图 6-1 二维数组的两种存储方式按行存储就是先存储第 1 行元素, 再存储第 2 行元素, 最后存储第 m 行元素 同一行元素按列号由小到大依次存储 下面以按行存储为例说明之 假设每个数据元素占 L 个存储单元, 则二维数组 A 中任一元素 a ij 的存储位置可由下式计算得出 loc(i, j)=loc(1, 1)+(ni+j)L (6-1) 式中,loc(1, 1) 是 a 11 的存储位置, 亦称二维数组的首地址 二维以上的多维数组按行存储是依照先变化元素的最后一个下标, 再变化其前一个下标的次序存储的 将式 (6-1) 推广, 可得到 n 维数组的数据元素存储位置的计算公式 : loc(j 1,j 2,,j n ) = loc(1, 1,, 1) + (b 2 b 3 b n j 1 +b 3 b 4 b n j 2 + +b n j n 1 +j n )L= 可以缩写成 loc(1, 1,, 1)+ n 1 n j b + j L i k n i= 1 k=+ i 1 其中,c n =L,c i 1 =b i c i,1<i n 6.3 矩阵的压缩存储 loc(j 1,j 2,,j n ) = loc(1, 1,, 1)+ ci j i (6-2) 在利用计算机解决工程领域及其他领域问题时, 常常会接触到 矩阵 这类对象 在高级语言中, 矩阵用二维数组表示, 如图 6-2, 矩阵 A 是一个 6 行 7 列的矩阵, 矩阵 B 是一个 7 行 5 列的矩阵 通常, 矩阵的行数和列数分别用 m 和 n 表示 n i= 1

81 第 6 章二维数组和广义表 71 A = B = (a) (b) 图 6-2 两个矩阵示例 概念在图 6-2 中的矩阵 A, 它有 6 7=42 个矩阵元素, 其中只有 9 个非零元素, 其他都是零元素, 我们称这样的矩阵为稀疏矩阵 ; 矩阵 B 中有许多值相同的元素和零元素 为了节省存储空间, 这两种矩阵都可以进行压缩存储 所谓压缩存储, 就是为多个值相同的元素只分配一个存储空间, 对零元素不分配空间 采用压缩存储可节省大量存储空间 特殊矩阵的压缩存储如果值相同的元素或者零元素在矩阵中的分布有一定的规律, 则称此类矩阵为特殊矩阵 例如, 下三角矩阵 A n n 中存在值等于零的元素, 这样的矩阵就可以采用压缩存储, 只存储 有效 元素, 而不存储零元素 A a a L 0 a 0 L n n= a31 a32 a33 L 0 M M M M a n1 an2 an3 a L nn 因为矩阵 A 的下三角部分只有 n(n+1)/2 个非零元素 ( 指元素可能不等于零 ), 所以可以用一维数组 B[n(n+1)/2] 存储 存储方式仍采用按行存储, 使 a 11 存于 B[0],a 21 和 a 22 分别存于 B[1] 和 B[2], 存储结构如图 6-3 所示 a 11 a 21 a 22 a 31 a n1 a nn k= n(n-1)/2 n(n+1)/2 图 6-3 下三角矩阵的压缩存储 一般地, 元素 a ij (1 j i n) 存于 B[k] 中, 存储公式为 k = i(i 1)/2+j 1 (6-3) 这种压缩存储方法同样适用于上三角形矩阵和对称矩阵 所谓上三角矩阵是指矩阵的下三角 ( 不包括对角线 ) 中的元素均为常数 c 或零的 n 阶矩

82 72 数据结构概论 阵 采用压缩存储只需存储其上三角中的元素和常数 c, 若元素 a ij (1 i j n) 存于 B[k] 中, 则存储公式为 k=j(j 1)/2+i 1 (6-4) 所谓 n 阶对称阵, 是指满足下述性质的矩阵, a ij = a ji 1 i j n 对于对称矩阵, 我们可以为每一对对称元素分配一个存储空间, 这样就可以把对称矩阵看成下 ( 或上 ) 三角形矩阵, 将 n 2 个元素压缩到 n(n+1)/2 个元素的空间中 数组下标和元素位置的对应关系参见式 (6-3) 和式 (6-4) 类似的特殊矩阵还有对角矩阵, 在对角矩阵中, 所有非零元素都集中在以主对角线为中心的带状区域中, 下面就是一个对角矩阵 A n n a11 a12 0 a21 a22 a23 a32 a33 a34 = O O O a n 1 n 0 ann 1 a nn 不论是哪一种特殊矩阵, 只要非零元素的分布遵循一定的规律, 都可以安排适当的存储机构, 只存储非零元素值, 而无须存储非零元素的下标, 导出类似于式 (6-3) 和式 (6-4) 的地址公式, 可以快速计算出任一非零元素的存储地址 ( 数组的下标 ) 稀疏矩阵的顺序存储表示和基本操作的实现 1. 概念和定义前面介绍过稀疏矩阵的概念, 实际上对于稀疏矩阵并没有确切的定义, 它只是一个凭人们的直觉了解的概念 如果一个矩阵的体积很大 ( 元素总数很多 ), 但是其中的非零元素极少, 绝大多数元素值为零, 而且非零元素分布也不一定有特定的规律 ( 如图 6-2(a)), 绝大多数元素值都相等的矩阵也被认为是稀疏矩阵 稀疏矩阵的抽象数据类型定义如下 ADT SparseMatrix 数据对象 a ij i= 1,2,,m;j=1,2,,n; a ij 为矩阵元素,m 和 n 分别为矩阵的行数和列数 数据关系 R=row, col row=<a ij,a i (j+1) > 1 i m,1 j n 1 col=<a ij,a (i+1) j > 1 i m 1,1 j n 基本操作 1 CreateSMatrix(&M) 操作结果创建稀疏矩阵 M 2 CopySMatrix(M, &T)

83 第 6 章二维数组和广义表 73 操作前提稀疏矩阵 M 存在 操作结果由稀疏矩阵 M 复制得到 T 3 AddSMatrix(M, N, &Q) 操作前提稀疏矩阵 M 和 N 的行数与列数对应相等 操作结果求稀疏矩阵的和 Q = M + N 4 SubSMatrix(M, N, &Q) 操作前提稀疏矩阵 M 和 N 的行数与列数对应相等 操作结果求稀疏矩阵的差 Q = M N 5 MulSMatrix(M, N, &Q) 操作前提稀疏矩阵 M 的列数等于 N 的行数 操作结果求稀疏矩阵乘积 Q = M N 6 TransposeSMatrix(M, &T) 操作前提稀疏矩阵 M 存在 操作结果求稀疏矩阵 M 的转置矩阵 T ADT SparseMatrix 2. 压缩表示在实际应用时, 需要处理的稀疏矩阵常常是很大的, 存储这样的矩阵一般采用压缩存储法 用压缩法存储稀疏矩阵 A m n 时, 不仅要存储非零元素值, 还要存储元素对应的下标, 这样可以用一个三元组 (row, col, val) 来惟一确定一个矩阵元素, 其中 row 为行号,col 为列号, val 为元素值 例如,a 24 =3.2 对应的三元组为 (2,4,3.2) 若稀疏矩阵 A 共有 t 个非零元素, 便对应 t 个三元组 将每个三元组看成是线性表的一个结点,t 个三元组构成长度为 t 的线性表 于是, 存储这个线性表, 也就相当于存储了稀疏矩阵 A 线性表的各种存储方式 ( 如顺序存储或链式存储 ) 都可以用来存储稀疏矩阵 为了便于查找, 稀疏矩阵可以按行顺序 ( 或按列顺序 ) 压缩存储, 如图 6-4 给出了按行顺序压缩存储的示例, 并由此得到矩阵 M 的三元组表 row col val M 4 5= (a) 稀疏矩阵 M (b) M 的三元组表 图 6-4 稀疏矩阵的三元组表表示 存储稀疏矩阵的三元组表的类型可定义如下

84 74 数据结构概论 #define Max 100 typedef struct int val; int row, col; Triple; typedef struct Triple data[max]; int m,n,len; TSMatrix; 3. 基本操作采用压缩顺序存储法, 矩阵的某些运算可以转换为有序表运算, 下面介绍两种 (1) 存取运算给定一组 合法 的下标 (i,j)(1 i m,1 j n), 确定元素 a ij 的存储地址 存取运算将对应于有序表的查找运算 算法思想用线性表的顺序查找算法, 在存储矩阵 A 的数组 B 中查找行列号 (row, col) 等于 (i,j) 的元素 若找到 ( 如下标为 k), 那么可从 B[k].val 中读取元素 a ij 的值 ; 若查找不成功, 则说明 a ij =0 算法实现 int AccessTSMatrix(TSMatrix A, int i, int j) int k; if((i<1) (i>a.m)) printf("i 值不合法 "); if((j<1) (j>a.n)) printf("j 值不合法 "); for(k=1;k<=a.len;k++) if((a.data[k].row==i)&&(a.data[k].col==j)) return(a.data[k].val); break; if(k==a.len) return(false); 下面的程序实现了从存储矩阵的数组中查找给定行号和列号的元素 main()

85 第 6 章二维数组和广义表 75 TSMatrix B; int i,r,c,v; B.len=5;B.m=5;B.n=5; printf("input TSMatrix B:\n"); for(i=0;i<5;i++) scanf("%d,%d,%d",&b.data[i].row,&b.data[i].col, &B.data [i]. val); printf("input access record place:\n"); scanf("%d,%d",&r,&c); v=accesstsmatrix(b,r,c); printf("the access data is:%d\n",v); 运行结果如下 (2) 稀疏矩阵的转置矩阵 A m n 的转置矩阵 A T n m 是把 A 的各行变成 A T 的各列而得到的一个矩阵 例如, 图 6-4(a) 的矩阵 M 4 5 的转置矩阵 T 5 4 如下 : T = 矩阵采用二维数组存储时, 实现矩阵转置的一般算法如下 void TransposeSMatrix(Ele_Type source[n][m],ele_type dest[m][n]) int i,j; for(i=0;i<m;i++) for(j=0;j<n;j++) dest[i][j]=source[j][i];

86 76 数据结构概论 若矩阵采用三元组表存储, 则上述转置算法就不能达到要求 因为原矩阵是按行顺序存储的, 要求转置后的矩阵也按行顺序存储 假设 a 和 b 是 TSMatrix 型的变量, 分别表示矩阵 M 及其转置矩阵 T, 那么如何由 a 得到 b 呢? 分析 a 和 b 之间的差异可见, 只要做到 :1 将矩阵的行列值相互交换 ;2 将三元组中的 i 和 j 相互调换 ;3 重排三元组之间的顺序 前两点很容易实现, 下面分析如何实现 3 row col val (a) data row col val (b) data 图 6-5 用三元组存储的矩阵的转置方法 1 因为 T 中的行序即是 M 的列序, 所以按照 M 的列序进行转置, 即可得到 b.data 中正确的行序 这就要对三元组表 a.data 从第一行起整个扫描 k 遍, 第一遍扫描 a.data 时, 逐个找出其中所有 col=1 的三元组, 转置后按顺序送到 b.data 中 ; 第二遍扫描时, 逐个找出其中所有 col=2 的三元组, 转置后按顺序送到 b.data 中 ; 直到第 k 遍扫描找出其中所有 col=k 的三元组, 转置后按顺序送到三元组表 b.data 中, 这里 1 k a.n 算法实现如下 TSMatrix *TransposeTSMatrix(TSMatrix a,tsmatrix *b) int i,j,k; b->m=a.n;b->n=a.m;b->len=a.len; if(b->len>0) j=1; for(k=1;k<=a.n;k++) for(i=1;i<=a.len;i++) if(a.data[i].col==k) b->data[j].row=a.data[i].col; b->data[j].col=a.data[i].row; b->data[j].val=a.data[i].val; j++;

87 第 6 章二维数组和广义表 77 return(b); main() TSMatrix A,*B; int i; A.len=5;A.m=5;A.n=5; printf("input TSMatrix A:\n"); for(i=1;i<=5;i++) scanf("%d,%d,%d\n",&a.data[i].row,&a.data[i].col, &A.data[i]. val); B=TransposeTSMatrix(A,B); printf("the Transpose TSMatrix is:\n"); for(i=1;i<=5;i++) printf("%d,%d,%d\n",b->data[i].row,b->data[i].col, B->data[i]. val); 运行结果如下 方法 2 按照 a.data 中三元组的次序进行转置, 并将转置后的三元组置入 b.data 中恰当位置 如果能求出 M 中每一列非零元素的个数, 就可以求出每一列的第一个非零元素在 b.data 中应有的位置 那么, 在对 a.data 中的三元组依次转置时, 便可直接放到 b.data 中应有的位置 可以附设两个矢量 num 和 cpot,num[col] 表示矩阵 M 中第 col 列中非零元素的个数, cpot[col] 指示 M 中第 col 列的第一个非零元素在 b.data 中的恰当位置 显然有 cpot[1]=1 cpot[col]=cpot[col-1]+num[col-1] 2 col a.n 对图 6-4(a) 矩阵 M 4 5,num[col] 和 cpot[col] 的值如表 6-1 该算法称为快速转置算法 表 6-1 矩阵 M 的 num[col] 和 cpot[col] 值 col

88 78 数据结构概论 num[col] cpot[col] 算法实现如下 TSMatrix *FastTransposeTSMatrix(TSMatrix a,tsmatrix *b) int col,t,i,j; int num[max],cpot[max]; b->len=a.len;b->n=a.m;b->m=a.n; if(b->len) for(col=1;col<=a.n;col++) num[col]=0; for(t=1;t<=a.len;t++) num[a.data[t].col]++; cpot[1]=1; for(col=2;col<a.n;col++) cpot[col]=cpot[col-1]+num[col-1]; for(i=1;i<=a.len;i++) col=a.data[i].col;j=cpot[col]; b->data[j].row=a.data[i].col; b->data[j].col=a.data[i].row; b->data[j].val=a.data[i].val; cpot[col]++; return(b); main() TSMatrix A,*B; int i; A.len=5;A.m=5;A.n=5; printf("input TSMatrix A:\n"); for(i=1;i<=5;i++) scanf("%d,%d,%d ",&A.data[i].row,&A.data[i].col, &A.data[i]. val); B=FastTransposeTSMatrix(A,B); printf("the Transpose TSMatrix is:\n"); for(i=1;i<=5;i++) printf("%d,%d,%d\n",b->data[i].row,b->data[i].col, B->data[i]. val);

89 第 6 章二维数组和广义表 79 运行结果如下 稀疏矩阵的链式存储表示和基本操作的实现当矩阵的非零元素个数和位置在操作过程中变化较大时, 不宜采用顺序存储结构表示三元组的线性表 例如, 在用三元组表存储的两个矩阵做加法运算时, 由于非零元素的插入和删除操作会引起表中大量元素的移动, 对于类似的操作, 稀疏矩阵采用另一种压缩存储形式 链式存储更为合适 通常, 存储稀疏矩阵的链表是十字链表 ( 也称为正交链表 ), 链表的每个结点代表矩阵中的一个非零元素, 包含有行号 列号 值域和行链域 (right) 和列链域 (down)( 如图 6-6(a)) 等信息 其中, 行链域用于链接同一行中的下一个非零元素, 列链域用于链接同一列中下一个非零元素 每个非零元素既是某个行链表中的一个结点, 又是某个列链表中的一个结点, 整个矩阵构成一个十字交叉的链表, 可用两个分别存储行链表的头指针和列链表的头指针来表示 例如, 图 6-4(a) 矩阵 M 的十字链表如图 6-6(b) 所示 图 6-6 稀疏矩阵 M 4 5 输入矩阵的非零元素, 建立十字链表, 并按行方式打印该十字链表的完整程序如下 struct MatNode /* 定义十字链表结点 */

90 80 数据结构概论 ; int row,col; struct MatNode *right,*down; union int val; struct MatNode *next; tag; struct MatNode *CreateMat() int m,n,t,s,i,r,c,v; struct MatNode *h[100],*p,*q; /* h[] 为十字链表每行的表头指针数组 */ printf(" 行数 m, 列数 n, 非零元素个数 t:"); scanf("%d,%d,%d",&m,&n,&t); p=(struct MatNode *)malloc(sizeof(struct MatNode)); h[0]=p; p->row=m; p->col=n; s=m>n? m:n; for(i=1;i<=s;i++) p=(struct MatNode *)malloc(sizeof(struct MatNode)); h[i]=p; h[i-1]->tag.next=p; p->row=p->col=0; p->down=p->right=p; h[s]->tag.next=h[0]; for(i=1;i<=t;i++) /* t 为非零元素的个数 */ printf("\t 第 %d 个元素 ( 行号 m, 列号 n, 值 v):",i); scanf("%d,%d,%d",&r,&c,&v); p=(struct MatNode *)malloc(sizeof(struct MatNode)); p->row=r; p->col=c; p->tag.val=v; q=h[r]; while(q->right!=h[r]&&q->right->col<c) q=q->right; p->right=q->right; q->right=p; q=h[c]; while(q->down!=h[c]&&q->down->row<r) q=q->down; p->down=q->down;

91 第 6 章二维数组和广义表 81 return(h[0]); void prmat(struct MatNode *hm) struct MatNode *p,*q; printf("\n 按行表输出矩阵元素 :\n"); printf("row=%d col=%d\n",hm->row,hm->col); p=hm->tag.next; while(p!=hm) q=p->right; while(p!=q) printf("\t %d,%d,%d\n",q->row,q->col,q->tag.val); q=q->right; p=p->tag.next; main() struct MatNode *hm; hm=createmat(); /* 创建十字链表 */ prmat(hm); /* 输出十字链表 */ 运行结果如下

92 82 数据结构概论 6.4 广义表的概念和定义 前面介绍的线性表 链表 栈和队列等数据结构都是线性结构, 结构中的元素都是同一类型的数据元素 下面介绍的广义表则放宽了对表中元素的限制, 允许表中元素自身具有某种结构 一个广义表 ( 也称列表 )LS 定义为 n(n 1) 个表元素 a 1,a 2,,a n 组成的有限序列 记作 LS = (a 1,a 2,,a n ) 其中,LS 为表名 ;a i (1 i n) 为表中元素, 它或者是数据元素 ( 称为原子 ), 或者是子表 ; N 为表的长度, 即表中元素的个数 长度为 0 的表为空表 习惯上, 用大写字母表示广义表的名称, 用小写字母表示原子 当广义表 LS 非空时, 称第一个元素 a 1 为 LS 的表头 (head), 称其余元素组成的表 (a 2,a 3,,a n ) 是 LS 的表尾 (tail) 广义表的定义是递归的, 因为在表的描述中还会用到表, 允许表中有表 这种递归的定义能够简捷地描述庞大而复杂的结构 下面我们给出几个广义表的例子 (1)A=( ) A 是一个空表, 表的长度为 0 它没有表头和表尾 (2)B=(8,4) B 是一个只包括原子的表, 表的长度为 2, 表头为 8, 表尾为 (4); 表尾还是一个表, 其表头为 4, 其表尾为空表 ( ) (3)C=(a,(5,3,x)) C 是一个长度为 2 的表, 其表头为 a, 表尾为 ((5,3,x)); 表尾仍然是一个表, 其表头为 (5,3,x), 表尾为空表 ( ) (4)D=(B,C,A) D 是一个长度为 3 的表, 其 3 个表元素都是子表, 表头为 B, 表尾为 (C,A) (5)E=(4,E) E 是一个长度为 2 的表, 表头为 4, 表尾为 (E); 这个表尾还是一个表, 其表头为 E, 表尾为空表 ( ) 对于表头来说, 出现了递归 由广义表的定义, 可得出广义表的性质如下 有序性 在广义表中, 各表元素在表中以线性序列排列, 每个表元素至多有一个直接前驱, 一个直接后继 有长度 广义表中表元素的个数是一定的, 不能是无限的, 但可以是空表 有深度 广义表的表元素可以是原表的子表, 子表的表元素还可以是子表, 因此, 广义表是多层次结构 表中括号的重数即为深度 可递归 广义表本身可以是自己的子表, 上面的表 E 就是一个递归的表 可共享 广义表可以为其他广义表共享 例如, 在表 D 中, 子表 B,C,A 即为此情况 它们在表 D 中通过子表名称引用, 而不必列出子表的值 注意广义表 () 和 (()) 不同, 前者是空表, 长度为 0; 后者长度为 1, 可分解得到其表头 和表尾均为空表 () 6.5 广义表的操作和链式存储结构 我们先介绍有关广义表 ( 也称为列表 ) 的操作

93 第 6 章二维数组和广义表 求广义表的表头操作 Head(LS) 功能若 LS 非空, 则返回 LS 的第一个元素的值, 否则函数没有定义 2. 求广义表的表尾操作 Tail(LS) 功能若 LS 非空, 则返回 LS 除第一个元素以外其他元素组成的表, 否则函数没有定义 例如, Head(B)=8 Tail(B)=(4) Head(C)=a Tail(C)=((5, 3, x)) 3. 求广义表的第一个元素操作 First(List) 功能返回表 List 的第一个元素 ( 若 List 空, 则返回一个特定的空值 NULL) 4. 求直接后继操作 Next(elem) 功能返回表元素 elem 的直接后继元素 下面介绍广义表的存储 由于广义表 LS 中的数据元素可以具有不同的结构 ( 或者是原子, 或是列表 ), 因此很难用顺序存储结构表示 通常, 广义表采用链式存储结构, 每个数据元素用一个结点表示 结点有两种, 一种是表结点, 用于表示广义表 ; 一种是原子结点, 用于表示原子 表结点由 3 个域组成 : 标志域 指示表头的指针域和指示表尾的指针域 原子结点有两个域 : 标志域和值域 ( 如图 6-7) 图 6-7 广义表的结点结构 广义表形式定义说明如下 typedef enumatom,list Elem_Tag typedef struct GLNode Elem_Tag tag; union Atom_Type atom; structstruct GLNode *hp,*tp;htp; atom_htp; *GList; 上述广义表 A,B,C,D,E 的存储结构如图 6-8 所示 广义表的这种存储表示的特点如下 除空表的表头指针为空外, 对任何非空列表, 其表头指针均指向一个表结点, 且该结 点中的 hp 域指示列表表头,tp 域指向列表表尾

94 84 数据结构概论 图 6-8 广义表的存储结构示例 容易分清楚列表中原子和子表所在层次 最高层的表结点个数即为列表的长度 广义表还有另外一种存储结构, 在该结构中每个结点都由 3 个域组成 (1) 标志域 tag tag=0 为原子结点,tag=1 为表结点 (2) 头指针域 / 值域若 tag=0, 该域为原子结点的值域 ; 若 tag=1, 该域为指向子表表头的指针 (3) 尾指针域 tp 指向同一层的下一个表结点 结点结构如图 6-9 该存储结构的形式说明如下 图 6-9 广义表的另一种结点结构 typedef enumatom,list Elem_Tag typedef struct GLNode Elem_Tag tag; union Atom_Type atom; struct GLNode *hp; atom_htp; struct GLNode *tp *GList; 广义表 A,B,C,D,E 的第二种存储结构如图 6-10 所示

95 第 6 章二维数组和广义表 85 图 6-10 广义表的另一种存储结构示例 小结 数组和广义表可以看成是一种扩展的线性数据结构, 其特殊性不像栈和队列那样表现在对数据元素的操作受限制, 而是反映在数据元素的构成上 在线性表中, 每个数据元素都是不可再分的原子类型 ; 而数组和广义表可以推广到是一种具有特定结构的数据 本章以抽象数据类型的形式讨论数组和广义表的定义和实现 二维数组 A m n =(a 1,a 2,,a n ) 可以看成是线性表的线性表, 其中 a j (1 j n) 称为列矢量,a j =(a 1j,a 2j,,a mj ) 同样, 数组 A m n 还可以看成是由行矢量构成的线性表,B=(b 1,b 2,,b m ), 其中 b i (1 i m) 称为矩阵的行矢量 对于数组, 一般采用顺序存储结构, 有按行存储和按列存储两种 本章中我们以按行存储为例给出了二维数组中任一元素 a ij 的存储位置与其下标的对应关系, 可由下面公式计算得出 : loc(i, j)=loc(1, 1)+(ni+j)L 通常, 矩阵采用二维数组来表示, 有些特殊矩阵 ( 如三角矩阵, 对角矩阵等 ) 可以采用压缩存储, 即为多个值相同的元素只分配一个存储空间, 对零元素不分配存储空间 这样可以有效地节省存储空间 例如, 可以将三角矩阵 A n n 按行序存储到一个大小为 n(n+1)/2 的一维数组中, 其元素 a ij 在数组中的存储位置为 loc(i, j)= loc(1, 1)+i(i 1)/2+j 1 i j loc(i, j)= loc(1, 1)+j(j 1)/2+i 1 i<j 还有一类矩阵可以看做稀疏矩阵, 在这种矩阵中非零元素的个数很少, 对于这种矩阵,

96 86 数据结构概论 我们可以采用三元组表存储, 只存储非零元素的信息 ( 行号 列号 值 ) 同样, 它也有两种存储方式 按行存储和按列存储 如果把三元组按行序用一维数组进行存放, 矩阵的某些运算就可以转换为有序表的运算 本章中我们讨论了稀疏矩阵的存取和转置运算的算法 用三元组表表示的稀疏矩阵不仅节约了空间, 而且使矩阵的某些运算的运算时间减少 但在进行矩阵加法 减法和乘法运算时, 会引起大量元素的移动, 浪费时间 因此, 我们引入了稀疏矩阵的链式存储法 十字链表 在十字链表中, 矩阵的每个元素用一个结点表示, 同一行的元素通过行链域连接成一个单链表, 同一列的元素通过列链域连接成一个单链表 这样元素在移动位置或插入 删除元素时, 只需修改结点的行链域和列链域指针即可 本章中, 我们还介绍了广义表的概念 一个广义表 LS 定义为 n(n 1) 个表元素 a 1,a 2,, a n 组成的有限序列 记作 LS=(a 1,a 2,,a n ) 广义表不同于前面介绍的线性表 队列 栈等线性结构, 广义表中的元素可以具有不同的结构, 或是原子, 或是列表 因此, 难以用顺序存储结构表示广义表, 通常采用链式存储结构 本章中给出了广义表的两种链式存储结构 习题 1. 数组 A[1 5,1 6] 的每个元素占 5 个单元, 将其按行优先次序存储在起始地址为 1000 的连续的内存单元中, 则元素 A[5,5] 的地址为 A.1140 B.1145 C.1120 D 常对数组进行的两种基本操作是 A. 建立和删除 B. 索引和修改 C. 查找和修改 D. 查找和索引 3. 二维数组 M 的元素是 4 个字符 ( 每个字符占一个存储单元 ) 组成的串, 行下标 i 的范 围从 0 到 4, 列下标 j 的范围从 0 到 5,M 按行优先顺序存储, 则元素 M[3][5] 的起始地址与 M 按列优先顺序存储时元素 的起始地址相同 A.M[2][4] B.M[3][4] C.M[3][5] D.M[4][4] 4. 稀疏矩阵一般的压缩存储方法有两种, 即 A. 二维数组和三维数组 B. 三元组和散列 C. 三元组和十字链表 D. 散列和十字链表 5. 数组 A 中, 每个元素的长度为 3 个字节, 行下标 i 从 1 到 8, 列下标 j 从 1 到 10, 从 首地址 SA 开始连续存放在存储器内, 该数组按行存放时, 元素 A[5][8] 的起始地址为 A.SA+141 B.SA+180 C.SA+222 D.SA 用十字链表表示一个有 k 个非零元素的 m n 的稀疏矩阵, 则其总结点数为 A.m n B.m n +m+n C.k+max(m, n)+1 D.k+m+n+1 7. 广义表 A=(a,b,(c,d),(e,(f,g))), 则式子 Head(Tail(Head(Tail(Tail(A))))) 的值为

97 第 6 章二维数组和广义表 87 A.(g) B.(d) C.c D.d 8. 广义表 ((a),a) 的表头是, 表尾是 A.a B.b C.(a) D.((a)) 9. 广义表 ((a,b),c,d) 的表头是, 表尾是 A.a B.b C.(a, b) D.(c, d) 10. 对矩阵采用压缩存储是为了 11. 一个 10 阶对称矩阵 A, 采用压缩存储方式 ( 以行序为主存储, 且 A[0][0]=1), 则 A [8][5] 的地址是 12. 已知广义表 A=(p,h,w), B =(b,k,p,h), C =((a,b),(c,d)), 试写出下列广义 表操作的结果 (1)Head(A)= (2)Tail(B)= (3)Head(C)= (4)Tail(C)= (5)Head(Tail(C))= (6)Tail(Head(C))= (7)Head(Tail(Head(C)))= 13. 广义表 ((a),((b),c),((d))) 的长度是 14. 假设按行下标优先存储整数数组 A 15 8 时, 第一个元素的字节地址是 100, 每个数占 4 个字节 求下列元素的存储地址是多少? (1)a 00 (2)a 11 (3)a 25 (4)a 写出下列矩阵对应的三元组 画出用十字链表表示的上题中的矩阵, 结点结构如下 : row col val collink rowlink 17. 画出广义表 B=(a,(b,(c))) 的链式存储结构图 18. 假设稀疏矩阵 A 和 B( 具有相同的大小 m n) 都采用三元组表示, 编写一个函数计算 C=A+B, 要求 C 也用三元组表示 19. 对于二维数组 A[m][n], 其中 m 50,n 50, 先读入 m 和 n, 然后读该数组的全部元素, 对如下 3 种情况分别编写相应的算法 (1) 求数组 A 靠边元素之和 (2) 求从 A[0][0] 开始的互不相邻的各元素之和 (3) 若 m=n, 分别求两条对角线元素之和

98 88 数据结构概论 20. 假设稀疏矩阵 A 采用三元组表示, 编写一个函数求其转置矩阵 B, 要求 B 也采用三元组表示 21. 假设稀疏矩阵 A 和 B 都采用十字链表存储, 编写一个函数计算 C=A+B, 要求 C 也用十字链表存储 实习 1. 实验目的 (1) 掌握数组结构的特点 ; 掌握数组元素存储位置与下标的对应关系 ; 并会用数组结构解决遇到的实际问题 ; (2) 加深理解如何用三元组存储稀疏矩阵 2. 实验内容 (1)n 只猴子要选猴王, 选举办法如下 : 所有猴子按 1,2,,n 编号围坐一圈, 从第 1 号开始按 1,2,,m 报数, 凡报 m 号的退出到圈外, 如此循环报数, 直到圈内剩下一只猴子时, 这只猴子就是大王 n 和 m 由键盘键入, 打印出最后剩下的猴子号 编写算法实现上述函数 (2) 三元组顺序表的一种变型是, 从三元组顺序表中去掉行下标域得到二元组顺序表, 另设一个行起始矢量, 其每个分量是二元组顺序表的一个下标值, 指示该行中第一个非零元素在二元组顺序表中的起始位置 试编写一算法, 由矩阵元素的下标值 i,j 求矩阵元素, 讨论这种方法和三元组顺序表相比有什么优缺点

99 第 7 章树与二叉树 本章要点 : 树型结构的基本概念二叉树树与森林哈夫曼树与哈夫曼编码 7.1 树的概念 定义树 (tree) 是由 n(n 0) 个结点组成的有限集合 T, 当 T 非空时满足 : 1) 有且仅有一个特定的结点, 称为根结点 2) 其余的结点可分为 m(m 0) 个互不相交的有限集合 T 0,T 1,T 2,,T m 1, 其中每个集合又都是一棵树 树 T 0,T 1,T 2,,T m 1 都称为根结点的子树 (subtree) 树的定义是递归的, 在树的定义中又会用到树的概念 它刻画了树的固有特性, 即一棵树由若干棵子树构成, 而子树又由更小的若干棵子树构成, 依此类推 只包括一个结点的树必然由根结点构成 特别地, 可以允许不包括任何结点的树, 称之为空树 树是一种非线性数据结构, 具有以下特点 : 它的每一结点都可以有不止一个直接后继 ; 除根以外的所有结点, 都有且只有一个直接前驱 ; 这些数据结点按分支关系组织起来, 清晰地反映了数据元素之间的层次关系 可以看出, 数据元素之间存在的关系是一对多, 或者多对一的关系, 与前面讨论的线性表 栈 队和数组等线性结构有很大差别 图 7-1 是一棵有 10 个结点的树 其中,A 是根结点, 其余结点分成 4 个互不相交的子集 :T 1 =B,T 2 =C,F, G,I,J,T 3 =D,H,T 4 =E 其中,T 1,T 2,T 3 和 T 4 都是根 A 的子树, 且本身也是一棵树 对于子树 T 2, 其根 为 C, 其余结点分为两个互不相交的子集 :T 21 =F,T 22 =G, I,J,T 21 和 T 22 都是 C 的子树 T 21 中,F 是根, 其余结 点为空, 没有子树 T 22 的根是 G, 其余结点成为子集 I, J 图 7-1 树

100 90 数据结构概论 从图 7-1 中还可以看到, 树根结点 A 没有直接前驱结点, 其他结点都有一个惟一的直接前驱结点, 如 B,C,D 和 E 的直接前驱为 A;F 和 G 的直接前驱为 C 等 每一结点可以有多个直接后继结点, 如 A 的直接后继有 B,C,D,E 4 个结点,C 的直接后继有 F 和 G 两个结点, 而 B,E,F,H,I 和 J 没有直接后继结点 图中清晰地反映了数据元素间的层次关系,A 在第一层,B,C,D 和 E 同处在 A 的下一层 树的基本操作如下 (1)Create(T) 操作结果构造一棵树 (2)Root(T) 操作前提树 T 已经存在 操作结果如果树 T 为空, 返回 FALSE; 否则, 取树的根结点 (3)Parent(T, x) 操作前提树 T 已经存在 操作结果如果树 T 为空, 或结点 x 不存在, 返回 FALSE; 否则, 取结点 x 的父结点 (4)Child(T, x, i) 操作前提树 T 已经存在 操作结果如果树 T 为空, 或结点 x 不存在, 或结点 x 不存在第 i 个孩子结点, 返回 FALSE; 否则, 返回结点 x 的第 i 个孩子结点 (5)Traversal(T) 操作前提树 T 已经存在 操作结果如果树 T 为空, 返回 FALSE; 否则, 对树进行遍历 (6)Clear(T) 操作前提树 T 已经存在 操作结果如果树 T 为空, 返回 FALSE; 否则, 将树 T 置为空树 表示方法树除了图 7-1 的树型表示方法外, 还有以下几种表示方法 1. 广义表表示法根作为由子树森林组成的表的名字写在表的左边, 如图 7-2(a) 所示 2. 集合图表示法每棵树对应一个圆, 圆内包含根结点和子树的圆, 同一个根结点下的各子树对应的圆是不能相交的, 如图 7-2(b) 所示 3. 凹入表示法每棵树的根对应于一个长方块, 子树的根对应于一个较短的长方块, 且树根在上, 子树的根在下, 同一个根下的各子树的根对应的长方块的长度是一样的, 如图 7-2(c) 所示

101 第 7 章树与二叉树 91 图 7-2 树的其他三种表示法 基本概念和常用术语本节结合图 7-1 所示树介绍关于树的基本概念和常用术语 1. 结点 结点的度和树的度树的结点包含了一个数据元素及若干指向其子树的分支 某个结点的子树的个数称为该结点的度 树的度是树中各结点度的最大值 例如, 在图 7-1 所示的树中, 结点 A 的度为 4,D 的度为 1,C 和 G 的度为 2, 其余结点的度都为 0 因结点度的最大值为 4, 所以树的度为 4 2. 分支结点和叶子结点度为 0 的结点称为叶子结点或终端结点 度不为 0 的结点称为非终端结点, 又称为分支结点 在分支结点中, 每个结点的分支数就是该结点的度 如在图 7-1 所示的树中,B,E,F, H,I 和 J 都是叶子结点,A,C,D 和 G 都是分支结点 3. 孩子结点和双亲结点为了形象地描绘树型结构中结点的关系, 我们常借用家族关系的称谓作为树的描述性语句 在一棵树中, 每个结点的各子树的根, 称为该结点的孩子 (child), 相应地, 该结点被称为孩子结点的双亲 (parent) 具有同一双亲的孩子称为兄弟(sibling) 进一步延伸这些关系, 可以把每一个结点的所有子树中的结点称为该结点的子孙, 从树根结点到达该结点的路径上经过的所有结点称为该结点的祖先 如在图 7-1 所示的树中,A 是 B,C,D,E 的双亲, 则 B, C,D,E 就是 A 的孩子 ; 而 C 为 F,G 的双亲 ;F,G 互为兄弟 由孩子结点和双亲结点的定义可知, 在一棵树中, 根结点没有双亲结点, 叶子结点没有孩子结点, 其余结点既有双亲结点也有孩子结点 4. 结点的层数树既是一种递归结构, 也是一种层次结构, 树中的每个结点都处在一定的层数上 结点的层数从树根开始定义, 根结点为第 1 层, 其余结点的层数等于其双亲结点的层数加 1 如在图 7-1 所示的树中,A 结点处于第 1 层 ;B,C,D,E 在第 2 层 ;F,G,H 在第 3 层 ;I,J 在第 4 层 5. 树的深度树中结点的最大层数为树的深度 ( 或称为高度 ) 在图 7-1 中, 树中结点的最大层数为 4,

102 92 数据结构概论 所以该树的深度为 4 6. 路径与路径长度从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径, 路径上的分支数目称为路径长度 显然, 从树的根结点到树中其余结点均存在一条路径 例如, 在图 7-1 所示的树中, 结点 A 到 I 有一条路径 (A,C,G,I), 长度为 3, 结点 F 到 G 之间不存在任何路径 7. 有序树和无序树若树中各结点的子树按照一定的次序从左向右排列, 且此顺序不能随意变换, 则称该树为有序树, 否则称为无序树 8. 森林森林 (forest) 是 m(m 0) 个互不相交的树的集合 森林的概念与树的概念十分相近, 因为只要把树的根结点删除, 它就成了森林 如在图 7-1 所示的树中, 若删除根结点 A, 就可得到由根结点为 B,C,D 和 E 的 4 棵树组成的森林 反之, 只要给 n 棵独立的树加上一个结点, 并把这 n 棵树作为该结点的子树, 则森林就变成了树 7.2 二叉树 二叉树是树型结构的一个重要类型, 许多实际问题抽象出来的数据结构往往是二叉树的 形式 即使是一般的树, 也能够简单地转换为二叉树, 而且二叉树的存储结构及其算法都比 较简单, 因此二叉树就显得特别的重要 概念和定义 二叉树 (binary tree) 是 n(n 0) 个结点的有限集, 它或者是空集 (n=0); 或者是由一 个根结点及至多两棵互不相交的 分别称为这个根的左子树和右子树的二叉树组成, 且左 右子树的位置不能互换 这也是一个递归定义 二叉树可以是空集, 因此根可以具有空的左子树或右子树, 或者左 右子树皆为空 由此可知, 二叉树有 5 种基本形态, 如图 7-3 所示 图 7-3 二叉树的 5 种基本形态

103 第 7 章树与二叉树 93 二叉树中, 每个结点最多只能有两棵子树, 并且有左 右之分 显然, 它与无序树不同 实际上, 它与度数为 2 的有序树也不相同 这是因为有序树中, 虽然一个结点的孩子之间是有左 右次序的, 但若该结点只有一个孩子, 就无需区分其左 右次序 而在二叉树中, 即使只有一个孩子也有左 右之分 例如, 图 7-3(c) 和 (d) 是两棵不同的二叉树 性质二叉树具有以下重要性质 性质 1 二叉树第 i 层上的结点数目最多为 2 i 1 (i 1) 证明用数学归纳法可以进行证明 归纳基础 i=1 时, 有 2 i 1 =2 0 =1 因为第 1 层上只有一个根结点, 所以命题成立 归纳假设假设对所有的 k(1 k<i) 命题成立, 即第 k 层上至多有 2 k 1 个结点, 证明 k=i 时命题亦成立 归纳步骤根据归纳假设, 第 i 1 层上至多有 2 (i 1) 1 个结点 由于二叉树的每个结点至多有两个孩子, 故第 i 层上的结点数至多是第 i 1 层上的最大结点数的 2 倍, 即 k=i 时, 该层上至多有 2 2 (i 1) 1 =2 i 1 个结点, 故命题成立 性质 2 深度为 k 的二叉树至多有 2 k 1 个结点 (k 1) 证明在具有相同深度的二叉树中, 仅当每一层都含有最大结点数时其树中的结点数最多 因此, 利用性质 1 可得, 深度为 k 的二叉树的结点数至多为 k 1 =2 k 1 故命题成立 满二叉树和完全二叉树是二叉树的两种特殊情形 一棵深度为 k 且有 2 k 1 个结点的二叉树称为满二叉树 (full binary tree) 图 7-4(a) 是一个深度为 3 的满二叉树 满二叉树的特点是每一层上的结点数都达到最大值, 即对给定的高度, 它是具有最多结点数的二叉树 满二叉树中不存在度数为 1 的结点, 每个分支结点均有两棵高度相同的子树, 且叶子结点都在最下一层上 深度为 k 有 n 个结点的二叉树, 当且仅当其每个结点都与深度为 k 的满二叉树中编号为 1~n 的结点一一对应时, 此二叉树称为完全二叉树 (complete binary tree) 图 7-4(b) 是一棵完全二叉树 显然, 满二叉树是完全二叉树, 但完全二叉树不一定是满二叉树 因此, 在完全二叉树中, 如果某个结点没有左孩子, 则它一定没有右孩子, 即该结点必是叶子结点 例如, 图 7-4(c) 不是一棵完全二叉树, 因为结点 3 没有左孩子, 而有右孩子结点 6 图 7-4 特殊形态的二叉树

104 94 数据结构概论 性质 3 具有 n 个结点的完全二叉树的深度为 lb n + 1 证明设二叉树的深度为 k, 根据完全二叉树的定义可知, 2 k 1 1<n 2 k 1 即 2 k 1 n<2 k 于是有 k 1<lb n<k 由于 k 是整数, 所以得到 k = lb n + 1 性质 4 对于具有 n 个结点的完全二叉树, 如果按照从上到下, 从左到右的顺序对树中所有结点进行编号, 则对编号为 i(1 i n) 的任意结点, 有 : (1) 若 i>1, 则结点 i 的双亲编号为 i /2 ; 若 i=1, 则 i 是根结点, 没有双亲 (2) 若 2i n, 则结点 i 的左孩子的编号是 2i; 否则,i 无左孩子, 即 i 必定是叶子结点 (3) 若 2i+1 n, 则结点 i 的右孩子的编号是 2i+1; 否则,i 无右孩子 性质 4 可由完全二叉树的定义及上述前 3 个性质加以证明, 在此不做证明 存储结构 1. 顺序存储结构该方法是把二叉树的所有结点按照一定的顺序存储到一组连续的存储单元中 可以把结点安排成一个线性序列, 使得结点在这个序列中的相互位置能够反映出树结点之间的相互逻辑关系 例如, 可以将图 7-4(b) 完全二叉树的 6 个结点按顺序编号用一维数组 btree[7] 来存放, btree[0] 不用或用来存储结点数目, 如图 7-5(a) 所示 完全二叉树中除最下面一层外, 每层都充满了结点, 每一层的结点个数恰好是上一层个数的 2 倍 因此, 可以通过 btree[ i /2 ] 来求出结点的双亲 ; 通过 btree[2i](2i n) 来求结点 i 的左孩子 ; 通过 btree[2i+1](2i+1 n) 来求出结点 i 的右孩子 例如, 图 7-5 所示的完全二叉树的顺序存储结构,btree[2] 的双亲, 左 右孩子分别是 btree[1],btree[4] 和 btree[5] 完全二叉树使用顺序存储结构是十分理想的, 它既压缩了存储空间, 又可以方便地检索父结点和左 右孩子结点 本书第 9 章所讲述的堆排序就是使用这种存储结构进行完全二叉树的存储 但是, 对于普通的二叉树而言, 也要按完全二叉树的形式来存储, 如图 7-5(b) 为图 7-4(c) 的存储结构, 可见采用该存储方式会造成空间的极大浪费 图 7-5 二叉树的顺序存储结构 2. 链式存储结构二叉树也可以使用链式存储结构 根据二叉树的定义, 可以设计 3 个域来分别存放二叉树的结点数据和左 右孩子指针 如图 7-6 所示, 其中 data 表示数据域,lchild 和 rchild 分别表示指向左 右孩子结点的指针 相应的类型说明如下

105 第 7 章树与二叉树 95 图 7-6 二叉树的链式存储结点结构 struct tree struct tree *lchild; /* 左孩子指针 */ int data; /* 数据元素类型 */ struct tree *rchild; /* 右孩子指针 */ ; typedef struct tree treenode; typedef treenode *bintree; 遍历二叉树的遍历 (traversal) 是指按某种特定的搜索路径访问二叉树的所有结点, 并且每个结点只被访问一次 访问是指对结点所做的某些处理操作 例如, 输出结点的值, 计算树的高度等 由二叉树的递归定义可知, 对二叉树的遍历实际上是对根结点 左子树和右子树三部分的依次遍历 若分别用 D,L,R 来表示对根结点 左子树和右子树的遍历, 则通常用 3 种方式进行二叉树的遍历, 即 DLR 先序 ( 根 ) 遍历 (preorder traversal), LDR 中序 ( 根 ) 遍历 (inorder traversal) 和 LRD 后序 ( 根 ) 遍历 (postorder traversal) 对二叉树做先序 ( 中序 后序 ) 遍历, 按访问的先后顺序把结点排成一个序列, 则称这个序列为先序 ( 中序 后序 ) 序列 二叉树的 3 种遍历的递归定义如下 先序遍历二叉树操作的定义如下 如果二叉树为空, 则不做任何操作, 否则, D: 访问根结点 ; L: 先序遍历左子树 ; R: 先序遍历右子树 中序遍历二叉树操作的定义如下 如果二叉树为空, 则不做任何操作, 否则, L: 中序遍历左子树 ; D: 访问根结点 ; R: 中序遍历右子树 后序遍历二叉树操作的定义如下 L: 后序遍历左子树 ; R: 后序遍历右子树 ; D: 访问根结点 若二叉树使用 节中定义的链式存储结构进行存储, 则先序遍历二叉树的递归算法如算法 7-1 所示 算法 7-1 先序遍历二叉树 /* 先序遍历二叉树的算法实现 */

106 96 数据结构概论 #include <stdio.h> #include <stdlib.h> /* 树结点结构定义 */ struct tree struct tree *lchild; int data; struct tree *rchild; ; typedef struct tree treenode; typedef treenode *bintree; /* 向二叉树中插入新结点 */ bintree insert_node(bintree root,int node) bintree newnode; bintree currentnode; bintree parentnode; newnode=(bintree)malloc(sizeof(treenode)); newnode->data=node; newnode->rchild=null; newnode->lchild=null; /* 空树, 新结点成为根结点 */ if(root==null) return newnode; else /* 插入孩子结点 */ currentnode=root; while(currentnode!=null) parentnode=currentnode; if(currentnode->data>node) currentnode=currentnode->lchild; else currentnode=currentnode->rchild; /* 新结点值比当前结点值小, 则插入后成为当前结点的左孩子 ; 否则, 插入后成为当前结点的右孩子 */ if(parentnode->data>node) parentnode->lchild=newnode; else parentnode->rchild=newnode; return root; /* 利用插入算法创建二叉树 */

107 第 7 章树与二叉树 97 bintree create_tree(int data[],int len) bintree root; int i; root=null; for(i=0;i<len;i++) root=insert_node(root,data[i]); return root; /* 先序遍历二叉树的递归算法实现 */ void preorder(bintree point) if(point!=null) printf("%d", point->data); preorder(point->lchild); preorder(point->rchild); /* 调试算法所用主程序 */ void main() bintree root=null; int i,index; int value; int nodelist[20]; clrscr(); printf("\npleaseinputtheelementsofbinarytree(exitfor0):\n\n"); index=0; /* 输入树的结点 */ scanf("%d",&value); while(value!=0) nodelist[index]=value; index++; scanf("%d",&value); /* 生成树 */ root=create_tree(nodelist,index); printf("\n\n The preorder reaversal result is: ["); /* 先序遍历 */ preorder(root); printf("]\n\n"); getch(); 运行结果如下

108 98 数据结构概论 中序遍历和后序遍历的算法类似, 程序实现如下 算法 7-2 中序遍历二叉树 /* 中序遍历二叉树递归算法 */ void inorder(bintree point) if(point!=null) inorder(point->lchild); printf("%d ",point->data); inorder(point->rchild); 算法 7-3 后序遍历二叉树 /* 后序遍历二叉树递归算法 */ void postorder(bintree point) if(point!=null) postorder(point->lchild); postorder(point->rchild); printf("%d ",point->data); 例如对图 7-7 给出的二叉树进行遍历, 可以得到该二叉树的先序序列为 ABDECFG; 中序 序列为 DBEACGF; 后序序列为 DEBGFCA 图 7-7 二叉树

109 第 7 章树与二叉树 二叉树的线索化用二叉链表作为二叉树的存储结构时, 因为每个结点中只有指向其左 右孩子结点的指针域, 所以从任一结点出发只能直接找到该结点的左 右孩子, 而一般情况下无法直接找到该结点在某种遍历序列中的前驱和后继结点信息, 这种信息只能通过遍历动态地得到 为此, 若在每个结点中增加两个指针域来存放遍历时的前驱和后继信息, 将大大降低存储空间的利用率 因为在 n 个结点的二叉链表中含有 n+1 个空指针域, 所以可以利用这些空指针域, 存放指向结点在某种遍历次序下的前驱和后继结点的指针, 这种附加的指针称为线索, 加上了线索的二叉链表称为线索链表, 相应的二叉树称为线索二叉树 (threaded binary tree) 为了区分一个结点的指针域是指向其孩子指针, 还是指向其前驱或后继的线索, 可在每个结点中增加两个标志域 这样, 线索链表中的结点结构变为 lchild ltag data rtag rchild 其中, 规定左 ltag=0 表示 lchild 是指向结点的左孩子的指针 ;ltag=1 表示 lchild 是指向结点的 前驱的左线索 ; 右标志 rtag=0 表示 rchild 是指向结点的右孩子的指针 ;rtag=1 表示 rchild 是指 向结点的后继的右线索 相应的存储结构如下 struct thread_tree struct thread_tree *lchild; int ltag; int data; int rtag; struct thread_tree *rchild; ; typedef struct thread_tree treenode; typedef treenode *thbintree; 例如, 图 7-8(a) 的中序线索二叉树, 其线索链表如图 7-8(b) 所示 图中的实线表示指针, 虚线表示线索 结点 B 的左线索为空, 表示 B 是中序序列的开始结点, 它没有前驱结点 E 的右线索为空, 表示 E 是中序序列的终端结点, 它没有后继结点 图 7-8 线索二叉树及存储结构

110 100 数据结构概论 将二叉树变为线索二叉树的过程称为线索化 二叉树线索化, 可以通过按某次序遍历二 叉树, 在遍历过程中用线索取代空指针来实现, 即将空的左 右孩子指针分别改为左 右线 索 为此, 可以附设一个指针 prenode 始终指向刚刚访问过的结点, 而指针 currentnode 指向 当前正在访问的结点 显然, 指针 prenode 所指结点是 currentnode 指针所指结点的前驱, 而 currentnode 指针所指结点是 prenode 所指结点的后继 为了遍历方便, 给线索树加上一个头指 针, 如图 7-8(b) 中的 thrt 指针 下面给出将二叉树按中序线索化的算法 ( 算法 7-4), 该算法在 生成二叉树的同时, 对二叉树进行线索化 算法 7-4 二叉树的中序线索化 thbintree insert_node(thbintree root,int node) thbintree newnode; thbintree currentnode; thbintree parentnode; thbintree prenode; newnode=(thbintree)malloc(sizeof(treenode)); newnode->data=node; newnode->rchild=null; newnode->lchild=null; newnode->rtag=1; newnode->ltag=1; currentnode=root->rchild; /* 插入树的根结点 */ if(currentnode==null) root->rchild=newnode; newnode->lchild=root; newnode->rchild=root; return root; parentnode=root; while(currentnode!=null) /* 寻找插入位置 */ prenode=parentnode; parentnode=currentnode; /* 在左子树中寻找插入位置 */ if(currentnode->data>=node) if(currentnode->ltag==0) currentnode=currentnode->lchild; else currentnode=null;

111 第 7 章树与二叉树 101 /* 在右子树中寻找插入位置 */ else if(currentnode->rtag==0) currentnode=currentnode->rchild; else currentnode=null; if(parentnode->data>=node) parentnode->ltag=0; parentnode->lchild=newnode; newnode->lchild=prenode; /* 生成前驱 */ newnode->rchild=parentnode; /* 生成后继 */ else parentnode->rtag=0; parentnode->rchild=newnode; newnode->lchild=parentnode; /* 生成前驱 */ newnode->rchild=prenode; /* 生成后继 */ return root; thbintree create_tree(int data[],int len) thbintree root=null; int i; /* 生成头结点 */ root=(thbintree)malloc(sizeof(treenode)); root->rchild=null; root->lchild=root; root->rtag=0; root->ltag=1; for(i=0;i<len;i++) root=insert_node(root,data[i]); return root;

112 102 数据结构概论 使用线索二叉树的最大优点是, 由于有了线索的存在, 在某些情况下可以很方便地查找 指定结点前驱和后继, 而不必再重新遍历二叉树 此外, 在线索树上进行某种次序的遍历比 在一般二叉树上遍历容易得多 对于在中序线索二叉树上遍历而言, 就不必再设栈, 而且算 法简捷, 只要找到序列中的第一个结点, 然后依次找到结点的后继结点, 直到其后继结点为 空 找序列的第一个结点也很简单, 从根结点出发, 沿着左指针一直向下走, 直到左指针为 空, 即到达二叉树的最左下结点, 该结点就是序列的第一个结点 如果要找某结点的后继, 那么当 rtag=1 时, 该结点的 rchild 指针即为其后继 ; 当 rtag = 0 时, 结点的 rchild 指向该结点 右子树的根, 而该结点的后继应是此右子树的最左下结点 如图 7-8(a) 中的 B 是树的最左下 结点, 也就是序列的第一个结点,A 的后继是结点 D 下面给出按中序遍历中序线索二叉树的遍历算法 算法 7-5 中序线索二叉树的遍历 void inorder(thbintree root) thbintree point; int i; point=root; for(i=0;i<index;i++) if(point->rtag==1) point=point->rchild; else point=point->rchild; while(point->ltag!=1) point=point->lchild; if(point!=root) printf("%d", point->data); 显然, 该算法的时间复杂度为 O(n) 因为它是非递归算法, 所以在常数因子上小于递归 的遍历算法 下面的程序代码生成中序线索二叉树并进行遍历 #include<stdio.h> #include<stdlib.h> int index; struct thread_tree struct thread_tree *lchild; int ltag; int data;

113 第 7 章树与二叉树 103 int rtag; struct thread_tree *rchild; ; typedef struct thread_tree treenode; typedef treenode *thbintree; thbintree insert_node(thbintree root,int node); thbintree create_tree(int data[],int len); void inorder(thbintree root); void main() thbintree root=null; int nodelist[20]; int i; int temp; clrscr(); printf("\n\n Please input the values to create threaded_binary_tree: \n\n"); printf("(exit for 0):"); index=0; scanf("%d",&temp); while(temp!=0) nodelist[index]=temp; index++; scanf("%d",&temp); root=create_tree(nodelist,8); printf("\n\n The node of threaded_binary_tree in *inorder*:\n\n"); inorder(root); getch(); 运行结果如下

114 104 数据结构概论 7.3 树和森林 树的存储结构树有多种存储方式, 这里只介绍 3 种常用的存储结构 1. 树的双亲表示法根据树的定义, 树的每个结点都有惟一一个父结点 因此, 可以用一组连续的存储空间, 即一维数组来存储树的结点 数组的一个元素表示树的一个结点, 结点的结构类型不仅要包括结点的信息, 同时要指明该结点的父结点在链表中的位置 图 7-9 树的双亲表示法双亲表示法的结构定义如下 #define MaxTreeSize 100 /* 矢量空间由用户定义 */ typedef struct DataType data; /* 结点数据, 类型由用户自己定义 */ int parent; /* 双亲指针, 指示结点的双亲在矢量中的位置 */ TreeNode; typedef struct TreeNode nodes[maxtreesize]; int n; /* 结点总数 */ ParentTree; ParentTree T; /*T 是双亲链表 */ 双亲表示法求指定结点的双亲操作很方便 例如, 在图 7-9 中可以很容易地求出结点 B, C,D,E 的父结点为 A 但求某个结点的孩子或其他后代时, 可能会遍历整个数组 2. 树的孩子表示法孩子表示法是比较通用的树的表示方法 该方法为树中每个结点设置一个孩子链表, 并

115 第 7 章树与二叉树 105 将这些结点及相应的孩子链表的头指针存放在一个矢量中 结点表可以用一维数组存储, 而 孩子链表由于其长度依赖于结点的度, 所以各不相同, 可以用单链表表示, 结点顺序按其在 树中从左到右的位置排列 图 7-10 给出了图 7-9(a) 所示树的孩子表示法 孩子结点的数据域 仅存放它们在矢量空间的序号 孩子表示法的结构定义如下 #define MaxTreeSize 100 /* 矢量空间由用户定义 */ typedef struct ChildNode /* 子链表结点 */ int child; /* 孩子结点在矢量中对应的序号 */ struct ChildNode *next; ChildNode; typedef struct DataType data; /* 存放树中的结点数据 */ ChildNode *firstchild; /* 孩子链表的头指针 */ Node; typedef struct Node nodes[maxtreesize]; int n; /* 结点总数 */ ChildTree; 图 7-10 树的孩子表示法与双亲表示法相反, 孩子表示法便于实现涉及孩子及其子孙的运算, 但不便于实现与双亲有关的运算 3. 树的孩子兄弟表示法在存储结点信息的同时, 附加两个分别指向该结点最左孩子和右邻兄弟的指针域 firstchild 和 rightsibling, 即可得到树的孩子兄弟链表表示, 因此这种表示法又称二叉树表示法或二叉链表表示法 孩子兄弟表示法的结点结构定义如下 : typedef struct CSNode /* 子链表结点 */

116 106 数据结构概论 DataType data; /* 存放树中的结点数据 */ struct CSNode *firstchild; /* 存放结点的最左孩子指针 */ struct CSNode *rightsibling; /* 存放结点的右兄弟指针 */ CSNode, *CSTree; 图 7-11 树的孩子兄弟表示法利用这种存储结构, 可以方便地实现树的各种操作 另外, 它与二叉树的二叉链表表示完全相同, 因此可以利用二叉树的算法来实现对树的操作 其缺点是, 寻找某个结点的父结点比较麻烦, 需要对树进行遍历 树和森林的遍历 1. 树的遍历由于树是由根结点和子树森林组成, 因此可以定义常用的两种树的遍历方式 : 先序 ( 根 ) 遍历和后序 ( 根 ) 遍历 (1) 先序 ( 根 ) 遍历树 1 访问根结点 ; 2 按照从左到右的顺序依次访问子树森林 (2) 后序 ( 根 ) 遍历树 1 按照从左到右的顺序依次访问子树森林 ; 2 访问根结点 例如, 对图 7-9(a) 的树进行先序遍历, 可以得到结点的序列为 ABCFGDHE; 对其进行后序遍历, 可以得到结点的序列为 BFGCHDEA 2. 森林的遍历森林的遍历方法有两种 : 先序遍历森林和中序遍历森林 (1) 先序遍历森林 1 访问森林中的第一棵树的根结点 ; 2 先序遍历第一棵树的子树森林 ; 3 先序遍历除去第一棵树的子树森林 图 7-12 的森林的先序遍历顺序为 ABCFGDHEIJKL (2) 中序遍历森林 1 中序遍历第一棵树的子树森林 ; 2 访问第一棵树的根结点 ; 3 中序遍历除去第一棵树之后的子树森林 图 7-12 的森林的中序遍历顺序为 BFGCHDEAKLJI

117 第 7 章树与二叉树 107 图 7-12 一个森林 树 森林与二叉树的转换 1. 树与二叉树之间的转换由于树可以用二叉链表表示法来表示, 而二叉树也可以用二叉链表表示法来表示, 因此, 可以通过二叉链表来实现二叉树与树之间的相互转换 对于树而言, 树中孩子的次序可以是无序的, 只要双亲与孩子的次序不颠倒即可 但在二叉树中, 左 右孩子的次序是不能颠倒的, 所以在讨论二叉树与树的转换时, 为了不引起转换结果的多样性, 约定按树的图形上的结点顺序进行转换 (1) 树转换为二叉树根据树的孩子兄弟表示法可以很容易地将树转化成二叉树 从根结点起, 结点的最左边第一个孩子变成二叉树中该结点的左孩子, 而结点的右兄弟成为二叉树中该结点的右孩子, 直到所有结点全部转化完 转换后的二叉树的根结点没有右子树 图 7-13 树转化成二叉树及其二叉链表 (2) 二叉树转换为树与树转换为二叉树的过程刚好相反, 从二叉树的根结点起, 二叉树中结点的左孩子变成转化后树的该结点的最左边第一个孩子, 而左孩子结点的右孩子成为转化后树中该结点的右兄弟, 直到所有结点全部转化完 2. 树与森林之间的转换森林是树的有限集合, 因此可以讨论森林与二叉树之间的转换 (1) 森林转化为二叉树将森林转化为二叉树的步骤如下 1) 森林中各棵树分别转化为二叉树 2) 按照森林中树的顺序, 依次将后一棵树作为前一棵树的根结点的右子树, 则第一棵树

118 108 数据结构概论 的根结点是转换后生成的二叉树的根结点 图 7-14 是森林转化成二叉树的具体步骤 (2) 二叉树还原成森林将一棵森林转化来的二叉树还原为森林, 与森林转化为二叉树的步骤正好相反 1) 去掉二叉树的根结点与右孩子间的连线 ; 2) 对右孩子二叉树以及沿此右孩子的右链不断搜索到的所有右孩子实施步骤 1), 直到所有右孩子的连线全部去掉, 从而得到若干棵孤立的二叉树 图 7-14 森林转化成二叉树 3) 将各棵孤立的二叉树, 按二叉树还原为一般树的方法还原为一般树 7.4 哈夫曼树 二叉树结构在实际问题中有着广泛的应用 本节讨论一种常见的二叉树 哈夫曼 (Huffman) 树, 又称最优树, 以及哈夫曼树在编码问题中的应用 概念和定义下面首先介绍相关的概念 从树中一个结点到另一个结点之间的分支构成结点之间的路径 路径上的分支个数称为该结点之间的路径长度 树的路径长度是从树根到每一个结点的路径长度之和 显然, 对于具有 n 个节点的二叉树, 满二叉树或完全二叉树具有最短的路径长度 为了满足应用的需求, 我们引入带权长度的概念 所谓权是给二叉树的每个终端结点赋以权值, 则二叉树的带权路径长度 (WPL) 为树中所有终端结点的带权路径长度之和, 记作

119 第 7 章树与二叉树 109 n WPL =Σ WL i= 1 其中,n 为二叉树的终端结点个数,W i 为第 i 个终端结点的权值,L i 为从根结点到第 i 个终端结点的路径长度 假设有 n 个权值 W 1,W 2,,W n, 试构造一棵有 n 个终端结点的二叉树, 且每个终端结点的权值为 W i 显然, 这样的二叉树可以构造出多棵, 其中必存在一棵带权路径长度最短的二叉树, 称这棵二叉树为哈夫曼树 (Huffman tree), 也称最优树 图 7-15 所示的 3 棵二叉树, 都有 4 个叶子结点, 且带相同的权值 5,3,4,1 它们的带权路径长度分别为 i i 图 7-15 几种不同带权路径长度的二叉树 (a)wpl= =25 (b)wpl= =26 (c)wpl= =32 由此可见, 带权路径长度最短的树不一定是完全二叉树 哈夫曼树的构造哈夫曼树的构造过程如下 1) 由给定的值 W 1,W 2,,W n 构造森林 F =(T 1,T 2,,T k ), 其中每个 T i 为一棵只有根结点且权值为 W i 的二叉树 2) 从 F 中选取根结点的权最小的二叉树 T i 和 T j, 构造一棵分别以 T i 和 T j 为左 右子树的新的二叉树 T k, 置 T k 根结点的权为 T i 和 T j 根结点的权之和 3) 从 F 中删去 T i,t j, 并将 T k 加入 F 若 F 中仍有不止一棵二叉树, 则返回步骤 2), 直到 F 只含一棵二叉树为止 这棵二叉树就是所求的哈夫曼树 图 7-16 为哈夫曼树的构造过程, 图中结点上标注的数字为该结点赋的权值 下面讨论哈夫曼树的生成算法 由上述哈夫曼树的构造过程可知, 要进行 N 1 次合并才能使森林中的二叉树的数目由 N 棵减少到只剩下一棵最终的哈夫曼树, 并且每次合并都将产生一个新结点 合并 N 1 次共产生 N 1 个新结点, 显然它们都是具有两个孩子的分支结点 由此可知, 最终求得的哈夫曼树中共有 2N 1 个结点, 其中 N 个叶子结点是初始森林中的 N 个孤立结点, 并且哈夫曼树中没有度数为 1 的分支结点 由于结点数已知且不会改变, 可以采用静态链表作为存储结构 设置一个大小为 2N 1 的数组, 令数组的每个元素由 4 个域组成, 它们分别用于存储权值 双亲指针和左 右孩子指针 结点结构定义如下

120 110 数据结构概论 图 7-16 哈夫曼树的构造过程 const int Num=20; /* 结点数目 */ typedef struct huffmannode float weight; /* 结点的权值 */ int parent; /* 父结点,-1 为无双亲, 否则为非根结点 */ int lchild; /* 左孩子指针,-1 表示该结点为叶子结点 */ int rchild; /* 右孩子指针 */ huffmannode; huffmannode HuffmanTree[2*Num-1]; 下面给出哈夫曼树的生成算法 算法 7-6 哈夫曼树的生成 在 huffmantree[i](0 i<n) 中, 选择两个双亲域为 0 且权值最小的两个结点, 通过 s 1 和 s 2 将其返回 void Select(int n, int* s1, int *s2) int i; if(n<2) return; for(i=0; i<n; i++) if(huffmantree[i].parent==0) *s1=i; break;

121 第 7 章树与二叉树 111 /* 选出第 1 个权值最小的结点 */ for(i=0; i<n; i++) if(huffmantree[i].parent==0) if(huffmantree[*s1].weight>huffmantree[i].weight) *s1=i; for(i=0; i<n; i++) if((huffmantree[i].parent==0) && (*s1!=i)) *s2=i; break; /* 选出第 2 个权值最小的结点 */ for(i=0; i<n; i++) if((huffmantree[i].parent==0) && (*s1!=i)) if(huffmantree[*s2].weight>huffmantree[i].weight) *s2=i; void CreateHuffmanTree(int data[],int len) int i; int m; int s1=0, s2=0; m=2*len-1; for(i=0; i<len; i++) huffmantree[i].weight=data[i]; for(i=len; i<m; i++) Select(i, &s1, &s2); /* 置父结点 */ huffmantree[s1].parent=i; huffmantree[s2].parent=i; /* 生成新结点 */

122 112 数据结构概论 huffmantree[i].lchild=s1; huffmantree[i].rchild=s2; huffmantree[i].weight=huffmantree[s1].weight+ huffmantree[s2]. weight; 哈夫曼编码的实现哈夫曼树在通信 编码和数据压缩等技术领域有着广泛的应用 下面讨论构造通信码的典型应用 在通信时, 有时需要将传送的文字转换成由二进制的字符组成的字符串 例如, 假设传送的电文为 BACADD, 它只有 4 个字符, 只需两位的串便可区分 若 A,B,C 和 D 的编码分别为 00,01,10 和 11, 则上述 6 个字符的电文便为 , 总长 12 位, 对方接收时可按两位一组进行编译 显然, 在传送电文时总是希望编码尽可能短, 自然会想到让电文中出现频率较高的字符采用尽可能短的编码, 这样传送电文的总长度便可以减少 例如, 为上述电文中的 A,B,C 和 D 设计的编码分别为 0,00,10,1, 则上述电文可转换成总长为 8 的字符串 但这样的编码在将电文译成原文时会产生歧义 比如, 对于 10, 可以译成 C, 也可译成 DA, 因为 D 的编码是 C 的编码的前缀 上述问题可以描述为, 设字符集合 C=c 1,c 2,,c n, 字符出现的频率为 W= w 1,w 2,, w n, 需要对 C 中的字符进行编码, 使得编码的总长最小, 且任何一个字符的编码不能是其他字符编码的前缀 用哈夫曼算法可以解决上述问题, 以集合 C 中的字符为待编码结点, 相应的 W 为权值, 构造一棵哈夫曼树 在哈夫曼树中, 约定左分支表示字符 0, 右分支表示字符 1, 那么从根结点到每个叶子结点的所有由字符 0 和 1 组成的字符串就是所求该字符的哈夫曼编码 例 7-1 C=A,B,C,D,E,F,G, 其权值为 W=6,25,12,8,14,27,3, 试设计哈夫曼编码 相应的哈夫曼编码为 A:0011 B:01 C:100 D:000 E:101 F:11 G:0010 假设待编码的字符个数为 n, 从上述哈夫曼树的构造过程可以看出, 各字符编码的长度可能各不相同, 但都不会超过字符的总数 n, 所以算法中可以用一个长度为 n 的数组来记录哈夫曼编码, 并用一个 start 变量来指示编码开始的前一位置 下面给出求各结点哈夫曼编码的算法 算法 7-7 哈夫曼编码的生成 char* huffmancode[num]; void HuffmanCoding(int n) int i,j; int start;

123 第 7 章树与二叉树 113 int current, father; char* cd=(char*)malloc(n*sizeof(char)); cd[n-1]= \0 ; for(i=0; i<n; i++) start=n-1; current=i; father=huffmantree[i].parent; while(father) if(huffmantree[father].lchild==current) cd[--start]= 0 ; else cd[--start]= 1 ; current=father; father=huffmantree[father].parent; huffmancode[i]=(char*)malloc((n-start)*sizeof(char)); StrCopy(huffmanCode[i], &cd[start]); free(cd); 下面的程序代码是应用上述哈夫曼树的生成算法和哈夫曼编码算法生成图 7-17 所示的哈 夫曼树, 并生成哈夫曼编码 图 7-17 构造哈夫曼编码 #include <stdio.h> #include <stdlib.h> #define Num 20 typedef struct huffmannode int weight; int parent;

124 114 数据结构概论 int lchild; int rchild; huffmannode; huffmannode huffmantree[2*num-1]; char* huffmancode[num]; void HuffmanCoding(int n); void Select(int n, int* s1, int *s2); void CreateHuffmanTree(int data[],int len); void main() int i; int index=0; int value; int nodelist[num]; clrscr(); printf("\npleaseinputtheelementsofbinarytree(exitfor0):\n\n"); index=0; scanf("%d",&value); while(value!=0) nodelist[index]=value; index++; scanf("%d",&value); CreateHuffmanTree(nodelist,index); HuffmanCoding(index); for(i=0; i<index; i++) printf("\n%3d: [", huffmantree[i].weight); printf("%s", huffmancode[i]); printf("]"); getch(); 运行结果如下

125 第 7 章树与二叉树 115 小结 本章是数据结构课程的重点之一, 也是本书后继许多章节的基础 本章的主要内容包括 : 树型结构的基本概念 ; 定义在树型结构上的两种重要的数据结构 二叉树和树, 它们的常见存储结构, 遍历运算的实现, 以及它们之间的转换 ; 详细讨论了树与森林的遍历和它们之间的转换 ; 介绍了哈夫曼树及哈夫曼编码的概念和实现 树型结构中的每个结点至多只有一个直接前驱, 但可以同时有多个直接后继 而线性表至多只能有一个直接后继 因此, 线性表可以看成是树型结构的特例 树型结构的表达能力比线性表强, 它可以描述数据元素之间的分支层次关系 二叉树是一种最简单也最重要的树型结构, 二叉树要严格区分左 右子树 二叉树的遍历方式有 3 种 : 先序遍历 中序遍历和后序遍历 二叉树的存储结构操作简便, 而且可以与树和森林相互转换 二叉树和树的常用存储表示方法是各种链式存储结构 通常, 只有在特殊情况下才使用顺序存储结构 二叉树和树的链式存储结构既可以是动态链表, 也可以是静态链表, 它们的共同点是用 ( 动态或静态 ) 指针表达数据元素之间的逻辑关系, 并依靠指针的链接作用实现数据的存储 树型结构的应用极其广泛, 哈夫曼树只是其中的一种应用方式, 可用于求解有效 ( 高性能 ) 分类问题 习题 1. 在有 n 个叶子结点的哈夫曼树中, 其结点总数为 A. 不确定 B.2n C.2n+1 D.2n 1 2. 对二叉树从 1 开始进行连续编号, 要求每个结点的编号大于其左 右孩子的编号, 同 一个结点的左 右孩子中, 其左孩子的编号小于其右孩子的编号, 则可采用 次序的 遍历实现编号 A. 先序 B. 中序 C. 后序 D. 从根开始的层次遍历 3. 某二叉树的先序序列和后序序列正好相反, 则该二叉树一定是 的二叉树 A. 空或只有一个结点 B. 高度等于其结点数 C. 任一结点无左孩子 D. 任一结点无右孩子 4. 一棵左 右子树均不空的二叉树在先序线索化后, 其空指针域数为 A.0 B.1 C.2 D. 不确定 5. 在有 n 个结点的二叉链表中, 值为非空的链域的个数为 A.n 1 B.2n 1 C.n+1 D.2n+1 6. 判断线索二叉树中某结点 p 指针有左孩子的条件是 A.p!= null B.p->lchild!= NULL C.p->ltag==0 D.p->ltag==1 7. 具有 100 个结点的完全二叉树的深度为 8.3 个结点可构成 棵不同形态的树

126 116 数据结构概论 9. 已知完全二叉树的第 8 层有 8 个结点, 则其叶子结点数是 10. 对于下图所示二叉树, 中序遍历得到的结点序列为 ; 后序遍历所得到的结 点序列为 ; 先序遍历所得到的结点序列为 11. 一棵度为 2 的树与一棵二叉树有什么区别? 12. 树与二叉树有什么区别? 13. 二叉树的性质有哪些? 14. 一棵完全二叉树上共有 21 个结点, 现按层次顺序存放在一矢量中, 矢量之下标正好为结点的序号, 试问序号为 12 的双亲结点存在吗? 15. 试分别找出满足下面条件的所有二叉树 : (1) 先序序列与中序序列相同 ; (2) 中序序列与后序序列相同 ; (3) 先序序列与后序序列相同 16. 什么是线索二叉树? 为什么要使用线索二叉树? 17. 已知二叉树的后序序列为 ABCDEFG, 中序序列为 ACBGEDF, 试构造出该二叉树 18. 对下面给出的数据序列, 构造一棵哈夫曼树, 并求出其带权路径长度 4,5,6,7,10,12,15,18, 满二叉树的分支数为 B, 叶子结点数目为 n 0, 证明满足 B=2(n 0 1) 20. 把序列 15, 20, 15, 7, 9, 18, 6 构造成对应的二叉排序树 21. 设计算法按层次遍历二叉树 T 22. 给出中序线索树的结点结构, 并画出一个具有头结点的中序线索树, 使其树结点至少为 6 个, 编写一算法在不使用栈和递归的情况下先序遍历一中序线索树, 并分析其时间复杂度 23. 已知在 lchild-rchild 存储法表示的二叉树中, 指针 t 指向该二叉树的根结点, 指针 p, q 分别指向树中的两个结点, 试编写一算法, 求距离这两个结点最近的共同的祖先结点 实习 1. 实验目的熟练掌握二叉树的存储结构, 理解二叉树的遍历方式, 并能够用高级语言实现各种遍历算法 ; 理解完全二叉树的特点 ; 理解并能应用哈夫曼算法解决实际问题 2. 实验内容 (1) 编写一个程序, 将一棵二叉树按树型结构 ( 树的层次 ) 进行打印, 并判断该二叉树是否为完全二叉树

127 第 7 章树与二叉树 117 (2) 如果两棵二叉树具有相同的结构 ( 树形状相同 ), 我们说这两棵二叉树是同构的 如果两棵二叉树是同构的, 或是把其中一棵树中的所有结点的左 右子树交换位置后, 两棵树是同构的, 那么我们称这两棵树是准同构的 编写程序, 判定两棵二叉树是否为同构的, 或是准同构的 (3) 已知一批学生的成绩分布为 百分制成绩 0~59 60~69 70~79 80~89 90~100 百分比 (%) 对应的五级分制不及格及格中良优 少 试设计算法输入这些学生的成绩, 转换成五级分制成绩输出, 要求使总的比较次数为最

128 第 8 章图 本章要点 : 图的概念图的存储结构图的遍历 AOE 网 AOV 网最短路径 8.1 图的概念 图 (graph) 是比树更为复杂的一种非线性结构 线性结构中, 每个结点只有一个直接前驱和直接后继 ; 树型结构中, 结点间为层次关系, 除根结点外, 每个结点可以与下层多个结点相关, 但只能与上层的一个结点相关 但在图中, 数据元素之间的联系是任意的, 每个元素都可以与其他元素相联系 在人工智能 工程 数学 物理学 化学 生物学和计算机科学等领域中, 图结构有着广泛的应用 图的重要性一方面在于很多实际问题直接与图有关, 例如网络分析 交通运输等 ; 另一方面在于还有很多实际问题可以间接地用图来表示, 处理起来比较方便, 例如工程进度的安排, 课程表的定制, 计算最短路径等 本章将介绍图的基本概念 存储结构及一些与图相关的算法, 对有关图的理论则不多涉及 定义图是由顶点 (vertex) 的有穷非空集 V 和顶点的偶对 ( 边 ) 集合 E 组成, 记为 G = (V,E) 若图中的每条边都是无向的, 则称该图为无向图 (undirected graph) 无向图的边是无序的, 用圆括号表示, 例如边 (v,w) 在无向图中, 边 (v,w) 和边 (w,v) 表示同一条边 图 8-1(a) 的 G 1 为无向图, 顶点和边的集合分别为 V 1 = 1,2,3,4,5,6,7 E 1 = (1, 2), (2, 3), (3, 1), (2, 4), (2, 5), (5, 6), (5, 7) 若图中的每条边都是有方向的, 则称该图为有向图 (directed graph) 有向图的边是两个顶点组成的有序对, 用尖括号表示 例如,<v,w> 表示一条有向的边, 边 <v,w> 和边 <w,v> 表示两条不同的有向边 例如, 图 8-1(b) 的 G 2 为有向图, 顶点和边的集合分别为 V 2 = 1,2,3,4,5,6

129 第 8 章图 119 E 2 = <1, 2>, <2, 1>, <2, 3>, <2, 4>, <3, 5>, <5, 6>, <6, 3> (a) 无向图 G 1 (b) 有向图 G 2 图 8-1 图的示例 基本概念和常用术语在一个有 n 个顶点的无向图中, 若每个顶点到其余 n 1 个顶点都有一条边, 则图中共有 n(n 1)/2 条边, 这种图称为完全图 (complete graph) 例如, 图 8-2 给出的就是顶点个数为 4 和 5 的完全图, 它们分别有 6 条边和 10 条边 有向图中边的取值范围是 0~n(n 1), 具有 n(n 1) 条边的有向图称为完全有向图 边数 e 少于 n lg n 的图称为稀疏图 (sparse graph), 反之, 称 e n lg n 的图为稠密图 (dense graph) (a) G 3 (b) G 4 图 8-2 完全图的两个示例如果图的每条边都有与之相关的数值, 这个数值称为该边的权 (weight), 这种图就称为网 (network) 边的权值可以表示从一个顶点到另一个顶点的耗费或距离 若 G=V,E 和 G ' = V ', E ' 是两个图, 存在关系 V ' V 和 E ' E, 则称 G ' 是 G 的子图 (subgraph) 例如, 图 8-3 所示的图是图 8-1 中 G 1 和 G 2 的一些子图 (a) G 1 的子图 (b) G 2 的子图 图 8-3 子图示例

130 120 数据结构概论 如果 <v,w> 是一条有向的边, 则称顶点 v 邻接到顶点 w, 顶点 w 邻接自顶点 v, 且边 <v,w> 与顶点 v,w 相关联 若 (v,w) 是一条无向的边, 则称顶点 v 和 w 是相邻顶点, 边 (v, w) 是与顶点 v,w 相关联的边 顶点的度 (degree) 是与顶点相关联的边数 例如, 图 8-1 的 G 1 中, 顶点 1 的度为 2, 顶点 2 的度为 4 对于有向图, 顶点的度有入度和出度之分 入度是指以该顶点为终点的边数, 出度则是指以该顶点为始点的边数 例如在图 8-1 的 G 2 中, 顶点 1 的入度和出度均为 1, 但顶点 2 的入度为 1, 出度为 3 在一个图中, 若从某顶点 v i 出发, 沿一些边经过顶点 v 1,v 2,,v m 到达 v j, 则称顶点序列 (v i,v 1,v 2,,v m,v j ) 为从 v i 到 v j 的路径 (path) 对于有向图, 路径也是有向的, 路径的方向是由起点到终点且与它所经过的每条边的方向一致, 所以在有向图中,v i 到 v j 的路径需由边 <v i,v 1 >,<v 1,v 2 >,,<v m,v j > 组成 对于无权的图, 路径的长度是指沿此路径上边的数目 对带权的图, 路径的长度一般是指沿路径各边的权之和 若路径上除 v i,v j 可以相同外,v 1,v 2,,v m 各点均不重复, 即路径经过每一顶点不超过一次, 则此路径称为简单路径 例如, 图 8-1 的 G 1 中的 (3,1,2,5,7) 就是简单路径, 但 (4,2,3,1,2,5) 就不是简单路径 如果从一个顶点出发经过一条路径又回到该顶点, 即 v i 与 v j 相同, 则此路径称为回路 (cycle) 在无向图中, 若从顶点 v i 到顶点 v j 之间有路径, 则称 v i 和 v j 是连通的 如果图中任意一对顶点是相连通的, 则称此图是连通图 (connected graph) 例如图 8-1 中的 G 1 和图 8-2 中的 G 3 与 G 4 都是连通图 图 8-4 中的图 G 5 就是非连通图 非连通图的每一个连通的部分称为连通分量 (connected component), G 5 包括 3 个连通分量, 如图 8-4(b) 所示 对于有向图, 若从顶点 v i 到顶点 v j 和从顶点 v j 到顶点 v i 之间都有路径, 则称这两个顶点是强连通的 若有向图中任何一对顶点都是强连通的, 则此图称为强连通图 图 8-1(b) 中的 G 2 不是强连通图, 它有 3 个强连通分量, 如图 8-5 所示 (a) 无向图 G 5 (b) G 5 的连通分量 图 8-4 无向图及其连通分量 图 8-5 G 2 的 3 个强连通分量 8.2 存储结构 图的结构比较复杂, 任意两顶点间都可能存在联系, 因而图的存储方法也很多, 应根据

131 第 8 章图 121 具体情况选择合适的存储结构 本节主要介绍两种常用的适合于一般图的存储结构 邻接矩阵表示及各操作的实现 1. 邻接矩阵表示法图的邻接矩阵是一个 n n 阶方阵,n 为图的顶点数 矩阵的每一行分别对应于图的各个顶点, 矩阵的每一列也分别对应于图的各个顶点 邻接矩阵表示各个顶点间的邻接关系, 所谓两点相邻接, 即它们之间有边相连 我们规定矩阵的元素 a ij 为 1 对无向图存在边 (v a ij = i,v j ), 对有向图存在边 <v i,v j > 0 反之若 G 是带权图, 边 (v i,v j ) 或 <v i,v j > 的权为 w ij, 则其邻接矩阵的元素为 a ij = w ij 对无向图存在边 (v i,v j ), 对有向图存在 <v i,v j > 边反之 例如, 对图 8-6(a) 的无向图 G 6, 其邻接矩阵如图 8-6(b) 所示 ; 对图 8-6(c) 的有向图 G 7, 其邻接矩阵如图 8-6(d) 所示 (a) G 6 (b) G 6 的邻接矩阵 (c) G 7 (d) G 7 的邻接矩阵 图 8-6 图的邻接矩阵的表示 由于无向图中的边 (v i,v j ) 与边 (v j,v i ) 相同, 只是写法不同, 所以如果 a ij =1, 必然有 a ji =1 这说明无向图的邻接矩阵是对称的, 只需输入和存储其上三角阵元素即可得到整个邻接矩阵, 由图 8-6(b) 的例子即可看到这一点 有向图则不同, 边 <v i,v j > 和边 <v j,v i > 不同, 故 a ij 不一 定等于 a ji, 所以有向图的邻接矩阵一般是不对称的, 例如图 8-6(d) 所示的邻接矩阵就是不对 称的 邻接矩阵可以用二维数组来存储, 对一般的图可以用 0/1 数组 ; 而对网可以用整数型或 实数型数组存储 图的存储结构可以定义如下 #define VertexNum 5 /* 顶点数 */ typedef int AdjType; /* 顶点类型 */ typedef struct graph AdjType vertex[vertexnum]; /* 顶点数组 */ int adjmatrix[vertexnum][vertexnum]; /* 邻接矩阵 */ Graph; 邻接矩阵表示法比较适用于以图的顶点为主的运算 例如, 在无向图中每个顶点 i 的度 等于邻接矩阵中第 i 行或第 i 列中非零元素的个数 ; 在有向图中, 第 i 行的非零元素的个数等 于该顶点的出度, 第 i 列非零元素的个数等于相应顶点的入度

132 122 数据结构概论 2. 操作的实现 下面介绍一些基于邻接矩阵存储结构实现的常用的图的基本操作 (1) 顶点定位函数 该函数确定顶点在图中的位置 若图中无此顶点, 返回 1 int GetPos(Graph *graph, AdjType vertex) int i; for(i=0;i<vertexnum;i++) if(graph->vertex[i]==vertex) return i; return -1; (2) 取顶点函数 求图的顶点数组中第 pos 个顶点 (pos 从 0 起计数 ) 若 pos 顶点数, 返回 ERROR Status GetVertex(Graph *graph, int pos, AdjType* vertex) if(pos>=vertexnum) return ERROR; *vertex=graph->vertex[pos]; return OK; (3) 求第一个邻接点函数 求图中某顶点 vertex 的第一个邻接点 如果图中没有该顶点, 或该顶点没有邻接点, 返 回 ERROR Status FirstAdj(Graph *graph, AdjType vertex, AdjType* firstadj) int i; int pos; pos=getpos(graph,vertex); if(pos==-1) return ERROR; for(i=0;i<vertexnum;i++) if(graph->adjmatrix[pos][i]==1) GetVertex(graph,i,firstAdj); return OK;

133 第 8 章图 123 return ERROR; (4) 求下一个邻接点函数 求图中某顶点 vertex 的某个邻接点 adj 的下一个邻接点 nextadj 如果没有下一个邻接点, 返回 ERROR Status NextAdj(Graph *graph, AdjType vertex, AdjType adj, AdjType* nextadj) int i; int vertexpos; int adjpos; vertexpos=getpos(graph,vertex); adjpos=getpos(graph,adj); for(i=adjpos+1;i<vertexnum;i++) if(graph->adjmatrix[vertexpos][i]==1) GetVertex(graph,i,nextAdj); return OK; return ERROR; 下面的程序代码利用上述存储结构和各操作函数求取图 8-6(a) 图 G 6 的顶点的邻接点 #include <conio.h> typedef enumerror=0,ok=1status; #define VertexNum 5 typedef int AdjType; typedef struct graph AdjType vertex[vertexnum]; int adjmatrix[vertexnum][vertexnum]; Graph; /* G 6 的顶点序列和邻接矩阵 */ AdjType vertex1[vertexnum]=1,2,3,4,5; int adjmatrix1[vertexnum][vertexnum]= 0,1,1,0,0, 1,0,1,1,0, 1,1,0,1,1, 0,1,1,0,1,

134 124 数据结构概论 0,0,1,1,0 ; Status GetVertex(Graph *graph, int pos, AdjType* vertex); int GetPos(Graph *graph, AdjType vertex); Status FirstAdj(Graph *graph, AdjType vertex, AdjType* firstadj); Status NextAdj(Graph *graph, AdjTypevertex,AdjType adj, AdjType* nextadj); main() int i, j; Graph graph; AdjType vertex; AdjType firstadj; AdjType nextadj; Status status; clrscr(); /* 生成图 */ for(i=0;i<vertexnum;i++) graph.vertex[i]=vertex1[i]; for(i=0;i<vertexnum;i++) for(j=0;j<vertexnum;j++) graph.adjmatrix[i][j]=adjmatrix1[i][j]; /* 图的操作 */ for(i=0;i<vertexnum;i++) GetVertex(&graph,i,&vertex); printf("\n vertex[%d] is: %d\n", i, vertex); /* 取第一个邻接点 */ status=firstadj(&graph,vertex,&firstadj); printf("adjecent node(s) is(are):"); while(status!=error) printf("%d", firstadj); /* 取下一个邻接点 */ status=nextadj(&graph,vertex,firstadj,&nextadj); firstadj=nextadj; getch();

135 第 8 章图 125 程序运行结果如下 除完全图以外, 一般的图不是任意两个顶点都相邻接, 因此邻接矩阵有很多零元素 特 别是当图是顶点数 n 较大的稀疏图时, 邻接矩阵表示法是较浪费存储空间的 邻接表的表示及各操作的实现 1. 邻接表表示法 邻接表是最常用的图的存储结构, 是由邻接矩阵改进的一种链式结构 它的特点是只考 虑非零元素, 因而节省了零元素所占的存储空间 在邻接表结构中, 为图中的每个顶点 v 建 立一个与 v 相关联的顶点的链表, 即邻接矩阵的每一行对应于一个线性链接表, 链接表的表 头对应于邻接矩阵该行的顶点, 链接表中的每个结点则对应于该行的一个非零元素 对于无向图, 一个非零元素表示与该行的顶点相邻接的另一个顶点 ; 对于有向图, 非零 元素则表示以该行顶点为起点的一条边的终点 邻接表表示法的存储结构定义如下 #define VertexNum 5 /* 图中顶点个数 */ typedef int VertexType; typedef struct adjnode int vertexpos; /* 该邻接顶点在顶点表中的位置 */ float weight; /* 边的权值, 如果没有可以省略 */ struct adjnode *next; AdjNode, *padjlist; /* 顶点列表 */ typedef struct VertexNode VertexType vertex; /* 顶点信息 */ padjlist firstadj; /* 与该顶点相邻的边表 */ VertexNode, Graph; 图 8-7 和图 8-8(a) 分别给出的是与图 8-6(a) 的无向图 G 6 和图 8-6(c) 的有向图 G 7 相对应的 邻接表 在邻接表的每个线性链接表中, 各顶点的顺序是任意的 需要指出的是, 不要因为 8-8(a) 顶点 1 的指针指向顶点 3, 顶点 3 的指针指向顶点 5, 就误认为顶点 3 到顶点 5 有边存 在 实际上, 线性链接表并不说明其他顶点之间的邻接关系

136 126 数据结构概论 对于无向图来说, 各顶点对应的线性链接表的结点数 ( 不算表头结点 ) 等于该顶点的度 ; 但对于有向图, 链接表的结点数只等于相应顶点的出度, 因为一个链接表对应原邻接矩阵的一行 若要计算有向图中某顶点 v i 的入度, 则需对整个邻接表的各链接表进行遍历, 邻接点为 v i 的所有顶点的个数为 v i 的入度 但这样计算入度不太方便, 对此有两种解决方法 一种方法是再同时构成一个逆邻接表, 在逆邻接表中, 也是与每个顶点相对应一个线性链接表, 但链接表的每一结点却是表示原邻接矩阵中该顶点的列中的每个非零元素 例如图 8-8(b) 是图 G 7 的逆邻接表 从逆邻接表求每个顶点的入度就方便多了, 只需计算相应链接表的结点数即可 另一种解决方法是建立一个十字邻接表, 其水平链相当于普通邻接表, 而垂直链相当于逆邻接表 图 8-7 图的邻接表表示法 (a) G 7 的邻接表表示法 (b) G 7 逆邻接表 2. 操作的实现 图 8-8 G 7 的邻接表表示法 下面介绍基于邻接表表示法的图的各种常用操作的实现 (1) 边的插入函数 本函数在无向图中插入一条边,flag=0 为向有向图中插入边,flag=1 为向无向图中插入边 void InsertEdge(Graph *graph, int flag, VertexType source, VertexType destination, float weight) int sourcepos, despos; AdjNode *newnode; AdjNode *pvertex=null, *p=null; /* 求与边关联的两个顶点在顶点表中的位置 */ sourcepos=getpos(graph,source); despos=getpos(graph,destination); if(sourcepos==-1 despos==-1) printf("\n*** Error ***: Out of range!\n"); return;

137 第 8 章图 127 else newnode=(adjnode*)malloc(sizeof(adjnode)); newnode->vertexpos=despos; newnode->weight=weight; newnode->next=null; pvertex=graph[sourcepos].firstadj; /* 生成顶点的第一个邻接点 */ if(pvertex==null) graph[sourcepos].firstadj=newnode; else /* 生成顶点的其他邻接点 */ while(pvertex!=null) p=pvertex; pvertex=pvertex->next; p->next=newnode; if(flag==1) /* 无向图插入边 (a,b) 的同时也插入边 (b,a)*/ newnode=(adjnode*)malloc(sizeof(adjnode)); newnode->vertexpos=sourcepos; newnode->next=null; pvertex=graph[despos].firstadj; /* 生成顶点的第一个邻接点 */ if(pvertex==null) graph[despos].firstadj=newnode; else /* 生成顶点的其他邻接点 */ while(pvertex!=null) p=pvertex; pvertex=pvertex->next; p->next=newnode; (2) 图的生成函数 void create_graph(graph *graph, int flag)

138 128 数据结构概论 VertexType vertex; VertexType vertex1, vertex2; int num=vertexnum; int i; float weight=0; clrscr(); printf("\n Total number of vertex is %d (0 %d).", num, num-1); printf("\n Input vertex:"); /* 输入顶点, 生成顶点表 */ for(i=0;i<vertexnum;i++) scanf("%d", &vertex); graph[i].vertex=vertex; graph[i].firstadj=null; printf("\ninput edge(s),-1,-1 to finish!\n"); /* 输入边, 生成邻接表 */ while(1) /*printf("please input an edge:");*/ scanf("%d,%d",&vertex1,&vertex2); if(vertex1==-1 vertex2==-1) break; InsertEdge(graph,flag,vertex1,vertex2,weight); (3) 顶点定位函数 该函数确定顶点在图的顶点表中的位置 若图中无此顶点, 返回 1 int GetPos(Graph *graph, VertexType vertex) int i; for(i=0;i<vertexnum;i++) if(graph[i].vertex==vertex) return i; return -1; (4) 取顶点函数 求图的顶点数组中第 pos 个顶点 ( 从 0 起 ) 若 pos 顶点数, 返回 ERROR Status GetVertex(Graph *graph, int pos, VertexType* vertex)

139 第 8 章图 129 if(pos>=vertexnum) return ERROR; *vertex=graph[pos].vertex; return OK; (5) 求第一个邻接点函数 求图中顶点 vertex 的第一个邻接点 如果图中没有该顶点, 或该顶点没有邻接点, 返回 ERROR Status FirstAdj(Graph *graph, VertexType vertex, VertexType* firstadj) int pos; AdjNode *pvertex=null; pos=getpos(graph,vertex); if(pos==-1) return ERROR; pvertex=graph[pos].firstadj; if(pvertex!=null) return(getvertex(graph,pvertex->vertexpos,firstadj)); return ERROR; (6) 求下一个邻接点函数 求图中顶点 vertex 的某个邻接点 adj 的下一个邻接点 nextadj, 如果没有下一个邻接点, 返回 ERROR StatusNextAdj(Graph *graph,vertextypevertex,vertextypeadj,vertextype* nextadj) int vertexpos; AdjNode *pvertex=null; VertexType adjnode; vertexpos=getpos(graph,vertex); pvertex=graph[vertexpos].firstadj; while(pvertex!=null) GetVertex(graph,pvertex->vertexPos,&adjNode); if(adjnode==adj && pvertex->next!=null) vertexpos=pvertex->next->vertexpos; return(getvertex(graph,vertexpos,nextadj));

140 130 数据结构概论 pvertex=pvertex->next; return ERROR; (7) 图的输出函数 void print_graph(graph *graph) int i; AdjNode *pvertex=null; VertexType adj; printf("\ngraph's adjcency list is:"); for(i=0;i<vertexnum;i++) printf("\n Vertex[%d]: %d-->", i, graph[i].vertex); pvertex=graph[i].firstadj; while(pvertex!=null) GetVertex(graph,pvertex->vertexPos,&adj); printf("%d",adj); pvertex=pvertex->next; (8) 释放图 因为使用链接表的存储结构要动态申请内存资源, 所以在程序结束时要释放内存空间 void free_graph(graph *graph) AdjNode *pvertex=null,*p=null; int i; for(i=0;i<vertexnum;i++) pvertex=graph[i].firstadj; while(pvertex!=null) p=pvertex; pvertex=pvertex->next; free(p); p=null; 下面的代码实现基于邻接表表示法来实现图 G 5 的生成和输出

141 第 8 章图 131 #include <stdlib.h> #include <stdio.h> #include <conio.h> #define VertexNum 9 #define NULL 0 typedef enumerror=0, OK=1Status; /* 图的邻接表存储结构 */ /* 基于图的邻接表存储结构的各操作的声明及定义 */ void main() Graph graph[vertexnum]; /* 生成无向图 */ create_graph(graph,1); print_graph(graph); free_graph(graph); getch(); 程序运行结果如下 8.3 图的遍历 与树的遍历类似, 若从图中某顶点出发, 按照某种方式沿着图中的边访问图中的所有顶点, 且使每个顶点仅被访问一次, 就称为图的遍历 图的遍历通常有深度优先搜索 (Depth First Search,DFS) 和广度优先搜索 (Breadth First Search,BFS) 由于图中每个顶点都与其他多个顶点邻接并可能存在回路, 因此在遍历的过程中, 为了避免对一个顶点的重复访问, 通常要对访问过的顶点做标记 深度优先搜索深度优先搜索定义如下 (1) 指定图的某个尚未被访问的顶点 v 作为起始点

142 132 数据结构概论 (2) 访问顶点 v (3) 以顶点 v 的所有未被访问的邻接点作为搜索起点, 进行深度优先搜索 (4) 如果图中仍有未被访问的顶点, 则转至 (1); 否则, 搜索结束 对图进行深度优先搜索时, 按被访问的顶点的先后顺序所得到的顶点序列, 称为该图的深度优先搜索序列, 简称 DFS 序列 以图 8-9(a) 的 G 8 为例, 深度优先搜索的遍历过程如图 8-9(b) 所示 在图中从顶点 v 1 出发进行遍历, 在访问了顶点 v 1 之后, 选择邻接点 v 2 因为 v 2 尚未被访问, 则从 v 2 出发, 访问 v 4, 访问 v 7, 在访问了 v 5 之后, 由于 v 5 的两个邻接点 v 2 和 v 7 都被访问过, 所说回溯至 v 7, 因为 v 7 的邻接点 v 6 还未被访问, 所以从 v 6 开始继续遍历, 直到访问过 v 3 后, 一直回溯至 v 1, 表明该连通子图内的所有顶点都已被访问过, 然后再从顶点 v 8 开始, 继续执行深度优先搜索 最终得到的深度优先搜索序列为 v 1 v 2 v 4 v 7 v 5 v 6 v 3 v 8 v 9 (a) G 8 (b) G 8 的深度优先搜索过程 (c) G 8 的广度优先搜索过程 图 8-9 图的遍历 从定义可以看出, 深度优先搜索是一个递归过程 算法 8-1 给出了图的深度优先搜索算 法 为了判别哪些顶点已被访问, 增设一个 visited 域 顶点在被访问前 visited 值为 0, 顶点 被访问后 visited 值为 1 算法 8-1 图的深度优先搜索算法 void dfs(graph *graph, VertexType vertex) AdjNode *pointer; int pos; VertexType v; VertexType adjvertex, nextadj; Status s; pos=getpos(graph,vertex); visited[pos]=1; /* 输出顶点 */ printf("%d",vertex); s=firstadj(graph,vertex,&adjvertex); while(s!=error) pos=getpos(graph,adjvertex);

143 第 8 章图 133 if(visited[pos]==0) /* 递归调用, 深度优先搜索邻接点 */ dfs(graph, adjvertex); s=nextadj(graph, vertex, adjvertex, &nextadj); adjvertex=nextadj; 下面的程序代码对图 G 8 进行深度优先遍历, 以邻接表为图的存储结构 #include <stdlib.h> #include <stdio.h> #include <conio.h> #include "adjlist.h" int visited[vertexnum]; /* 图的邻接表存储结构 */ /* 基于图的邻接表存储结构的各操作的声明及定义 */ void dfs(graph *graph, VertexType vertex); main() int i; Graph graph[vertexnum]; clrscr(); /* 生成图 */ create_graph(graph); print_graph(graph); /* 记录顶点是否被访问过 */ for(i=0;i<vertexnum;i++) visited[i]=0; /* 深度优先搜索 */ printf("\ndepth-first-search:\n"); for(i=0;i<vertexnum;i++) if(visited[i]==0) dfs(graph,graph[i].vertex); free_graph(graph); getch(); return 0; 输出结果如下

144 134 数据结构概论 广度优先搜索 广度优先搜索定义如下 (1) 指定图的某个尚未被访问的顶点 v 作为起始点 (2) 访问顶点 v (3) 访问 v 的所有邻接点 w 1,w 2,,w n, 并依次访问顶点 w 1,w 2,,w n 的未被访 问的所有邻接顶点 反复如此, 直到找不到这样的顶点 (4) 如果图中仍有未被访问的顶点, 则转至 (1); 否则, 搜索结束 例如, 对图 8-9(a) 的图 G 8 进行广度优先搜索时, 假设以顶点 v 1 为搜索起始点, 先访问 v 1, 然后访问 v 1 的两个邻接点 v 2 和 v 3, 然后访问 v 2 和 v 3 的邻接点 v 4,v 5,v 6, 接着访问它们 的邻接点 v 7, 此时 G 8 的一个连通子图的全部顶点都被访问过, 但图中仍有未被访问的顶点, 选 v 8 作为新的起点, 访问完 v 8 后访问 v 9, 这时图中所有的顶点都已被访问过, 搜索结束, 得到一个顶点的广度优先搜索序列为 v 1 v 2 v 3 v 4 v 5 v 6 v 7 v 8 v 9 图 8-9(c) 显示了广度优先搜索的过程 广度优先搜索类似于树的按层次遍历的过程, 它与队列有很多相似之处 广度优先搜索 算法运用了队列的许多思想 在队列中存放着等待被访问的顶点, 在出队 ( 访问某个顶点 ) 的同时将其所有邻接点存入队列 显然, 与深度优先搜索类似, 在搜索过程中也需要设立入 队标志位 算法 8-2 给出了图的广度优先搜索算法 算法 8-2 图的广度优先搜索算法 void bfs(graph *graph, int vertex) AdjNode *pointer; int pos; VertexType v; VertexType adjvertex, nextadj; Status s; /* 搜索起始点入队 */ EnQueue(vertex);

145 第 8 章图 135 pos=getpos(graph,vertex); /* 置入队标志 */ visited[pos]=1; /* 判断队列是否为空 */ while(front!=rear) vertex=dequeue(); /* 输出顶点 */ printf("%d",vertex); s=firstadj(graph,vertex,&adjvertex); while(s!=error) pos=getpos(graph,adjvertex); /* 如果该邻接点没有入队, 将其入队 */ if(visited[pos]==0) EnQueue(adjVertex); visited[pos]=1; s=nextadj(graph,vertex,adjvertex,&nextadj); adjvertex=nextadj; 广度优先搜索算法的调用方式和深度优先搜索的调用方式相似, 在此不再举例 从图的深度优先搜索和广度优先搜索算法可知, 遍历图的过程实质上是通过边找邻接点的 过程, 因而两种算法的时间复杂度相同, 都是 O(n)+O(e)=O(n+e), 两者不同之处仅在于顶点的 访问序列不同 广度优先搜索是从横向一层层地寻找目标顶点, 深度优先搜索则纵向寻找 8.4 生成树和最小生成树 生成树的概念和分类 1. 概念无向连通图的生成树 (spanning tree) 是图的一个连通子图, 它含有图的全部 n 个顶点, 但只有足以使图连通的 n+1 条边 生成树是连通图的包含图中所有顶点的极小连通子图 有 m 个连通分量的图, 它的每个连通分量都有一棵生成树, 它们构成图的生成森林 (spanning forest) 有向图的生成树概念与无向图的生成树概念类似, 即 (1) 若 G 是强连通的有向图, 则从图的任一顶点 v 出发, 都可以访问 G 中的所有顶点, 从而得到以 v 为根的生成树 ( 2 ) 若 G 是有根的有向图, 设根为 v, 则从根 v 出发可以完成对 G 的遍历, 得到 G 的以 v 为根的生成树

146 136 数据结构概论 2. 分类在遍历图的同时, 记录每条经过的边, 图的所有顶点与这些被记录的边就构成了图的生成树 构造生成树的过程可以通过深度优先搜索, 也可以通过广度优先搜索 根据这两种不同的构造方式, 可以将生成树分为深度优先生成树和广度优先生成树 图的生成树不惟一, 从不同的顶点出发进行遍历, 或是从同一顶点出发, 但经过不同的搜索路径, 可以得到不同的生成树 对于非连通图, 根据遍历方式的不同, 可以有深度优先生成森林或广度优先生成森林 例如, 图 8-10 是 G 5 的广度优先生成森林, 图 8-11 是 G 5 的深度优先生成森林 图 8-10 G 5 的广度优先生成森林 图 8-11 G 5 的深度优先生成森林 最小生成树的概念和实现方法对于连通的带权图 ( 连通网 ), 其生成树也是带权的 生成树各边的权值总和称为该树的权, 权值最小的生成树称为最小生成树 (Mininum Spanning Tree,MST) 最小生成树有很多重要的应用 例如, 网络 G 表示 n 个城市之间的通信线路网, 其中顶点表示城市, 边表示两个城市之间的通信线路, 边上的权值表示线路的长度或造价 通过求该网络的最小生成树, 可以得到求解通信线路长度或总代价最小的最佳方案 下面介绍最小生成树性质 最小生成树性质 (MST 性质 ) 设 G = (V,E) 是一个连通网络,U 是顶点集 V 的一个真子集 若 (u,v) 是 G 中所有的一个端点在 U(u U) 里而另一个端点不在 U( 即 v V U) 里的边中具有最小权值的一条边, 则一定存在 G 的一棵最小生成树包括此边 (u,v) 用反证法证明 MST 性质 假设 G 中任何一棵 MST 都不含边 (u,v) 若生成树 T 是 G 的一棵 MST, 则它不含边 (u,v) 由于树 T 包含了 G 中所有顶点的连通图, 所以当把边 (u,v) 加入树 T 时, 根据生成树的概念, 该边必构成一个回路, 且树上必定存在边 (u',v') 删除边(u',v') 后回路也随之消失, 由此可得另一生成树 T ' 因为边 (u,v) 的权值不大于边 (u',v') 的权值, 所以树 T ' 的权值不大于树 T 的权值, 故树 T ' 也是 G 的 MST, 它包含边 (u,v), 与假设矛盾 所以,MST 性质成立 利用 MST 性质构造最小生成树的算法主要有 Prim 算法和 Kruskal 算法 1.Prim 算法 Prim 算法把无向连通图 G = (V,E) 的顶点集 V 划分为 U 和 V U, 并使用一个待选边表 TE,TE 的初值为空, 即 TE= 起初 U 中只含有任意一个顶点 u, 选择具有最小代价的边 (u, v),u U,v V U 加入 TE, 并将顶点 v 并入集合 U; 重复上述过程, 直到 U=V 为止 此时, TE 中必定有 n 1 条边,T = (V,TE ) 是图 G 的一棵最小生成树 边表可以采用数组或链式存储结构, 因为需要经常修改待选边, 所以图的存储结构最好

147 第 8 章图 137 选用邻接矩阵表示法 边表的存储结构定义如下 typedef struct VertexType adjvertex; /* 邻接的顶点 */ int lowcost; /* 权值 */ MST; MST mst[vertexnum]; 开始时,mst[i] 中存放顶点 i 到顶点 adjvertex 的边的权值 ; 算法结束时,mst 中存放求出 的最小生成树的 n 1 条边 应用 Prim 算法生成最小生成树的过程如下 1) 任选图中的某个顶点 v i 作为起始点, 并入 U 在算法中, 用 mst[i].lowcost=0 表示顶 点 v i 并入 U mst 中存放在 V U 中的 n 1 个顶点到 v i 的边及相应的权值 如果没有边, 权值 设为无穷大 2) 在 mst 中选出 lowcost 的最小值 例如 mst[k]( 保证顶点 v k 不在 U 中 ), 则将顶点 v k 并入 U, 即 mst[k].lowcost=0 3) 对 U 中的所有顶点, 比较 U 中的顶点 u 到 V U 中的顶点 v i 的距离和 v k 到 V U 中的 顶点 v i 的距离哪个更短 如果是 v k 到 v i 的距离短, 那么修改 (u,v i ) 为 (v k,v i ), 算法中通过修 改 mst[i]. adjvertex = v k,mst[i]. lowcost = weight(v k,v i ) 来实现 ; 否则, 不做调整 4) 重复步骤 2), 3 ) 操作, 直到所有顶点都被并入 U 为止 图 8-12 为按 Prim 算法对 8-12(a) 的图 G 9 求最小生成树的过程 初始状态时,U=1, 与 顶点 1 相关联的边有 3 条 :(1,2),(1,4) 和 (1,5), 权值分别为 8,2 和 4, 则 U 到 V U 中 各顶点代价最小的边为 (1,4), 该边为生成树上的第一条边, 置 mst[3].lowcost =0, 表示顶点 4 并入 U; 然后对 mst 进行调整 调整方法是 :1 由于边 (2,4) 的权值小于边 (2,1) 的权值, (a) G 9 (b) (c) (d) (e) (f) (g) 图 8-12 应用 Prim 算法构造的最小生成树 修改 mst[1].adjvertex=4,mst[i].lowcost=3;2 因为顶点 4 到顶点 3 顶点 6 存在边, 所以修改

148 138 数据结构概论 mst[2] 为边 (3,4), 权值为 6; 修改 mst[5] 为边 (6,4), 权值为 11; 依次类推, 直到所有顶点都 并入 U 图 8-13 是构造过程中 mst 各分量的变化情况, 图中阴影部分为当前选中的权值最小 的边 图 8-13 应用 Prim 算法构造最小生成树过程中 mst 的变化 下面是 Prim 算法的实现, 图的存储结构选用邻接矩阵表示法 算法 8-3 Prim 算法 /* 求代价最小的边 */ int GetMinimumCost(MST *mst) int i; int min=max, pos=-1; for(i=0;i<vertexnum;i++) if((mst[i].lowcost!=0)&&(mst[i].lowcost<min)) min=mst[i].lowcost; pos=i; printf("mst[%d]:%d,%d;", i, mst[i].adjvertex,mst[i].lowcost); printf("\n"); return pos; /*prim 算法, 生成最小代价生成树 */ void prim(graph *graph, VertexType s) int i,j; int pos; int minpos; VertexType vertex, adjvertex;

149 第 8 章图 139 pos=getpos(graph,s); /* 初始化 mst*/ for(i=0;i<vertexnum;i++) if(graph->vertex[i]!=s) mst[i].adjvertex=s; mst[i].lowcost=graph->adjmatrix[pos][i]; /*printf("[%d]: %d, %d\n", i, s,mst[i].lowcost); */ mst[pos].lowcost=0; for(i=0;i<vertexnum-1;i++) minpos=getminimumcost(mst); GetVertex(graph, minpos, &vertex); printf("(%d,%d):%d",mst[minpos].adjvertex,vertex, mst[minpos].lowcost); /* 将顶点 vertex 并入边表 */ mst[minpos].lowcost=0; /* 调整 mst*/ for(j=0;j<vertexnum;j++) if(graph->adjmatrix[minpos][j]<mst[j].lowcost) mst[j].lowcost=graph->adjmatrix[minpos][j]; mst[j].adjvertex=vertex; 从上面算法可以看出,Prim 算法的时间主要花费在选择最小生成树的 n 1 条边上 该算 法有两层循环, 所以时间复杂度为 O(n 2 ), 与图的边数无关, 因此该算法适合于稠密图 下面的程序代码应用 Prim 算法对图 G 9 求最小生成树, 图采用邻接矩阵存储结构 #include <conio.h> #define Max #define VertexNum 6 typedef int VertexType; /* 邻接矩阵存储结构的定义 */ /*MST 的定义 */ /*Prim 算法 */ typedef enumerror=0, OK=1Status;

150 140 数据结构概论 /* G 9 的顶点 */ VertexType vertex1[vertexnum]=1,2,3,4,5,6; /* G 9 的邻接矩阵 */ int adjmatrix1[vertexnum][vertexnum]= Max, 8, Max, 2, 4, Max, 8, Max, 5, 3, Max, Max, Max, 5, Max, 6, Max, Max, 2, 3, 6, Max, Max, 11, 4, Max, Max, Max, Max, 7, Max, Max, Max, 11, 7, Max ; Status GetVertex(Graph *graph, int pos, VertexType* vertex); int GetPos(Graph *graph, VertexType vertex); Status FirstAdj(Graph *graph, VertexType vertex, VertexType* firstadj) int i; int pos; pos=getpos(graph,vertex); if(pos==-1) return ERROR; for(i=0;i<vertexnum;i++) if(graph->adjmatrix[pos][i]!=max) GetVertex(graph,i,firstAdj); return OK; return ERROR; Status NextAdj(Graph *graph, VertexType vertex, VertexType adj, VertexType* nextadj) int i; int vertexpos; int adjpos; vertexpos=getpos(graph,vertex); adjpos=getpos(graph,adj); for(i=adjpos+1;i<vertexnum;i++) if(graph->adjmatrix[vertexpos][i]!=max)

151 第 8 章图 141 GetVertex(graph,i,nextAdj); return OK; return ERROR; void create_graph(graph *graph, VertexType vertex[], int adjmatrix [VertexNum][VertexNum]) VertexType vertex1, vertex2; int i, j; for(i=0;i<vertexnum;i++) graph->vertex[i]=vertex[i]; for(j=0;j<vertexnum;j++) graph->adjmatrix[i][j]=adjmatrix[i][j]; /* 输出邻接矩阵 */ void print_adjmatrix(graph *graph) int i, j; printf("\nadjacency Matrix:\n"); for(i=0;i<vertexnum;i++) for(j=0;j<vertexnum;j++) printf("%2d\t", graph->adjmatrix[i][j]); printf("\n"); void main() int i, j; Graph graph; clrscr(); /* 生成图 */ create_graph(&graph,vertex1,adjmatrix1); print_adjmatrix(&graph); printf("\nfinding minimum spanning tree.\n"); printf("prim's algorithm, source is %d.\n", graph.vertex[0]); prim(&graph, graph.vertex[0]);

152 142 数据结构概论 getch(); 输出结果如下 2.Kruskal 算法 Kruskal 提出了另一种构造最小生成树的算法 该算法思想是, 尽可能地选取图 G 中最短的边 ( 即权值最小的边 ) 作为生成树的边, 当依次把最短的边加入生成树 T 时, 若添加某条边后形成了回路, 就舍弃这条边, 选下一条边 ; 反复执行上述过程, 最后得到一棵最小生成树 因为每一次添加到 T 中的边均是当前权值最小的边,MST 性质也能保证最终得到的 T 是一棵最小生成树 例如, 图 8-14 所示为依照 Kruskal 算法对图 G 9 求最小生成树的过程 在图 (b)~(e) 中, 权值为 2,3,4,5 的边分别被选中, 加入到 T 中, 接下来边的最小权值为 6, 但该边依附的两个顶点已经在生成树 T 中, 所以舍弃, 那么剩下的权值为 7 的边被选中, 加入 T 此时, 形成了一棵最小生成树 该算法的时间复杂度为 O(e lg e) Kruskal 算法的时间主要取决于边数, 它较适合于稀疏图 (a) (b) (c) (d) (e) (f) 图 8-14 应用 Kruskal 算法构造最小生成树

153 第 8 章图 AOV 网及其应用 不含回路的有向无环图 (Directed Acyclic Graph,DAG) 在计算机系统, 以及工程 管理 经济领域有着重要的作用 AOV 网和 AOE 网都应用到了 DAG 的概念 概念在一个有向图中, 若用顶点代表活动, 边代表活动之间的先后关系, 称该有向图为顶点活动网 (Activity On Vertex,AOV) 在 AOV 网中, 若从顶点 i 到 j 之间存在一条有向路径, 称顶点 i 是顶点 j 的前驱, 或者称顶点 j 是顶点 i 的后继 若 <i,j> 是图中的边, 则称顶点 i 是顶点 j 的直接前驱, 顶点 j 是顶点 i 的直接后继 实际的工程管理中, 为了分析和实施一项工程计划, 往往把一个较大的工程划分为许多子工程 ( 或称活动 ) 在整个工程实施过程中, 而有些活动必须在其他有关活动完成以后才能开始, 而有些活动没有先决条件, 可以安排在任何时间开始 AOV 网就是一种可以形象地反映整个工程中各个活动之间的先后关系的有向图 显然, 在一项工程中, 每个活动都不能直接或间接地以自己为先决条件, 否则工程不能进行, 也就是说 AOV 网中没有环 例如, 对一个计算机专业的学生来说, 他学习的基础课和专业课之间的先后关系就构成了一个 AOV 网 假设这些课程的名称与相应代号如图 8-15(a) 所示, 有一些课程必须在先学完某些课程之后才能开始学习, 如数据结构课程就必须安排在学完它的两门先修课程 离散数学和程序设计之后 ; 有些课程可以随时安排学习, 因为它们是独立于其他课程的基础课, 不需要先修其他课程, 如高等数学 学习某门课程的先决条件是学完它的全部先修课程, 于是先决条件定义了各课程之间的先后关系 这种课程安排的先后关系可用如图 8-15(b) 所示的 AOV 网表示, 图的每一个顶点代表一门课程, 每条有向边代表起点对应的课程是终点对应课程的先修课 从图中可以清楚地看出各课程之间的先修和后继的关系, 如 C 5 的先修课为 C 2 和 C 4 ;C 5 后继课为 C 6,C 7 课程代号 课程名称 先修课程 C 1 高等数学 / C 2 程序设计 / C 3 线性代数 C 1 C 4 离散数学 C 2 C 5 数据结构 C 2,C 4 C 6 编译原理 C 2,C 5 C 7 操作系统 C 5,C 6 C 8 计算方法 C 1,C 2,C 3 (a) 课程之间的相互优先关系 (b) 课程的 AOV 网 G 10 图 8-15 AOV 网 拓扑排序 给出有向图 G = (V,E), 对于 V 中顶点的线性序列 (v 1,v 2,,v n ), 如果满足如下条件 :

154 144 数据结构概论 若在 G 中从顶点 v i 到 v j 有一条路径, 则在序列中顶点 v i 必在顶点 v j 之前, 则该序列成为 G 的一个拓扑序列 (Topological Order) 构造有向图的一个拓扑序列的过程称为拓扑排序 (Topological Sort) 例如, 对图 8-15 中的 AOV 网进行拓扑排序, 得到的一个拓扑序列为 C 1 C 3 C 2 C 4 C 5 C 6 C 7 C 8, 也可以得到另一个拓扑序列 C 1 C 2 C 3 C 4 C 5 C 6 C 7 C 8, 学生的选课顺序只能按拓扑序列进行 可以看出, 一个 AOV 网可能有多个拓扑序列 任何无回路的 AOV 网, 其顶点都可以排成一个拓扑序列, 拓扑排序方法如下 1) 从 AOV 网中选择一个入度为 0 的顶点将其输出 2) 在 AOV 网中删除此顶点及其所有的边 3) 重复上述两步, 直到所有顶点全部输出为止, 此时整个拓扑序列完成 或者直到剩下的顶点的入度都不为 0 为止, 此时说明 AOV 网中存在回路, 拓扑排序无法继续进行下去 例如, 对图 8-16 中 G 11 进行拓扑排序的过程如图 8-16(b)~(f) 所示, 得到顶点的拓扑序列为 v 1 v 4 v 3 v 2 v 5 (a) 图 G 11 (b) 输出 v 1, 得到序列 v 1 (c) 输出 v 4, 得到序列 v 1 v 4 (d) 输出 v 3, 得到序列 v 1 v 4 v 3 (e) 输出 v 2, 得到序列 v 1 v 4 v 3 v 2 (f) 输出 v 5, 得到序列 v 1 v 4 v 3 v 2 v 5 图 8-16 拓扑排序过程 算法 8-4 给出了拓扑排序的算法 AOV 网采用邻接表的存储结构 算法中定义一个 indegree 数组, 存放顶点的入度 另外, 为了避免重复计算顶点的入度, 可以设立一个栈 ( 或 队列 ), 用来存放入度为 0 的顶点 排序前, 将入度为 0 的顶点全部入栈, 从栈中输出一个 顶点后, 将所有以该顶点为起点的边的终点的入度减 1, 即删除这些边 ; 如果又有某个顶点的 入度为 0, 则也将其入栈 反复如此, 直到所有顶点都被输出 如果栈空时仍有顶点没有被输 出, 表明 AOV 网中存在回路 算法 8-4 拓扑排序 int indegree[vertexnum]; /* 计算顶点的入度 */ void CountInDegree(Graph *graph) VertexType adj; int i, j, count=0; AdjNode *pvertex=null; /* 遍历邻接表 */

155 第 8 章图 145 for(i=0;i<vertexnum;i++) for(j=0;j<vertexnum;j++) pvertex=graph[j].firstadj; while(pvertex!=null) if(pvertex->vertexpos==i) count++; pvertex=pvertex->next; indegree[i]=count; count=0; /*printf("indegree[%d] is: %d\n", i, indegree[i]); */ void topo(graph *graph) int num=0; int i; SeqStack stack; /* 顺序栈 */ VertexType v,firstadj, nextadj; Status s; int pos; CountInDegree(graph); stack.top=-1; for(i=0;i<vertexnum;i++) /* 将入度为 0 的顶点入栈 */ if(indegree[i]==0) GetVertex(graph,i,&v); Push(&stack,v); printf("\n\ntopo List is:"); while(stack.top!=-1) /* 输出入度为 0 的顶点, 并将邻接自该顶点的顶点的入度减 1*/ Pop(&stack,&v); num++; printf("%d", v); s=firstadj(graph,v,&firstadj);

156 146 数据结构概论 while(s!=error) pos=getpos(graph,firstadj); indegree[pos]--; if(indegree[pos]==0) Push(&stack,firstAdj); s=nextadj(graph,v,firstadj,&nextadj); firstadj=nextadj; /* 看是否所有的顶点都被输出 */ if(num<vertexnum) printf("\nfailed! There is a circle in graph."); 在算法 8-4 中, 设 AOE 网有 n 个顶点 e 条边, 算法最初搜索入度为 0 的顶点, 并将这些 顶点入栈的时间为 O(n+e) 进行拓扑排序时, 每个顶点入栈, 出栈时间为 O(n), 同时边表也 被遍历, 时间花费为 O(e) 因此, 拓扑排序算法的时间复杂度为 O(n+e) 对图 G 11 求顶点的拓扑序列的程序代码如下 #include <stdlib.h> #include <stdio.h> #include <conio.h> /* 栈的定义见第 3 章 */ #include "stack.h" #define VertexNum 5 #define EdgeNum 10 #define NULL 0 typedef enumerror=0, OK=1Status; /* 图的邻接表存储结构 */ /* 基于图的邻接表存储结构的各操作的声明及定义 */ /* 拓扑排序算法 */ typedef struct int start; int end; float weight; Edge; /* 图 G 11 的顶点和边 */ int vertice[]=1,2,3,4,5; Edge edges[]=1,2,0,1,3,0,1,4,0,2,5,0,3,2,0,3,5,0,4,5,0, -1,-1,0;

157 第 8 章图 147 /*flag=0 为有向图,flag=1 为无向图 */ void create_graph(graph *graph, int flag) VertexType vertex1, vertex2; int num=vertexnum; int i; printf("\ntotal number of vertex is %d (0 %d).", num, num-1); for(i=0;i<vertexnum;i++) graph[i].vertex=vertice[i]; graph[i].firstadj=null; indegree[i]=0; for(i=0;i<edgenum;i++) vertex1=edges[i].start; vertex2=edges[i].end; if(vertex1==-1 vertex2==-1) break; InsertEdge(graph,flag,vertex1,vertex2,edges[i].weight); void main() Graph graph[vertexnum]; clrscr(); create_graph(graph,0); /* 生成有向图 */ print_graph(graph); topo(graph); free_graph(graph) ; getch(); 运行结果如下

158 148 数据结构概论 8.6 AOE 网及其应用 概念 AOE 网是工程中经常使用的另一种图的模型, 使用该模型可以方便地分析和计算工程中 的关键路径, 从而可以有效地实现对工程进度的管理 若在带权的有向图中, 以顶点表示事件, 有向边表示活动, 边上的权值表示完成该活动 的开销 ( 如该活动所需时间 ), 则称此带权的有向图为用边表示活动的网络 (Activity On Edge, AOE) 如果用 AOE 网表示一项工程, 那么在 AOE 网中要体现出完成各个子工程 ( 用边表示 ) 的确切关系, 列出完成预定工程计划所需进行的活动, 每个活动计划的完成时间, 哪些事件 将要发生, 以及这些事件与活动的关系, 从而可以确定工程是否可行, 估算工程的完成时间, 以及确定哪些活动是影响工程进度的关键活动 对于 AOE 网, 我们所关心的问题是, 完成整个工程至少需要多少时间, 哪些活动是关键 活动, 以及哪些活动的进度是影响整个工程的关键 在 AOE 网中, 只有在某顶点所代表的事情发生后, 从该顶点出发的各有向边所代表的活 动才能够开始 只有在进入某一顶点的各有向边所代表的活动都已经结束后, 该顶点所代表 的事件才能够发生 在图 8-17 中, 事件 4 只有在活动 a 3,a 4 全部完成后才能开始, 而事件 7 只有在活动 a 7 和 a 8 完成后才能开始 在一个表示工程的 AOE 网中, 应该不存在回路, 网中仅存在一个入度为 0 的顶点 ( 事件 ), 称为开始顶点, 它表示整个工程的开始 ; 网中也仅存在一个出度为 0 的顶点 ( 事件 ), 称为结 束顶点, 它表示整个工程的结束 关键路径 1. 相关术语 AOE 网中一条路径的长度是指该路径上各活动所需时间的总和 关键路径是指 AOE 网 中, 从开始顶点到结束顶点之间路径长度最长的路径 如图 8-17 中, 路径 v 1 v 3 v 4 v 5 v 7 就是一条关键路径 由于 AOE 网中的某些子工程 ( 活动 ) 可以同时进行, 要保证每个子工程 都能完成, 完成该工程的最少时间就是该工程 AOE 网的关键路径长度 图 8-17 中, 完成该 工程的最少时间为 =20 对任意图来说, 假设工程的开始点为 v, 从开始顶点 v 到 v i 的路径长度的最大值是事件 v i 的最早发生时间 事件的最迟发生时间是指在不推迟整个工程 完成的前提下, 该事件最迟必须发生的时间 图 8-17 中, 事件 4 的最早发生时间是 9, 事件 2 的最迟发生事件是 9 1=8 活动的最早开始时间是指活动 a j 的起始点所表示的事件的最早发 生时间 活动的最迟开始时间是该活动的终点所表示的事件最迟发生时间与该活动的所需时 间之差 活动 a 3 的最早发生时间是 6, 最迟发生时间是 8 一个活动的最迟开始时间与其最早 开始时间的差额是该活动完成的时间余量 它是在不增加完成整个工程所需总时间的情况下, 活动 a j 可以拖延的时间 当一个活动的时间余量为 0 时, 说明该活动必须如期完成, 否则就

159 第 8 章图 149 会拖延完成整个工程的进度 所以, 我们称最早开始时间和最迟开始时间相同的活动为关键活动 图 8-17 中, 活动 a 2,a 4,a 5,a 7 为关键活动 当时间余量大于 0 时, 活动 a j 不是关键活动, 只要拖延的时间不超过时间余量, 就不会影响整个工程进度 ; 但如果拖延的时间超过时间余量, 则关键活动就可能发生变化 2. 关键路径的确定 图 8-17 AOE 网 G 12 下面介绍如何确定 AOE 网的关键路径 首先定义几个变量 (1) 计算事件 v k 的最早发生时间 Event_e (k) 事件的最早发生时间决定了所有从 v k 发出的有向边所代表的活动能够开始的最早时间 根据 AOE 网的性质, 只有进入 v k 的所有活动 <v j, v k > 都结束时,v k 代表的事件才能发生 ; 而活 动 <v j, v k > 的最早结束时间为 Event_e (j)+weight (<v j, v k >), weight (<v j, v k >) 表示边 <v j, v k > 上的 权, 即活动的开销 因此, 可以用下面的递推公式计算 Event_e (k) Event_e (1)=0 Event_e (k)=maxevent_e (j)+weight (<v j, v k >) <v j, v k > T 2 j n 式中,T 为所有到达 v k 的有向边集合 上述公式是一个从起始顶点开始的向后递推公式 Event_e (k) 的计算必须在 v k 的所有前驱的最早发生时间全部计算出来以后才能够进行, 即要 在拓扑有序的前提下进行 (2) 计算事件的最迟发生时间 Event_l (k) 事件的最迟发生时间可由下面递推公式计算 Event_l(n)= Event_e (n) Event_l(k)=minEvent_l(j) weight (<v k, v j >) 式中,T 为所有由 v k 出发的有向边集合 <v k, v j > T 1 j n 1 上述两个递推公式表明,Event_l(k) 必须在 v k 的所有后继的最迟发生时间求出之后才能够 确定, 即在逆拓扑有序的前提下进行 (3) 计算活动 a j 的最早开始时间 Activity_e (j) 在求出事件的最早发生时间 Event_e (k) 后, 由定义可知, 若活动 a j 由边 <v j, v k > 表示, 则 活动 a j 的最早开始时间 Activity_e (j) 可由下式计算 Activity_e (j)= Event_e (j) (4) 计算活动 a j 的最迟开始时间 Activity_l (j) 在求出事件的最迟发生时间 Event_l (k) 后, 由定义可知, 若活动由边 <v j, v k > 表示, 则活动 a j 的最迟开始时间 Activity_l (j) 可由下式计算 Activity_l (j)= Event_l (k) weight (<v j, v k >) 例如, 求图 8-18(a) 所示的 AOE 网的关键路径结果如图 8-18(b) 所示 拖延关键路径的时

160 150 数据结构概论 间, 将导致整个工程工期的推迟 加快关键活动可以缩短工期, 但并不是加快任何一个关键活动都可以缩短整个工程的完成时间 只有加快那些包含所有的关键路径上的关键活动才能达到这个目的 如果一个活动处在所有的关键路径上, 那么提高这个活动的速度, 就能缩短整个工程的完成时间 注意, 完成时间不能缩短太多, 否则会使原来的关键路径变成不是关键路径, 这时, 必须重新寻找关键路径 例如, 如果把 G 13 中活动 a 4 的速度由原来的 5 天变成 2 天, 那么整个工程完成时间不会缩短 3 天变成 17 天, 而是 18 天, 这是因为这时的关键路径变成 v 1 v 3 v 5 v 7 (a) G 13,AOE 网 (b) 关键路径 3. 计算方法 图 8-18 AOE 网及关键路径 关键路径可以通过首先在拓扑排序基础上计算出 Event_e(k) 和在逆拓扑排序基础上计算 出 Event_l(k), 然后在此基础上计算 Activity_e(j) 和 Activity_l(j), 通过判断 Activity_e(j) 和 Activity_l(j) 是否相等来求出关键路径 拓扑排序仍然通过将入度为 0 的顶点入栈来实现, 而 逆拓扑排序则另外使用一个栈 stackel, 将拓扑排序的出栈的顶点全部入栈 stackel 中, 那么, 将栈 stackel 中的元素全部出栈即可得到顶点的逆拓扑排序 算法 8-5 求关键路径的算法 /* 计算顶点的入度 */ void CountInDegree(Graph *graph) VertexType adj; int i, j, count=0; AdjNode *pvertex=null; /* 遍历整个邻接表 */ for(i=0;i<vertexnum;i++) for(j=0;j<vertexnum;j++) pvertex=graph[j].firstadj; while(pvertex!=null) if(pvertex->vertexpos==i) count++; pvertex=pvertex->next;

161 第 8 章图 151 indegree[i]=count; count=0; /* 计算事件 ( 顶点 ) 的最迟发生时间 */ void ComputeEL(Graph *graph, SeqStack *stackel, int Event_l[]) VertexType v; int pos, adjpos; AdjNode *pvertex=null; /* 在逆拓扑排序的基础上计算 Event_l[]*/ while(stackel->top!=-1) Pop(stackel,&v); pos=getpos(graph,v); pvertex=graph[pos].firstadj; while(pvertex!=null) adjpos=pvertex->vertexpos; /* 调整 Event_l[] 的值 */ if((event_l[adjpos]-pvertex->weight)<event_l[pos]) Event_l[pos]=Event_l[adjpos]-pvertex->weight; pvertex=pvertex->next; Status TopoOrder(Graph *graph, int Event_e[], int Event_l[]) int num=0; int i; SeqStack stackee, stackel; VertexType v, adj; int pos, adjpos; AdjNode *pvertex=null; char flag; stackee.top=-1; stackel.top=-1; CountInDegree(graph); /* 入度为 0 的顶点入栈 */ for(i=0;i<vertexnum;i++) if(indegree[i]==0)

162 152 数据结构概论 GetVertex(graph,i,&v); Push(&stackee,v); printf("\ntopo List is:"); /* 在拓扑排序的基础上计算 Event_e[]*/ while(stackee.top!=-1) Pop(&stackee, &v); /* 为求逆拓扑排序, 在出栈的同时将顶点入到栈 stackel 中 */ Push(&stackel, v); num++; printf("%d", v); pos=getpos(graph, v); pvertex=graph[pos].firstadj; while(pvertex!=null) adjpos=pvertex->vertexpos; indegree[adjpos]--; if(indegree[adjpos]==0) GetVertex(graph, adjpos, &adj); Push(&stackee, adj); /* 调整 Event_e[] 的值 */ if((event_e[pos]+pvertex->weight)>event_e[adjpos]) Event_e[adjPos]=Event_e[pos]+pvertex->weight; pvertex=pvertex->next; for(i=0;i<vertexnum;i++) Event_l[i]=Event_e[VertexNum-1]; /* 检查是否有回路 */ if(num<vertexnum) return ERROR; else ComputeEL(graph,&stackel,Event_l); for(i=0;i<vertexnum;i++) /* 置关键路径标志 */

163 第 8 章图 153 if(event_e[i]==event_l[i]) flag='*'; else flag=' '; printf("\n Event_e[%d]=%d\tEvent_l[%d]=%d\t %c", i,event_e[i], i, Event_l[i], flag); return OK; void CriticalPath(Graph *graph) int Event_e[VertexNum]=0,0,0,0,0,0,0; int Event_l[VertexNum]; int Activity_e, Activity_l; int pos; AdjNode *pvertex=null; VertexType adj; int i; /* 在拓扑排序的基础上, 计算 Event_e 和 Event_l */ if(error==topoorder(graph, Event_e, Event_l)) printf("there is a cycle in graph!\n"); return; printf("\ncritical path is:\n"); for(i=0;i<vertexnum;i++) pvertex=graph[i].firstadj; while(pvertex!=null) Activity_e=Event_e[i]; Activity_l=Event_l[pvertex->vertexPos]-pvertex->weight; /*critical path*/ if(activity_e==activity_l) GetVertex(graph, pvertex->vertexpos, &adj); printf(" (%d, %d):%f\n", graph[i].vertex, adj, pvertex ->weight); pvertex=pvertex->next; 从上述算法可以看出, 对 n 个顶点 e 条边的 AOE 网来说, 求事件和边的最早和最迟发生

164 154 数据结构概论 时间时, 都要访问所有的顶点和边, 所以算法的时间复杂度为 O(n+e) 对图 G 13 求关键路径的程序代码如下 #include<stdlib.h> #include<stdio.h> #include<conio.h> /* 栈的定义见第 3 章 */ #include" stack.h" /* 图的邻接表存储结构 */ /* 基于图的邻接表存储结构的各操作的声明和定义 */ /* 关键路径算法 */ /* 图的生成函数与拓扑排序一节中的相同 */ typedef struct int start; int end; float weight; Edge; /* G 13 的顶点和边及边上的权值 */ int vertice[]=1,2,3,4,5,6,7; Edge edges[]=1,2,5,1,3,8,2,4,6, 3,5,1,3,6,3,4,6,4, 5,7,9,6,7,6,-1,-1,-1 ; void main() int i; VertexType v,firstadj,nextadj; Status s; Graph graph[vertexnum]; SeqStack stack; clrscr(); /* 生成有向图 */ create_graph(graph,0); print_graph(graph); /* 求关键路径 */ CriticalPath(graph); free_graph(graph) ; getch(); 输出结果如下

165 第 8 章图 最短路径 如果图中从一顶点可以到达另一个顶点, 则称这两个顶点之间存在一条路径 对于带权图来说, 通常把一条路径上所经过边的权值之和定义为该路径的路径长度或称带权路径长度 从源点到终点可能存在不止一条路径, 其中的带权路径长度最短的那条路径称为最短路径, 其路径长度 ( 权值之和 ) 称为最短路径长度或者最短距离 对于无权图, 路径长度指的是该路径上所经过的边的数目, 它等于该路径上的顶点数减 1 路径长度最短( 即经过的边数最少 ) 的那条路径称为最短路径, 其路径长度称为最短路径长度或最短距离 实际上, 只要把无权图上的每条边看成是权值为 1 的边, 那么无权图和带权图的最短路径或最短距离的定义是一致的 所以在以后的讨论中, 若不特别指明, 均认为是指带权图的最短路径 求图的最短路径问题用途很广, 例如, 若用一个图表示城市之间的运输网, 图的顶点代表城市, 图上的边对应于城市之间存在运输线, 边上的权表示该运输线上的运输时间或运费, 如何使从一个城市到另一个城市的运输时间最短或者运费最低就是一个求两个城市间的最短路径问题 任意源点到其余各点的最短路径设 G = (V,E) 是一个带权有向图, 对如何求出从 G 的某个源点 s 到其余任意一个顶点的最短路径,Dijkstra 给出了解决此问题的一般方法, 基本思想如下 把图中顶点集合 V 分成两组, 第一组为已求出最短路径的顶点集合, 用 U 表示 ; 第二组为其余未确定最短路径的顶点集合, 用 V U 表示 起初,U 中只含源点 s, 即 U=s 按最短路径长度的递增次序, 依次把 V U 的顶点加入 U 中 在加入过程中, 对每个 v V U 保持一个从源点 s, 仅经过 U 中的点 ( 称为中间点 ), 到达 v 的最短路径 ; 如果不存在这样的路径, 将两顶点间的距离记为, 如此对 U 中所有的顶点 u 确定 s 到 u 的最短路径

166 156 数据结构概论 具体的实施步骤如下 1) 每次从 V U 中选取一个距离最小的顶点 k, 把 k 加入 U 中, 该选定的距离就是 s 到 k 的最短路径长度 2) 以顶点 k 作为新考虑的中间点, 修改源点 s 到 V U 中顶点的距离 若从源点 s, 经过顶点 k, 到顶点 v(v V U) 的距离比原来距离 ( 不经过 k) 短, 则修改源点 s 到 v 的距离值, 修改后的距离值为顶点 s 到 k 的距离加上边 <k,v> 上的权 3) 重复上述过程直到所有顶点都包含在 U 中 例如, 对图 8-19 中的图 G 14, 使用 Dijkstra 算法求出源点 1 到其余各顶点的最短路径的步骤如图 8-20 所示, 图中有阴影的数字为当前选中的源点到该顶点的最短距离 源点中间点 源点到 V U 中顶点的距离 顶点 2 顶点 3 顶点 4 顶点 5 顶点 , , 5, , 5, 2, , 5, 2, 4, 图 8-19 图 G 14 图 8-20 Dijkstra 算法求最短路径 算法 8-6 Dijkstra 算法的实现 int visited[vertexnum]; /* 表示顶点是否在 U 中 */ int distance[vertexnum]; void dijkstra(int begin) int minedge; int vertex; int i,j; int VertexNum; VertexNum=1; visited[begin]=1; for(i=1;i<vertexnum;i++) distance[i]=graph[begin][i]; distance[begin]=0; printf("\n\n Vertex "); for(i=1;i<vertexnum;i++) printf("%5d",i); printf("\n"); printf("\n\n Step %d:", VertexNum); for(i=1;i<vertexnum;i++) printf("%5d",distance[i]); printf("\n"); while(vertexnum <(VertexNum-1))

167 第 8 章图 157 VertexNum++; minedge=max; for(j=1;j<vertexnum;j++) /* 寻找当前的最短路径 */ if((visited[j]==0) && (minedge>distance[j])) vertex=j; minedge=distance[j]; visited[vertex]=1; printf("\n\n Step %d:", VertexNum); for(j=1;j<vertexnum;j++) /* 调整 U 到集合 U-V 中顶点的最短路径 */ if((visited[j]==0) && ((distance[vertex]+graph[vertex][j]) <distance[j])) distance[j]=distance[vertex]+graph[vertex][j]; printf("%5d",distance[j]); printf("\n\n"); 从上面的算法可以看出, 函数体内有两重循环, 所以时间复杂 度为 O(n 2 ) 下面的程序代码应用 Dijkstra 算法求图 8-21 图 G 15 的顶 点 1 到其他顶点的最短距离 以邻接矩阵为图的存储结构 #include <conio.h> #define Max 999 #define VertexNum 7 #define EdgeNum 8 /* 图 G 15 顶点和边 */ int graph[vertexnum][vertexnum]; int edge[edgenum][3]=1,3,10,1,5,30,1,6,100, 2,3,5,3,4,50,4,6,10, 5,4,20,5,6,60; 图 8-21 图 G /*Dijkstra 算法的声明和定义 */ void print_graph() int i,j; printf("vertex"); for(i=1;i<vertexnum;i++)

168 158 数据结构概论 printf("%5d",i); printf("\n\n\n"); for(i=1;i<vertexnum;i++) printf("%5d",i); for(j=1;j<vertexnum;j++) printf("%5d",graph[i][j]); printf("\n\n"); void create_graph(int vertex1, int vertex2, int weight) graph[vertex1][vertex2]=weight; void main() int beginvertex=1; int i,j; clrscr(); for(i=0;i<vertexnum;i++) visited[i]=0; for(i=0;i<vertexnum;i++) for(j=0;j<vertexnum;j++) graph[i][j]=max; for(i=0;i<edgenum;i++) create_graph(edge[i][0],edge[i][1],edge[i][2]); printf("\n\n ## Graph ##\n\n"); print_graph(); printf("\n\n Dijkstra Algorithm:\n"); dijkstra(beginvertex); getch(); 运行结果如下

169 第 8 章图 任意两点间的最短路径求图中每对顶点之间的最短路径是指将图中任意两个顶点之间的最短路径全部计算出来 若图具有 n 个顶点, 则共需求出 n(n 1) 条最短路径 解决这个问题的方法有下面两种 1. 重复调用 Dijkstra 算法依次把带权有向图 G 中 n 个顶点的每一个顶点作为源点重复调用上节所给的 Dijkstra 函数 n 次, 即可求出每一对顶点之间的最短路径 利用该方法求具有 n 个顶点的带权有向图中每对顶点之间的最短路径, 其时间复杂度为 O(n 3 ) 2.Floyd 算法 Floyd 给出了一个解决这个问题的更简单的算法, 其执行时间仍然为 O(n 3 ) 求两顶点之间最短路径的 Floyd 算法的算法描述如下 对有向图 G 的 n 个顶点加以编号, 求图中的两顶点 i 和 j 最短路径的方法仍然从有向图的带权邻接矩阵出发, 若边 <i, j> 在图 G 中, 则从 i 到 j 存在一条长度为 cost(i, j) 的路径, 但它不一定是最短路径, 还需进行 n 次试探才能确定 试探过程如下 1) 首先确定顶点 i 和顶点 j 之间是否存在仅通过顶点 1 的路径, 即在有向图中是否有边 <i, 1> 和 <1, j> 若有, 比较路径 <i, 1>,<1, j> 和路径 <i, j>, 取长度较短者作为当前求得的最短路径, 该路径是中间顶点序号不超过 1 的最短路径 2) 考察从 i 到 j 是否有包含顶点 2 的路径 1 如果没有, 则从 i 到 j 中间顶点序号不大于 2 的最短路径就是前次求出的从 i 到 j, 其中间顶点序号不大于 1 的最短路径 2 若顶点 i 到 j 的路径通过顶点 2, 则从 i 到 j 的中间顶点序号不大于 2 的路径为 <i,, 2,,j>, 它是由 <i,,2> 和 <2,,j> 连起来所构成的路径 ; 而 <i,,2> 和 <2,, j> 为当前找到的中间顶点序号不大于 1 的最短路径 此时, 再将这条新求得的从顶点 i 到 j, 其中间顶点序号不大于 2 的路径与上一次求得的中间顶点序号不大于 1 的最短路径进行比较, 取其较短者为当前求得的中间顶点序号不大于 2 的最短路径 3) 再选择另一顶点 3, 仍按上述步骤进行比较, 再次求得另一条最短路径, 以此类推, 直至最后 n 个顶点试探完毕, 即可求得从 i 到 j 的最短路径 Floyd 算法基本思想如下 递推产生的一个矩阵序列为 A(0), A(1), A(2),, A(n), 其中 A(0) 为给定的带权邻接矩阵, A(k)(1 i,j n) 表示从顶点 i 到顶点 j 的中间顶点序号不大于 k 的最短路径长度 由于图中顶点序号不大于 n, 所以 A(n)(i, j) 就表示了从 i 到 j 的最短路径长度 若从 i 到 j 的路径没有中

170 160 数据结构概论 间顶点, 则对于 1 k n, 有 A ( k )(i, j) = A(0)(i, j) = cost(i, j) 递推产生的 A(0), A(1), A(2),, A(n) 的过程就是逐步允许越来越多的顶点作为路径的中间顶点, 直至找到所有允许作为中间顶点的顶点, 算法就结束, 最短路径也就求出来了 图 8-22 就是应用 Floyd 算法求 G 16 中每对顶点间的最短路径的过程, 图中加粗的数字为经过调整的两顶点间的距离 (a) G 16 (b) (c) (d) (e) (f) 小结 图 8-22 用 Floyd 算法求每对顶点间的最短距离 图是一种较线性表和树更为复杂的数据结构, 也是一种重要的数据结构 它在解决实际问题中有着重要的应用 在图状结构中, 顶点之间的关系可以是任意的, 任意两个数据元素之间都可能相关 本章主要讨论了图的逻辑表示, 在计算机中的存储方法及一些有关图的算法和应用 本章首先介绍了图的概念和图的相关术语 图分为有向图和无向图, 根据这两种不同的分类, 定义了图的相关概念 ; 然后介绍了两种常用的图的存储结构 邻接矩阵表示法和邻接表表示法, 以及基于这两种存储结构的各种图的基本操作的实现 图有两种比较常用的遍历方式 深度优先搜索和广度优先搜索 图可以通过遍历的方式转化成树, 本章讲述了最小生成树构造的两种算法 最后, 介绍了图在实际问题中的应用, 主要包括 : 通过求图的关键路径合理地安排工期 ; 应用 Dijkstra 方法求解单源最短路径问题 ; 以及求解图中任意两点最短路径的 Floyd 算法 习题 1. 任何一个无向连通图的最小生成树 A. 只有一棵 B. 有一棵或多棵 C. 一定有多棵 D. 可能不存在 2. 在一个图中, 所有顶点的度之和等于所有边数的倍 A. 1/2 B. 1 C. 2 D 在一个有向图中, 所有顶点的入度之和等于所有顶点的出度之和的倍 A. 1/2 B. 1 C. 2 D. 4

171 第 8 章图 求最短路径的 Dijkstra 算法的时间复杂度为 A. O(n) B. O(n+e) C. O(n 2 ) D. O(n e) 5. 带权有向图 G 用邻接矩阵 A 存储, 则顶点 i 的入度等于 A 中 A. 第 i 行非无穷元素的个数之和 B. 第 i 列非无穷元素的个数之和 C. 第 i 行非无穷且非零元素的个数 D. 第 i 列非无穷且非零元素的个数 6.n 个顶点的有向完全图中含有向边的数目最多为 A. n 1 B. n C. n(n 1)/2 D. n(n 1) 7. 具有 4 个顶点的无向完全图有 条边 A. 6 B. 12 C. 16 D 有向图 G 用邻接矩阵 A[1 n,1 n] 存储, 其第 i 行的所有元素之和等于顶点 i 的 9. 有 n 个顶点的强连通有向图 G 至少有 条弧 10. 在有 n 个顶点的无向图中, 其边数最多为 11. 如果含有 n 个顶点的图是一个环, 则它有 棵生成树 12. 证明 n 个顶点的无向图的边数的最大值为 n(n 1)/2 13. 设 G 为具有 n 个顶点的无向连通图, 证明 G 至少有 n 1 条边 14. 一个带权连通图的最小生成树是否惟一? 什么情况下可能不惟一? 15. 源点到图中其他各顶点的所有最短路径构成一棵生成树, 该生成树是否为最小生成 树? 为什么? 16. 已知图 G=(V,E),V=1,2,3,4,5,6,E=<1,2>,<1,3>,<2,5>,<3,6>,<6,5>,<5,4>,<6,4>, 试写出图 G 中顶点的所有拓扑序列 17. 求出下图中 AOE 网中的关键路径, 要求标明每个顶点的最早和最迟发生时间, 并画 出关键路径 18. 已知无向图 G,V(G) = 1,2,3,4,E(G) = (1,2), (1,3), (2,3), (2,4), (3,4), 试画出 G 的邻接多表 (adjacency multilists), 并说明若已知点 i, 如何根据邻接多表找到与 i 相邻的点 j? 19. 试编写一算法, 建立无向图 G 的邻接多表, 要求说明算法中主要的数据结构和变量的意义 20. 设无向图 G 有 n 个点 e 条边, 编写一算法建立 G 的邻接多表, 要求该算法的时间复杂性为 O(n+e), 且除邻接多表本身所占空间外只用 O(1) 辅助空间 21. 设图 G 有 n 个点, 利用从某个源点到其余各点最短路径的算法思想, 设计产生 G 的最小生成树的算法 22. 下面是求无向连通图的最小生成树的一种算法,

172 162 数据结构概论 实习 将图中所有边按权重从大到小排序为 (e 1,e 2,,e m ); i=1; While ( 所剩边数 顶点数 ) 从图中删去 e i ; 若图不再连通, 则恢复 e i ; i=i+1; 试证明这个算法所得的图是原图的最小生成树 1. 实验目的掌握图的存储结构, 图的两种遍历方式并加深对两种遍历方式的理解, 能够应用图的遍历方式解决问题 2. 实验内容 (1) 设计一个旅游景点导游程序, 为旅游者提供各种信息查询服务 要求设计某个城市的旅游图, 所含景点不能少于 5 个, 以图中顶点代表实际的旅游景点, 需要存放景点的名称 代码 简介等信息 编写程序为游人提供图中任意景点相关信息的查询, 上机验证 (2)4 皇后问题 设有一个 4 4 的棋盘 ( 即每行每列有 4 个正方形格 ), 用 4 个棋子布在格子中, 要求满足条件 1 任意 2 个棋子不在同一行和同一列上 ; 2 任意 2 个棋子不在同一对角线上 试问有多少种棋局? 编程找出并打印

173 第 9 章排 序 本章要点 : 插入排序交换排序选择排序 K- 路归并排序链式基数排序 9.1 概念及分类 排序也称为分类, 是指将一组记录的无序序列按关键字的大小重新调整为一个有序序列 它是计算机数据处理及其他许多软件系统中常用的一种操作, 排序的目的是为了便于查找和处理, 提高数据检索的效率 通常, 将给定的一组无序记录称为待排记录, 待排序的操作对象称为数据表, 它是数据元素的有限集合 在每一个数据元素中有多个属性, 我们可以按不同的属性值分别对数据表进行排序, 从而得到多个排序文件, 而每个排序文件对应的属性就称为关键字 每个数据元素中应当有一个属性, 其值可以惟一地标识该记录, 该属性就称为主关键字 例如, 在一个学生信息系统中, 学号即为主关键字 ; 按主关键字进行排序, 排序的结果一定是惟一的 次关键字是指待排序的记录序列中可能存在两个或两个以上记录的关键字相等的情况 例如, 学生姓名即为次关键字, 按次关键字进行排序, 则排序结果可能不是惟一的 假设在待排记录的序列中, 有两个数据元素 R i 和 R j, 其关键字分别为 K i 和 K j, 若 K i =K j, 且 K i 排在 K j 的前面 如果在排序之后, 元素 R i 仍在元素 R j 的前面, 则称这种排序方法为稳定的, 否则称这种排序方法为不稳定的 例如, 一组记录的关键字为 72,65,33,58,40, 65,12, 若一种排序方法使排序后的结果为 12,33,40,58,65,65,72, 则此方法是稳定的 ; 如果排序后的结果为 12,33,40,58,65,65,72, 则此方法是不稳定的 根据在排序过程中待排记录是否完全存入内存, 可分为内部排序和外部排序两种排序方法 内部排序是指在排序过程中待排记录全部存放在内存中, 并在内存中调整记录的位置进行排序的方法 ; 外部排序是指待排记录的数量较大, 以至内存中不能一次容纳全部记录时, 将记录的主要部分存放在外存储器中, 通过计算机的内存储器调整外存储器上的记录的位置进行排序的方法 本章将集中讨论内部排序 为了方便后面排序方法的讨论, 我们将等待排序的一组记录存储在地址连续的存储单元中, 其类型定义如下

174 164 数据结构概论 #define Maxsize 100; /* 数据表最大长度 */ typedef int Keytype; /* 定义关键字类型为整型 */ typedef struct Keytype key; /* 关键字 */ M /* 其他数据项 */ Rtype; /* 数据元素类型 */ typedef struct Rtype r[maxsize+1]; /* r[0] 作为监视哨 */ int length; /* 当前数据表长度 */ SList; 排序过程就是按关键字非递减 ( 或非递增 ) 的顺序把一组记录重新排列, 即将上面的记 录数组 r 经过排序操作后, 满足如下关系 : r[1].key r[2].key r[maxsize].key 排序的方法很多, 如果依据各种排序方法的基本处理思想及基本的执行过程来进行分类, 大致可分为插入排序 交换排序 选择排序 归并排序和基数排序 5 类 下面详细介绍每一 种排序方法 9.2 插入排序 插入排序的基本思想是, 每次将一个待排序的记录按其关键字的大小插入到前面已经排好序的有序序列中的适当位置, 直到全部记录插入完成为止 一般来说, 插入排序又可分为直接插入排序, 折半插入排序,2- 路插入排序和希尔排序 4 种 直接插入排序 1. 基本思想直接插入排序是一种最简单的排序方法, 它的基本思想是依次将每个记录插入到一个有序的序列中 假设待排序的记录存放在数组 r[1 n] 中, 在排序过程的某一中间时刻,r 被划分成两个子区间 r[1 i 1] 和 r[i n], 其中, 前一个子区间是已排好序的有序区, 而后一个子区间则是当前未排序的部分, 称为无序区 直接插入排序的操作是将当前无序区的第一个记录 r[i] 插入到有序区 r[1 i 1] 中适当的位置上, 使 r[1 i] 成为新的有序区 它每次使有序区增加一个记录 r[i] 的插入过程就是一趟直接插入排序的过程, 随着有序区的不断扩大, 使 r[1 n] 全部有序, 完成排序操作 2. 排序过程直接插入排序的处理过程是, 初始时, 因为一个记录自然有序, 所以将序列中的第一个记录 r[1] 看成已排好序的有序序列, 无序区为 r[2 n]; 然后将第二个元素 r[2] 插入到这个有序序列中 ; 依此类推, 将第 i 个元素 r[i] 插入到由前 i 1 个元素组成的有序序列中, 直至最后一个元素 r[n] 经过 n 1 次插入后, 原无序序列变成了新的有序序列 若待排序的关键字序列为 63,51,80,63,88,18,25,92, 按上述方法进行直接插

175 第 9 章排序 165 入排序, 为了区别两个相同的关键字 63, 我们在后一个 63 的下面加了一个下划线以示区别 其每一趟排序的结果如图 9-1 所示, 图中用方括号表示当前的有序区 图 9-1 直接插入排序过程 在插入过程中, 应将记录 r[i](i=2,3,,n 1) 插入到当前的有序区, 使得插入后仍 保证该区间里的记录是按关键字有序的 具体做法是, 将待插入记录 r[i] 的关键字从右向左依 次与有序区中的记录 r[j](j=i 1,i 2,,1) 的关键字进行比较, 若 r[j] 的关键字大于 r[i] 的关键字, 则将 r[j] 后移一个位置 ; 若 r [ j ] 的关键字小于或等于 r[i] 的关键字, 则查找过程结束, j+1 即为 r[i] 的插入位置 因为比 r[i] 的关键字大的记录均已后移, 所以 j+1 的位置已经空出, 只要将 r[i] 直接插入此位置即可 3. 算法的实现 下面是一个直接插入排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int index; struct rcdtype int key; int otheritem; ; void straipass(struct rcdtype r[],int i) struct rcdtype look; int j,k,x; look=r[i]; j=i-1; x=r[i].key; while((j>=0) && (x<r[j].key)) r[j+1]=r[j]; j--; r[j+1]=look; printf("\ncurrent sorting result:");

176 166 数据结构概论 for(k=0;k<index;k++) printf("%d",r[k].key); void straisort(struct rcdtype r[]) int i; for(i=1;i<index;i++) straipass(r,i); void main() struct rcdtype r[20]; int temp; int i,j; index=0; clrscr(); printf("\n Please input the values for sorting(end by 0):\n"); scanf("%d",&temp); while(temp!=0) r[index].key=temp; index++; scanf("%d",&temp); straisort(r); printf("\n\n\n Final sorting result is:"); for(i=0;i<index;i++) printf("%d",r[i].key); getch(); 运行结果如下 从上面的算法可以看出, 直接插入排序的算法简单 容易实现, 排序的结果是稳定的 ;

177 第 9 章排序 167 从时间来看, 时间复杂度为 O(n 2 ); 从空间来看, 它只需要一个记录的辅助空间 当待排序记 录的数量 n 很小时, 这是一种很好的排序方法 由于直接插入排序是按顺序进行关键字的比较和记录的移动的, 当 n 较大时, 这种比较 和移动的次数就会很多, 为了减少比较和移动的次数, 可以采用下面的插入排序方法 折半插入排序 1. 基本思想 由于插入排序的基本操作是在一个有序表中进行查找和插入, 在一个有序表中要想找到 插入的位置, 除了用直接插入排序方法中的顺序查找以外, 还可以利用折半查找来实现, 由 此进行的插入排序称为折半插入排序 折半插入排序方法的排序过程与直接插入排序方法的排序过程类似, 只是查找插入位置 的算法不同, 而每一趟的排序结果是相同的, 故在此不再举例 2. 算法的实现 下面是一个折半插入排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int index; struct rcdtype int key; int otheritem; ; void binpass(struct rcdtype r[],int i) struct rcdtype watch; int s,m,j,k,x; watch=r[i]; j=i-1; x=r[i].key; s=0; while(s<=j) m=((s+j)/2); if(x<r[m].key) j=m-1; else s=m+1; for(k=i-1;k>=j+1;k--) r[k+1]=r[k]; r[j+1]=watch;

178 168 数据结构概论 void binsort(struct rcdtype r[]) int i; for(i=1;i<index;i++) binpass(r,i); void main() struct rcdtype r[20]; int temp; int i,j; index=0; clrscr(); printf("\n\n Please input the values for bininsert_sorting(end by 0):\n\n"); scanf("%d",&temp); while(temp!=0) r[index].key=temp; index++; scanf("%d",&temp); binsort(r); printf("\n\n\n Final sorting result: \n\n"); for(i=0;i<index;i++) printf("%d",r[i].key); getch(); 运行结果如下 从上面的算法可以看出, 折半插入排序所需的辅助存储空间和直接插入排序的相同, 即只需要一个记录的辅助存储空间 ; 从时间上比较, 折半插入排序只减少了与关键字进行比较的次数, 而记录的移动次数并没有改变, 因此折半插入排序的时间复杂度仍为 O(n 2 ) 从排序的过程可以知道, 折半插入排序的排序结果是稳定的

179 第 9 章排序 路插入排序 1. 基本思想 2- 路插入排序是对折半插入排序方法的改进 折半插入排序只减少了与关键字进行比较的次数, 并没有改变记录的移动次数, 为了减少在排序过程中移动记录的次数, 可以采用 2- 路插入排序方法 2- 路插入排序方法的基本思想是, 另外设一个与待排记录数组 r 同类型的数组 d, 首先将 r[1] 赋值给 d[1], 并将 d[1] 看成是在排好序的序列中处于中间位置的记录 ; 然后将数组 r 中从第二个记录 r[2] 开始, 依次插入 d[1] 之前或之后的有序序列中 先将待插记录的关键字与 d[1] 的关键字进行比较, 若 r[i].key<d[1].key, 则将 r[i] 插入 d[1] 之前的有序表中 ; 否则, 则将 r[i] 插入 d[1] 之后的有序表中 在设计算法时, 可以把数组 d 看成是一个循环矢量, 并设两个指针 start 和 end 分别表示排序过程中得到的有序序列的开始和结束位置, 待全部记录都按上述过程放入数组 d 中, 则从 start 位置开始依次将 d 中记录放入数组 r 中, 直到 end 位置的记录放入完毕为止 2. 排序过程仍以前面的关键字序列 63,51,80,63,88,18,25,92 为例, 按上述方法进行 2- 路插入排序, 其每一趟排序的结果如图 9-2 所示, 在数组 d 中第 1 趟排序的结果将是将 r[1] 的关键字值 63 赋值给 d[1], 同时令 start 和 end 的位置值都为 1; 第 2 趟排序时, 是将 r[2] 的关键字值 51 与 d[1] 的关键字值 63 比较, 由于 r[2].key<d[1].key, 则将 r[2] 插入到 d[1] 之前的有序表中, 即将 51 放到数组 d 中从 start 位置开始到最后一个位置的有序序列中, 并改变 start 指针的位置值 ; 同理, 在第 3 趟排序时, 是将 r[3] 的关键字值 80 与 d[1] 的关键字值 63 比较, 由于 r[3].key>d[1].key, 则将 r[3] 插入到 d[1] 后面的有序表中, 即将 80 插入到数组 d 中从第 1 个到 end 位置的有序序列中, 并改变 end 指针的位置值 ; 直到第 8 趟排序完成 ; 最后将数组 d 中的记录从 start 位置开始依次放入数组 r 中, 直到 end 位置的记录放完为止 图中用方括号表示当前的两个有序区, 最后形成一个有序区 图 路插入排序过程

180 170 数据结构概论 在 2- 路插入排序中可以看出, 因为在数组 d 中设了两个有序序列, 每次插入只是在一个有序序列中移动记录的位置, 所以说它减少了记录的移动次数, 但它并不能避免记录的移动 并且, 当 r[1].key 是待排序记录中关键字最小或最大的记录时,2- 路插入排序就失去了它的意义 希尔排序 1. 基本思想希尔排序 (Shell sort) 又称缩小增量排序, 它实质上是一种分组插入的排序方法 但在时间和效率上都较前面几种排序方法有较大的改进 从对直接插入排序方法的分析可知, 一方面, 它是一种最简单的排序方法, 在 n 较小时效率也较高 ; 另一方面, 若待排序的记录序列为正序时, 其时间复杂度可提高到 O(n) 希尔排序就是从这两点出发对直接插入排序方法进行改进而得到的一种插入排序方法 希尔排序的基本思想是, 先选取一个小于 n 的整数 d 1 作为第一个增量, 把待排序的全部记录分成 d 1 个组, 所有距离为 d 1 的倍数的记录放在同一个组中 ; 在各组内进行直接插入排序, 然后取第二个增量 d 2, 使 d 2 小于 d 1 ; 重复上述分组和排序, 直至所取的增量 d t =1 为止, 即所有记录放在同一组中进行直接插入排序为止 注意, 增量的取值是递减的 2. 排序过程设待排序的记录有 9 个, 分别为 R 1,R 2,R 3,R 4,R 5,R 6,R 7,R 8 和 R 9, 其关键字序列为 63,51,80,63,88,18,25,92,47, 按上述方法进行希尔排序, 其每一趟排序的结果如图 9-3 所示 图 9-3 希尔排序过程第 1 趟排序时, 首先选取增量 d 1 为 5, 则将初始序列分成 5 个子序列 R 1,R 6,R 2,R 7, R 3,R 8,R 4,R 9 和 R 5, 各组中的第一个记录都自成一个有序区, 然后依次将各组的第二个记录分别插入各组的有序区中, 使每组记录均是有序的, 完成第 1 趟排序 第 2 趟排序时, 选取增量 d 2 为 3, 则将经过第 1 趟排序后得到的序列分成 3 个子序列 R 1,

181 第 9 章排序 171 R 4,R 7,R 2,R 5,R 8 和 R 3,R 6,R 9 ; 同样对每个子序列进行直接插入排序, 得到 3 个有 序序列, 完成第 2 趟排序 第 3 趟排序时, 选取增量 d 3 为 1, 此时对整个序列进行一趟直接插入排序 使待排序列 按关键字有序 3. 算法的实现 下面是一个希尔排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int index; void shellsort(int r[]) int i,j,k; int change; int temp; int len; int process; len=index/2; while(len!=0) for(j=len;j<index;j++) change=0; temp=r[j]; process=j-len; while(temp<r[process] && process>=0 && j<=index) r[process+len]=r[process]; process=process-len; change=1; r[process+len]=temp; if(change!=0) printf("\n Current sorting result:"); for(k=0;k<index;k++) printf("%d",r[k]); len=len/2; void main() int r[20];

182 172 数据结构概论 int temp; int i; clrscr(); index=0; printf("\n Please input the values for shell_sort(end by 0):\n"); scanf("%d",&temp); while(temp!=0) r[index]=temp; index++; scanf("%d",&temp); shellsort(r); printf("\n\n Final sorting result: "); for(i=0;i<index;i++) printf("%d",r[i]); getch(); 运行结果如下 由于希尔排序的执行时间依赖于增量序列, 如何选择一个好的增量序列, 使记录间的比较次数和记录的移动次数达到最少, 这是一个很复杂的问题, 它涉及一些数学上尚未解决的难题 但应注意, 增量序列中的值应该没有除 1 之外的公因子, 并且最后一个增量的值必须等于 1 希尔排序在开始时增量较大, 分组较多, 每组的记录数目较少, 故各组内直接插入排序较快 ; 后来增量值逐渐缩小, 分组数逐渐减少, 而各组的记录数逐渐增多 ; 但由于前面的排序过程已经使记录序列接近于有序状态, 故新的一趟排序的过程也是较快的 所以, 希尔排序在效率上比直接插入排序有较大的改进 希尔排序是不稳定的, 这一点大家可以举例得到验证 9.3 交换排序 在排序的执行过程中, 主要是通过记录间关键字的比较与存储位置的交换来达到排序的

183 第 9 章排序 173 目的, 这种排序方法通常称为交换排序 一般来说, 交换排序又可分为冒泡排序和快速排序两种 冒泡排序 1. 基本思想冒泡排序是交换排序中最简单的排序方法 它的基本思想是, 通过对相邻两个记录的关键字进行比较和交换来实现排序的 首先, 将第 1 个记录的关键字和第 2 个记录的关键字进行比较, 若 r[1].key>r[2].key, 则将两个记录的存储位置交换 ; 然后, 同样地将第 2 个记录的关键字和第 3 个记录的关键字进行比较, 看是否需要交换 ; 依此类推, 直至将第 n 1 个记录的关键字和第 n 个记录的关键字进行比较完为止, 一趟冒泡排序完成, 此时将整个序列中关键字值最大的记录放在了最后的位置 然后进行第 2 趟冒泡排序, 对前 n 1 个记录进行上面同样的操作, 其结果将整个序列中关键字值次大的记录放在了 n 1 的位置上 一般地, 第 i 趟冒泡排序是从 r[1] 到 r[n i+1] 依次比较相邻两个记录的关键字, 若需要交换时则交换两个记录的位置, 其结果是这 n i+1 个记录中关键字最大的记录被交换到 n i+1 的位置上 所以, 判断冒泡排序结束的条件应该是在一趟排序过程中没有进行过记录间的交换操作 因此, 整个排序过程需要进行 k 趟, 其中 1 k<n 2. 排序过程仍以前面的关键字序列 63,51,80,63,88,18,25,92 为例, 按上述方法进行冒泡排序, 其每一趟排序的结果如图 9-4 所示 图 9-4 冒泡排序过程 从上述的执行过程可以看到, 在冒泡排序的过程中, 关键字较小的记录就像是水中的气 泡逐渐向上冒, 而关键字较大的记录就像石块沉入水底, 每一趟都有一块 最大的 石块沉 入水底 在上面的排序过程中, 当执行到第 6 趟时, 由于没有进行记录间的交换, 所以整个排序 过程只需进行 6 趟 3. 算法的实现 下面是一个冒泡排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int index; struct rcdtype

184 174 数据结构概论 ; int key; char otheritem; void bubblesort(struct rcdtype r[]) int i,j; int changeflag=0; struct rcdtype temp; while(changeflag==0) changeflag=1; for(j=index;j>0;j--) for(i=0;i<j-1;i++) if(r[i].key>r[i+1].key) temp=r[i+1]; r[i+1]=r[i]; r[i]=temp; changeflag=0; printf("\n Current sorting result:"); for(i=0;i<index;i++) printf("%d",r[i]); void main() struct rcdtype r[20]; int temp; int i; clrscr(); index=0; printf("\n Please input the data for sorting(end by 0):\n"); scanf("%d",&temp); while(temp!=0) r[index].key=temp; index++; scanf("%d",&temp);

185 第 9 章排序 175 bubblesort(r); printf("\n Sorting result is:\n"); for(i=0;i<index;i++) printf("[%d]",r[i].key); getch(); 运行结果如下 分析冒泡排序的过程可以知道, 最好情况是待排序的记录已经是从小到大排好序的 ( 即正序 ), 则只需进行一趟排序, 最坏情况是待排序的记录为从大到小排好序的 ( 即逆序 ), 则需要进行 n 1 趟排序 在平均情况下, 比较和移动记录的次数大约为最坏情况下的一半 故冒泡排序的时间复杂度为 O(n 2 ) 在冒泡排序中, 由于只是在记录交换时使用了一个辅助存储单元, 所以它的空间复杂度为 O(1) 并且由于值相同的记录之间不会互换位置, 所以冒泡排序是稳定的 快速排序 1. 基本思想快速排序也属于交换排序, 它是对冒泡排序的一种改进 它的基本思想是, 在待排序的序列中任意选择一个记录 ( 通常选择序列中的第一个记录 ), 通过某种方法将该记录放到适当位置上, 使得序列中值小于该记录的所有记录都在该记录的左边, 值大于该记录的所有记录都在该记录的右边, 这样所选择的记录所在位置恰好是它应该在的排序的最终位置上, 同时它将原有序列划分成左 右两个子序列 ( 不包括所选记录 ), 称这个过程为一次划分 ; 然后对这两个子序列分别重复上述划分过程, 直到每个子序列中的记录只有一个为止 这样, 所有记录都在排好序后它们应该处的位置上, 从而使整个序列成为按关键字有序的序列 2. 排序过程假设待排序的序列为 r[s], r[s+1],, r[t], 快速排序方法的排序过程是, 首先任意选取一个记录 ( 通常选择第一个记录 r[s]) 作为基记录, 其一趟快速排序采用从两头到中间扫描的

186 176 数据结构概论 方法, 同时交换与基准记录逆序的记录, 具体方法是设两个整型变量 i 和 j, 它们的初值分别为无序区中第一个记录和最后一个记录的位置, 即 i 的初值为 s,j 的初值为 t 将 r[ s] 放到 r[0] 中作为基准记录, 令 j 从 t 开始向左扫描直至 r[j].key<r[0].key 时, 将 r [ j ] 放到 i 所指的位置上 ; 然后令 i 从 i+1 开始向右扫描直至 r[i].key>r[0].key 时, 将 r[ i] 放到 j 所指的位置上 ; 依次重复上述过程, 直至 i=j, 此时所有 r[k](k=s, s+1,, i 1) 的关键字都小于 r[0].key, 而所有 r [ k ](k=i+1, i+2,, t) 的关键字都将大于 r[0].key, 这样可将 r[0] 中的记录移到 i 所指的位置 r[i] 上, 它将无序序列 r[s], r[s+1],, r[t] 分割成两个子序列 r[s], r[s+1],, r[i 1] 和 r[i+1], r[i+2],, r[t], 完成一趟快速排序过程 经过了一次划分处理后, 将一个序列分割成两个待排子序列 若待排序序列中只有一个元素, 显然已有序, 否则进行一趟处理后再分别对分割所得的两个子序列进行快速排序 子序列的快速排序可通过递归地调用上述过程来实现 仍以前面的关键字序列 63,51,80,63,88,18,25,92 为例, 按上述方法进行快速排序, 其一次划分的处理过程如图 9-5(a) 所示 图 9-5(b) 是分别对各子序列进行每一次划分过程的结果 (a) 一次划分过程 (b) 每一次划分过程 图 9-5 快速排序过程 3. 算法的实现下面是一个快速排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h>

187 第 9 章排序 177 #include <conio.h> int index; struct rcdtype int key; char otheritem; ; void qksort(struct rcdtype r[],int s,int t) int i,j,k; int x; struct rcdtype rp; i=s; j=t+1; rp=r[s]; x=r[i].key; if(s<t) while(i<j) while((i<j)&&(r[j].key>=x)) j=j-1; r[i]=r[j]; while((i<j)&&(r[i].key<=x)) i=i+1; r[j]=r[i]; r[i]=rp; qksort(r,s,j-1); qksort(r,j+1,t); void main() struct rcdtype r[20]; int temp; int i; clrscr(); index=0; printf("\n Please input the datas for sorting(end by 0):\n\n"); scanf("%d",&temp); while(temp!=0) r[index].key=temp;

188 178 数据结构概论 index++; scanf("%d",&temp); qksort(r,0,index-1); printf("\n\n Sorting result is:\n\n"); for(i=0;i<index;i++) printf("%d",r[i].key); getch(); 运行结果如下 分析快速排序的过程可以知道, 它的平均时间复杂度为 O(n lb n), 当 n 较大时, 它是目前平均情况下速度最快的一种排序方法 ; 但当待排序的序列为正序或逆序时, 出现最坏情况, 其时间复杂度为 O(n 2 ) 在快速排序中, 由于每次划分只使用了一个辅助存储单元, 因此它的空间复杂度由递归调用次数来决定 在平均情况下和最好情况下, 其空间复杂度为 O(lb n); 在最坏情况下, 由于需要进行 n 1 次递归处理, 所以其空间复杂度为 O(n) 并且由于记录交换的随机性, 使值相同的记录之间可能会互换位置, 所以快速排序是不稳定的 从上面的分析可以知道, 快速排序适用于元素个数较多的情况 9.4 选择排序 一般来说, 选择排序又可分为简单选择排序, 树型选择排序和堆排序 3 种 简单选择排序 1. 基本思想选择排序是另一种常用的排序方法 它的基本思想是, 每一趟处理都是在 n i+1(i=1, 2,, n 1) 个记录中选取关键字最小的记录与其最后对应位置的记录交换, 作为有序序列中的第 i 个记录 选择排序中最简单的一种是简单选择排序 简单选择排序的处理过程是, 首先从 n 个待排序的记录中选择出关键字最小的记录, 使之与第 1 个位置的记录交换, 作为有序序列中的第 1 个记录, 这是第 1 趟处理过程 ; 然后再从后面的 n 1 个记录中选择出最小的记录并与第 2 个位置的记录交换, 完成第 2 趟处理过程 ; 依此类推, 在第 n 1 趟处理时, 是从最后两个记录 (n 1, n) 中选出关键字小的记录并与第 n 1 个位置的记录交换, 作为该序列的第 n 1 个记录, 整个排序过程结束

189 第 9 章排序 排序过程 仍以前面的关键字序列 63,51,80,63,88,18,25,92 为例, 按上述方法进行简单 选择排序, 其每一趟排序的结果如图 9-6 所示 其中方括号内为有序序列 图 9-6 简单选择排序过程 在上述示例的执行过程中, 每一趟处理是如何找出一个关键字最小的记录呢? 这里设一 个整型变量 p, 其作用是存放在比较过程中当前关键字最小的记录的位置 在第 i 趟排序时, 首先令 p 为 i, 然后依次与第 i+1,i+2,,n 记录的关键字进行比较, 若有关键字值小于 p 位置的记录关键字的值, 则更新 p 中的值, 都比较完后, 将 p 位置的记录和 i 位置的记录交 换, 完成一趟排序过程 3. 算法的实现 下面是一个简单选择排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int index; struct rcdtype int key; int otheritem; ; void smp_selectsort(struct rcdtype r[]) int i,j,k; struct rcdtype temp; for(i=0;i<index-1;i++) k=i; for(j=i;j<index;j++) if(r[j].key<r[k].key) k=j; if(k!=i) temp=r[k];

190 180 数据结构概论 r[k]=r[i]; r[i]=temp; printf("\n Current sorting result:"); for(k=0;k<index;k++) printf("%d",r[k].key); void main() struct rcdtype r[20]; int temp; int i,j,k; index=0; clrscr(); printf("\n Please input the values for sorting(end by 0): \n"); scanf("%d",&temp); while(temp!=0) r[index].key=temp; index++; scanf("%d",&temp); smp_selectsort(r); printf("\n\n Final result is:"); for(i=0;i<index;i++) printf("%d",r[i].key); getch(); 运行结果如下 从上面算法中可以看出, 第 i 次选择需比较 n i 次, 共需要进行 n 1 次选择, 故其时间复 杂度为 O(n 2 ) 在简单选择排序中, 由于只是在元素交换时使用了一个临时单元, 所以其空间复杂度为

191 第 9 章排序 181 O(1) 由于在简单选择排序中存在不相邻记录间的交换, 可能会改变具有相同关键字的记录的 前后位置, 所以其排序结果是不稳定的 简单选择排序适用于记录个数较少的情况 树型选择排序 1. 基本思想 由简单选择排序可见, 选择排序的主要操作是进行关键字间的比较, 因此改进简单选择 排序应从如何减少比较的次数出发 显然, 在 n 个关键字中选出最小值, 至少需进行 n 1 次 比较, 但继续在剩余的 n 1 个关键字中选择次小值就并非一定要进行 n 2 次比较, 若能利用 前 n 1 次比较所得信息, 则可减少以后各趟选择排序中所用的比较次数 实际中, 体育比赛 中的锦标赛便是一种选择排序 例如, 在 8 个运动员参加的乒乓球比赛中, 要想决出前 3 名 至多需要 11 场比赛 如果采用简单选择排序, 则需要进行 7+6+5=18 场比赛 在这里, 经过 第一轮的 4 场比赛之后即可选拔出 4 个优胜者, 然后经过两场半决赛和一场决赛之后, 选拔 出冠军 按照锦标赛的传递关系, 亚军只能产生于分别在决赛 半决赛和第一轮比赛中输给 冠军的选手中, 这样就不需要重复比较了 同理, 选拔第三名的比赛也可以利用前面的比赛 结果, 只需个别比较就可以了 按照这种锦标赛的思想可以引出树型选择排序 树型选择排序又称锦标赛排序, 它是一种按照锦标赛的思想进行选择排序的方法 它的 基本思想是, 首先对 n 个记录的关键字进行两两比较, 然后在其中 n /2 个较小者之间再进行两两比较, 如此重复, 直至选出最小关键字的记录为止, 输出该最小关键字 将最小关键 字记录对应的位置放入最大值, 再将发生变化的记录重新进行比较, 即可找到次小的关键字, 输出该关键字, 这样减少了比较的次数 依此类推, 直到找到最大的关键字且输出为止 2. 排序过程 仍以前面的关键字序列 63,51,80,63,88,18,25,92 为例, 按上述方法进行树型 选择排序, 其每一趟排序的结果如图 9-7 所示 这个过程可用一棵有 n 个叶子结点的完全二叉树表示 例如, 图 9-7(a) 中的二叉树表示从 8 个关键字中选出最小关键字的过程 8 个叶子结点中依次存放排序之前的 8 个关键字, 每个 非终端结点中的关键字均等于其左 右孩子结点中较小的关键字, 则根结点中的关键字即为 叶子结点中的最小关键字, 输出该最小关键字, 即完成一趟选择排序 根据锦标赛的传递关系, 欲选出次小的关键字, 仅需将叶子结点中的最小关键字 (18) 改为 最大值, 然后从该叶子结点开始, 与其左 ( 或右 ) 兄弟的关键字间进行比较, 只需 修改从叶子结点到根的路径上各结点的关键字, 则根结点中的关键字即为次小关键字 25, 输 出该关键字, 如图 9-7(b) 所示 图 9-7(c) 是将叶子结点中的 25 改为 最大值 后, 再从该叶 子结点到根进行比较的过程中找到第三小的关键字 51, 输出该关键字 依此类推, 直到找到 最大的关键字且输出为止, 整个排序过程结束 由于含有 n 个叶子结点的完全二叉树的深度为 lb n+1, 则在树型选择排序中, 除了最小 关键字之外, 每选择一个次小关键字仅需进行 lb n 次比较 因此, 它的时间复杂度为 O(nlbn) 但这种排序方法的缺点是占用的辅助存储空间较多, 并且要与 最大值 进行多余的比较 为此, 对树型选择排序进行改进, 引出下面的堆排序

192 182 数据结构概论 (a) 选择最小关键字的过程 (b) 选择次小关键字的过程 (c) 选择第三小关键字的过程 图 9-7 树型选择排序过程 堆排序 1. 基本思想堆排序 (head sorting) 是一种选择排序的方法, 它利用堆的特性进行排序 堆排序是对树型选择排序方法的改进, 它使用的辅助存储空间较少, 只有一个单元用于交换, 而关键字的比较次数与树型选择排序的相当 堆的定义对于 n 个关键字序列 k 1,k 2,,k n, 以 k 1 为根将关键字序列构成完全二叉树, 且完全二叉树中所有非终端结点的值均不大于 ( 或不小于 ) 其左 右孩子结点的值, 即 k i k 2i k i k 2i+1 或者 k i k 2i k i k 2i+1 i=1,2,, n /2 由此根结点的值 k 1 必为 n 个元素中的最小值或最大值, 分别称满足上述关系的序列为小根堆或大根堆 堆排序的基本思想是, 对 n 个待排记录序列, 首先按关键字建立一个堆 ( 称为初建堆 ), 取出堆顶元素 ( 即序列中最小或最大的元素 ) 后将剩余元素调整成一个新堆 ; 再取出新堆的堆顶元素 ( 即为序列中次小或次大的元素 ) 后将剩余元素调整成一个新堆 ; 如此反复执行以上过程, 直至取出序列中的所有元素 这样, 按元素的取出顺序构成一个有序序列, 即完成了堆排序的过程 2. 排序过程从堆排序的基本思想可知, 在堆排序过程中, 主要要解决初建堆和调整堆两个问题 初建堆是把不符合堆定义的待排序的记录序列, 按关键字排列成符合堆定义的记录序列 它的处理过程是将序列中的所有记录的关键字依次存放到一棵完全二叉树的各个结点中, 这时的完全二叉树中各结点之间的关系不一定满足堆的定义 但是, 由于所有的叶子结点都没有子结点, 因此可以看成这些结点都满足了堆的定义 这样, 我们可以从最后一个非终端结

193 第 9 章排序 183 点开始直至根结点, 依次将各结点调整成堆, 使每个非终端结点都满足堆的定义, 这就完成了建立堆的过程 在初始建立堆的过程和堆排序的过程中可以看到, 它们都离不开调整成堆的过程 对某一个非终端结点 k i 进行调整, 也就是要将以 k i 为根的子树建成堆 由于调整过程是按最后一个非终端结点至根结点的顺序进行的, 因此在对 k i 进行调整时, 序号为 i+1 至 n 的结点都已满足了堆的条件 这时, 如果有 k i k 2i,k i k 2i+1, 说明以 k i 为根的子树已经是堆, 不必进行任何调整 ; 否则就要对其进行调整以满足堆的定义 调整是在 k i,k 2i 和 k 2i+1 之间进行, 调整时先比较 k 2i 和 k 2i+1, 取两者中较小的与 k i 进行交换, 交换后以 k 2i 或 k 2i+1 为根的子树又可能不满足堆的定义, 接下来还要进行调整, 如此一层层地往下调整直至满足堆的条件为止, 这样就将以 k i 为根的子树调整成堆 当所有结点都满足堆的定义后, 输出堆顶元素, 即为整个序列中的最小值, 然后将根结点元素与最后一个叶子结点元素交换, 再将除交换后的叶子结点外的剩余元素所组成的序列重新调整成堆, 重复上述过程, 直至所有元素都输出, 整个堆排序过程结束 例如, 待排序的关键字序列为 88,60,73,45,92,10,31,60, 它所对应的完全二叉树如图 9-8(a) 所示 其最后一个非终端结点是序号为 4 的结点, 在对该结点进行调整时, 由于 45 小于 60, 所以不需要进行交换, 结果如图 9-8(b) 所示 ; 对 3 号结点进行调整时, 要将 73 与 10 和 31 中较小者 10 进行交换, 结果如图 9-8(c) 所示 ; 对 2 号结点进行调整时, 要将 60 与 45 和 92 中的较小者 45 进行交换, 结果如图 9-8(d) 所示 在对根结点进行调整时要将 88 与 45 和 10 中的较小者 10 进行交换, 结果如图 9-8(e) 所示 交换后, 以 88 为根的子树又不满足堆的定义, 还要继续进行调整, 将 88 与 73 和 31 中的较小者 31 进行交换, 最后建成的堆如图 9-8(f) 所示 (a) (b) (c) (d) (e) (f) 图 9-8 建立初始堆的过程 在图 9-8(f) 所示的堆中, 输出堆顶元素 10, 然后以堆中最后一个元素 60 替代之, 如图

194 184 数据结构概论 9-9(a) 所示 此时根结点的左 右子树均为堆, 则只需自上至下进行调整即可 首先, 根结点的值 60 与 45 和 31 中的较小者 31 进行交换, 结果如图 9-9(b) 所示 此时, 以 60 为根的子树也符合堆的定义, 故得到调整后的新堆 此时堆顶为 n 1 个元素中的最小值 重复上述过程, 将堆顶元素 31 与堆中最后一个元素 88 交换且调整后, 得到如图 9-9(c) 所示的新堆 直到所有结点都输出为止, 堆排序完成 (a) (b) (c) 3. 算法的实现 图 9-9 输出堆顶元素并调整成新堆的过程 下面是一个堆排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int index; struct rcdtype int key; char otheritem; ; void sift(struct rcdtype r[],int k,int m) int i,j,x; struct rcdtype t; int finished=0; t=r[k]; i=k; j=2*i; x=r[k].key; while((j<=m) && (finished==0)) if((j<m) && (r[j].key>r[j+1].key)) j++; if(x<=r[j].key) finished=1; else r[i]=r[j];

195 第 9 章排序 185 i=j; j=2*i; r[i]=t; void heapsort(struct rcdtype r[]) int i,j; struct rcdtype temp; for(i=(index/2);i>=1;i--) sift(r,i,index); for(i=index;i>1;i--) temp=r[i]; r[i]=r[1]; r[1]=temp; sift(r,1,i-1); void main() struct rcdtype r[20]; int temp; int i; clrscr(); index=1; r[0].key=0; printf("\n Please input the data for sorting(end by 0):\n\n"); scanf("%d",&temp); while(temp!=0) r[index].key=temp; index++; scanf("%d",&temp); index--; printf("\n Source values:"); for(i=1;i<=index;i++) printf("%d",r[i].key); printf("\n\n"); heapsort(r); printf("sorting result is:\n\n"); for(i=1;i<=index;i++)

196 186 数据结构概论 printf("[%d]",r[i].key); getch(); 运行结果如下 由上例可以看出, 由于堆排序中需要进行不相邻位置间元素的移动和交换, 它是一种不 稳定的排序方法, 时间复杂度为 O(n lb n) 当元素序列的长度 n 很大时, 它是一个很有效的 排序方法 9.5 K- 路归并排序 1. 基本思想 归并排序 (merge sorting) 是指利用归并操作的一种排序方法 归并 是指将两个或两 个以上的有序表组合成一个新的有序表的过程 若将两个有序表合并成一个有序表称为 2- 路 归并, 同理有 3- 路归并 K- 路归并等 我们这里只讨论 2- 路归并方法 2- 路归并排序的基本思想是, 将 n 个元素的初始序列看成 n 个长度为 1 的子序列, 两两 归并, 得到 n /2 个长度为 2 或 1 的子序列, 再两两归并, 直至得到一个长度为 n 的有序序列为止 归并排序的核心操作是将两个有序序列归并成一个有序序列 2. 排序过程 给定关键字序列 67,21,89,56,02,45,32, 进行 2- 路归并排序的执行过程为, 因 序列中共有 7 个元素, 故可看成 7 个长度为 1 的有序子序列, 首先进行第 1 趟归并, 得到 4 个长度为 2 的有序子序列 ( 最后一个除外 ), 然后再进行第二趟归并, 将 4 个有序子序列归并 成 2 个有序子序列, 依此类推, 在进行第 3 趟归并时, 最后将 2 个有序子序列归并成 1 个有 序子序列, 即得到一个长度为 7 的有序序列 其归并执行过程如图 9-10 所示 第 3 趟归并后, 归并完毕, 排序结束 3. 算法的实现 图 路归并排序执行过程 归并算法的处理过程是, 设两个有序记录子序列 r 1 [1 m],r 1 [m+1 n] 归并为有序记录序

197 第 9 章排序 187 列 r 2 [1 n], 设置一个辅助数组 r[n],3 个指针 i, j, k 分别指向 3 个表的当前记录 初始时,i, j, k 分别指向 3 个有序表的第一个元素, 即 i=1,j=m+1,k=1 将待排序列的两个有序表的当前 记录的关键字进行比较, 取出关键字较小的元素作为归并后的一个记录存入 r 中, 并将相应 表的当前指针后移 重复执行直至其中一个有序表中的元素全部取出, 再将另一个有序表剩 余元素放入归并后的有序表中 下面是一个 2- 路归并排序算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int index; struct rcdtype int key; int otheritem; ; void mergetwo(struct rcdtype r[],struct rcdtype r2[],int l,int m,int n) int i,j,k,t; i=l; k=l;j=m+1; while((i<=m) && (j<=n)) if(r[i].key<=r[j].key) r2[k]=r[i]; i++; else r2[k]=r[j]; j++; k++; if(i>m) for(t=j;t<=n;t++) r2[k+t-j]=r[t]; else for(t=i;t<=m;t++) r2[k+t-i]=r[t]; void mergeall(struct rcdtype r[],struct rcdtype r2[],int n,int len)

198 188 数据结构概论 int i,t; int count=0; i=0; while(i<=(n-2*len+1)) mergetwo(r,r2,i,i+len-1,i+2*len-1); i=i+2*len; if(i+len-1<n) mergetwo(r,r2,i,i+len-1,n); else for(t=i;t<=n;t++) r2[t]=r[t]; for(t=0;t<=n;t++) r[t]=r2[t]; for(i=0;i<=n;i++) printf("%d",r2[i].key); count++; if(count==2*len) printf(" "); count=0; printf("\n\n"); void main() struct rcdtype r1[20],r2[20]; int temp; int len; int count; int i; clrscr(); index=0; printf("\n Please input the values for merge_sort(end by 0):\n"); printf(" "); scanf("%d",&temp); while(temp!=0) r1[index].key=temp;

199 第 9 章排序 189 index++; scanf("%d",&temp); for(i=0;i<=19;i++) r2[i].key=0; printf("\n Source values are:"); for(i=0;i<index;i++) printf("%d",r1[i].key); printf("\n\n"); len=1; count=index-1; while(len<index) printf("merge length=%d:",len); mergeall(r1,r2,count,len); len=2*len; if(len<index) printf("merge length = %d:",len); mergeall(r2,r1,count,len); len=2*len; printf("\n Final sorting result:"); for(i=0;i<index;i++) printf("%d",r1[i].key); printf("\n"); getch(); 运行结果如下 归并是通过递归完成记录排序的, 它的时间复杂度为 O(n lb n) 若两个有序表中出现关键 字相等的记录时, 能够使前一个有序表中的记录先被复制, 从而确保它们相对次序不变, 所以 说归并排序算法是稳定的 但它的实现过程需要与待排序记录数量相等的辅助存储空间

200 190 数据结构概论 9.6 基数排序 前面介绍的几种排序方法, 是把关键字作为一个整体, 通过比较关键字的大小来调整记录的位置进行排序的 基数排序与前面介绍的各种排序完全不同, 它是借助于多关键字排序的思想进行排序的 例如, 对一副扑克中的 52 张牌面进行排序, 每张牌有两个关键字 : 花色和面值 (2<3 < <A), 且花色优先于 面值 花色为最高关键字, 面值为最低关键字 整个一副牌的大小依次为 : 梅花 2< 梅花 3< 梅花 A< 方块 2< 方块 3< 方块 A< 红心 2< < 黑桃 A 将扑克牌整理成上述次序有两种方法 第一种方法先按花色分成由小到大的 4 堆, 再将每一堆按面值由小到大排列, 得到要求的排序结果 这种排序方法是先按最高关键字由小到大分成若干序列, 再对每一子序列按次高关键字排序, 逐级细分直至最低关键字, 最后按所有子序列依次连接在一起 这种方法称为最高位优先方法 第二种方法先按面值大小分成 13 堆, 由小到大收集起来, 再按花色将整副牌重新分成 4 堆, 最后将这 4 堆牌由小到大收集起来, 得到要求的结果 这种排序方法是将整个序列先按最低关键字排序, 即先按该关键字的大小分成若干个子序列, 将它们收集起来得到新的序列后, 再按次低关键字排序, 依此类推, 最后按最高关键字排序, 得到一个有序序列 这种方法称为最低位优先方法 1. 基本思想基数排序的基本思想是, 对由多关键字组成的序列, 采用最低位优先方法对该序列进行多关键字排序, 由最低关键字开始, 将整个序列元素 分配 到相应序列中, 再依次重新收集成新的序列, 再由次低关键字开始重新 分配, 再收集, 直至按最高关键字进行 分配, 再收集, 即完成排序的过程 基数排序也称为分配排序 如果待排序的关键字是数值, 值在 [0,999] 范围内, 则把每个待排序的数看成一个关键字 它是由 3 个关键字 (k 1,k 2,k 3 ) 组成 k 1 为百位数, 为最高关键字 ;k 2 为十位数, 为次高关键字 ;k 3 为个位数, 是最低关键字 每位数的取值均为 0~9, 称为基数 具体排序过程是, 先以单链表存储待排序的记录, 表头指向第 1 个记录 第 1 趟处理是, 按最低关键字 ( 个位数 ) 将记录分配至 10( 基数个数 ) 个链队列中, 称为分配 每个队列中记录的关键字的个位数值相等 其中,f[i] 和 e[i] 分别为第 i 个队列的头指针与尾指针 然后改变所有非空队列队尾记录的指针域, 令其指向下一个非空队列的队头记录, 重新将这 10 个队列的记录构成一个链表 这个过程称为收集 再经过第 2 趟分配和收集 ( 按十位 ), 以及第 3 趟分配和收集 ( 按百位即最高位关键字 ), 至此排序完毕 2. 排序过程给定待排序记录的关键字序列为 110,421,185,202,267,69,129,809,404,313, 对其进行基数排序的过程如图 9-11 所示 图 9-11(a) 表示由初始关键字序列连成的链表 第 1 趟分配是对最低位关键字 ( 个位数 ) 进行的, 分别将链表中的关键字分配到 10 个链队列中去, 每个队列中关键字的个位数相等, 如图 9-11(b) 所示, 其中 f [i] 和 e[i] 分别为第 i 个队列的头指针和尾指针 第 1 趟收集是将所有

201 第 9 章排序 191 非空队列的尾指针指向下一个非空队列的队头元素, 从而将 10 个队列中的元素连成一个链表, 如图 9-11(c) 所示 第 2 趟和第 3 趟分配与收集分别是对关键字的十位数和百位数进行的, 其 过程与个位数相同, 如图 9-11(d)~(g) 所示 这样排序完成 (a) 初始状态 (b) 第 1 趟分配后 (c) 第 1 趟收集后 (d) 第 2 趟分配后 (e) 第 2 趟收集后 (f) 第 3 趟分配后 (g) 第 3 趟收集后 图 9-11 基数排序执行过程如果记录的关键字是由 d 位数字或字母组成的, 需要进行 d 趟基数排序 每一趟都要对 n 个记录进行分配和收集, 则基数排序的时间复杂度为 O(d n) 空间复杂度为 O(n) 9.7 内部排序方法的比较 综合上述各种内部排序方法, 每一种排序方法各有优缺点, 很难说出哪一种是最好或最 不好的排序方法 对排序方法的选择应视具体情况而定 我们在前面已经对各种排序方法的 性能做了一些分析, 结果归纳见表 9-1

202 192 数据结构概论 表 9-1 各种排序方法的比较 排序方法 时间复杂度 最好情况平均情况最坏情况 空间复杂度 稳定性 插入排序 O(n) O(n 2 ) O(n 2 ) O(1) 稳定 希尔排序 O(n lb n) O(n lb n) O(n lb n) O(l) 不稳定 冒泡排序 O(n) O(n 2 ) O(n 2 ) O(1) 稳定 快速排序 O(n lb n) O(n lb n) O(n 2 ) O(lb n) 不稳定 选择排序 O(n 2 ) O(n 2 ) O(n 2 ) O(1) 不稳定 堆排序 O(n lb n) O(n lb n) O(n lb n) O(1) 不稳定 归并排序 O(n lb n) O(n lb n) O(n lb n) O(n) 稳定 基数排序 O(d(n+r)) O(d(n+r)) O(d(n+r)) O(n+r) 稳定 时间性能从各种算法的时间复杂度可以分析其时间性能, 由表 9-1 可知每种算法的时间复杂度, 下面对其不同情况下的性能情况进行比较 1) 在平均情况下, 希尔排序法 快速排序法 堆排序法及归并排序法的时间复杂度都是 O(n lb n), 它们都能达到较快的排序速度 进一步分析它们的时间复杂度系数可知, 快速排序法的系数最小, 堆排序法和归并排序法次之, 所以快速排序法最快 插入排序法 冒泡排序法和选择排序法的时间复杂度都是 O(n 2 ), 它们的排序速度较慢, 但如果参加排序的序列开始就局部有序时, 前两种排序方法就能达到较快的排序速度 分析它们的时间复杂度系数, 则直接插入排序法的系数最小, 选择排序法次之 ( 但它的移动次数最小 ), 冒泡排序法最大, 所以插入排序法和选择排序法比冒泡排序法速度快 ; 基数排序法的时间复杂度为 O(d(n+r)), 当结点数比元素的位数大得多时, 可简化成 O(n), 这时, 可以达到较快的排序速度 2) 在最好的情况下, 插入排序法和冒泡排序法的时间复杂度最好, 为 O(n); 其他方法的时间复杂度与平均情况时相同 3) 在最坏的情况下, 快速排序法的时间复杂度为 O(n 2 ), 变为排序最慢的方法 ; 插入排序 希尔排序和冒泡排序虽然与平均情况下相同, 但系数约增加一倍, 所以运行速度将降低 50%; 对于其他排序方法, 最坏情况与平均情况时相差不大 空间性能从表 9-1 中可以知道, 插入排序 希尔排序 冒泡排序 选择排序和堆排序的空间复杂度都是 O(1), 它们所需的辅助存储空间相对较少, 与问题的规模 n 的关系是常量阶关系 ; 基数排序的空间复杂度为 O(n+r), 它所需的辅助存储空间相对较多 ; 而归并排序的空间复杂度为 O(n), 它与问题的规模 n 的关系是线性阶关系 稳定性希尔排序 选择排序 快速排序和堆排序均为不稳定排序, 其他排序为稳定排序 从方法的稳定性比较, 基数排序是稳定的内排方法, 所有时间复杂度为 O(n 2 ) 的简单排序法也是稳定的 ; 但快速排序 堆排序和希尔排序等时间性能较好的排序方法都是不稳定的 一般来说,

203 第 9 章排序 193 排序过程中的 比较 是在 相邻的两个记录关键字 间进行的排序方法是稳定的 需要指出的是, 稳定性是由方法本身决定的, 对不稳定的排序方法而言, 不论其描述形式如何, 总能举出一个说明不稳定的实例来 反之, 对稳定的排序方法, 总能找到一种不引起不稳定的描述形式 由于大多数情况下排序是按记录的主关键字进行的, 因此所用的排序方法是否稳定无关紧要 若排序按记录的次关键字进行, 则应根据问题所需慎重选择排序方法及其描述算法 排序方法的选择 1. 考虑的因素因为不同的排序方法适用于不同的应用环境和要求, 所以选择排序方法时应综合考虑以下因素 待排序的记录数目即问题的规模 n 记录的大小 关键字的结构及其初始状态 对稳定性的要求 选择的语言工具 待排序记录的存储结构 时间复杂度和空间复杂度 2. 排序方法的选择综合考虑上面几个方面, 可以得出如下结论 1) 当待排序的结点数 n 较大, 关键字的值分布比较随机, 并且对排序稳定性没有要求时, 可以选用快速排序法 2) 当待排序的结点数 n 较大, 内存空间又允许, 并且对排序稳定性有要求时, 可以采用归并排序法 3) 当待排序的结点数 n 较大, 关键字值的分布可能出现正序或者逆序, 并且对排序稳定性没有要求时, 可以采用堆排序法或者归并排序法 4) 当待排序的结点数 n 较小, 关键字的值基本有序 ( 升序 ) 或者分布比较随机, 并且对排序稳定性有要求时, 可以采用插入排序法 5) 当待排序的结点数 n 较小, 对排序稳定性没有要求时, 宜采用选择排序方法 ; 若关键字的值不接近逆序, 亦可采用直接插入排序法 6) 基数排序的时间复杂度 O(d n) 当待排序记录 n 值很大而位数小时, 可选用基数排序 综合上述, 没有一种排序方法具有绝对的优势, 因此,n 较小时, 宜采用简单排序 ;n 较大时, 宜采用先进排序法 在实际应用中, 可以将排序方法综合应用, 例如可以将待排序记录序列逐段进行插入, 然后再利用两两归并排序, 直至整个序列有序 小结 本章的基本内容包括插入排序 交换排序 选择排序 归并排序和基数排序等内部排序

204 194 数据结构概论 方法, 并对各种排序方法进行了比较 基本学习要点如下 1) 掌握各种插入排序方法的基本思想 特性及它们之间的差异, 以及各种排序方法的使用前提条件 2) 掌握冒泡排序和简单选择排序方法的基本思想 特点及适用范围 3) 重点掌握各种改进的算法的排序基本思想 特点及其性能分析 4) 掌握各种排序方法的优缺点, 不同算法的时间复杂度 空间复杂度及稳定性的区别, 以及如何选择一种排序算法 习题 1. 对于键值序列 (12,13,11,18,60,15,7,18,25,100), 用筛选法建堆, 必须 从键值为 的结点开始 A. 100 B. 12 C. 60 D 下列排序算法中, 时间复杂度不受数据初始状态的影响, 恒为 O(n lb n) 的是 A. 堆排序 B. 冒泡排序 C. 直接选择排序 D. 快速排序 3. 下列排序算法中, 算法可能会出现下面情况 : 初始数据有序时, 花费的时间反 而最多 A. 堆排序 B. 冒泡排序 C. 快速排序 D. 希尔排序 4. 下列排序算法中, 稳定的排序算法是 A. 选择排序 B. 堆排序 C. 快速排序 D. 直接插入排序 5. 下列排序算法中, 某一趟结束后未必能选出一个元素放在其最终位置上的是 A. 堆排序 B. 冒泡排序 C. 快速排序 D. 直接插入排序 6. 设有 1000 个无序的元素, 希望用最快的速度挑选出其中前 10 个最大的元素, 最好选 用 排序法 A. 冒泡排序 B. 堆排序 C. 快速排序 D. 基数排序 7. 在所有排序方法中, 关键字比较的次数与记录的初始排列次序无关的是 A. 希尔排序 B. 起跑排序 C. 插入排序 D. 选择排序 8. 在待排序的元素序列基本有序的情况下, 效率最高的排序方法是 A. 插入排序 B. 选择排序 C. 快速排序 D. 基数排序 9. 排序方法中, 从未排序序列中挑选元素, 并将其存入已排序序列 ( 初始时为空 ) 的一 端的排序方法为 A. 希尔排序 B. 归并排序 C. 插入排序 D. 选择排序 10. 下面几种排序方法中, 平均查找长度最小的是 A. 插入排序 B. 选择排序 C. 快速排序 D. 归并排序 11. 下面几种排序方法中, 要求内存量最大的是 A. 插入排序 B. 选择排序 C. 快速排序 D. 归并排序 12. 直接选择排序算法所执行的元素交换次数最多为 13. 冒泡排序算法在最好情况下的元素交换次数为 14. 在插入排序 希尔排序 选择排序 快速排序 堆排序 归并排序和基数排序中,

205 第 9 章排序 195 排序是不稳定的有 15. 在堆排序和快速排序中, 若原始数据接近于正序或反序, 则选用 ; 若原始数据无序, 则选用 16. 在插入排序和选择排序中, 若初始数据基本正序, 则选用 ; 若初始数据基本反序, 则选用 17. 有一组关键码序列 (38,19,65,13,97,49,41,95,1,73), 采用冒泡排序方法由小到大进行排序, 试写出每趟的结果 18. 对数据表 (60,20,31,1,5,44,55,61,200,30,80,137,4,35), 写出采用快速排序算法排序的每一趟的结果 19. 已知序列 503,87,521,907,63,654,275,463, 写出采用堆排序法对该序列作降序排列的每一趟的结果 20. 已知序列 503,17,321,97,63,154,275,463,902,462, 写出采用基数排序法对该序列作升序排列的每一趟的结果 21. 已知序列 403,17,521,907,163,654,275,463,426,154,509,612,677, 765,703,94, 写出采用希尔排序法 (d 1 =8) 对该序列作升序排列的每一趟的结果 22. 已知序列 70,83,100,65,10,32,7,9, 请写出采用插入排序算法对该序列作升序排序的每一趟的结果 23. 已知序列 10,18,4,3,6,32,7,9,1,8,12, 请写出采用归并排序算法对该序列作升序排序的每一趟的结果 实习 1. 实验目的掌握常用的排序方法, 并且用高级语言实现各种排序算法 ; 深刻理解排序的定义和各种排序方法的特点, 并且在实际中灵活运用 ; 了解各种方法的排序过程并掌握各种排序方法的时间复杂度和空间复杂度分析 2. 实验内容 (1) 给出 n 个学生的考试成绩表, 每条记录由姓名和分数组成, 试设计不同的算法, 按分数从高到低的次序, 打印出每个学生在考试中获得的名次 其中, 分数相同的为同一名次, 同时按名次列出每个学生的姓名和分数 (2) 设计一个算法, 在含有 n 个元素的堆中增加一个元素, 且调整为一个新堆 (3) 输入若干个国家名称, 设计一个算法, 采用直接选择排序或希尔排序, 按字典顺序将这些国家排序

206 第 10 章查 找 本章要点 : 顺序查找折半查找分块查找二叉排序树 B- 树哈希表 10.1 概念 查找又称检索, 是计算机数据处理中经常使用的一种重要操作, 与我们的日常生活和工作密切相关 例如, 在产品目录表中要查找某一产品, 查找某一电话号码等, 由于查找运算的使用频率很高, 几乎在任何一个计算机系统软件中都会遇到 所以, 当查找所涉及的数据量较大时, 查找方法的效率将直接影响到计算机的使用效率 本章将详细地讨论各种查找方法, 并通过性能分析给出各种查找方法的优缺点及适用情况 通常, 我们称用于查找的数据集合为查找表, 查找表是由同一类型的数据元素或记录组成 在查找表的结构中, 每一个数据元素称为查找对象 例如, 在产品目录中要查找某一产品的例子中, 整个产品的目录表即为查找表, 而产品目录表中的产品名称 生产厂家等任意一个数据项即为查找对象 一般地, 在查找表的每个数据元素或记录中有若干个数据项, 当某一数据项作为查找条件时就称为关键字 其中应有一个关键字能够唯一地标识一个数据元素或记录, 这个数据项就称为主关键字, 其他的数据项则称为次关键字 主关键字可用于标识或识别一个数据元素或记录 所谓查找, 就是根据给定的某个值, 在查找表中确定一个其关键字等于给定值的数据元素或记录 若按主关键字查找, 则查找的结果是唯一的 ; 若按次关键字查找, 则查找结果可能不是唯一的 例如, 在产品目录表中查找某一产品时, 若按产品编号查找, 则可以找到唯一的一种产品, 但若按产品名称 生产厂家或产品规格查找, 则查找结果可能是多个产品记录 查找的结果有两种情况, 若在查找表中存在满足给定条件的数据元素或记录, 则称查找是成功的 此时查找的结果为该记录在查找表中的位置, 或给出该记录的全部信息 否则, 称查找不成功, 此时查找的结果可给出一个 空 记录或 空 指针 例如, 在产品查找系统中, 全部产品的数据信息可以用图 10-1 所示结构存储在计算机中,

207 第 10 章查找 197 表中的每一行为一个数据元素或记录, 产品编号为记录的关键字 假设给定的产品编号是 Bx2000, 则通过查找可得到编号为 Bx2000 的产品的名称 规格 生产厂家和生产日期等信息, 此时查找是成功的 ; 若给定的产品编号是 Bx2003, 由于表中没有关键字为 Bx2003 的记录, 则此时查找不成功 产品编号 产品名称 产品规格 生产厂家 生产日期 Dsj1000 电视机 Qqqq-1 北京 Dsj1001 电视机 Qqqq-2 上海 Bx2000 冰箱 Dddd-78 天津 Bx3000 冰箱 Dddd-78 天津 M M M M M 图 10-1 产品目录信息由于查找是对已存入计算机中的数据进行操作, 所以采用何种查找方法, 首先取决于使用哪种数据结构来表示 表, 即表中的记录是按何种方式组织的 为了提高查找速度, 我们常常使用某些特殊的数据结构来组织表, 或对表预先进行比如排序之类的操作 因此, 在研究各种查找方法时, 首先必须清楚某种查找方法所需要的数据结构是什么, 对表中的关键字的次序有何要求, 是对无序表进行查找还是对有序表进行查找, 然后再选择查找方法进行查找 按查找表的结构, 可将查找表分为静态查找表和动态查找表两类 静态查找表是指在查找过程中只对表进行查找操作, 其结构始终不发生变化的查找表 ; 而动态查找表是指在查找过程中同时插入查找表中不存在的数据元素, 或者从查找表中删除已存在的某个数据元素, 其结构在查找过程中会发生变化的查找表 静态查找表又可分为顺序表和静态树表, 顺序表又可分为无序表和有序表两种 动态查找表则由于其结构在查找过程中会发生变化, 故一般都采用树表的存储方式 用于查找的方法很多, 对于基于关键字的各种查找方法, 其查找过程都是用给定值同每个记录的关键字的值按照一定的次序进行比较的过程 比较次数的多少影响算法的时间复杂度, 它是衡量一个查找算法优劣的重要指标, 通常采用平均查找长度 (Average Search Length,ASL) 来表示, 即在查找成功情况下的平均比较次数 平均查找长度的计算公式可以表示为 n ASL = pc i= 1 其中,n 为查找的数据结构中所包含的数据元素的个数,p i 为查找第 i 个元素的概率,c i 为查找第 i 个元素所需要的比较次数 在查找操作中, 若不特殊说明, 均认为查找每个元素的概率相同, 即 p 1 =p 2 = = p n = 1/n, 在这种情况下, 平均查找长度公式可以简化为 n i i 1 ASL = c i n i= 1 另外, 算法所需存储空间, 算法本身的复杂性也是衡量算法效率的重要指标 但这里我们只对 ASL 进行分析

208 198 数据结构概论 10.2 顺序存储结构查找 一般来说, 顺序存储结构的查找方法有顺序查找 折半查找和分块查找 3 种 顺序查找顺序查找又称为线性查找, 是一种最常用 最简单的查找方法 顺序查找方法既适用于线性表的顺序存储结构, 也适用于线性表的链式存储结构 使用单链表作为存储结构时, 扫描必须从第一个结点开始, 其方法在第 2 章中已经讨论过, 这里只介绍线性表以顺序存储结构存储时顺序查找方法的实现 1. 基本思想顺序查找的基本思想是, 从表的一端开始顺序扫描线性表, 依次将每个记录的关键字和给定的值 k 相比较 若当前记录的关键字与 k 相等, 则查找成功 ; 若扫描结束后仍未找到关键字的值等于 k 的记录, 则查找失败 查找过程可以从线性表中的第一个记录开始, 也可从线性表中的最后一个记录开始 一般情况下, 若从线性表中的第一个记录开始, 则向后逐个用记录的关键字值与给定值 k 进行比较, 若某个记录的关键字值与给定的 k 值相等, 则查找成功, 返回该记录在线性表中的位置 i; 反之, 若直至最后一个记录, 其关键字值与给定值 k 都不相等, 则表示表中没有要找的记录, 查找不成功, 此时返回 0 或其他提示信息 2. 查找过程在关键字序列为 23,67,45,64,32,58,8,37,6,21 的线性表中采用顺序查找方法查找关键字为 58 的记录, 其查找过程如图 10-2 所示 图 10-2 顺序查找过程

209 第 10 章查找 199 此时查找成功, 返回记录的位置 5 如果在上面表中要查找关键字为 88 的记录, 则查找 过程与图 10-2 类似, 直到 i=10 为止, 没有找到, 返回提示信息 3. 类型定义 线性表采用顺序存储结构存储时, 其类型定义如下 #define Maxsize 100 /* 表中最多元素个数, 由用户定义 */ typedef struct KeyType key; /* 关键字定义 */ ElemType data; /* 其他数据元素定义, 它依赖于应用 */ NodeType; typedef NodeType SeqList[Maxsize]; 其中,KeyType 和 ElemType 分别为关键字的数据类型和数据元素的数据类型, 都是由用户定 义的, 可以是任何相应的数据类型,KeyType 默认为 int 类型 4. 算法的实现 下面是一个顺序查找算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> int r[21]=2,12,21,3,23,9,8,54,42,43,14,52,87,71,84,63,26,46,93,45,0; int seqsrch(int r[],int k) int i=0; r[21]=k; while (r[i]!=k) i=i++; return(i); void main() int key,i; int resindex; clrscr(); printf("\n Please input the key value:"); scanf("%d",&key); resindex=seqsrch(r,key); if(resindex<21) printf("\n\n Data[%d]=%d\n",resindex,key); else printf("\n\n Not found this key value!!!"); getch(); 运行结果如下

210 200 数据结构概论 5. 性能分析对于含有 n 个结点的线性表, 结点的查找在等概率的情况下, 即 p 1 = p 2 = = p n = 1/n 时, 成功查找的平均查找长度为 n n 1 1 nn ( + 1) n+ 1 ASL = pc = ci = = n 2 2 i i i= 1 ni= 1 由此可知, 成功查找的平均查找长度约为表长的一半, 查找成功最多需比较 n 次, 查找失败则需比较 n+1 次 所以顺序查找算法的时间复杂度 T(n)= O(n) 顺序查找算法的缺点是, 查找的效率较低, 特别是 n 很大的线性表, 这种方法是非常浪费时间的, 所以它只适合于 n 较小的情况 顺序查找算法的优点是, 它既适用于顺序表, 也适用于单链表, 并且对线性表中元素的排列次序也无任何要求, 算法比较简单 折半查找折半查找又称为二分查找, 是一种效率较高的查找方法 但该方法只适用于线性表的顺序存储结构, 并且要求线性表中结点的关键字必须是已经按从小到大 ( 或从大到小 ) 的顺序排列 通常, 若不特殊说明, 有序表是按关键字值递增的顺序排列的 1. 基本思想折半查找的基本思想是, 设 R[low high] 是当前的查找区间, 待查记录的关键字值为 k 首先确定该区间的中间位置 mid= (low+ high)/2 ; 然后, 将待查的 k 值与 R[mid].key 比较, 若相等, 则查找成功, 返回该元素的位置, 否则需要确定新的查找区间 ; 如果 R[mid].key>k, 则由表的有序性可知, 若表中存在关键字值等于 k 的结点, 则该结点一定落在 mid 左边的子表 R[1 mid 1] 中, 故新的查找区间是左子表 R[1 mid 1], 只要在左子表中继续进行折半查找即可 ; 同理, 如果 R[mid].key<k, 则说明待查记录一定落在 mid 右边的子表 R[mid+1 n] 中, 即新的查找区间是右子表 R[mid+1 n], 只要在右子表中继续进行折半查找即可 这样, 我们可以从初始的查找区间 R[1 n] 开始, 每经过一次与当前查找区间的中间位置上结点的关键字值的比较, 就可以确定查找是否成功, 不成功则当前的查找区间就缩小一半 如此进行下去, 直至找到关键字值等于 k 的结点, 或者直至当前的查找区间为空时, 此时查找失败 2. 查找过程在关键字序列为 5,17,53,65,87,88,92,99,107,188,392 的线性表中, 采用折半查找方法查找关键字为 65 的记录, 其查找过程如图 10-3 所示 此时查找成功, 返回记录的位置 4 如果在上表中要查找关键字值为 68 的记录, 则查找过程与图 10-3 类似, 经过 4 次比较, 都没找到关键字值等于 68 的记录, 则返回 0 或相应的提示信息

211 第 10 章查找 类型定义 图 10-3 折半查找过程 线性表必须采用顺序存储结构存储, 其类型定义如下 #define Maxsize 100 /* 表中最多元素个数, 由用户定义 */ typedef struct KeyType key; /* 关键字的值必须是有序的 */ ElemType data; /* 其他数据元素定义, 由用户定义 */ NodeType; typedef NodeType SeqList[Maxsize-1]; /* 从 0 号单元开始存储 */ 其中,KeyType 和 ElemType 分别为关键字类型和数据元素的数据类型, 都是由用户定义的, 可以是任何相应的数据类型, 这里 KeyType 默认为 int 类型 4. 算法的实现 下面是一个折半查找算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> #define Max 11 int r[max]=5,17,53,65,87,88,92,99,107,188,392; int binsrch(int r[],int k) int mid,low,hig,found; low=0; hig=max-1; found=0; while((low<=hig) && (found==0)) mid=(low+hig)/2; if(k>r[mid]) low=mid+1; else if(k<r[mid]) hig=mid-1;

212 202 数据结构概论 else found=1; if(found==1) return(mid); else return(-1); void main() int key; int index; clrscr(); printf("\n Please input the key value:"); scanf("%d",&key); index=binsrch(r,key); if(index==-1) printf("\n\n Not found this key value!!!"); else printf("\n\n Data[%d]=%d\n",index,key); getch(); 运行结果如下 5. 性能分析从上例 11 个元素的查找过程可知, 如果要查找编号为 6 的元素, 则只需比较 1 次 ; 如果要查找编号为 3 和 9 的元素, 则需要比较 2 次 ; 要查找编号为 1,4,7 和 10 的元素, 则需要比较 3 次 ; 如果要查找其他元素则最多需要比较 4 次 这个查找过程可以用图 10-4 所示的二叉树来描述 树中的每个结点表示查找表中的一个记录, 结点中的值为该记录在表中的位置序号, 通常把这棵描述查找过程的二叉树称为判定树 从判定树中可知, 查找 65 的过程恰好是走了一条从根结点到 4 号结点的路径, 比较的次数恰好是路径上的结点数或 4 号结点在判定树中的层次数 类似地, 找到有序表中的任一记录的过程就是走了一条从根结点到该记录对应结点的路径, 比较的次数恰好是该结点在判定树中的层次数 因图 10-4 折半查找过程对应的判定树

213 第 10 章查找 203 此, 折半查找法在查找成功时进行比较的关键字个数 ( 即比较次数 ) 最多不超过判定树的深度, 而具有 n 个结点的判定树的深度为 lb n +1, 所以折半查找法在查找成功时的比较次数最多为 lb n +1 同理, 从判定树中可知, 若要查找 60, 先与 6 号结点比较, 再与 3 号 4 号结点比较, 此时值为 60 的结点应在 4 号结点的左子树中找, 而左子树为空, 则表明没有找到, 查找不成功, 此时共比较 3 次 类似地, 要查找 100 时, 应先与 6 号结点比较, 再与 9 号 7 号 8 号结点比较, 此时值为 100 的结点应在 8 号结点的右子树中找, 而右子树为空, 则表明没有找到, 查找不成功, 此时共比较 4 次 由此可见, 折半查找法在查找不成功时进行比较的次数最多也不超过判定树的深度, 即 lb n +1 根据二叉判定树, 很容易求出折半查找的平均查找长度 假设表长为 n, 则平均查找长度为 n n 1 i 1 n+ 1 ASL = pc = i g 2 = lb ( n+ 1) 1 n i i i= 1 ni= 1 经推导可知, 对任意 n, 当 n 较大 (n>50) 时, 上式可近似为 ASL = lb (n+1) 1 因此, 折半查找算法的时间复杂度 T(n) = O(lb n) 折半查找算法的缺点是, 需要对 n 个数据元素预先进行排序, 且元素必须采用顺序存储结构形式存储 折半查找算法的优点是, 从算法的时间复杂度来看, 它比顺序查找的速度快 效率高 分块查找分块查找又称为索引顺序查找, 是顺序查找的改进, 其性能介于顺序查找和折半查找之间 分块查找把线性表分成若干块, 其中每一块中的元素存储顺序是任意的, 但块与块之间必须按关键字大小排序 即前一块中的最大关键字值小于 ( 或大于 ) 后一块中的最小 ( 或最大 ) 关键字值, 也就是分块有序 因此, 在此查找方法中除线性表本身以外, 还需要建立一个索引表, 索引表中的一项对应于线性表中的一块, 每个索引项包括关键字项 ( 其值为该块内的最大关键字值 ) 和指针项 ( 指示该块的第一个记录在线性表中的位置 ) 两项内容 索引表按关键字值递增 ( 或递减 ) 顺序排列 1. 基本思想分块查找的基本思想是, 首先确定待查记录应该在哪一块内, 即查找记录所在的块, 然后在该块内查找要找的记录, 如果找到, 则查找成功, 返回记录所在的位置 ; 否则查找失败, 返回 0 或相应提示信息 由于索引表是有序的, 在查找记录所在的块时一般采用折半查找, 但也可以采用顺序查找 ; 而在块内查找要找的记录时, 由于块内记录是无序的, 故只能采用顺序查找方法进行查找 2. 查找过程在关键字序列为 13,34,23,12,5,41,45,43,56,62,80,68,85,70,81,88 的线性表中, 采用分块查找法查找关键字为 56 的记录

214 204 数据结构概论 在整个查找表中共有 16 项, 可分为 3 个块, 对每一块建立一个索引项, 索引表中共建立 3 个索引项, 每一块中的最大关键字分别为 34,62 和 88, 而每块的起始位置分别为 0,5 和 10, 其索引表和查找表如图 10-5 所示 图 10-5 索引表和查找表 查找给定值 k=56 记录的查找过程为, 先在索引表中查找, 即将 k 值与索引表中的最大关 键字相比较, 因为 34<k<62, 则关键字为 56 的记录如果存在, 一定落在第二块中, 且通过 上面查找知道第二块的起始位置为 5 然后就是在块内进行查找, 从第 5 个位置开始顺序查找, 直到第 8 个记录的关键字值与给定值 k 相等, 此时查找成功, 返回记录的位置 8 假如该块中 没有关键字值等于给定值 k 的记录 ( 如 k 为 50), 则直到第 9 个记录时该块的记录查完, 没有 关键字值等于给定值 k 的记录, 则查找不成功, 返回 0 或相应提示信息 3. 类型定义 索引表的类型定义如下 #define Maxsize 20 /* 索引表中最多元素个数, 由用户定义 */ typedef struct KeyType key; /* 关键字定义 */ int start; /* 定义块的起始位置 */ indextype; typedef indextype SeqList[Maxsize]; 数据表采用顺序存储结构存储, 其类型定义如下 #define Maxsize 100 /* 数据表中最多元素个数, 由用户定义 */ typedef struct KeyType key; /* 关键字定义 */ ElemType data; /* 其他数据元素定义, 取决于应用 */ NodeType; typedef NodeType SeqList[Maxsize]; 其中,KeyType 和 ElemType 分别为关键字数据类型和数据元素的数据类型, 都是用户定义的, 可以是任何相应的数据类型, 这里 KeyType 默认为 int 类型 4. 算法的实现 下面是一个分块查找算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> #define Max 20 struct indexchart

215 第 10 章查找 205 int key; int start; int len; b[4]; int blocksrc(int r[],struct indexchart b[],int k,int bnum) int i,j; for(i=0;i<bnum;i++) if(k<=b[i].key) break; if(i==bnum) return -1; j=b[i].start; while(j<b[i].start+b[i].len) if(k==r[j]) break; else j++; if(j<b[i].start+b[i].len) return j; else return -1; void main() int mark; int key,i; int r[20]=13,34,23,12,5,41,45,43,56,62, 80,68,85,70,81,88,86,99,98,90; b[0].key=34; b[0].start=0; b[0].len=5; b[1].key=62; b[1].start=5; b[1].len=5; b[2].key=85; b[2].start=10; b[2].len=5; b[3].key=99; b[3].start=15; b[3].len=5; clrscr(); printf("\n\n Please input the data for Block_search:"); scanf("%d",&key); mark=blocksrc(r,b,key,4); if(mark==-1) printf("\n\n Data is not found!!!"); else printf("\n\n The location is: Data[%d]=%d",mark,key); getch(); 运行结果如下

216 206 数据结构概论 5. 性能分析实际上, 分块查找是进行两次查找, 故整个算法的平均查找长度是两次查找的平均查找长度之和 对于含有 n 个结点的线性表, 分成 m 块, 每块中有 d 个结点, 在等概率情况下, 每块的查找概率为 1/m, 块内每个结点的查找概率为 1/d 故有 ASL lb (m+1) 1+(d+1)/2 lb (n/d+1)+d/2 分块查找算法的优点是, 在线性表中插入或删除一个元素时, 只要找到相应的块, 在该块内进行插入或删除即可 由于块内元素个数相对较少, 而且是任意存储的, 所以插入或删除比较容易, 不需要移动大量的元素 10.3 树存储结构查找 上一节介绍的各种查找表都有一个共同的特点, 就是查找表的结构在查找过程中不发生变化, 因此称为静态查找表 本节将介绍另一类查找表 动态查找表 动态查找表的特点是, 查找表的结构在查找过程中会发生变化 即对于给定值 k, 若表中存在其关键字等于 k 的记录, 则查找成功 ; 否则插入关键字等于 k 的记录 动态查找表可以在查找过程中动态地生成, 即从初始状态的空表开始, 在查找过程中逐渐形成一个完整的查找表 动态查找表的操作除查找以外, 还包括插入 删除和输出等操作 由于查找表在查找过程会发生变化, 故不采用顺序存储结构, 一般采用树存储结构存储 一般来说, 树存储结构的查找方法有二叉排序树和 B- 树两种 二叉排序树二叉排序树又称为二叉查找树, 是一种特殊的二叉树 在一般的二叉树中, 只区分左子树和右子树, 但结点的值是无序的 在二叉排序树中, 不仅区分左子树和右子树, 而且整个树所有结点的值是有序的 1. 定义二叉排序树或者是一棵空树, 或者具有如下特性 1) 若它的左子树不空, 则左子树上所有结点的值均小于它的根结点的值 2) 若它的右子树不空, 则右子树上所有结点的值均大于它的根结点的值 3) 它的左 右子树也分别为二叉排序树 当二叉排序树中每个结点的元素类型为简单类型时, 结点的关键字就是该结点的值 ; 当每个结点的元素类型为记录类型时, 结点的关键字就是该结点的某一个域的值 我们一般讨论结点元素类型为简单类型的情况 图 10-6 为一棵二叉排序树的示例, 其结点元素类型为整

217 第 10 章查找 207 型 2. 查找从二叉排序树的构造方法可以看出, 在一棵二叉排序树中查找关键字值等于给定值的结点, 比在线性表中顺序查找的效率要高得多 其查找过程是, 如果二叉排序树的根指针为空, 则查找失败, 没有与给定值相等的结点 ; 否则, 将给定值与二叉排序树的根结点的关键字值进行比较 如果相等, 则查找成功 ; 否则, 若根结点的关键字值大于给定值, 则在左子树中继续查找 ; 若根结点的关键字值小于给定值, 则在右子树中继续查找, 直到查找成功或者查找失败, 整个查找过程完成 以图 10-6 所示的二叉排序树为例, 若在该二叉排序树中查找关键字值等于 80 的结点, 则查找过程如下 1) 设指针 p 首先指向二叉排序树的根结点, 将 p 所指结点的图 10-6 二叉排序树示例关键字值 51 与给定值 80 比较, 因为 51<80, 说明如果二叉排序树中存在关键字值为 80 的结点, 则一定在根结点的右子树中, 于是, 将 p 指向根结点的右子树 2) 当前 p 所指结点的关键字值为 85, 大于给定值 80, 于是 p 指向当前结点的左子树 3) 当前 p 所指结点的关键字值为 72, 小于给定值 80, 于是 p 指向当前结点的右子树 4) 当前 p 所指结点的关键字值等于 80, 则查找成功, 返回 p 所指结点的存储地址 若在该二叉排序树中查找关键字值等于 82 的结点, 则按上述查找过程继续查找, 因为 80<82, 所以 p 指向当前结点的右子树, 此时右子树为空, 也就是 p 指针为空, 则查找失败, 说明在该二叉排序树中不存在关键字值为 82 的结点, 即没有与给定值相等的结点 3. 建立二叉排序树是一种动态树表, 其结构通常不是一次生成的, 而是在查找过程中, 当树中不存在关键字值等于给定值的结点时再进行插入的 由上面的查找过程可以看出, 新插入的结点一定是一个新添加的叶子结点, 并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点 所以, 若从空树出发, 经过一系列的查找 插入操作之后, 就可以建立一棵二叉排序树 设查找的关键字序列为 60,70,62,30,65,15, 则建立二叉排序树的过程如图 10-7 所示 (a) (b) (c) (d) (e) (f) 图 10-7 二叉排序树的建立过程 对二叉排序树进行中序遍历可以看出, 得到了一个关键字的有序序列 这就是说, 一个 无序序列可以通过建立一棵二叉排序树而变成一个有序序列, 建立树的过程就是对无序序列

218 208 数据结构概论 进行排序的过程 不仅如此, 从上面的插入过程还可以看到, 每次插入的新结点都是二叉排 序树上新的叶子结点, 因此在进行插入时, 不需要移动其他结点, 只需将某个结点的指针由 空变为非空即可 这就相当于在一个有序序列上插入一个新记录而不需要移动其他记录, 同 时它的查找特性又类似于折半查找特性, 所以说, 二叉排序树既具有顺序存储结构的优点, 又具有链式存储结构的特性, 是一种常用的动态查找表 4. 二叉排序树的建立和查找算法 下面是一棵二叉排序树的建立和查找算法的完整的 C 语言程序及程序运行结果 #include <stdio.h> #include <conio.h> #include <stdlib.h> int index; struct bnodetp struct bnodetp *lchild; int data; struct bnodetp *rchild; typedef struct bnodetp treenode; typedef treenode *bintree; bintree inserttree(int r[],int index) bintree tree; if((r[index]==0) (index>20)) return NULL; else tree=(bintree)malloc(sizeof(treenode)); tree->data=r[index]; tree->lchild=inserttree(r,2*index); tree->rchild=inserttree(r,2*index+1); return tree; bintree bintree_search(bintree node,int searchnode) bintree node1,node2; if(node!=null) if(node->data==searchnode) return node; else

219 第 10 章查找 209 node1=bintree_search(node->lchild,searchnode); node2=bintree_search(node->rchild,searchnode); if(node1!=null) return node1; else if(node2!=null) return node2; else return NULL; else return NULL; void main() int r[20]; int temp; int i; int searchnode; bintree root=null; bintree node=null; clrscr(); index=1; printf("\n\n Please input the values to create a binary_tree(exit with 0):\n\n"); scanf("%d",&temp); while(temp!=0) r[index]=temp; index++; scanf("%d",&temp); index--; root=inserttree(r,1); printf("\n\n Please input the value to binarytree_search:"); scanf("%d",&searchnode); node=bintree_search(root,searchnode); if(node!=null) printf("\n The search result: \n\n"); printf("the finding node value is [%d]\n\n",node->data);

220 210 数据结构概论 else printf("\n\n Traversal search result: Not find!!!\n\n"); getch(); 运行结果如下 5. 查找分析从前面的查找和建立两个例子可以看出, 在二叉排序树上查找其关键字值, 等于给定值的结点的过程, 恰好是走了一条从根结点到该结点的路径的过程, 与给定值比较的关键字个数等于路径长度加 1, 或者是该结点所在的层次数 因此, 与折半查找类似, 与给定值比较的关键字个数不超过树的深度 假设查找每个结点的概率是相等的, 则平均查找长度应等于二叉排序树中每个结点所在的层次数之和除以结点的总个数 例如, 图 10-6 所示的二叉排序树的平均查找长度为 ASL = ( )/10 = 31/10 图 10-7 所示的二叉排序树的平均查找长度为 ASL = ( )/6 = 15/ B- 树二叉排序树适用于组织规模较小, 内存中能够容纳的情况 对于较大的必须存放在外存储器上的数据, 用二叉排序树组织索引就不合适 因为若以结点作为内 外存交换的单位, 则在按关键字查找一个结点时, 平均要对外存进行 lb n 次访问 由于外存不仅存取速度比内存慢得多, 而且每次访问时首先要花费大量时间找到数据存放的位置 为了减少对外存的访问次数, 主要采用分块存取技术, 即把外存分成若干个固定大小的块, 称为物理块, 当内 外存交换数据时以物理块为单位, 这样经过一次访问就可以交换一个物理块的数据 本节介绍的 B- 树就是专门为大型数据在外存中存储而设计的一种结构 它也是一种动态的查找树, 是磁盘文件系统索引技术中常用的一种数据结构 1. 定义一棵 m 阶的 B- 树, 或者为空树, 或者为满足下列条件的 m 叉树 1) 树中的每个结点至多有 m 棵子树 2) 若根结点不是叶子结点, 则至少有两棵子树 3) 除根结点以外的所有非终端结点至少有 m /2 棵子树

221 第 10 章查找 211 4) 所有的叶子结点都出现在同一层次上, 并且不带信息 ( 可以看做是外部结点或查找失 败的结点, 实际上这些结点不存在, 指向这些结点的指针为空 ) 5) 所有非终端结点中包含下列信息 : (n, p 0, k 1, p 1, k 2, p 2,, k n, p n ) 其中,n 为结点中关键字的个数, 满足 m /2 1 n m 1;k i(i=1, 2,, n) 为关键字,k i <k i+1 (i=1, 2,, n 1),p i (i=0, 1, 2,, n) 为指向根结点的指针,p i 1 所指向的子树中的关 键字均小于 k i,p i 所指向的子树中的关键字均大于 k i 图 10-8 是一棵 3 阶的 B- 树 图 10-8 一棵 3 阶的 B- 树 2. 查找由 B- 树的定义可知, 在 B- 树上进行查找的过程与二叉排序树的查找过程类似 不同的是, 二叉排序树的结点中含有一个关键字两个指针, 而在 B- 树的结点中可以有 m 1 个关键字和 m 个指针 例如在图 10-8 中, 查找关键字为 88 的过程如下 1) 首先从根结点开始, 根据根结点指针 t 找到结点 a, 因为结点 a 中只有一个关键字, 且给定值 88> 关键字值 50, 则若存在待查找的关键字 88, 必在 a 结点中的指针 p 1 所指向的子树中 2) 顺指针 p 1 找到结点 c, 该结点中有两个关键字 (70,100), 由于 88 在这两个关键字值之间, 因此若存在待查找的关键字 88, 则必在 c 结点中的指针 p 1 所指向的子树中 3) 顺指针 p 1 找到结点 g, 在结点 g 中顺序查找, 找到关键字 88, 至此查找成功 查找不成功的过程也类似, 例如在同一棵树中查找 35, 从根开始, 因为 35<50, 则顺该结点中的指针 p 0 找到结点 b, 又因为 b 结点中只有一个关键字 30, 且 35>30, 所以顺结点中的第二个指针 p 1 找到结点 e, 在 e 结点中有两个关键字 (40,45), 因为 35<40, 则顺该结点中的指针 p 0 往下找, 此时因指针所指为叶子结点, 说明此棵 B- 树中不存在关键字值为 35 的记录, 因此查找失败 由此可见,B- 树的查找过程包括两种基本操作 : 顺指针查找结点和在结点的关键字中进行查找 这两种基本操作是交叉进行的 在结点的关键字中进行查找的方法可以采用顺序查找的方法, 也可以采用折半查找的方法, 因为在每个结点中的关键字是有序的 3. 建立 B- 树也是动态查找树, 因此它的建立过程也是从空树开始, 在查找过程中逐个插入关键

222 212 数据结构概论 字而得到的 但由于 B- 树的关键字个数必须大于等于 m /2 1, 因此每次插入关键字时不是在树中添加一个叶子结点, 而是首先在最低层的某个非终端结点中添加一个关键字 若该结 点的关键字个数不超过 m 1, 则插入完成, 否则将产生结点的 分裂 在插入操作时, 首先要找到要插入的位置, 再判断是否需要 分裂 它包括下面两种情 况 1) 若插入关键字的结点在插入后关键字的个数不超过 m 1, 则将给定的关键字插入到该 结点的相应位置, 插入过程即完成 如图 10-9(a) 所示, 要插入关键字 40, 结点原来有一个关键字 45, 插入 40 后为 2 个, 仍 不超过 m 1=2 个, 故插入过程完成 2) 若插入关键字的结点在插入后关键字的个数超过 m 1, 则进行分裂处理 设当前要插 入关键字的结点指针为 p, 则以中间的关键字为界, 将 p 结点一分为二, 产生一个新结点, 前 面部分仍由 p 指向, 后面部分由 p 1 指向, 而中间的一个关键字带着指针 p 1 被插入到双亲结点 中 ; 若双亲结点中在插入后关键字的个数也超过 m 1, 则需要再次分裂 最坏的情况是一直 分裂到根结点, 建立一个新的根结点, 使整个 B- 树增加一层 如图 10-9(b) 所示, 要插入关键字 32, 结点 p 原来已有两个关键字 40 和 45, 插入 32 后 为 3 个, 超过 m 1=2 个, 因此要分裂成 p 和 p 1 两个结点, 中间的一个关键字 40 带着指针 p 1 被插入到双亲结点中 ; 插入后, 双亲结点中的关键字个数未超过 m 1=2 个, 插入过程即完成 插入后的状态如图 10-9(c) 所示 如果在上面插入处理后, 双亲结点中的关键字个数超过 m 1=2 个, 则必须以该双亲结点 为当前结点, 进行相同的处理, 一旦根结点中的关键字个数超过 m 1=2 个, 如图 10-9(d) 所示, 则对根结点进行分裂处理, 处理后的状态如图 10-9(e) 所示, 整个 B- 树增加一层 (a) 插入后即完成 (b) 插入后分裂 (c) 插入后分裂 (d) 最高层分裂 (e) 最高层分裂 图 阶 B- 树的插入过程 10.4 哈希表查找 哈希表查找的方法不同于前面介绍的几种方法 本节讨论哈希表查找的基本概念 构造 哈希函数的方法 解决冲突的方法和哈希表的查找方法

223 第 10 章查找 基本概念在前面讨论的各种查找方法中, 查找的过程都需要用关键字值与给定的值进行若干次比较判断, 最后确定在数据集中是否存在关键字值等于给定值的结点, 若存在则查找成功, 否则查找失败 ; 在查找过程中, 只考虑各结点的关键字之间的相对大小, 而记录在存储结构中的位置与其关键字没有直接关系 如果在结点的存储位置和关键字之间建立某种直接关系, 那么在进行查找时, 就无需做比较或者进行比较的次数非常少 按照这种关系直接由关键字找到相应的记录, 就是哈希表查找的基本思想 它通过对结点的关键字值进行某种运算, 直接求出结点的地址, 即使用关键字到地址的直接转换方法, 而不需要进行反复比较 哈希表查找法又称杂凑法或散列法 对给定 n 个记录的数据集合, 设置一个长度为 m 的表 A, 可以用一个长度为 m 的一维数组表示 用一个函数 H 把数据集合中 n 个结点的关键字唯一地转换成 0, 1, 2,, m 1 范围内的数值, 即对于集合中任意结点的关键字 k i 都有 0 H(k i ) m 1 (1 i n) 这样, 就可以利用函数 H 将数据集合中的结点映射到表 A 中,H(k i ) 便是 k i 在表中的存储位置 这里,H 是表与结点关键字之间映射关系的函数, 称为哈希函数 在查找操作时, 我们只需对给定的关键字计算出其哈希函数值, 该哈希函数值对应的位置即为其存储位置 但由于集合中各记录关键字的取值可能在一个很大的范围, 所以即使是集合中的记录个数不是很多时, 也很难找到一个合适的哈希函数 H, 使它能保证对于任意不同的 k i 和 k j 都有 H(k i ) H(k j ) 我们把 k i k j, 而 H(k i )=H(k j ) 的现象称为冲突 从上面的分析可以看出, 尽管冲突现象是难免的, 但我们还是希望能够找到尽可能产生均匀映射的哈希函数, 从而尽可能地降低发生冲突的概率 同时, 当发生冲突时, 还必须有相应的解决冲突的方法 因此, 构造哈希函数和建立解决冲突的方法是哈希表查找的两个重要任务 哈希函数的构造方法构造哈希函数的方法很多, 其原则是使哈希地址尽可能均匀地分布在整个地址空间中, 使得用此哈希函数产生的映射所发生冲突的可能性最小, 同时使计算尽量简单, 以节省计算时间 下面介绍几种常用的哈希函数构造方法 1. 直接定址法取关键字本身或关键字的某个线性函数为哈希地址, 该哈希函数为 H(k i ) = ak i +b (a,b 为常量 ) 在使用时, 为了使哈希地址与存储空间一致, 可以调整 a 和 b 的数值 直接定址法的特点是, 计算简单, 一个关键字对应于一个存储地址, 不会产生冲突 这种方法适用于关键字分布连续的情况 但在实际问题中, 由于关键字集合中的元素很少是连续的, 用该方法产生的哈希表会造成空间的浪费 因此, 这种方法很少使用 例如, 在对 1980 年以后出生的人做调查时, 以出生年份作为关键字, 为了将 1980 年出生的人作为哈希表中的第一个记录, 可以构造相应的哈希函数为 H(k i ) = k i 1979

224 214 数据结构概论 则对应的哈希表如图 所示 哈希地址 出生年份 (k i) 人数 图 直接定址法构造哈希表 2. 数字分析法 数字分析法是在预先知道的, 可能出现的一组关键字中, 每个关键字由 n 位数字组成, 如 k 1 k 2 k 3 k n, 选取数字分布比较均匀的若干位作为哈希地址 数字位数的选取与哈希表的 最大长度有关, 也就是与关键字的个数有关 例如, 下面一组关键字, 每个关键字由 8 位十进制数组成 k k k k k k k k k 如果关键字的个数不超过 100, 即哈希表长为 100, 那么只需选取 2 位关键字 我们对上 面关键字的每一位数字进行分析, 发现第 2 位和第 6 位数字分布得比较均匀, 所以取第 2 位 和第 6 位作为哈希地址, 即 H(k 1 )= 94 H(k 2 )= 33 H(k 3 )= 51 H(k 4 )= 8 H(k 5 )= 40 H(k 6 )= 84 H(k 7 )= 25 H(k 8 )= 12 H(k 9 )= 除留余数法 除留余数法是用关键字 k i 除以一个不大于哈希表长度的正整数 p, 所得余数作为哈希地 址的方法 对应的哈希函数 H(k i ) 为 H(k i ) = k i %p 式中 % 表示求余数运算 用该方法产生的哈希函数的优劣取决于 p 值的选取 实践证明, 当 p 取最接近于表长的素数时, 产生的哈希函数较好 该方法的特点是比较简单, 它可以对关键字直接求余, 也可以对其进行其他运算后再求 余 这种方法也是最常用的一种构造哈希函数的方法 具体例子后面介绍 4. 平方取中法 平方取中法是将取关键字平方后的中间几位作为哈希地址 因为一个数的平方数与该数 的每一位数都有关 这里所选取的位数同样与表长有关

225 第 10 章查找 215 例如, 给定一组关键字, 若哈希表长为 1000, 则哈希地址需要选取 3 位, 其关键字 关 键字的平方数和哈希地址如图 所示 关键字关键字的平方数哈希地址 M M M 图 平方取中法构造哈希表 平方取中法适用于关键字中每一位的取值都不太分散, 或分散的位数小于哈希地址所需要的位数的情况 5. 折叠法折叠法是首先把关键字分割成位数相同的数段 ( 最后一段的位数可以少一些 ), 每一段长度的选择与哈希表的表长有关, 然后将这几段的叠加和 ( 舍去最高进位 ) 作为哈希地址的方法 与平方取中法类似, 折叠法也会使关键字的各位值都对哈希地址产生影响 在折叠法中, 数位叠加有移位叠加和间界叠加两种方法 移位叠加是将分割后的每一部分的最低位对齐, 然后相加 ; 间界叠加是从一端向另一端沿分割界来回折叠, 然后对齐相加 例如, 关键字为 k i = , 若哈希表长为 1000, 则每一段的长度为 3, 共分为 4 段, 每段数字为 038,145,603 和 781, 则移位叠加和间界叠加两种方法的叠加过程分别如图 10-12(a) 和 (b) 所示 ) 038 +) H(ki) = 567 H(ki) = 062 (a) 移位叠加 (b) 间界叠加 图 折叠法构造哈希表折叠法适用于关键字位数很多, 而且关键字中每一位上的数字分布大致均匀的情况 解决冲突的方法通过构造哈希函数的过程可以看出, 无论如何构造哈希函数, 在实际应用中冲突是不可避免的 为此, 如何解决冲突也是建立哈希表时不可忽视的方面 假设哈希表的地址集为 0~n 1, 冲突是指由关键字得到的哈希地址的位置上已经有了记录, 而解决冲突就是为该关键字的记录找到另一个新的哈希地址 若新地址的位置上也有记录了, 则再找另一个新的哈希地址, 依此类推, 直至找到一个空的地址为止 在这个过程中, 有可能得到一个地址序列 H i,i = 1,2,,H i 的值在 0~n 1 之间

226 216 数据结构概论 常用的解决冲突的方法有两种 : 开放定址法和链地址法 另外, 也可以采用再哈希法和公共溢出区法处理冲突, 下面分别介绍之 1. 开放定址法开放定址法求下一个新的哈希地址的公式为 H i = (H(k)+d i )%m i = 1,2,,k(k m 1) 其中,H(k) 为关键字为 k 的记录所对应的哈希地址 ( 即发生冲突的地址 ),m 为哈希表表长, d i 为增量序列 根据 d i 的取法不同, 开放定址法分为线性探测法 二次探测法和随机探测法 下面分别介绍之 (1) 线性探测法线性探测法的 d i 的取值为 :d i =1,2,,m 1 即它从发生冲突的地址单元起, 依次探测下一个地址 ( 当探测达到地址为 m 1 的单元时, 再从地址为 0 的单元依次探测 ), 直到遇到空闲地址或探测完所有地址为止 线性探测法处理冲突容易造成元素的聚集, 使探测下一个空闲单元的长度大大增加 造成这种堆积的根本原因是, 探测序列都集中在发生冲突的单元后面, 没有在整个哈希表上分散开 为此, 引入二次探测法 (2) 二次探测法二次探测法的 d i 的取值为 :d i =1 2, 1 2,2 2, 2 2,, 这样使连续两次探测到的单元分别在发生冲突单元的前后两侧, 将探测的单元分散开 (3) 随机探测法随机探测法是指选择一个随机函数产生随机数序列, 并在建立和查找时使用同一随机函数生成随机数序列 例 10-1 已知关键字集合 k=18,73,20,5,68,99,27,41,51,32,25, 设哈希表长 m=13, 哈希函数为 H(k i ) = k i %13, 用线性探测法解决冲突, 试构造该哈希表 为了构造哈希表, 首先要计算哈希地址 若地址未被占用, 则插入新结点 ; 否则进行线性探测 用线性探测法求下一个地址的方法为 H i = (H(k)+d i )%13 i = 1,2,,k d i = 1,2,,m 1 本哈希表的构造过程如下 H(18) = 18%13 = 5 H(73) = 73%13 = 8 H(20) = 20%13 = 7 H(5) = 5%13 = 5 ( 冲突 ) H 1 (5) = (5+1)%13 = 6 H(68) = 68%13 = 3 H(99) = 99%13 = 8 ( 冲突 ) H 1 (99) = (8+1)%13 = 9 H(27) = 27%13 = 1 H(41) = 41%13 = 2 H(51) = 51%13 = 12

227 第 10 章查找 217 H(32) = 32%13 = 6 ( 冲突 ) H 1 (32) = (6+1)%13 = 7 ( 再冲突 ) H 2 (32) = (6+2)%13 = 8 ( 再冲突 ) H 3 (32) = (6+3)%13 = 9 ( 再冲突 ) H 4 (32) = (6+4)%13 = 10 H(25) = 25%13 = 12 ( 冲突 ) H 1 (25) = (12+1)%13 = 0 存放后的哈希表如图 所示 哈希地址 关键字 图 用线性探测解决冲突构造哈希表 2. 链地址法链地址法是把具有相同哈希地址的关键字值放在同一个链表中, 称为同义词链表, 将该链表的头指针存储在相应的哈希地址对应的存储单元中 假设某哈希函数产生的哈希地址在 0~m 1 区间, 则用数组 hash[m] 存放 m 个链表的头指针 所有哈希地址为 i 的记录都插入头指针为 hash[i] 的链表中, 在链表中的插入位置可以任意, 但要保持同义词在同一线性表中按关键字有序 例 10-2 已知关键字集合 k=18,73,20,5,68,99,27,41,51,32,25, 设哈希表长 m=13, 哈希函数为 H(k i ) = k i %13, 用链地址法解决冲突, 并构造该哈希表 插入新结点时, 将结点插入链表中, 并保持同义词在同一链表中关键字的值从小到大有序 本哈希表的构造过程如下 H(18) = 18%13 = 5 H(73) = 73%13 = 8 H(20) = 20%13 = 7 H(5) = 5%13 = 5 H(68) = 68%13 = 3 H(99) = 99%13 = 8 H(27) = 27%13 = 1 H(41) = 41%13 = 2 H(51) = 51%13 = 12 H(32) = 32%13 = 6 H(25) = 25%13 = 12 存放后的哈希表如图 所示 3. 再哈希法再哈希法的基本思想是, 当发生冲突时, 用另一个哈希函数得到下一个哈希地址, 如果再发图 用链地址法解决冲突构造哈希表

228 218 数据结构概论 生冲突, 则再使用另一个哈希函数, 直至不发生冲突为止 为此, 需要预先设置一个哈希函 数的序列 H i = RH i (k) i = 1,2,,k 其中,RH i 分别是不同的哈希函数, 每次发生冲突时使用不同的哈希函数, 直到没有冲突发生 这种方法的优点是不易产生聚集, 但增加了计算的时间 4. 公共溢出区法 公共溢出区法的基本思想是, 假设哈希函数的值域为 0~m 1, 定义矢量 hash[0 m 1] 为基本表, 其中每个元素存放一个记录 ; 另外定义一个矢量 over[0 v] 为溢出表 当不发生冲 突时, 记录按相应的哈希地址存入基本表中 ; 如果发生冲突, 不论哈希地址是什么, 都按顺 序存入溢出表中 查找方法 1. 查找过程 哈希表的查找过程与造表过程基本相同, 对于给定值 k, 按照造表时使用的哈希函数求出 哈希地址 如果表中此位置上没有记录, 则查找不成功 ; 否则, 用该位置上记录的关键字值 与 k 值比较, 若与 k 值相等, 则查找成功 ; 否则根据造表时使用的解决冲突的方法找下一个 地址, 直至哈希表中的某个位置为空, 说明查找不成功, 或者是某个位置记录的关键字值等 于给定值 k 为止, 说明查找成功 例如, 在例 10-1 中查找给定值为 99 的查找过程为, 首先按给定的哈希函数求出 99 的哈 希地址 H(99)=8, 因为地址为 8 的单元不空且关键字值不等于 99, 说明发生了冲突, 则按要 求使用线性探测法解决冲突, 求出的地址为 9, 这时 9 号单元不空且关键字值等于 99, 则查 找成功, 返回记录在表中的序号 9 查找给定值为 42 的查找过程为, 首先求出 42 的哈希地址 H(42)=3, 因为地址为 3 的单元 不空且关键字值不等于 42, 则按线性探测法解决冲突, 求出下一个地址为 4, 由于 4 号单元 是空记录, 则说明在表中不存在关键字值等于 42 的记录 采用其他解决冲突的方法时, 其查找过程与上面类似, 这里不再说明 2. 建立和查找算法 下面是给定一组关键字序列 13,2,45,32,12,16,48,56,72,29,21,84, 按照 哈希函数 H(k i ) = k i %13 和线性探测法解决冲突的方法建立哈希表, 以及在此哈希表中查找 k 值为 72 的算法及其运行结果 #include <stdio.h> #include <stdlib.h> #define Max 16 #define hashmax 13 int data[max]=13,2,45,32,12,16,48,56,72,29,21,84; int hashtable[hashmax]; int hash_mod(int key) return (key % hashmax);

229 第 10 章查找 219 int collision(int i) int d=1; return((i+d)%hashmax); int createtable(int key) int i,adr; int collisiontime=0; int hashtime=0; adr=hash_mod(key); while(hashtime<hashmax) if(hashtable[adr]==0) else hashtable[adr]=key; printf("\n\n key %d is assigned in address %d\n\n",key,adr); for(i=0;i<hashmax;i++) printf("[%d]",hashtable[i]); return 1; collisiontime++; adr=collision(adr); hashtime++; return 0; int hashsrch(int table[],int k) int j; j=hash_mod(k); if(table[j]==null) return NULL; else if(table[j]==k) return j; else while((table!=0) && (table[j]!=k)) j=collision(j); if(table[j]==0)

230 220 数据结构概论 return 0; else return j; void main() int keyvalue; int index; int i; int temp; int result; index=0; 运行结果如下 printf("\n Input data:"); for(i=0;i<hashmax;i++) printf("[%d]",data[i]); for(i=0;i<hashmax;i++) hashtable[i]=0; while(index<max) if(createtable(data[index])) printf("\n\n SUCCESS for creating HashTable!!!\n\n"); else printf("\n\n FAILED for creating HashTable!!!\n\n"); index++; printf("\n\n HashTable is :\n\n"); for(i=0;i<max;i++) printf("[%d]",hashtable[i]); printf("\n\n Please input the key for searching:"); scanf("%d",&temp); result=hashsrch(hashtable,temp); if(result!=0) printf("\n\n The address of this key is %d",result); else printf("\n\n Not find this given key!"); getch();

231 第 10 章查找 221 小结 本章的基本内容包括 : 线性表的顺序查找, 折半查找, 分块查找, 二叉排序树查找, B- 树查找和哈希表查找 基本学习要点如下 1) 掌握各种查找方法的特性及它们之间的差异, 知道在什么前提条件下使用哪种查找方法 2) 重点掌握顺序查找 折半查找和分块查找的基本算法 3) 重点掌握构造哈希函数的方法和解决冲突的方法 习题 1. 对有 18 个元素的有序表作折半查找, 则查找 A[3] 的比较序列的下标为 A. 1,2,3 B. 9,5,2,3 C. 9,5,3 D. 9,4,2,3 2. 折半查找法要求查找表中各元素的关键字值必须是 排列 A. 递增或递减 B. 递增 C. 递减 D. 无序 3. 顺序查找法适合于存储结构为 的线性表 A. 散列存储 B. 顺序存储或链式存储 C. 压缩存储 D. 索引存储 4. 对线性表进行折半查找时, 要求线性表必须 A. 以顺序方式存储 B. 以链式方式存储 C. 以顺序存储方式存储, 且结点按关键字有序排序 D. 以链式方式存储, 且结点按关键字有序排序 5. 如果要求一个线性表既能较快地查找, 又能适应动态变化的要求, 可以采用 查 找方法

Microsoft Word - 专升本练习2:线性表.doc

Microsoft Word - 专升本练习2:线性表.doc 第二章 线性表 一 选择题 1. 线性表是 ( ) A. 一个有限序列, 可以为空 B. 一个有限序列, 不能为空 C. 一个有限序列, 可以为空 D. 一个无序序列, 不能为空 2. 对顺序存储的线性表, 设其长度为 n, 在任何位置上插入或删除操作都是等概率 插入一个元素 时大约要移动表中的 ( ) 个元素, 删除一个元素时大约要移动表中的 ( ) 个元素 A. n/2 B. (n+1)/2 C.

More information

2.3 链表

2.3  链表 数据结构与算法 ( 二 ) 张铭主讲 采用教材 : 张铭, 王腾蛟, 赵海燕编写高等教育出版社,2008. 6 ( 十一五 国家级规划教材 ) https://pkumooc.coursera.org/bdsalgo-001/ 第二章线性表 2.1 线性表 2.2 顺序表 tail head a 0 a 1 a n-1 2.4 顺序表和链表的比较 2 链表 (linked list) 通过指针把它的一串存储结点链接成一个链

More information

Microsoft PowerPoint - ch3.pptx

Microsoft PowerPoint - ch3.pptx 第 3 章栈和队列 第 3 章栈和队列 3.1 栈 3.2 栈的应用举例 3.3 队列 哈尔滨工业大学 ( 威海 ) 计算机科学与技术学院 (2014/2015 学年秋季版 ) 1 本章重点难点 第 3 章栈和队列 重点 : (1) 栈 队列的定义 特点 性质和应用 ;(2)AT 栈 AT 队列的设计和实现以及基本操作及相关算法 难点 : (1) 循环队列中对边界条件的处理 ;(2) 分析栈和队列在表达式求值

More information

Microsoft Word - 数据结构实训与习题725xdy.doc

Microsoft Word - 数据结构实训与习题725xdy.doc 第一部分学习指导与实训 3 第 2 章线性表 2.1 学习指南 (1) 理解线性表的类型定义, 掌握顺序表和链表的结构差别 (2) 熟练掌握顺序表的结构特性, 熟悉顺序表的存储结构 (3) 熟练掌握顺序表的各种运算, 并能灵活运用各种相关操作 (4) 熟练掌握链式存储结构特性, 掌握链表的各种运算 2.2 内容提要 线性表的特点 : 线性表由一组数据元素构成, 表中元素属于同一数据对象 在线性表中,

More information

正文.doc

正文.doc 第 3 章 栈 实验三 3.1 实验目的及要求 1. 理解特殊的线性结构 顺序栈的抽象数据类型的定义, 及其在 C 语言环境中的表示方法 2. 理解顺序栈的基本操作的算法, 及其在 C 语言环境中一些主要基本操作的实现 3. 在 C 语言环境下实现顺序栈的应用操作 : 1 利用栈实现十进制数转换成八进制数 2 利用栈实现一位数的加减乘除的表达式求解 3.2 实验内容 经过对实验目的及要求的分析, 本实验仍然采用首先描述栈的基本操作集函数,

More information

C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 5 月 3 日 1

C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 5 月 3 日 1 C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 月 3 日 1 1 INPUTOUTPUT 1 InputOutput 题目描述 用 cin 输入你的姓名 ( 没有空格 ) 和年龄 ( 整数 ), 并用 cout 输出 输入输出符合以下范例 输入 master 999 输出 I am master, 999 years old. 注意 "," 后面有一个空格,"." 结束,

More information

<4D F736F F D B8BDBCFE4220D7A8D2B5BBF9B4A1D3EBBACBD0C4BFCEB3CCC3E8CAF62E646F6378>

<4D F736F F D B8BDBCFE4220D7A8D2B5BBF9B4A1D3EBBACBD0C4BFCEB3CCC3E8CAF62E646F6378> B212CC: 数据结构与算法 课程描述 0 课程基本信息 课程编号 : B212CC 课程名称 : 数据结构与算法英文名称 : Data Structures and Algorithms 英文简称 : DSA 预备课程 : 计算系统基础 离散数学授课时间 : 二年级第一学期时间分配 : 课堂教学 (48 课时 )+ 实验安排 (48 课时 )+ 课后作业与阅读 (48 课时 ) 学分数 : 3

More information

40 第二部分试题部分 9. 假设栈初始为空, 将中缀表达式 a/b+(c*d-e*f)/g 转换为等价的后缀表达式的过程中, 当扫描 到 f 时, 栈中的元素依次是 ( ) 2014 年全国试题 2(2) 分 A. +(*- B. +(-* C. /+(*-* D. /+-* 10. 循环队列存放

40 第二部分试题部分 9. 假设栈初始为空, 将中缀表达式 a/b+(c*d-e*f)/g 转换为等价的后缀表达式的过程中, 当扫描 到 f 时, 栈中的元素依次是 ( ) 2014 年全国试题 2(2) 分 A. +(*- B. +(-* C. /+(*-* D. /+-* 10. 循环队列存放 第 3 章栈和队列 39 第 3 章 栈和队列 一 选择题 1. 为解决计算机主机与打印机之间速度不匹配问题, 通常设置一个打印数据缓冲区, 主机将要 输出的数据依次写入该缓冲区, 而打印机则依次从该缓冲区中取出数据 该缓冲区的逻辑结 构应该是 ( ) 2009 年全国试题 1(2) 分 A. 栈 B. 队列 C. 树 D. 图 2. 设栈 S 和队列 Q 的初始状态均为空, 元素 a, b, c,

More information

数据结构 Data Structure

数据结构 Data Structure 数据结构 : 线性表 Data Structure 2016 年 3 月 15 日星期二 1 线性表 栈和队列 线性表 字典 ADT 栈 队列 2016 年 3 月 15 日星期二 2 线性表 定义 : 线性表 L 是 n 个数据元素 a 0,a 1, a n-1 的有限序列, 记作 L=(a 0,a 1, a n-1 ) 其中元素个数 n(n 0) 定义为表 L 的长度 当 n=0 时,L 为空表,

More information

PowerPoint Presentation

PowerPoint Presentation 数据结构与算法 ( 三 ) 张铭主讲 采用教材 : 张铭, 王腾蛟, 赵海燕编写高等教育出版社,2008. 6 ( 十一五 国家级规划教材 ) http://www.jpk.pku.edu.cn/pkujpk/course/sjjg 第 3 章栈与队列 栈 栈的应用 递归到非递归的转换 队列 2 栈 (Stack) 操作受限的线性表 运算只在表的一端进行 队列 (Queue) 运算只在表的两端进行

More information

图书在版编目穴 CIP 雪数据做事细节全书 / 赵彦锋编著郾 北京 : 企业管理出版社, ISBN Ⅰ 郾做... Ⅱ 郾赵... Ⅲ 郾工作方法 通俗读物 Ⅳ 郾 B 中国版本图书馆 CIP 数据核字 (2005) 第 号 书

图书在版编目穴 CIP 雪数据做事细节全书 / 赵彦锋编著郾 北京 : 企业管理出版社, ISBN Ⅰ 郾做... Ⅱ 郾赵... Ⅲ 郾工作方法 通俗读物 Ⅳ 郾 B 中国版本图书馆 CIP 数据核字 (2005) 第 号 书 做事细节全书 赵彦锋著 企业管理出版社 图书在版编目穴 CIP 雪数据做事细节全书 / 赵彦锋编著郾 北京 : 企业管理出版社, 2005.11 ISBN 7-80197-338-0 Ⅰ 郾做... Ⅱ 郾赵... Ⅲ 郾工作方法 通俗读物 Ⅳ 郾 B026-49 中国版本图书馆 CIP 数据核字 (2005) 第 136676 号 书 名 : 做事细节全书 作 者 : 赵彦锋 责任编辑 : 吴太刚

More information

Microsoft PowerPoint - DS_Ch2.ppt [兼容模式]

Microsoft PowerPoint - DS_Ch2.ppt [兼容模式] 数据结构 Ch.2 线性表 计算机学院 肖明军 Email: xiaomj@ustc.edu.cn http://staff.ustc.edu.cn/~xiaomj 2.1 线性表的逻辑结构 线性表 : 由 n(n 0) 个结点 a 1,, a n 组成的有限序列 记作 :L = (a 1, a 2,, a n ), 属性 : 长度 ---- 结点数目 n,n=0 时为空表 a i ---- 一般是同一类型

More information

湖北工业大学二 八年招收硕士学位研究生试卷 则从顶点 A 出发进行深度优先遍历可以得到的序列是 : A.ACEDBFG B.ACDGFBE C.AECDBGF D.ABDGFEC 9 在对 n 个元素的序列进行排序时, 堆排序所需要的附加存储空间是 ( ) A. O(log 2 n) B. O(1)

湖北工业大学二 八年招收硕士学位研究生试卷 则从顶点 A 出发进行深度优先遍历可以得到的序列是 : A.ACEDBFG B.ACDGFBE C.AECDBGF D.ABDGFEC 9 在对 n 个元素的序列进行排序时, 堆排序所需要的附加存储空间是 ( ) A. O(log 2 n) B. O(1) 二 八年招收硕士学位研究生试卷 试卷代号 917 试卷名称数据结构 1 试题内容不得超过画线范围, 试题必须打印, 图表清晰, 标注准确 2 考生请注意 : 答案一律做在答题纸上, 做在试卷上一律无效 一 单项选择题 ( 在每小题列出四个供选择的答案 A B C D 中, 选一个正确的答案, 将其代号填在答卷纸相应题号后的下横线上, 每小题 2 分, 共 20 分 ) 1 以下术语与数据的存储结构无关的是(

More information

PowerPoint Presentation

PowerPoint Presentation 第 章 栈与队列 本章主题 : 栈和队列的应用 教学目的 : 掌握栈和队列的应用方法, 理解栈的重要作用 教学重点 : 利用栈实现行编辑, 利用栈实现表达式求值 教学难点 : 利用栈实现表达式求值 2011-10-18 1 .1 ADT 栈 ( 定义和运算 ) 1.. 栈的定义 栈 stack 是一种特殊的 ( 有序表 ) 线性表, 插入 或删除栈元素的运算只能在表的一端进行, 称运算 的一端为栈顶,

More information

《C语言程序设计》教材习题参考答案

《C语言程序设计》教材习题参考答案 教材名称 : C 语言程序设计 ( 第 1 版 ) 黄保和 江弋编著清华大学出版社 ISBN:978-7-302-13599-9, 红色封面 答案制作时间 :2011 年 2 月 -5 月 一 选择题 1. 设已定义 int a, * p, 下列赋值表达式中正确的是 :C)p=&a 2. 设已定义 int x,*p=&x;, 则下列表达式中错误的是 :B)&*x 3. 若已定义 int a=1,*b=&a;,

More information

Microsoft PowerPoint - 2线性表.ppt [兼容模式]

Microsoft PowerPoint - 2线性表.ppt [兼容模式] 2 线性表 董洪伟 http://hwdong.com 1 主要内容 线性表的类型定义 即抽象数据类型 顺序实现 即用一连续的存储空间来表示 链式实现 即用链表实现 一元稀疏多项式 链表实现 2 线性表的类型定义 线性表 n 个元素的有限序列 数据项 元素 ( 记录 ) 姓名 学号 性别 年龄 班级 健康状况 王小林 790631 男 18 计 91 健康 陈红 790632 女 20 计 91 一般

More information

2

2 孙猛 http://www.math.pku.edu.cn/teachers/sunm 2017 年 9 月 18 日 课程主 页 : http://www.math.pku.edu.cn/teachers/sunm/ds2017/ 作业通过 course.pku.edu.cn 提交 2 线性表的概念和抽象数据类型 顺序表示 链接表示 3 4 线性表 ( 简称为表 ) 是零个或多个元素的有穷序列列

More information

《C语言程序设计》第2版教材习题参考答案

《C语言程序设计》第2版教材习题参考答案 教材 C 语言程序设计 ( 第 2 版 ) 清华大学出版社, 黄保和, 江弋编著 2011 年 10 月第二版 ISBN:978-7-302-26972-4 售价 :35 元 答案版本 本习题答案为 2012 年 2 月修订版本 一 选择题 1. 设已定义 int a, * p, 下列赋值表达式中正确的是 :C)p = &a A. *p = *a B. p = *a C.p = &a D. *p =

More information

PowerPoint Presentation

PowerPoint Presentation 数据结构与算法 ( 六 ) 张铭主讲 采用教材 : 张铭, 王腾蛟, 赵海燕编写高等教育出版社,2008. 6 ( 十一五 国家级规划教材 ) http://www.jpk.pku.edu.cn/pkujpk/course/sjjg 第 6 章树 C 树的定义和基本术语 树的链式存储结构 子结点表 表示方法 静态 左孩子 / 右兄弟 表示法 动态表示法 动态 左孩子 / 右兄弟 表示法 父指针表示法及其在并查集中的应用

More information

Microsoft PowerPoint - 1绪论.ppt [兼容模式]

Microsoft PowerPoint - 1绪论.ppt [兼容模式] 1 绪论 董洪伟 http://hwdong.com 主要内容 什么是数据结构 定义 内容 基本术语 数据 : 数据对象 数据元素 数据项 数据结构 : 逻辑结构 物理结构 抽象数据类型 定义 表示 算法和算法分析 算法的概念 算法复杂度 什么是数据结构 程序 = 数据结构 + 算法 Pascal 之父,Niklaus Wirth 数据结构 : 问题的数学模型 数据表示 算法 : 处理问题的策略 数据处理

More information

Microsoft PowerPoint - 第+2+章+线性表[check].ppt [兼容模式]

Microsoft PowerPoint - 第+2+章+线性表[check].ppt [兼容模式] 教学内容 1 线性表的定义和性质及基本运算 2 线性表的顺序存储结构 3 线性表的链式存储结构 4 多项式的代数运算 线性结构的特点 : 数据元素的非空有限集 存在唯一的一个被称作 第一个 的数据元素 ; 存在唯一的一个被称作 最后一个 的数据元素 ; 除第一个之外的数据元素均只有一个前驱 ; 除最后一个之外的数据元素均只有一个后继 例 : 法学系 8523101 第一个 数据元素 国贸系 8522105

More information

Microsoft PowerPoint - ds-1.ppt [兼容模式]

Microsoft PowerPoint - ds-1.ppt [兼容模式] http://jwc..edu.cn/jxgl/ HomePage/Default.asp 2 说 明 总 学 时 : 72( 学 时 )= 56( 课 时 )+ 16( 实 验 ) 行 课 时 间 : 第 1 ~14 周 周 学 时 : 平 均 每 周 4 学 时 上 机 安 排 待 定 考 试 时 间 : 课 程 束 第 8 11 12 章 的 内 容 为 自 学 内 容 ; 目 录 中 标 有

More information

没有幻灯片标题

没有幻灯片标题 指针作为函数参数 : 原因 : 1 需要修改一个或多个值,( 用 return 语句不能解决问题 ) 2 执行效率的角度 使用方法 : 在函数原型以及函数首部中需要声明能够接受指针值的形参, 具体的写法为 : 数据类型 * 形参名 如果有多个指针型形参, 则用逗号分隔, 例如 : void swap(int *p1, int *p2) 它说明了形参 p1 p2 是指向整型变量的指针 在函数调用时,

More information

型来实现 首先进行输入 然后将数据存储在结构体类型中 最后根据需要进行输出 任务实现 定义一个结构体类型实现学生成绩信息的存储! "# $ "%!$&& 输入三个学生的成绩 "' "' "'"# " 学号 姓名 成绩 输出三个学生成绩 "%!$&& "''' "# 程序运行结果如图 所示 图 简单学

型来实现 首先进行输入 然后将数据存储在结构体类型中 最后根据需要进行输出 任务实现 定义一个结构体类型实现学生成绩信息的存储! # $ %!$&& 输入三个学生的成绩 ' ' '#  学号 姓名 成绩 输出三个学生成绩 %!$&& ''' # 程序运行结果如图 所示 图 简单学 项目目标知识目标 理解和掌握结构中的基本概念 理解和掌握线性结构 树形结构和图形结构的概念 以及二元组的表示方法 理解算法评价的规则 算法时间复杂度和空间复杂度的概念 以及数量级的表示方法 技能目标 具有对现实世界的数据进行抽象表示的能力 具有对算法时间复杂度和空间复杂度进行简单分析的能力 素质目标 正确认识计算机中数据的表示与存储方法 培养团队协作精神 培养分析问题解决问题的能力 任务 简单学生成绩管理系统

More information

《计算概论》课程 第十九讲 C 程序设计语言应用

《计算概论》课程 第十九讲  C 程序设计语言应用 计算概论 A 程序设计部分 字符数组与字符串 李戈 北京大学信息科学技术学院软件研究所 lige@sei.pku.edu.cn 字符数组的定义 #include int main() char a[10] = 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' ; for (int i = 0; i < 10; i++) cout

More information

立 志 于 打 造 最 贴 近 考 生 实 际 的 辅 导 书 计 算 机 考 研 之 数 据 结 构 高 分 笔 记 率 辉 编 著 周 伟 张 浩 审 核 讨 论 群 :15945769

立 志 于 打 造 最 贴 近 考 生 实 际 的 辅 导 书 计 算 机 考 研 之 数 据 结 构 高 分 笔 记 率 辉 编 著 周 伟 张 浩 审 核 讨 论 群 :15945769 立 志 于 打 造 最 贴 近 考 生 实 际 的 辅 导 书 计 算 机 考 研 之 数 据 结 构 高 分 笔 记 率 辉 编 著 周 伟 张 浩 审 核 讨 论 群 :15945769 前 言 在 计 算 机 统 考 的 四 门 专 业 课 中, 最 难 拿 高 分 的 就 是 数 据 结 构 但 是 这 门 课 本 身 的 难 度 并 不 是 考 生 最 大 的 障 碍, 真 正 的 障 碍

More information

试卷代号 :1253 座位号 E 口 国家开放大学 ( 中央广播电视大学 )2014 年秋季学期 " 开放本科 " 期末考试 C 语言程序设计 A 试题 2015 年 1 月 E 四! 五 总分! 一 单选题 ( 每小题 2 分, 共 20 分 ) 1. 由 C 语言源程序文件编译而成的目标文件的默

试卷代号 :1253 座位号 E 口 国家开放大学 ( 中央广播电视大学 )2014 年秋季学期  开放本科  期末考试 C 语言程序设计 A 试题 2015 年 1 月 E 四! 五 总分! 一 单选题 ( 每小题 2 分, 共 20 分 ) 1. 由 C 语言源程序文件编译而成的目标文件的默 试卷代号 :1253 座位号 E 口 国家开放大学 ( 中央广播电视大学 )2014 年秋季学期 " 开放本科 " 期末考试 C 语言程序设计 A 试题 2015 年 1 月 E 四! 五 总分! 一 单选题 ( 每小题 2 分, 共 20 分 ) 1. 由 C 语言源程序文件编译而成的目标文件的默认扩展名为 ( ) A. cpp B. c C. exe D. obj 2. 设 x 和 y 均为逻辑值,

More information

Microsoft PowerPoint - Ch3 [兼容模式]

Microsoft PowerPoint - Ch3 [兼容模式] Ch.3 栈和队列 1 3.1 栈 定义和运算 栈 仅在表的一端插 删的线性表插入 进 ( 入 ) 栈 删除 出 ( 退 ) 栈 栈顶 插删的一端 栈底 另一端 结构特征 -- 后进先出 修改原则 : 退栈者总是最近入栈者 服务原则 : 后来者先服务 (LIFO 表 ) 例 : 入栈出栈 a n a 2 a 1 2 3.1 栈 Note: 后入栈者先出栈, 但不排除后者未进栈, 先入栈者先出栈 an,,

More information

download.kaoyan.com_2006ÄêÌì½ò¹¤Òµ´óѧ¸ß¼¶ÓïÑÔ³ÌÐòÉè¼Æ£¨409£©¿¼ÑÐÊÔÌâ

download.kaoyan.com_2006ÄêÌì½ò¹¤Òµ´óѧ¸ß¼¶ÓïÑÔ³ÌÐòÉè¼Æ£¨409£©¿¼ÑÐÊÔÌâ 考生注意 : 本试卷共七大题, 满分 150 分 考试时间为 3 小时 ; 所有答案均写在答题纸上 ( 注明题号 ), 在此答题一律无效无效 一 选择题 ( 本题共 20 小题, 每小题 2 分, 满分 40 分 ) 1 char ch 1 2 A 0

More information

FJXBQ

FJXBQ 高等医学院校选用教材 ( 供成人教育中医药专业 中西医结合专业使用 ) 方剂学 闫润红 主编 2 0 0 1 内容简介本书是供成人教育中医药专业 中西医结合专业使用的教材 全书分总论和各论两部分, 总论部分对中医方剂的基本理论, 如治法 君臣佐使 剂型 剂量等及其现代研究进展进行了介绍 各论部分对常用方剂的主治病证 配伍意义 临床应用 加减变化规律及现代研究概况等内容, 按分类进行了系统阐述 在保证方剂学学科知识结构完整性的前提下,

More information

Microsoft PowerPoint - 4.pptx

Microsoft PowerPoint - 4.pptx 第 4 章栈和队列 运算受限的线性表 栈 表达式求值 搜索与回溯 队列 队列的应用 4.1 栈 只在称为栈顶 (top) 的一端插入和删除的线性表 另一端称为栈底 (bottom) 数据通过栈的顺序 后进先出 (LIFO) top bottom a n-1 a n-2 a 0 栈的抽象数据类型 class Stack { public: Stack ( ) { ; ~Stack ( ) { ; int

More information

C++ 程序设计 OJ9 - 参考答案 MASTER 2019 年 6 月 7 日 1

C++ 程序设计 OJ9 - 参考答案 MASTER 2019 年 6 月 7 日 1 C++ 程序设计 OJ9 - 参考答案 MASTER 2019 年 6 月 7 日 1 1 CARDGAME 1 CardGame 题目描述 桌上有一叠牌, 从第一张牌 ( 即位于顶面的牌 ) 开始从上往下依次编号为 1~n 当至少还剩两张牌时进行以下操作 : 把第一张牌扔掉, 然后把新的第一张放到整叠牌的最后 请模拟这个过程, 依次输出每次扔掉的牌以及最后剩下的牌的编号 输入 输入正整数 n(n

More information

第四章 102 图 4唱16 基于图像渲染的理论基础 三张拍摄图像以及它们投影到球面上生成的球面图像 拼图的圆心是相同的 而拼图是由球面图像上的弧线图像组成的 因此我 们称之为同心球拼图 如图 4唱18 所示 这些拼图中半径最大的是圆 Ck 最小的是圆 C0 设圆 Ck 的半径为 r 虚拟相机水平视域为 θ 有 r R sin θ 2 4畅11 由此可见 构造同心球拼图的过程实际上就是对投影图像中的弧线图像

More information

Microsoft PowerPoint - 3栈和队列.ppt [兼容模式]

Microsoft PowerPoint - 3栈和队列.ppt [兼容模式] 队列的类型定义 定义 队列是必须在一端删除 ( 队头 front), 在另一端插入 ( 队尾 rear) 的线性表 特性 先进先出 (FIFO, First In First Out) rear front 117 队列的类型定义 ADT Queue{ 数据对象 : 具有线形关系的一组数据操作 : bool EnQueue(Queue &Q, ElemType e); // 入队 bool DeQueue(Queue

More information

内 容 简 介 本书基于我们多年的教学经验 从实用的角度出发 对线性和非线性数据结构的顺序和链式存储及 其操作进行了详细讲解 书中的每一章均配有实践练习及大量习题 实现了理论与实践相结合 让学生 学以致用 本书免费提供电子课件 源程序及习题答案 全部案例均在 Visual C 环境中成功

内 容 简 介 本书基于我们多年的教学经验 从实用的角度出发 对线性和非线性数据结构的顺序和链式存储及 其操作进行了详细讲解 书中的每一章均配有实践练习及大量习题 实现了理论与实践相结合 让学生 学以致用 本书免费提供电子课件 源程序及习题答案 全部案例均在 Visual C 环境中成功 高等学校计算机应用规划教材 数据结构 (C 语言版 ) 梁海英王凤领谭晓东巫湘林张波胡元闯 主编副主编 北 京 内 容 简 介 本书基于我们多年的教学经验 从实用的角度出发 对线性和非线性数据结构的顺序和链式存储及 其操作进行了详细讲解 书中的每一章均配有实践练习及大量习题 实现了理论与实践相结合 让学生 学以致用 本书免费提供电子课件 源程序及习题答案 全部案例均在 Visual C++ 6.0

More information

Microsoft PowerPoint - 4. 数组和字符串Arrays and Strings.ppt [兼容模式]

Microsoft PowerPoint - 4. 数组和字符串Arrays and Strings.ppt [兼容模式] Arrays and Strings 存储同类型的多个元素 Store multi elements of the same type 数组 (array) 存储固定数目的同类型元素 如整型数组存储的是一组整数, 字符数组存储的是一组字符 数组的大小称为数组的尺度 (dimension). 定义格式 : type arrayname[dimension]; 如声明 4 个元素的整型数组 :intarr[4];

More information

Generated by Unregistered Batch DOC TO PDF Converter , please register! 浙江大学 C 程序设计及实验 试题卷 学年春季学期考试时间 : 2003 年 6 月 20 日上午 8:3

Generated by Unregistered Batch DOC TO PDF Converter , please register! 浙江大学 C 程序设计及实验 试题卷 学年春季学期考试时间 : 2003 年 6 月 20 日上午 8:3 浙江大学 C 程序设计及实验 试题卷 2002-2003 学年春季学期考试时间 : 2003 年 6 月 20 日上午 8:30-10:30 注意 : 答题内容必须写在答题卷上, 写在本试题卷上无效 一. 单项选择题 ( 每题 1 分, 共 10 分 ) 1. 下列运算符中, 优先级最低的是 A.

More information

泽雨教育 打造中国大学生知名品牌 开创大学生综合学习平台 A 确定性 B 可行性 C 无穷性 D 拥有足够的情报 解析 : 作为一个算法, 一般应具有以下几个基本特征 1 可行性 2 确定性 3 有穷性 4 拥有足够的情 报本题答案为 C 5 在计算机中, 算法是指 A 查询方法 B 加工方法 C

泽雨教育 打造中国大学生知名品牌 开创大学生综合学习平台 A 确定性 B 可行性 C 无穷性 D 拥有足够的情报 解析 : 作为一个算法, 一般应具有以下几个基本特征 1 可行性 2 确定性 3 有穷性 4 拥有足够的情 报本题答案为 C 5 在计算机中, 算法是指 A 查询方法 B 加工方法 C 二级公共基础知识 第一章 第一节算法 1 下列叙述中正确的是 A 所谓算法就是计算方法 B 程序可以作为算法的一种描述方法 C 算法设计只需考虑得到计算结果 D 算法设计可以忽略算法的运算时间 解析 : 本题考查知识点是算法的概念 算法不等于程序, 也不等于计算方法 当然, 程序也可以作为算法的 一种描述, 但程序通常还需考虑很多与方法和分析无关的细节问题, 这是因为在编写程序是要受到计算 机系统运行环境的限制

More information

Microsoft Word - 《C语言开发入门》课程教学大纲-2.doc

Microsoft Word - 《C语言开发入门》课程教学大纲-2.doc C 语言开发入门 课程教学大纲 ( 课程英文名称 ) 课程编号 :201409210011 学分 :5 学分学时 :60 学时 ( 其中 : 讲课学时 :37 学时上机学时 :23 学时 ) 先修课程 : 计算机导论后续课程 :C++ 程序设计适用专业 : 信息及其计算机相关专业开课部门 : 计算机系 一 课程的性质与目标 C 语言开发入门 是计算机各专业必修的基础课程, 是数据结构 C++ Java

More information

PowerPoint Presentation

PowerPoint Presentation 数据结构与算法 ( 二 ) 张铭主讲 采用教材 : 张铭, 王腾蛟, 赵海燕编写高等教育出版社,2008. 6 ( 十一五 国家级规划教材 ) https://pkumooc.coursera.org/bdsalgo-001/ 第二章 线性表 第二章线性表 2.1 线性表 2.2 顺序表 2.3 链表 {a 0, a 1,, a n 1 } a 0 a 1 a 2 a n-1 tail head a

More information

试卷代号 : 座位号 中央广播电视大学 学年度第一学期 " 开放本科 " 期末考试 数据结构试题 2011 年 1 月 题号一四五总分一一 分数 得分 评卷人 一 单项选择题, 在括号内填写所选择的标号 ( 每小题 2 分, 共 1 8 分 ) 1. 执行下

试卷代号 : 座位号 中央广播电视大学 学年度第一学期  开放本科  期末考试 数据结构试题 2011 年 1 月 题号一四五总分一一 分数 得分 评卷人 一 单项选择题, 在括号内填写所选择的标号 ( 每小题 2 分, 共 1 8 分 ) 1. 执行下 试卷代号 : 1 0 1 0 座位号 中央广播电视大学 2 0 1 0 2011 学年度第一学期 " 开放本科 " 期末考试 数据结构试题 2011 年 1 月 题号一四五总分一一 分数 一 单项选择题, 在括号内填写所选择的标号 ( 每小题 2 分, 共 1 8 分 ) 1. 执行下面程序段时, s 语句的执行次数为 ( ) forcint i= 1; i

More information

目 录 目 录 前言 第 章 绪论 知识点串讲 典型例题详解 课后习题与解答 第 章 线性表 知识点串讲 典型例题详解 课后习题与解答 第 章 栈和队列 知识点串讲 典型例题详解 课后习题与解答 第 章 串 知识点串讲 典型例题详解 课后习题与解答 第 章 数组和广义表 知识点串讲 典型例题详解 课

目 录 目 录 前言 第 章 绪论 知识点串讲 典型例题详解 课后习题与解答 第 章 线性表 知识点串讲 典型例题详解 课后习题与解答 第 章 栈和队列 知识点串讲 典型例题详解 课后习题与解答 第 章 串 知识点串讲 典型例题详解 课后习题与解答 第 章 数组和广义表 知识点串讲 典型例题详解 课 数据结构 语言版 例题详解与课程设计指导 主 编 秦 锋 袁志祥副主编 陈学进 王森玉郑 啸 程泽凯 合肥 目 录 目 录 前言 第 章 绪论 知识点串讲 典型例题详解 课后习题与解答 第 章 线性表 知识点串讲 典型例题详解 课后习题与解答 第 章 栈和队列 知识点串讲 典型例题详解 课后习题与解答 第 章 串 知识点串讲 典型例题详解 课后习题与解答 第 章 数组和广义表 知识点串讲 典型例题详解

More information

试卷代号 : 座位号 I II 中央广播电视大学 学年度第二学期 " 开放本科 " 期末考试 数据结构试题 2011 年 7 月! 题号 I - I 二 三 四! 五! 六 总分 分数 I I I 1 1- I ---1 I 得分 评卷人 一 单项选择

试卷代号 : 座位号 I II 中央广播电视大学 学年度第二学期  开放本科  期末考试 数据结构试题 2011 年 7 月! 题号 I - I 二 三 四! 五! 六 总分 分数 I I I 1 1- I ---1 I 得分 评卷人 一 单项选择 试卷代号 : 1 0 1 0 座位号 I II 中央广播电视大学 2 0 1 0-2 0 1 1 学年度第二学期 " 开放本科 " 期末考试 数据结构试题 2011 年 7 月! 题号 I - I 二 三 四! 五! 六 总分 分数 I I I 1 1- I ---1 I 得分 评卷人 一 单项选择题 ( 在括号内填写所选择的标号 每小题 2 分, 共 1 8 分 ) 1. 一种抽象数据类型包括数据和

More information

2 数据结构 (C 语言版 ) 夹 ), 每个一级子目录中又包含若干个二级子目录 ( 子文件夹 ), 如图 1 1 所示 T a b c d e f g h i j k l m 图 1 1 树形结构示意图 在此种结构中, 数据之间呈现一对多的非线性关系, 这也是我们常用的一种数据结构 ( 非 线性结

2 数据结构 (C 语言版 ) 夹 ), 每个一级子目录中又包含若干个二级子目录 ( 子文件夹 ), 如图 1 1 所示 T a b c d e f g h i j k l m 图 1 1 树形结构示意图 在此种结构中, 数据之间呈现一对多的非线性关系, 这也是我们常用的一种数据结构 ( 非 线性结 第 1 章绪论 本章学习目标 本章主要介绍数据结构中的一些常用术语以及集合 线性结构 树形结构和图形结构等常用数据结构的表示, 用 C 语言实现算法描述的一般规则, 算法的时间复杂度和空间复杂度分析与评价 通过本章的学习, 读者应掌握如下内容 : 数据结构中的常用基本术语 集合 线性结构 树形结构和图形结构等每一种常用数据结构的逻辑特点 抽象数据类型的定义 使用, 算法的定义 特性及用 C 语言描述算法的规则

More information

工程项目进度管理 西北工业大学管理学院 黄柯鑫博士 甘特图 A B C D E F G 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 甘特图的优点 : 直观明了 ( 图形化概要 ); 简单易懂 ( 易于理解 ); 应用广泛 ( 技术通用 ) 甘特图的缺点 : 不能清晰表示活动间的逻辑关系 WBS 责任分配矩阵 ( 负责〇审批

More information

chap07.key

chap07.key #include void two(); void three(); int main() printf("i'm in main.\n"); two(); return 0; void two() printf("i'm in two.\n"); three(); void three() printf("i'm in three.\n"); void, int 标识符逗号分隔,

More information

untitled

untitled 图书在版编目 (CIP) 数据 家居美化中的巧 / 陈赞等编著. 北京 : 中国林业出版社,2003.4 ISBN 7-5038-3399-8 I. 家 II. 陈 III. 住宅 室内装饰 基本知识 IV.TU241 中国版本图书馆 CIP 数据核字 (2003) 第 022376 号 版权所有翻印必究 1 2002.10 1 ...1...1...2...2...3...4...5...6...7...8...8...10...10...11...12...12...13...13...15...15...16...17...18...19...20...20...20...21...22

More information

图书在版编目 (CIP) 数据程序员的数学. 3, 线性代数 /( 日 ) 平冈和幸, ( 日 ) 堀玄著 ; 卢晓南译. 北京 : 人民邮电出版社, ( 图灵程序设计丛书 ) ISBN Ⅰ. 1 程 Ⅱ. 1 平 2 堀 3 卢 Ⅲ. 1 电子计算

图书在版编目 (CIP) 数据程序员的数学. 3, 线性代数 /( 日 ) 平冈和幸, ( 日 ) 堀玄著 ; 卢晓南译. 北京 : 人民邮电出版社, ( 图灵程序设计丛书 ) ISBN Ⅰ. 1 程 Ⅱ. 1 平 2 堀 3 卢 Ⅲ. 1 电子计算 图灵程序设计丛书 程序员的数学 3: 线性代数 [ 日 ] 平冈和幸堀玄著 卢晓南译 图书在版编目 (CIP) 数据程序员的数学. 3, 线性代数 /( 日 ) 平冈和幸, ( 日 ) 堀玄著 ; 卢晓南译. 北京 : 人民邮电出版社, 2016.3 ( 图灵程序设计丛书 ) ISBN 978-7-115-41774-9 Ⅰ. 1 程 Ⅱ. 1 平 2 堀 3 卢 Ⅲ. 1 电子计算机 数学基础 2

More information

数据结构习题

数据结构习题 数据结构 习题集 第一章序论 思考题 : 1.1 简述下列术语 : 数据 数据元素 数据对象 数据结构 存储结构 数据类型 抽象数据类型 作业题 : 1.2 设有数据结构 (D,R), 其中 D={d1, d2, d3, d4 R={r1, r2 r1={ , , , , , r2={ (d1, d2),

More information

前 言 数据结构 课程是计算机类 电子信息类及相关专业的专业基础课 它在整个课程体系中处于承上启下的核心地位 : 一方面扩展和深化在离散数学 程序设计语言等课程学到的基本技术和方法 ; 另一方面为进一步学习操作系统 编译原理 数据库等专业知识奠定坚实的理论与实践基础 本课程在教给学生数据结构设计和算法设计的同时, 培养学生的抽象思维能力 逻辑推理能力和形式化思维方法, 增强分析问题 解决问题和总结问题的能力,

More information

试卷代号 : 座位号 CD 中央广播电视大学 学年度第二学期 " 开放本科 " 期末考试 数据结构 ( 本 ) 试题 I 题号 - - I 二 l 三 l 四 l 总 分 分数 I I I I I I 2009 年 7 月 得分 评卷人 I I I 一

试卷代号 : 座位号 CD 中央广播电视大学 学年度第二学期  开放本科  期末考试 数据结构 ( 本 ) 试题 I 题号 - - I 二 l 三 l 四 l 总 分 分数 I I I I I I 2009 年 7 月 得分 评卷人 I I I 一 试卷代号 : 1 2 5 2 座位号 CD 中央广播电视大学 2 0 0 8-2 0 0 9 学年度第二学期 " 开放本科 " 期末考试 数据结构 ( 本 ) 试题 I 题号 - - I 二 l 三 l 四 l 总 分 分数 I I I I I I 2009 年 7 月 得分 评卷人 I I I 一 单项选择题 ( 每小题 2 分如 崎盯扫, 共 3t 3ω O 1. 针对线性表, 在存储后如果最常用的操作是取第

More information

图书在版编目 (CIP) 数据 满堂花醉 / 沈胜衣著. 南京 : 江苏教育出版社, ( 沈郎文字 ) ISBN Ⅰ. 满... Ⅱ. 沈... Ⅲ. 作家 - 人物研究 - 世界 Ⅳ.K815.6 中国版本图书馆 CIP 数据核字 (2005) 第 041

图书在版编目 (CIP) 数据 满堂花醉 / 沈胜衣著. 南京 : 江苏教育出版社, ( 沈郎文字 ) ISBN Ⅰ. 满... Ⅱ. 沈... Ⅲ. 作家 - 人物研究 - 世界 Ⅳ.K815.6 中国版本图书馆 CIP 数据核字 (2005) 第 041 图书在版编目 (CIP) 数据 满堂花醉 / 沈胜衣著. 南京 : 江苏教育出版社, 2005.4 ( 沈郎文字 ) ISBN 7-5343-6512-0 Ⅰ. 满... Ⅱ. 沈... Ⅲ. 作家 - 人物研究 - 世界 Ⅳ.K815.6 中国版本图书馆 CIP 数据核字 (2005) 第 041843 号 出版者社址网址出版人 南京市马家街 31 号邮编 :210009 http://www.1088.com.cn

More information

第一章三角函数 1.3 三角函数的诱导公式 A 组 ( ) 一 选择题 : 共 6 小题 1 ( 易诱导公式 ) 若 A B C 分别为 ABC 的内角, 则下列关系中正确的是 A. sin( A B) sin C C. tan( A B) tan C 2 ( 中诱导公式 ) ( ) B. cos(

第一章三角函数 1.3 三角函数的诱导公式 A 组 ( ) 一 选择题 : 共 6 小题 1 ( 易诱导公式 ) 若 A B C 分别为 ABC 的内角, 则下列关系中正确的是 A. sin( A B) sin C C. tan( A B) tan C 2 ( 中诱导公式 ) ( ) B. cos( 第一章三角函数 1. 三角函数的诱导公式 A 组 一 选择题 : 共 6 小题 1 ( 易诱导公式 ) 若 A B C 分别为 ABC 的内角 则下列关系中正确的是 A. sin( A B) sin C C. tan( A B) tan C ( 中诱导公式 ) B. cos( B C) cos A D. sin( B C) sin A sin60 cos( ) sin( 0 )cos( 70 ) 的值等于

More information

Microsoft PowerPoint - ds_2.ppt

Microsoft PowerPoint - ds_2.ppt 第二章线性表 2.1 线性表的概念 2.2 顺序表示 2.3 链接表示 2.4 应用举例 -Josehus 问题另外介绍 动态顺序表 程序里常需要保存一批某种类型的元素, 这些元素的数目可能变化 ( 可以加入或删除元素 ) 有时需要把这组元素看成一个序列, 元素的顺序可能表示实际应用中的某种有意义的关系这样一组元素可以抽象为元素的一个线性表 线性表是元素的集合, 同时记录了元素的顺序关系 线性表是一种最基本的数据结构,

More information

C++ 程序设计 OJ4 - 参考答案 MASTER 2019 年 5 月 3 日 1

C++ 程序设计 OJ4 - 参考答案 MASTER 2019 年 5 月 3 日 1 C++ 程序设计 OJ4 - 参考答案 MASTER 2019 年 5 月 3 日 1 1 MYQUEUE 1 MyQueue 题目描述 设计一个 MyQueue 类模板, 类模板说明如下 : template class MyQueue; template std::ostream & operator

More information

Microsoft PowerPoint - 5. 指针Pointers.ppt [兼容模式]

Microsoft PowerPoint - 5. 指针Pointers.ppt [兼容模式] 指针 Pointers 变量指针与指针变量 Pointer of a variable 变量与内存 (Variables and Memory) 当你声明一个变量时, 计算机将给该变量一个内存, 可以存储变量的值 当你使用变量时, 计算机将做两步操作 : - 根据变量名查找其对应的地址 ; - 通过地址对该地址的变量内容进行读 (retrieve) 或写 (set) 变量的地址称为变量的指针! C++

More information

C++ 程序设计 OJ4 - 参考答案 MASTER 2017 年 5 月 21 日 1

C++ 程序设计 OJ4 - 参考答案 MASTER 2017 年 5 月 21 日 1 C++ 程序设计 OJ4 - 参考答案 MASTER 2017 年 5 月 21 日 1 1 SWAP 1 Swap 题目描述 用函数模板的方式实现对不同数据类型的数组中的数据进行输入 从小到大排序和输出 使用如下主函数测试你的模板设计一个函数模板 Swap, 实现任意数据类型的两个数据的交换, 分别用 int 型 double 型和 char 型的数据进行测试 main 函数如下 : int main()

More information

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例 帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例 这篇文章主要介绍了帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例, 本文还详细介绍了帝国 CMS 数据库类中的一些常用方法, 需要的朋友可以参考下 例 1: 连接 MYSQL 数据库例子 (a.php)

More information

器之 间 向一致时为正 相反时则为负 ③大量电荷的定向移动形成电 流 单个电荷的定向移动同样形成电流 3 电势与电势差 1 陈述概念 电场中某点处 电荷的电势能 E p 与电荷量 q Ep 的比值叫做该点处的电势 表达式为 V 电场中两点之间的 q 电势之差叫做电势差 表达式为 UAB V A VB 2 理解概念 电势差是电场中任意两点之间的电势之差 与参考点的选择无关 电势是反映电场能的性质的物理量

More information

4

4 孙猛 http://www.math.pku.edu.cn/teachers/sunm 2017 年 9 月 28 日 2 栈及其抽象数据类型 栈的实现 栈的应 用 3 基本概念 栈是 一种特殊的线性表, 它所有的插 入和删除都限制在表的同 一端进 行行 表中允许进 行行插 入 删除操作的 一端叫做栈的顶 表的另 一端则叫做栈的底 当栈中没有元素时, 称之为空栈 栈的插 入运算通常称为进栈或 入栈,

More information

<4D F736F F F696E74202D BDE1B9B9BBAFB3CCD0F2C9E8BCC D20D1ADBBB7>

<4D F736F F F696E74202D BDE1B9B9BBAFB3CCD0F2C9E8BCC D20D1ADBBB7> 能源与动力工程学院 结构化编程 结构化程序设计 循环 循环结构 确定性循环 非确定性循环 I=1 sum=sum+i I = I +1 陈 斌 I>100 Yes No 目录 求和 :1+2+3++100 第四节循环的应用 PROGRAM GAUSS INTEGER I, SUM 计数器 SUM = 0 DO I = 1, 100, 1 SUM = SUM + I print*, I, SUM DO

More information

Summary

Summary Summary 暑假开始准备转移博客, 试了几个都不怎么满意 ( 我还去试了下 LineBlog 不知道那时候在想 什么 ) 现在暂时转移至 WordPress, 不过还在完善中, 预计 算了不瞎预计的好 课上说最好做个代码集, 嗯嗯我也觉得挺有必要的 毕竟现在我连 Floyd 怎么写都忘了无脑 SPFA_(:з )_ 反正有用没用都稍微写一下, 暂定是目录这些, 有些还在找例题 整理代码什么的,

More information

C/C++ - 文件IO

C/C++ - 文件IO C/C++ IO Table of contents 1. 2. 3. 4. 1 C ASCII ASCII ASCII 2 10000 00100111 00010000 31H, 30H, 30H, 30H, 30H 1, 0, 0, 0, 0 ASCII 3 4 5 UNIX ANSI C 5 FILE FILE 6 stdio.h typedef struct { int level ;

More information

Microsoft Word - 第3章.doc

Microsoft Word - 第3章.doc 第 3 章流程控制和数组 3.1 实验目的 (1) 熟练掌握控制台应用程序的代码编写和调试, 以及运行方法 (2) 掌握选择结构的一般语法格式和应用 (3) 掌握 switch 语句的用法 (4) 掌握选择结构的嵌套的用法, 能灵活使用选择结构解决实际问题 (5) 掌握 while 循环语句的一般语法格式 (6) 掌握 for 循环语句的一般语法格式 (7) 掌握循环嵌套的语法格式 (8) 掌握一维数组的定义

More information

第七章数组 掌握一维数组的定义 初始化及元素引用 ; 掌握二维数组的定义 初始化及元素引用 ; 掌握字符数组的定义及使用 ; 4. 了解字符串处理函数 ; 第八章函数 掌握函数的定义与调用 ; 掌握函数调用时的实参与形参的结合 ; 理解函数原型声明与函数在源程序中的相对位置的关系 ; 理解函数的嵌套

第七章数组 掌握一维数组的定义 初始化及元素引用 ; 掌握二维数组的定义 初始化及元素引用 ; 掌握字符数组的定义及使用 ; 4. 了解字符串处理函数 ; 第八章函数 掌握函数的定义与调用 ; 掌握函数调用时的实参与形参的结合 ; 理解函数原型声明与函数在源程序中的相对位置的关系 ; 理解函数的嵌套 2015 年福建省专升本考试计算机科学类专业基础课考试大纲 C 语言程序设计 ( 100 分 ) 一 考试要求 : 1. 对 C 语言的语法 语义有较好的理解 2. 能熟练地阅读 C 源程序, 并具有初步分析程序的能力 3. 初步掌握结构化程序设计的方法和技巧, 能从分析问题入手, 设计可行的算法, 进而用 C 语言编写结构良好的面向过程的程序 4. 通过上机实验, 掌握程序的调试和测试方法 二 考试内容第一章

More information

求出所有的正整数 n 使得 20n + 2 能整除 2003n n 20n n n 20n n 求所有的正整数对 (x, y), 满足 x y = y x y (x, y) x y = y x y. (x, y) x y =

求出所有的正整数 n 使得 20n + 2 能整除 2003n n 20n n n 20n n 求所有的正整数对 (x, y), 满足 x y = y x y (x, y) x y = y x y. (x, y) x y = 求出所有的正整数 n 使得 20n + 2 能整除 2003n + 2002 n 20n + 2 2003n + 2002 n 20n + 2 2003n + 2002 求所有的正整数对 (x, y), 满足 x y = y x y (x, y) x y = y x y. (x, y) x y = y x y 对于任意正整数 n, 记 n 的所有正约数组成的集合为 S n 证明 : S n 中至多有一半元素的个位数为

More information

数学分析(I)短课程 [Part 2] 4mm 自然数、整数和有理数

数学分析(I)短课程 [Part 2]   4mm 自然数、整数和有理数 .. 数学分析 (I) 短课程 [Part 2] 自然数 整数和有理数 孙伟 华东师范大学数学系算子代数中心 Week 2 to 18. Fall 2014 孙伟 ( 数学系算子代数中心 ) 数学分析 (I) 短课程 Week 2 to 18. Fall 2014 1 / 78 3. 自然数理论初步 孙伟 ( 数学系算子代数中心 ) 数学分析 (I) 短课程 Week 2 to 18. Fall 2014

More information

期中考试试题讲解

期中考试试题讲解 一 选择题 ( 一 ) 1. 结构化程序设计所规定的三种基本结构是 C A 主程序 子程序 函数 B 树形 网形 环形 C 顺序 选择 循环 D 输入 处理 输出 2. 下列关于 C 语言的叙述错误的是 A A 对大小写不敏感 B 不同类型的变量可以在一个表达式中 C main 函数可以写在程序文件的任何位置 D 同一个运算符号在不同的场合可以有不同的含义 3. 以下合法的实型常数是 C A.E4

More information

Microsoft PowerPoint - Lecture3.ppt

Microsoft PowerPoint - Lecture3.ppt Chap 4. Links, Stacks and Queue 1 Lists A list is a finite, ordered sequence of data items. Important concept: List elements have a position. Notation: What operations should we implement?

More information

Microsoft PowerPoint - ch1.pptx

Microsoft PowerPoint - ch1.pptx 本章内容提要 第 1 章 绪论 哈尔滨工业大学 ( 威海 ) 计算机科学与技术学院 (2014/2015 学年秋季版 ) 1 本章重点难点 本章内容提要 重点 : 1 数据结构的逻辑结构 存储结构以及基本操作的概念及相互关系 ;2 抽象数据类型 (ADT) 的概念和实现方法, 算法的时间复杂性和空间复杂性分析 难点 : 1 抽象数据类型 (ADT) 的概念和实现方法 ;2 算法的时间复杂性和空间复杂性分析

More information

OOP with Java 通知 Project 2 提交时间 : 3 月 14 日晚 9 点 另一名助教 : 王桢 学习使用文本编辑器 学习使用 cmd: Power shell 阅读参考资料

OOP with Java 通知 Project 2 提交时间 : 3 月 14 日晚 9 点 另一名助教 : 王桢   学习使用文本编辑器 学习使用 cmd: Power shell 阅读参考资料 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 2 提交时间 : 3 月 14 日晚 9 点 另一名助教 : 王桢 Email: 51141201063@ecnu.cn 学习使用文本编辑器 学习使用 cmd: Power shell 阅读参考资料 OOP with Java Java 类型 引用 不可变类型 对象存储位置 作用域 OOP

More information

C++ 程序设计 OJ4 - 参考答案 MASTER 2019 年 5 月 30 日 1

C++ 程序设计 OJ4 - 参考答案 MASTER 2019 年 5 月 30 日 1 C++ 程序设计 OJ4 - 参考答案 MASTER 2019 年 月 30 日 1 1 STRINGSORT 1 StringSort 题目描述 编写程序, 利用 string 类完成一个字符串中字符的排序 ( 降序 ) 并输出 输入描述 输入仅一行, 是一个仅由大小写字母和数字组成的字符串 输出描述 输出排序后的字符串 样例输入 abcde 样例输出 edcba 提示 使用 std::sort

More information

网C试题(08上).doc

网C试题(08上).doc 学习中心 姓名 学号 西安电子科技大学网络与继续教育学院 高级语言程序设计 (C) 全真试题 ( 闭卷 90 分钟 ) 题号一二三总分 题分 60 20 20 得分 一 单项选择题 ( 每小题 3 分, 共 60 分 ) 1.C 语言程序的基本单位是 A) 程序行 B) 语句 C) 函数 D) 字符 2. 下列四组选项中, 均是不合法的用户标识符的选项是 A)A B)getc C)include D)while

More information

2015年计算机二级(C语言)模拟试题及答案(三)

2015年计算机二级(C语言)模拟试题及答案(三) 2016 年计算机二级 (C 语言 ) 模拟试题及答案 (3) 1.( A ) 是构成 C 语言程序的基本单位 A 函数 B 过程 C 子程序 D 子例程 2.C 语言程序从 ( C ) 开始执行 A 程序中第一条可执行语句 B 程序中第一个函数 C 程序中的 main 函数 D 包含文件中的第一个函数 3 以下说法中正确的是( C ) A C 语言程序总是从第一个定义的函数开始执行 B 在 C 语言程序中,

More information

《C语言程序设计》教材习题参考答案

《C语言程序设计》教材习题参考答案 教材名称 : C 语言程序设计 ( 第 1 版 ) 黄保和 江弋编著清华大学出版社 ISBN: 978-7-302-13599-9, 红色封面答案制作时间 :2011 年 2 月 -5 月一 选择题 1. 以下数组定义中, 错误的是 :C)int a[3]=1,2,3,4; 2. 以下数组定义中, 正确的是 :B) int a[][2]=1,2,3,4; 3. 设有定义 int a[8][10];,

More information

试卷代号 : 座位号 中央广播电视大学 学年度第二学期 " 开放本科 " 期末考试 数据结构试题 2012 年 7 月 题号一四五总分一一 分数 得分 评卷人 - 单项选择题, 在括号内填写所选择的标号 { 每小题 2 分, 共 1 8 分 ) 1. 下面算法

试卷代号 : 座位号 中央广播电视大学 学年度第二学期  开放本科  期末考试 数据结构试题 2012 年 7 月 题号一四五总分一一 分数 得分 评卷人 - 单项选择题, 在括号内填写所选择的标号 { 每小题 2 分, 共 1 8 分 ) 1. 下面算法 试卷代号 : 1 0 1 0 座位号 中央广播电视大学 2 0 11 2012 学年度第二学期 " 开放本科 " 期末考试 数据结构试题 2012 年 7 月 题号一四五总分一一 分数 得分 评卷人 - 单项选择题, 在括号内填写所选择的标号 { 每小题 2 分, 共 1 8 分 ) 1. 下面算法的时间复杂度为 ( ) int f( unsigned int n) { if(n= =0 II n=

More information

吉林大学学报 工学版 244 第 4 卷 复杂 鉴于本文篇幅所限 具体公式可详见参考文 献 7 每帧的动力学方程建立及其解算方法如图 3 所示 图4 滚转角速度与输入量 η 随时间的变化波形 Fig 4 Waveform of roll rate and input η with time changing 图5 Fig 5 滚转角随时间的变化波形 Waveform of roll angle with

More information

Microsoft Word - 专升本练习5:图.doc

Microsoft Word - 专升本练习5:图.doc 第五章 图 一 选择题 1. 关键路径是事件结点网络中的 ( ) A. 从源点到汇点的最长路径 B. 从源点到汇点的最短路径 C. 最长的回路 D. 最短的回路 2. 一个具有 n 个顶点和 e 条边的无向图, 采用邻接表表示, 表向量的大小为 ( 1 ), 所有顶点 邻接表的结点总数为 ( 2 ) 1A. n B. n+1 C. n-1 D. n+e 2A. e/2 B. e C. 2e D. n+e

More information

3. 給 定 一 整 數 陣 列 a[0] a[1] a[99] 且 a[k]=3k+1, 以 value=100 呼 叫 以 下 兩 函 式, 假 設 函 式 f1 及 f2 之 while 迴 圈 主 體 分 別 執 行 n1 與 n2 次 (i.e, 計 算 if 敘 述 執 行 次 數, 不

3. 給 定 一 整 數 陣 列 a[0] a[1] a[99] 且 a[k]=3k+1, 以 value=100 呼 叫 以 下 兩 函 式, 假 設 函 式 f1 及 f2 之 while 迴 圈 主 體 分 別 執 行 n1 與 n2 次 (i.e, 計 算 if 敘 述 執 行 次 數, 不 1. 右 側 程 式 正 確 的 輸 出 應 該 如 下 : * *** ***** ******* ********* 在 不 修 改 右 側 程 式 之 第 4 行 及 第 7 行 程 式 碼 的 前 提 下, 最 少 需 修 改 幾 行 程 式 碼 以 得 到 正 確 輸 出? (A) 1 (B) 2 (C) 3 (D) 4 1 int k = 4; 2 int m = 1; 3 for (int

More information

Guava学习之Resources

Guava学习之Resources Resources 提供提供操作 classpath 路径下所有资源的方法 除非另有说明, 否则类中所有方法的参数都不能为 null 虽然有些方法的参数是 URL 类型的, 但是这些方法实现通常不是以 HTTP 完成的 ; 同时这些资源也非 classpath 路径下的 下面两个函数都是根据资源的名称得到其绝对路径, 从函数里面可以看出,Resources 类中的 getresource 函数都是基于

More information

1

1 基本練習題 1 答 :(A) 2 答 :(B) 3 答 :(C) 4 答 :(B) 5 答 :(D) 6 答 :2 7 答 :(B) 8 答 : (A) A B C / D E * + F G / - (B) A B + C D - * E / (C) A B C * + E F + - 9 答 : (A) - + A * - / BCDE / F G (B) / * + A B C D E (C)

More information

大侠素材铺

大侠素材铺 编译原理与技术 词法分析 Ⅱ 计算机科学与技术学院李诚 13/09/2018 主要内容 记号 (token) 源程序 词法分析器 getnexttoken 语法分析器 符号表 词法分析器的自动生成 正则表达式 NFA DFA 化简的 DFA 词法分析器的生成器 Lex: flex jflex Fst lexicl nlyzer genertor 2/51 Regulr Expr to NFA 正则表达式

More information

6.3 正定二次型

6.3 正定二次型 6.3 正定二次型 一个实二次型, 既可以通过正交变换化为标准形, 也可以通过拉格朗日配方法化为标准形, 显然, 其标准形一般来说是不惟一的, 但标准形中所含有的项数是确定的, 项数等于二次型的秩 当变换为实变换时, 标准形中正系数和负系数的个数均是不变的 定理 ( 惯性定理 ) 设有二次型 f =x T Ax, 它的秩为 r, 如果有两个实的可逆变换 x=c y 及 x=c z 分别使 f =k

More information

第5章修改稿

第5章修改稿 (Programming Language), ok,, if then else,(), ()() 5.0 5.0.0, (Variable Declaration) var x : T x, T, x,,,, var x : T P = x, x' : T P P, () var x:t P,,, yz, var x : int x:=2. y := x+z = x, x' : int x' =2

More information

标题

标题 本章学习导读 本章主要介绍串的定义及基本运算, 重点介绍串的存储结构, 基本运算与串的 C# 实现方法 读者学习本章后应能掌握串的定义, 串的基本运算, 运用串来实现文本输入和输出 4.1.1 串的定义 4.1 串的基本概念 字符串在应用程序中的使用非常频繁 字符串简称串, 是一种特殊的线性表, 其特殊性在于串中的数据元素是一个个的字符 在事务处理程序中, 顾客的信息如姓名 地址等及货物的名称 产地和规格等,

More information

全国计算机等级考试笔试模拟试卷(1)

全国计算机等级考试笔试模拟试卷(1) 2009 年 9 月全国计算机等级考试笔试试卷 二级公共基础知识和 C 语言程序设计 ( 考试时间 90 分钟, 满分 100 分 ) 一 选择题 ((1)~(10) (21)~(40) 每题 2 分,(11)~(20) 每题 1 分,70 分 ) (1) 下列数据结构中, 属于非线性结构的是 ( ) A) 循环队列 B) 带链队列 C) 二叉树 D) 带链栈 (2) 下列数据结构中, 能够按照 先进后出

More information

Microsoft Word - FM{new}.doc

Microsoft Word - FM{new}.doc Lanczos 方法 Louis Komzsik 著张伟廖本善译 演变与应用 清华大学出版社 北京 内容简介 Lanczos 方法是 20 世纪计算数学方向最有影响的方法之一, 并且已经在工程中得到了广泛应用. 本书兼顾了 Lanczos 方法的理论演变和工程中的实际应用, 其内容分为两部分 : 第一部分阐述了方法的演变, 并提供了具体算法 ; 第二部分讨论了工业中的实际应用, 包括常用的模态分析

More information

4.C ( 详细解析见视频课程 绝对值 01 约 21 分 15 秒处 ) 5.E ( 详细解析见视频课程 绝对值 01 约 32 分 05 秒处 ) 6.D ( 详细解析见视频课程 绝对值 02 约 4 分 28 秒处 ) 7.C ( 详细解析见视频课程 绝对值 02 约 14 分 05 秒处 )

4.C ( 详细解析见视频课程 绝对值 01 约 21 分 15 秒处 ) 5.E ( 详细解析见视频课程 绝对值 01 约 32 分 05 秒处 ) 6.D ( 详细解析见视频课程 绝对值 02 约 4 分 28 秒处 ) 7.C ( 详细解析见视频课程 绝对值 02 约 14 分 05 秒处 ) [ 说明 ] 1. 以下所指教材是指朱杰老师的 管理类联考综合能力数学套路化攻略 2. 该文档中所标答案和参见的教材答案, 与视频有冲突的, 以视频答案为准! 基础篇 第 1 章 数 1.2.1 整数例题答案 : 1. A ( 详细解析见教材 P7 例 2) 2. D ( 详细解析见视频课程 数的性质 约 10 分 53 秒处 ) 3. C ( 详细解析见教材 P7 例 3) 4.E ( 详细解析见视频课程

More information

( 四 ) 指令流水线 六 总线 ( 一 ) 总线概述 ( 二 ) 总线仲裁 ( 三 ) 总线操作和定时 ( 四 ) 总线标准 七 输入输出 (I/O) 系统 ( 一 )I/O 系统基本概念 ( 二 ) 外部设备 ( 三 )I/O 接口 (I/O 控制器 ) ( 四 )I/O 方式 操作系统 : 第

( 四 ) 指令流水线 六 总线 ( 一 ) 总线概述 ( 二 ) 总线仲裁 ( 三 ) 总线操作和定时 ( 四 ) 总线标准 七 输入输出 (I/O) 系统 ( 一 )I/O 系统基本概念 ( 二 ) 外部设备 ( 三 )I/O 接口 (I/O 控制器 ) ( 四 )I/O 方式 操作系统 : 第 大连民族大学硕士研究生招生考试大纲 专业领域 科目代码及名称 计算机技术 810 计算机专业基础综合 数据结构 : 第 1 章绪论第 2 章线性表第 3 章栈和队列第 5 章树和二叉树第 6 章图第 7 章查找技术第 8 章排序技术 计算机组成原理 : 考试内容 一 计算机系统概述 ( 一 ) 计算机发展历程 ( 二 ) 计算机系统层次结构 ( 三 ) 计算机性能指标二 数据的表示和运算 ( 一 )

More information

生成word文档

生成word文档 希赛网, 专注于软考 PMP 通信考试的专业 IT 知识库和在线教育平台 希赛网在线题库, 提供历年考试真题 模拟试题 章节练习 知识点练习 错题本练习等在线做题服务, 更有能力评估报告, 让你告别盲目做题, 针对性地攻破自己的薄弱点, 更高效的备考 希赛网官网 :http://www.educity.cn/ 希赛网软件水平考试网 :http://www.educity.cn/rk/ 希赛网在线题库

More information

C 1

C 1 C homepage: xpzhangme 2018 5 30 C 1 C min(x, y) double C // min c # include # include double min ( double x, double y); int main ( int argc, char * argv []) { double x, y; if( argc!=

More information

Microsoft Word - 2008年9月二级C真卷.doc

Microsoft Word - 2008年9月二级C真卷.doc 机 密 启 用 前 2008 年 9 月 全 国 计 算 机 等 级 考 试 二 级 笔 试 试 卷 C 语 言 程 序 设 计 24 注 意 事 项 一 考 生 应 严 格 遵 守 考 场 规 则, 得 到 监 考 人 员 指 令 后 方 可 作 答 二 考 生 拿 到 试 卷 后 应 首 先 将 自 己 的 姓 名 准 考 证 号 等 内 容 涂 写 在 答 题 卡 的 相 应 位 置 上 三

More information

C C

C C C C 2017 3 8 1. 2. 3. 4. char 5. 2/101 C 1. 3/101 C C = 5 (F 32). 9 F C 4/101 C 1 // fal2cel.c: Convert Fah temperature to Cel temperature 2 #include 3 int main(void) 4 { 5 float fah, cel; 6 printf("please

More information

Microsoft Word - 把时间当作朋友(2011第3版)3.0.b.06.doc

Microsoft Word - 把时间当作朋友(2011第3版)3.0.b.06.doc 2 5 8 11 0 13 1. 13 2. 15 3. 18 1 23 1. 23 2. 26 3. 28 2 36 1. 36 2. 39 3. 42 4. 44 5. 49 6. 51 3 57 1. 57 2. 60 3. 64 4. 66 5. 70 6. 75 7. 83 8. 85 9. 88 10. 98 11. 103 12. 108 13. 112 4 115 1. 115 2.

More information

四 读算法 ( 每题 7 分, 共 14 分 ) 1. (1) 查询链表的尾结点 (2) 将第一个结点链接到链表的尾部, 作为新的尾结点 (3) 返回的线性表为 (a 2,a 3,,a n,a 1 ) 2. 递归地后序遍历链式存储的二叉树 五 法填空 ( 每空 2 分, 共 8 分 ) true B

四 读算法 ( 每题 7 分, 共 14 分 ) 1. (1) 查询链表的尾结点 (2) 将第一个结点链接到链表的尾部, 作为新的尾结点 (3) 返回的线性表为 (a 2,a 3,,a n,a 1 ) 2. 递归地后序遍历链式存储的二叉树 五 法填空 ( 每空 2 分, 共 8 分 ) true B 数据结构试卷 ( 一 ) 参考答案 一 选择题 ( 每题 2 分, 共 20 分 ) 1.A 2.D 3.D 4.C 5.C 6.D 7.D 8.C 9.D 10.A 二 填空题 ( 每空 1 分, 共 26 分 ) 1. 正确性 易读性 强壮性 高效率 2. O(n) 3. 9 3 3 4. -1 3 4 X * + 2 Y * 3 / - 5. 2n n-1 n+1 6. e 2e 7. 有向无回路

More information

Microsoft PowerPoint - DS_Ch5.ppt [兼容模式]

Microsoft PowerPoint - DS_Ch5.ppt [兼容模式] 数据结构 Ch.5 数组和广义表 计算机学院 肖明军 Email: xiaomj@ustc.edu.cn http://staff.ustc.edu.cn/~xiaomj 多维数组 是最易处理的非线性结构 因为各元素类型一致, 各维上下界固定, 所以它最容易线性化, 故可看做是线性表的拓广 例如 : 二维数组可以看做是由列向量组成的线性表 1. 结构特性 例 : 二维数组, 它属于两个向量 ;i th

More information

串 零个或多个字符组成的有限序列 将元素类型限制为字符线性表 具有相同类型的数据元素的有限序列 将元素类型扩充为线性表 ( 多维 ) 数组 线性表中的数据元素可以是线性表 2

串 零个或多个字符组成的有限序列 将元素类型限制为字符线性表 具有相同类型的数据元素的有限序列 将元素类型扩充为线性表 ( 多维 ) 数组 线性表中的数据元素可以是线性表 2 Array Zibin Zheng( 郑子彬 ) School of Data and Computer Science, SYSU http://www.inpluslab.com 课程主页 :http://inpluslab.sysu.edu.cn/dsa2016/ 串 零个或多个字符组成的有限序列 将元素类型限制为字符线性表 具有相同类型的数据元素的有限序列 将元素类型扩充为线性表 ( 多维

More information

SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 "odps-sdk" 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基

SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 odps-sdk 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基 开放数据处理服务 ODPS SDK SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 "odps-sdk" 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基础功能的主体接口, 搜索关键词 "odpssdk-core" 一些

More information

华侨大学 2013 年硕士研究生入学考试专业课试卷 ( 答案必须写在答题纸上 ) 招生专业 计算机技术 科目名称 数据结构与 C++ 科目代码 850 第一部分数据结构 ( 共 75 分 ) 一 单项选择题 ( 每小题 2 分, 共 24 分 ) 1. 执行下面程序段时, 则 S 语句的语句频度是

华侨大学 2013 年硕士研究生入学考试专业课试卷 ( 答案必须写在答题纸上 ) 招生专业 计算机技术 科目名称 数据结构与 C++ 科目代码 850 第一部分数据结构 ( 共 75 分 ) 一 单项选择题 ( 每小题 2 分, 共 24 分 ) 1. 执行下面程序段时, 则 S 语句的语句频度是 华侨大学 2013 年硕士研究生入学考试专业课试卷 ( 答案必须写在答题纸上 ) 招生专业 计算机技术 科目名称 数据结构与 C++ 科目代码 850 第一部分数据结构 ( 共 75 分 ) 一 单项选择题 ( 每小题 2 分, 共 24 分 ) 1. 执行下面程序段时, 则 S 语句的语句频度是 () for(int i =1;i

More information

3 堆栈与队列 (1) 堆栈与队列的基本概念 基本操作 (2) 堆栈与队列的顺序存储结构与链式存储结构的构造原理 (3) 在不同存储结构的基础上对堆栈与队列实施插入与删除等基本操作对应的算法设计 4 串 (1) 串的基本概念 串的基本操作和存储结构 (2) 串的模式匹配算法和改进的 KMP 算法 5

3 堆栈与队列 (1) 堆栈与队列的基本概念 基本操作 (2) 堆栈与队列的顺序存储结构与链式存储结构的构造原理 (3) 在不同存储结构的基础上对堆栈与队列实施插入与删除等基本操作对应的算法设计 4 串 (1) 串的基本概念 串的基本操作和存储结构 (2) 串的模式匹配算法和改进的 KMP 算法 5 中国科学院大学硕士研究生入学考试 计算机原理 考试大纲 本 计算机原理 考试大纲适用于中国科学院大学非计算机科学与技术一级学科下各专业的硕士研究生入学考试 计算机原理是计算机科学与技术及相关学科的重要基础, 主要内容包括数据结构 计算机组成原理和计算机网络 要求考生对计算机科学与技术及相关学科的基本概念有较深入 系统的理解, 掌握各种数据结构的定义和实现算法, 掌握计算机组成原理所涉及的关键内容,

More information