《数据结构》算法实现与分析


《数据结构》算法实现及解析(第二版) ——配合严蔚敏、吴伟民编著的《数据结构》( C 语言版) 高一凡 编著 西安电子科技大学出版社 2 0 0 4 内 容 简 介 本书是在第一版的基础上修订而成的。 本书为清华大学出版社出版、由严蔚敏和吴伟民编著的《数据结构》(C 语言版)(以下简 称教科书)的学习辅导书。主要内容包括:教科书中的每一种数据存储结构的图示;教科书中 每一种存储结构的基本操作函数及调用这些基本操作的主程序和程序运行结果;教科书中几 乎每一种算法的实现。对于教科书中一些较复杂的算法,本书提供了详细的解析。有些在教 科书中一带而过的存储结构(如第 2 章的静态链表和第 6 章的二叉树的三叉链表),本书也提 供了完整的基本操作函数及主程序和程序运行结果。本书配有光盘,光盘中包括书中所有程 序及用标准 C 语言改写的程序。所有程序均在计算机上运行通过。 本书适用于使用教科书的大中专学生和自学者。书中的基本操作函数也可供从事计算机 工程与应用工作的科技人员参考和采用。 图书在版编目(CIP)数据 《数据结构》算法实现及解析 / 高一凡编著. — 2 版. —西安:西安电子科技大学出版社, 2004.10 ISBN 7 Ⅰ 数 Ⅱ 高 Ⅲ 数据结构算法分析高等学校教学参考资料 Ⅳ 中国版本图书馆 CIP 数据核字(2004)第 100807 号 策 划 马武装 责任编辑 马武装 出版发行 西安电子科技大学出版社(西安市太白南路 号) 电 话 邮 编 http://www.xduph.com xdupfxb@pub.xaonline.com 经 销 新华书店 印 刷 西安文化彩印厂 版 次 年 月第 版 年 月第 版 年 月第 次印刷 开 本 毫米× 毫米 印张 字 数 千字 印 数 ~ 册 定 价 含光盘元 /· XDUP 1447012 * * * 如有印装问题可调换 * * * 本社图书封面为激光防伪覆膜,谨防盗版。 第 一 版 前 言 “数据结构”并非一门纯数学课程。它要求学生能根据所学的“数据结构”理论完成 较复杂的程序设计。而程序设计能力的提高有个学习、观摩、借鉴和实践的过程。 学生在学习“数据结构”课程时,虽然已学过 C 语言,但仅是初学,并不精通。对于 抽象的数据类型、动态分配存储空间等概念,在理解上还是有一定困难的。如何理解数据 存储结构,消化算法,将算法转化成 C 语言的函数并能编写出运行该函数的主程序,往往 是摆在他们面前的一道难关。 作者多次讲授“数据结构”课,所用教科书为清华大学出版社出版的严蔚敏、吴伟民 编著的《数据结构》(C 语言版)(以下简称为教科书)。该教科书内容较全面,有一定深度。 但在叙述一些基本概念和算法时过于精炼,使学生在理解上有一定的困难。作者根据多年 的授课经验,编写了教科书中各种数据存储结构示意图,并给出了基本操作函数以及调用 这些基本操作的主程序。作者力图把抽象的问题具体化,使学生深刻、透彻地理解教科书 中的各种存储结构和基于这种存储结构的算法,掌握数据结构基本操作函数的编写和应用, 并在此基础上,能针对具体的工程问题选择甚至创建恰当的数据存储结构,正确应用基本 操作函数编程解决之。 本书内容包括: (1) 教科书中的每一种数据存储结构的图示; (2) 教科书中每一种存储结构的基本操作函数及调用这些基本操作的主程序和程序运 行结果。教科书中几乎每一种算法的实现。对于教科书中一些较复杂的算法,本书提供了 详细的解析。有些在教科书中一带而过的存储结构(如第 2 章的静态链表和第 6 章的二叉树 的三叉链表),本书也提供了完整的基本操作函数及主程序和程序运行结果; (3) 教科书中每一个程序在 Borland C++ Version 3.1 下的运行结果。 本书附带包含书中所有程序的光盘。所有程序(在光盘的\BC 子目录下)都在 Borland C++ Version 3.1 和 Microsoft Visual C++ 6.0 下运行通过。为了方便使用标准 C 语言的读者, 光盘中也附有用标准 C 语言改写的所有程序(在光盘的\TC 子目录下)。用标准 C 语言改写 的所有程序都在 Turbo C 2.0 下运行通过。要注意的是,光盘中文件的属性是“只读”的。 本书紧密配合教科书,故在章节编排上与教科书保持一致,以便读者对照查找。在引 用教科书中的算法和基本操作时,尽量与其保持一致,不做或少做修改。有些章节内容因 易于理解和掌握,故未提供学习指导,只保留了章节目录。 本书曾以讲义的形式印过两次(第一次仅包括前 7 章),受到学生的欢迎和好评。学生 普遍反映本书对于理解教科书内容很有帮助,有的学生还建议正式出版。正是学生对本书 的认可给了我极大的鼓励,谨在此对他们表示深深的感谢。 作者对于学习方法的建议: 对于每一种数据类型,要注重主要结构的基本操作。如第 2 章,要注重顺序表和单链 表的基本操作。有余力再看次要结构的基本操作。 对于每一个程序,不应仅仅满足于运行出结果,应根据自己的研究目的修改主程序, 或在函数中加一些输出语句,以便更好地理解各函数的意义。 各种数据类型的结构都有相通之处,可多做对比,达到融会贯通之效力。 尽管作者尽了最大努力,但限于水平,书中疏漏之处在所难免。希望读者不吝赐教, 以便再版时修订。作者 E-mail:gyfan@chd.edu.cn。读者也可通过出版社与我取得联系。 作 者 2002 年 6 月 第 二 版 前 言 本书第一版面市以来,受到广大读者的欢迎并被部分院校选为教材。看到自己辛勤劳 动的成果得到读者认可,欣慰之余更感到责任重大。本着对读者负责、精益求精的精神, 为了更好地发挥本书的作用,对第一版进行了修订。修订内容主要有以下几个方面: (1) 增加了一些叙述性的文字。如在 2.1 节,不仅说明算法 2.1 和 2.2 在两个程序中实 现,而且提示其原因。 (2) 增加了一些图示。如在程序 algo4-3.cpp 中,增加了图 4 关于文本结构的示例。 目的是克服仅通过文字描述存储结构不直观、不形象的缺点,便于读者掌握程序和算法的 精髓。 增删了一些存储结构及其基本操作。在第 章增加了不带头结点的单链表的存储 结构及其基本操作。另外,删去了诸如线性表的扩展操作等有些重复的程序。这样,既使 得程序简明,又将本书前后内容有机结合,融会贯通。 修改了一些算法。如在程序 中加了一个常量,使 从将十进 制的整数转换为八进制的功能扩展到将十进制的整数转换为二~九进制。 增加了几个算法。如在 节,教科书中描述了克鲁斯卡尔算法的原理,但没有 给出相应的算法,程序 实现了克鲁斯卡尔算法。 增加了几个算法应用程序。如程序 将算法 应用于教科书的图 (全国交通网),显示了算法的实用性,同时也增加了趣味性。 增加了 个可视化的应用程序。将 改编为 下的可视化的 程序在光盘的 子目录下。其目的是增加趣味性,提高读者的学习兴趣。同时 也说明,“算法与数据结构”不仅能用于传统的 界面,也能应用于视窗界面。 订正了第 版中出现的错误。如在程序 的 函数中将语句 改为 (9) 修改了一些程序。目的是使程序更加合理、简明。如将程序 bo7-1.cpp 的 DFS()函 数中的 for(w=FirstAdjVex(G,v1);w>=0;w=NextAdjVex(G,v1,strcpy(w1,GetVex(G,w)))) 改为 for(w=FirstAdjVex(G,G.vexs[v]);w>=0;w=NextAdjVex(G,G.vexs[v],G.vexs[w])) 虽然经过作者的精心修订,书中疏漏、错误及可改进之处恐仍在所难免,希望读者不 吝赐教,以便有机会时改正。 作 者 2004 年 7 月 于长安大学 - 1 - 目 录 第1章 绪论........................................................................................................................................1 1.1 什么是数据结构 .................................................................................................................................1 1.2 基本概念和术语 .................................................................................................................................1 1.3 抽象数据类型的表示与实现...............................................................................................................1 1.4 算法和算法分析 .................................................................................................................................7 1.4.1 算法.............................................................................................................................................7 1.4.2 算法设计的要求..........................................................................................................................7 1.4.3 算法效率的度量..........................................................................................................................7 第2章 线性表....................................................................................................................................9 2.1 线性表的类型定义..............................................................................................................................9 2.2 线性表的顺序表示和实现 ..................................................................................................................9 2.3 线性表的链式表示和实现 ................................................................................................................21 2.3.1 线性链表 ...................................................................................................................................21 2.3.2 循环链表 ...................................................................................................................................60 2.3.3 双向链表 ...................................................................................................................................65 2.4 一元多项式的表示及相加 ................................................................................................................80 第3章 栈和队列 .............................................................................................................................86 3.1 栈......................................................................................................................................................86 3.1.1 抽象数据类型栈的定义.............................................................................................................86 3.1.2 栈的表示和实现........................................................................................................................86 3.2 栈的应用举例...................................................................................................................................90 3.2.1 数制转换 ...................................................................................................................................90 3.2.2 括号匹配的检验........................................................................................................................92 3.2.3 行编辑程序................................................................................................................................93 3.2.4 迷宫求解 ...................................................................................................................................95 3.2.5 表达式求值.............................................................................................................................. 100 3.3 栈与递归的实现 ............................................................................................................................. 104 3.4 队列................................................................................................................................................ 108 3.4.1 抽象数据类型的定义............................................................................................................... 108 3.4.2 链队列队列的链式表示和实现 循环队列队列的顺序表示和实现 离散事件模拟 第4章 串 串类型的定义 - 2 - 4.2 串的表示和实现 ............................................................................................................................. 140 4.2.1 定长顺序存储表示 .................................................................................................................. 140 4.2.2 堆分配存储表示...................................................................................................................... 146 4.2.3 串的块链存储表示 .................................................................................................................. 151 4.3 串的模式匹配算法.......................................................................................................................... 158 4.3.1 求子串位置的定位函数Index(S,T,pos)..................................................................................... 158 4.3.2 模式匹配的一种改进算法....................................................................................................... 159 4.4 串操作应用举例 ............................................................................................................................. 161 4.4.1 文本编辑 ................................................................................................................................. 161 4.4.2 建立词索引表.......................................................................................................................... 167 第5章 数组和广义表 .................................................................................................................. 177 5.1 数组的定义..................................................................................................................................... 177 5.2 数组的顺序表示和实现.................................................................................................................. 177 5.3 矩阵的压缩存储 ............................................................................................................................. 181 5.3.1 特殊矩阵 ................................................................................................................................. 181 5.3.2 稀疏矩阵 ................................................................................................................................. 181 5.4 广义表的定义................................................................................................................................. 206 5.5 广义表的存储结构.......................................................................................................................... 206 5.6 m元多项式的表示 .......................................................................................................................... 207 5.7 广义表的递归算法.......................................................................................................................... 207 5.7.1 求广义表的深度...................................................................................................................... 207 5.7.2 复制广义表.............................................................................................................................. 208 5.7.3 建立广义表的存储结构........................................................................................................... 208 第6章 树和二叉树....................................................................................................................... 218 6.1 树的定义和基本术语...................................................................................................................... 218 6.2 二叉树 ............................................................................................................................................ 218 6.2.1 二叉树的定义.......................................................................................................................... 218 6.2.2 二叉树的性质.......................................................................................................................... 218 6.2.3 二叉树的存储结构 .................................................................................................................. 218 6.3 遍历二叉树和线索二叉树 .............................................................................................................. 245 6.3.1 遍历二叉树.............................................................................................................................. 245 6.3.2 线索二叉树.............................................................................................................................. 245 6.4 树和森林......................................................................................................................................... 254 6.4.1 树的存储结构.......................................................................................................................... 254 6.4.2 森林与二叉树的转换............................................................................................................... 270 6.4.3 树和森林的遍历...................................................................................................................... 270 6.5 树与等价问题................................................................................................................................. 271 6.6 赫夫曼树及其应用.......................................................................................................................... 271 6.6.1 最优二叉树(赫夫曼树)........................................................................................................... 271 6.6.2 赫夫曼编码.............................................................................................................................. 271 - 3 - 第7章 图 ........................................................................................................................................ 277 7.1 图的定义和术语 ............................................................................................................................. 277 7.2 图的存储结构................................................................................................................................. 277 7.2.1 数组表示法.............................................................................................................................. 277 7.2.2 邻接表..................................................................................................................................... 292 7.2.3 十字链表 ................................................................................................................................. 306 7.2.4 邻接多重表.............................................................................................................................. 317 7.3 图的遍历......................................................................................................................................... 328 7.3.1 深度优先搜索.......................................................................................................................... 328 7.3.2 广度优先搜索.......................................................................................................................... 329 7.4 图的连通性问题 ............................................................................................................................. 335 7.4.1 无向图的连通分量和生成树 ................................................................................................... 335 7.4.2 有向图的强连通分量............................................................................................................... 338 7.4.3 最小生成树.............................................................................................................................. 338 7.4.4 关节点和重连通分量............................................................................................................... 343 7.5 有向无环图及其应用...................................................................................................................... 347 7.5.1 拓扑排序 ................................................................................................................................. 347 7.5.2 关键路径 ................................................................................................................................. 350 7.6 最短路径......................................................................................................................................... 353 7.6.1 从某个源点到其余各顶点的最短路径 .................................................................................... 353 7.6.2 每一对顶点之间的最短路径 ................................................................................................... 356 第8章 动态存储管理 .................................................................................................................. 366 8.1 概述................................................................................................................................................ 366 8.2 可利用空间表................................................................................................................................. 366 8.3 边界标识法..................................................................................................................................... 366 8.3.1 可利用空间表的结构............................................................................................................... 366 8.3.2 分配算法 ................................................................................................................................. 367 8.3.3 回收算法 ................................................................................................................................. 374 8.4 伙伴系统......................................................................................................................................... 374 8.4.1 可利用空间表的结构............................................................................................................... 374 8.4.2 分配算法 ................................................................................................................................. 375 8.4.3 回收算法 ................................................................................................................................. 380 8.5 无用单元收集................................................................................................................................. 381 第9章 查找.................................................................................................................................... 384 9.1 静态查找表..................................................................................................................................... 384 9.1.1 顺序表的查找.......................................................................................................................... 384 9.1.2 有序表的查找.......................................................................................................................... 387 9.1.3 静态树表的查找...................................................................................................................... 388 9.1.4 索引顺序表的查找 .................................................................................................................. 390 - 4 - 9.2 动态查找表..................................................................................................................................... 391 9.2.1 二叉排序树和平衡二叉树....................................................................................................... 391 9.2.2 BÁ树和B+树.............................................................................................................................. 399 9.2.3 键树......................................................................................................................................... 404 9.3 哈希表 ............................................................................................................................................ 413 9.3.1 什么是哈希表.......................................................................................................................... 413 9.3.2 哈希函数的构造方法............................................................................................................... 413 9.3.3 处理冲突的方法...................................................................................................................... 413 9.3.4 哈希表的查找及其分析........................................................................................................... 414 第10章 内部排序......................................................................................................................... 419 10.1 概述.............................................................................................................................................. 419 10.2 插入排序....................................................................................................................................... 419 10.2.1 直接插入排序........................................................................................................................ 419 10.2.2 其它插入排序........................................................................................................................ 422 10.2.3 希尔排序 ............................................................................................................................... 425 10.3 快速排序....................................................................................................................................... 426 10.4 选择排序....................................................................................................................................... 429 10.4.1 简单选择排序........................................................................................................................ 429 10.4.2 树形选择排序........................................................................................................................ 431 10.4.3 堆排序................................................................................................................................... 432 10.5 归并排序....................................................................................................................................... 434 10.6 基数排序....................................................................................................................................... 435 10.6.1 多关键字的排序.................................................................................................................... 435 10.6.2 链式基数排序........................................................................................................................ 435 10.7 各种内部排序方法的比较讨论..................................................................................................... 440 第11章 外部排序......................................................................................................................... 441 11.1 外存信息的存取............................................................................................................................ 441 11.2 外部排序的方法............................................................................................................................ 441 11.3 多路平衡归并的实现.................................................................................................................... 441 11.4 置换—选择排序 ............................................................................................................................ 446 第12章 文件.................................................................................................................................. 452 12.1 有关文件的基本概念.................................................................................................................... 452 12.2 顺序文件....................................................................................................................................... 452 附录A 关于标准C程序 ................................................................................................................ 456 附录B 光盘文件目录 .................................................................................................................. 461 第 1 章 绪 论 · 1· 第 1 章 绪 论 本书主要由实现基本操作和算法的程序构成。这些程序文件有 6 类: (1) 数据存储结构。文件名第一个字母为 c,以 h 为扩展名。如 c1-1.h 是第 1 章的第 1 种存储结构。 (2) 每种存储结构的一组基本操作函数。以 bo(bo 表示基本操作)开头,cpp 为扩展 名。如 bo2-4.cpp 是第 2 章第 4 种存储结构的一组基本操作函数。教科书中涉及基本操作 的算法也收到基本操作函数中且在函数中注明算法的编号。 (3) 调用基本操作的主程序。以 main(main 表示主程序)开头,cpp 为扩展名。如 main3-5.cpp 是调用 bo3-5.cpp 的主程序。 (4) 实现算法的程序。以 algo(algo 表示算法)开头,cpp 为扩展名。如 algo6-2.cpp 是 实现算法 6.13 的程序。 (5) 不属于基本操作又被多次调用的函数。以 func 开头,cpp 为扩展名。如 func2-3.cpp 中的函数 equal()、comp()、print()等被许多程序调用。为节约篇幅,将这些 函数放到 func2-3.cpp 中。 (6) 数据文件。以 txt 为扩展名。如第 4 章的 bookinfo.txt 等。 只有以 main、algo 开头的程序文件有主函数 main(),而且是可执行的程序。其它程 序都是被调用的程序,通过#include 命令包括在可执行程序中。它们可能被多次调用。为 了节省篇幅,只出现在书中第 1 次调用前。 为了便于查找,附录 B 中给出了每个程序文件出现的章节号及其在光盘中的位置。 1.1 什么是数据结构 1.2 基本概念和术语 1.3 抽象数据类型的表示与实现 教科书定义 OK、ERROR 等为函数的结果状态代码,Status 为其类型。我们把这些信 息放到头文件 c1.h 中。c1.h 还包含一些常用的头文件,如 string.h、stdio.h 等。为了操作 方便,本书几乎每一个程序都把 c1.h 包含进去,也就把这些结果状态代码的定义和头文 件包含了进去。对于某一个程序来说,有些结果状态代码和头文件并没有用到,不过这不 · 2· 《数据结构》算法实现及解析(第二版) 影响使用。头文件 c1.h 内容如下: // c1.h (程序名) #include #include #include // malloc()等 #include // INT_MAX等 #include // EOF(=^Z或F6),NULL #include // atoi() #include // eof() #include // floor(),ceil(),abs() #include // exit() #include // cout,cin // 函数结果状态代码 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 // #define OVERFLOW -2 因为在math.h中已定义OVERFLOW的值为3,故去掉此行 typedef int Status; // Status是函数的类型,其值是函数结果状态代码,如OK等 typedef int Boolean; // Boolean是布尔类型,其值是TRUE或FALSE 教科书中的例 17 定义了一个三元组的抽象数据类型 Triplet。通过例 17,给出了 这个三元组的表示方法和所定义的基本操作函数的实现。 例 17 实际上是教科书第 2~7 章中各种存储结构的一个范例:定义一种存储结构及 建立在这种存储结构上的一组基本操作,并给出基本操作的实现。下面就是例 17 在程 序中的具体实现: 头文件 c1-1.h 定义了三元组的抽象数据类型 Triplet。它采用动态分配的顺序存储结构(见图 1): // c1-1.h 采用动态分配的顺序存储结构 typedef ElemType *Triplet; // 由InitTriplet分配3个元素存储空间 // Triplet类型是ElemType类型的指针,存放ElemType类型的地址 在 -. 中 我 们 遇 到 ( 元 素 类 型 ),在后面的章节中我们还会遇到 (栈元素类型)、(队列元素类型)和 (树元素类型)等。在 诸如 -. 这类头文件中,它们是抽象的数据类型。也就是说,还没有确定它们是 语 言的哪一种具体的类型。 -. 是有关抽象数据类型 和 的 个基本操作函数。这 个函 数返回值的类型都是 ,即函数返回值只能是头文件 中定义的 、 等。 // bo1-1.cpp 抽象数据类型Triplet和ElemType(由c1-1.h定义)的基本操作(8个) Status InitTriplet(Triplet &T,ElemType v1,ElemType v2,ElemType v3) { // 操作结果:构造三元组T,依次置T的3个元素的初值为v1,v2和v3(见图12) 图 11 采用动态分配的顺序存储结构 ElemTypeTriplet 第 1 章 绪 论 · 3· if(!(T=(ElemType *)malloc(3*sizeof(ElemType)))) exit(OVERFLOW); T[0]=v1,T[1]=v2,T[2]=v3; return OK; } Status DestroyTriplet(Triplet &T) { // 操作结果:三元组T被销毁(见图13) free(T); T=NULL; return OK; } Status Get(Triplet T,int i,ElemType &e) { // 初始条件:三元组T已存在,1≤ i≤ 3。操作结果:用e返回T的第i元的值 if(i<1||i>3) return ERROR; e=T[i-1]; return OK; } Status Put(Triplet T,int i,ElemType e) { // 初始条件:三元组T已存在,1≤ i≤ 3。操作结果:改变T的第i元的值为e if(i<1||i>3) return ERROR; T[i-1]=e; return OK; } Status IsAscending(Triplet T) { // 初始条件:三元组T已存在。操作结果:如果T的3个元素按升序排列,则返回1;否则返回0 return(T[0]<=T[1]&&T[1]<=T[2]); } Status IsDescending(Triplet T) { // 初始条件:三元组T已存在。操作结果:如果T的3个元素按降序排列,则返回1;否则返回0 return(T[0]>=T[1]&&T[1]>=T[2]); } Status Max(Triplet T,ElemType &e) { // 初始条件:三元组T已存在。操作结果:用e返回指向T的最大元素的值 e=T[0]>=T[1]?T[0]>=T[2]?T[0]:T[2]:T[1]>=T[2]?T[1]:T[2]; return OK; } Status Min(Triplet T,ElemType &e) { // 初始条件:三元组T已存在。操作结果:用e返回指向T的最小元素的值 e=T[0]<=T[1]?T[0]<=T[2]?T[0]:T[2]:T[1]<=T[2]?T[1]:T[2]; return OK; } main1-1.cpp 是检验 bo1-1.cpp 中的各基本操作函数是否正确的主函数。在 main1-1.cpp 中可根据需要定义抽象数据类型 ElemType 为 int、double 或其它数值型类型(只能是数值类 型,因为其中有几个函数是比较大小的),而不需改变基本操作 bo1-1.cpp,这使得 bo1-1.cpp 的通用性大大增强。在 main1-1.cpp 中,有些基本操作的实参(ElemType 类型) 要根据 ElemType 的类型做相应改变,而另一些基本操作的实参(Triplet 类型,总是 图 12 构造三元组 T T v1 v2 v3 T[0] T[1] T[2] 图 13 三元组 T 被销毁 T NULL · 4· 《数据结构》算法实现及解析(第二版) ElemType 类型的指针类型)不需改变。 // main1-1.cpp 检验基本操作bo1-1.cpp的主函数 #include"c1.h" // 要将程序中所有#include命令所包含的文件拷贝到当前目录下 // 以下2行可根据需要选用一个(且只能选用一个),而不需改变基本操作bo1-1.cpp typedef int ElemType; // 定义抽象数据类型ElemType在本程序中为整型 //typedef double ElemType; // 定义抽象数据类型ElemType在本程序中为双精度型 #include"c1-1.h" // 在此命令之前要定义ElemType的类型 #include"bo1-1.cpp" // 在此命令之前要包括c1-1.h文件(因为其中定义了Triplet) void main() { Triplet T; ElemType m; Status i; i=InitTriplet(T,5,7,9); // 初始化三元组T,其3个元素依次为5,7,9 //i=InitTriplet(T,5.0,7.1,9.3); // 当ElemType为双精度型时,可取代上句 printf("调用初始化函数后,i=%d(1:成功)T的3个值为",i); cout< void fa(int a) // 在函数中改变a,将不会带回主调函数(主调函数中的a仍是原值) { a++; printf("在函数fa中:a=%d\n",a); } void fb(int &a) // 由于a为引用类型,在函数中改变a,其值将带回主调函数 { a++; printf("在函数fb中:a=%d\n",a); } void main() { int n=1; printf("在主程中,调用函数fa之前:n=%d\n",n); fa(n); printf("在主程中,调用函数fa之后,调用函数fb之前:n=%d\n",n); fb(n); printf("在主程中,调用函数fb之后:n=%d\n",n); } · 6· 《数据结构》算法实现及解析(第二版) 程序运行结果: 在主程中,调用函数fa之前:n=1 在函数fa中:a=2 在主程中,调用函数fa之后,调用函数fb之前:n=1 在函数fb中:a=2 在主程中,调用函数fb之后:n=2 标准 C 语言中没有引用类型,把 C++中含有引用类型的函数转换为标准 C 语言可执行 的语句的方法详见附录 A。为了方便使用标准 C 语言的读者,本书所附光盘中 TC\子目录 下有本书中所有程序的标准 C 语言版。 bo1-1.cpp 中的 exit()函数是 C 语言的库函数。它的作用是中断程序的运行。程序 algo1-4.cpp 说明了 exit()函数的作用: // algo1-4.cpp 说明exit()函数作用的程序 #include"c1.h" int a(int i) { if(i==1) { printf("退出程序的运行\n"); exit(1); } return i; } void main() { int i; printf("请输入i:"); scanf("%d",&i); printf("a(i)=%d\n",a(i)); } 程序运行结果: 请输入i:1 退出程序的运行 请输入i:2 a(i)=2 当输入为 1 时,执行 exit()语句,退出程序的运行,不执行主程序的 printf()语句;而 当输入为 2 时,不执行 exit()语句,返回主程序后,执行 printf()语句。 第 1 章 绪 论 · 7· 1.4 算法和算法分析 1.4.1  1.4.2  1.4.3   同样是计算 1-1/x+1/x*x⋯, algo1-1.cpp 的语句频度表达式为(1+n)*n/2,它的时间复 杂度 T(n)=O(n2);而 algo1-2.cpp 的语句频度表达式为 n,它的时间复杂度 T(n)=O(n)。 从两个程序的运行结果可以看出:当输入数据一样时,计算结果是一样的,但运行时间的 差别很大。在算法正确的前提下,应该选择算法效率高的。 // algo1-1.cpp 计算1-1/x+1/x*x⋯ #include #include void main() { timeb t1,t2; long t; double x,sum=1,sum1; int i,j,n; printf("请输入x n:"); scanf("%lf%d",&x,&n); ftime(&t1); // 求得当前时间 for(i=1;i<=n;i++) { sum1=1; for(j=1;j<=i;j++) sum1=sum1*(-1.0/x); sum+=sum1; } ftime(&t2); // 求得当前时间 t=(t2.time-t1.time)*1000+(t2.millitm-t1.millitm); // 计算时间差 printf("sum=%lf 用时%ld毫秒\n",sum,t); } 程序运行结果(其中用时与计算机的配置有关,带下划线的字符为键盘输入): 请输入x n:123 10000 sum=0.991935 用时5440毫秒 // algo1-2.cpp 计算1-1/x+1/x*x⋯的更快捷的算法 #include #include · 8· 《数据结构》算法实现及解析(第二版) void main() { timeb t1,t2; long t; double x,sum1=1,sum=1; int i,n; printf("请输入x n: "); scanf("%lf%d",&x,&n); ftime(&t1); // 求得当前时间 for(i=1;i<=n;i++) { sum1*=-1.0/x; sum+=sum1; } ftime(&t2); // 求得当前时间 t=(t2.time-t1.time)*1000+(t2.millitm-t1.millitm); // 计算时间差 printf("sum=%lf 用时%ld毫秒\n",sum,t); } 程序运行结果(其中用时与计算机的配置有关): 请输入x n: 123 10000 sum=0.991935 用时0毫秒 第 2 章 线 性 表 · 9· 第 2 章 线 性 表 2.1 线性表的类型定义 线性表的基本操作共有 12 个。通过将基本操作有机地组合,还可以对线性表进行较 复杂的处理。算法 2.1 和算法 2.2 就是这样的例子。 注意到算法 2.1 和算法 2.2 的形参 La、Lb 和 Lc 的类型是 List(表),List 并不是稍后 将要介绍的具体的线性表存储结构如 SqList 和 LinkList 等,它是抽象的线性表类型。算法 2.1 和算法 2.2 中所涉及的函数都是基本操作,如 GetElem()、ListLength()等,所涉及的 变量类型也都是 C 语言的数据类型,不涉及具体的数据存储结构。这样,算法 2.1 和算法 2.2 就可以应用到任何一种具体的线性表存储结构中。 采用 SqList 和 LinkList 两种线性表存储结构实现算法 2.1 的程序分别在 algo2-1.cpp 和 algo2-12.cpp 中,实现算法 2.2 的程序分别在 algo2-2.cpp 和 algo2-13.cpp 中。 2.2 线性表的顺序表示和实现 顺序表存储结构容易实现随机存取线性表的第 i 个数据元素的操作,但在实现插入、 删除的操作时要移动大量数据元素,所以,它适用于数据相对稳定的线性表,如职工工资 表、学生学籍表等。 c2-1.h 是动态分配的顺序表存储结构,bo2-1.cpp 是基于顺序表的基本操作。由于 C++函数可重载,故去掉 bo2-1.cpp 中算法 2.3 等函数名中表示存储类型的后缀_Sq。 c2-1.h 不采用固定数组作为线性表的存储结构,而采用动态分配的存储结构,这样可以合 理地利用空间,使长表占用的存储空间多,短表占用的存储空间少。这些可通过 bo2-1.cpp 中基本操作函数 ListInsert()和图 24 清楚看出。 // c2-1.h 线性表的动态分配顺序存储结构(见图21) #define LIST_INIT_SIZE 10 // 线性表存储空间的初始分配量 #define LIST_INCREMENT 2 // 线性表存储空间的分配增量 struct SqList { ElemType *elem; // 存储空间基址 int length; // 当前长度 int listsize; // 当前分配的存储容量(以sizeof(ElemType)为单位) }; 图 21 动态分配顺序存储结构 elem length listsize SqList ElemType · 10· 《数据结构》算法实现及解析(第二版) 图 21 表示结构体 SqList 的存储结构。用一个始于 elem 的箭头表示 elem 是指针类 型。指针又是分类型的。用该箭头止于 ElemType 表明 elem 指针的类型。因为此处并不是 说明 ElemType,故用虚线表示。 // bo2-1.cpp 顺序表示的线性表(存储结构由c2-1.h定义)的基本操作(12个),包括算法2.3,2.4,2.5,2.6 void InitList(SqList &L) // 算法2.3 { // 操作结果:构造一个空的顺序线性表L(见图22) L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType)); if(!L.elem) exit(OVERFLOW); // 存储分配失败 L.length=0; // 空表长度为0 L.listsize=LIST_INIT_SIZE; // 初始存储容量 } void DestroyList(SqList &L) { // 初始条件:顺序线性表L已存在。操作结果:销毁顺序线性表L(见图23) free(L.elem); L.elem=NULL; L.length=0; L.listsize=0; } void ClearList(SqList &L) { // 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 L.length=0; } Status ListEmpty(SqList L) { // 初始条件:顺序线性表L已存在。操作结果:若L为空表,则返回TRUE;否则返回FALSE if(L.length==0) return TRUE; else return FALSE; } int ListLength(SqList L) { // 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素的个数 return L.length; } Status GetElem(SqList L,int i,ElemType &e) { // 初始条件:顺序线性表L已存在,1≤ i≤ ListLength(L)。操作结果:用e返回L中第i个数据元素的值 if(i<1||i>L.length) return ERROR; e=*(L.elem+i-1); return OK; } int LocateElem(SqList L,ElemType e,Status(*compare)(ElemType,ElemType)) { // 初始条件:顺序线性表L已存在,compare()是数据元素判定函数(满足为1,否则为0) // 操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。 // 若这样的数据元素不存在,则返回值为0。算法2.6 ElemType *p; int i=1; // i的初值为第1个元素的位序 p=L.elem; // p的初值为第1个元素的存储位置 while(i<=L.length&&!compare(*p++,e)) ++i; 图 23 销毁顺序线性表 L NULL 0 0 L 图 22 构造一个空的顺序线性表 L L [0] [1] [8] [9] 0 10   第 2 章 线 性 表 · 11· if(i<=L.length) return i; else return 0; } Status PriorElem(SqList L,ElemType cur_e,ElemType &pre_e) { // 初始条件:顺序线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱; // 否则操作失败,pre_e无定义 int i=2; ElemType *p=L.elem+1; while(i<=L.length&&*p!=cur_e) { p++; i++; } if(i>L.length) return INFEASIBLE; // 操作失败 else { pre_e=*--p; return OK; } } Status NextElem(SqList L,ElemType cur_e,ElemType &next_e) { // 初始条件:顺序线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继; // 否则操作失败,next_e无定义 int i=1; ElemType *p=L.elem; while(iL.length+1) // i值不合法 return ERROR; if(L.length>=L.listsize) // 当前存储空间已满,增加分配 { if(!(newbase=(ElemType *)realloc(L.elem,(L.listsize+LIST_INCREMENT)*sizeof(ElemType)))) · 12· 《数据结构》算法实现及解析(第二版) exit(OVERFLOW); // 存储分配失败 L.elem=newbase; // 新基址 L.listsize+=LIST_INCREMENT; // 增加存储容量 } q=L.elem+i-1; // q为插入位置 for(p=L.elem+L.length-1;p>=q;--p) // 插入位置及之后的元素右移 *(p+1)=*p; *q=e; // 插入e ++L.length; // 表长增1 return OK; } Status ListDelete(SqList &L,int i,ElemType &e) // 算法2.5 { // 初始条件:顺序线性表L已存在,1≤ i≤ ListLength(L) // 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1(见图25) ElemType *p,*q; if(i<1||i>L.length) // i值不合法 return ERROR; p=L.elem+i-1; // p为被删除元素的位置 e=*p; // 被删除元素的值赋给e q=L.elem+L.length-1; // 表尾元素的位置 for(++p;p<=q;++p) // 被删除元素之后的元素左移 *(p-1)=*p; 图 24 调用 ListInsert()示例(i=7, e=11) (a) L 调用函数之前的状态 (b) L 增加存储容量、移动元素 (c) L 调用函数之后的状态 1 2 3 4 5 6 7 8 9 0 [0] [1] [8] [9] L 10 10 p (最初位置) 1 2 3 4 5 6 7 8 9 0 [0] [1] [10] [11] L 10 12 q 1 2 3 4 5 6 11 7 8 9 0 [0] [1] [10] [11] L 11 12 图 25 调用 ListDelete()示例(i=7) (a) L 调用函数之前的状态 (b) L 移动元素 (c) L 调用函数之后的状态 1 2 3 4 5 6 7 8 9 10 11 [0] [1] [10] [11] L 11 12 p 1 2 3 4 5 6 7 8 9 10 11 [0] [1] [10] [11] L 11 12 q (最初位置) 返回参数: e=7 1 2 3 4 5 6 8 9 10 11 11 [0] [1] [10] [11] L 10 12       第 2 章 线 性 表 · 13· L.length--; // 表长减1 return OK; } void ListTraverse(SqList L,void(*vi)(ElemType&)) { // 初始条件:顺序线性表L已存在 // 操作结果:依次对L的每个数据元素调用函数vi() // vi()的形参加′&′,表明可通过调用vi()改变元素的值 ElemType *p; int i; p=L.elem; for(i=1;i<=L.length;i++) vi(*p++); printf("\n"); } bo2-1.cpp 中的 ListTraverse()函数要调用 vi()函数,vi()是 ListTraverse()的形参。因 为 vi()不是一个特定的函数,它是满足一定条件的一类函数,所以称其为函数类形参。在 函数类形参的声明中指定了 vi()的函数类型,也就是函数返回值的类型(void)。在声明中 还指定了 vi()函数形参的个数(1 个)和类型(ElemType 的引用类型)。所有满足条件的函数 (返回值为 void 类型,有一个形参,且类型为 ElemType&),都可以作为 ListTraverse()函 数的实参。 LocateElem()函数也要调用一类函数 compare()。由 LocateElem()函数的声明可以看 出:要求这类函数具有 2 个形参,其类型均为 ElemType;函数的返回值为 Status 类型。不 仅如此,根据 LocateElem()函数的说明(初始条件),当 compare()函数的 2 个形参满足给 定条件时,返回值为 1;否则为 0。只有满足这种条件的函数才能作为替代 compare()函数 的实参函数。 main2-1.cpp 是检验 bo2-1.cpp 中的各基本操作函数是否正确的主函数。其中 equal() 和 sq() 函 数 满 足 LocateElem() 函 数 对 函 数 类 形 参 compare() 的 要 求 , 可 以 作 为 LocateElem()的调用函数。print1()函数和 dbl()函数满足 ListTraverse()函数对函数类形参 vi()的要求,可以作为 ListTraverse()的调用函数。其中,print1()函数只是向屏幕输出形 参 c,并不改变 c,所以形参不需要是引用类型,但为了和 ListTraverse()函数的要求一 致,其形参被定为引用类型。ListTraverse()函数之所以要求 vi()的形参为引用类型,是因 为另一个实参函数 dbl()是给形参的值加倍,且要将形参值的改变带回主调函数,故必须 是引用类型。 // func2-3.cpp 几个常用的函数 Status equal(ElemType c1,ElemType c2) { // 判断是否相等的函数 if(c1==c2) return TRUE; else return FALSE; } int comp(ElemType a,ElemType b) · 14· 《数据结构》算法实现及解析(第二版) { // 根据a<、=或>b,分别返回-1、0或1 if(a==b) return 0; else return (a-b)/abs(a-b); } void print(ElemType c) { printf("%d ",c); } void print2(ElemType c) { printf("%c ",c); } void print1(ElemType &c) { printf("%d ",c); } // main2-1.cpp 检验bo2-1.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c2-1.h" #include"bo2-1.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 Status sq(ElemType c1,ElemType c2) { // 数据元素判定函数(平方关系),LocateElem()调用的函数 if(c1==c2*c2) return TRUE; else return FALSE; } void dbl(ElemType &c) { // ListTraverse()调用的另一函数(元素值加倍) c*=2; } void main() { SqList L; ElemType e,e0; Status i; int j,k; InitList(L); printf("初始化L后:L.elem=%u L.length=%d L.listsize=%d\n",L.elem,L.length,L.listsize); for(j=1;j<=5;j++) i=ListInsert(L,1,j); printf("在L的表头依次插入1~5后:*L.elem="); for(j=1;j<=5;j++) cout<<*(L.elem+j-1)<<′′; cout<=k;j--) { i=ListDelete(L,j,e); // 删除第j个数据 if(i==ERROR) printf("删除第%d个元素失败\n",j); else printf("删除第%d个元素成功,其值为%d\n",j,e); } printf("依次输出L的元素:"); ListTraverse(L,print1); // 依次对元素调用print1(),输出元素的值 printf("L的元素值加倍后:"); ListTraverse(L,dbl); // 依次对元素调用dbl(),元素值乘2 ListTraverse(L,print1); DestroyList(L); printf("销毁L后:L.elem=%u L.length=%d L.listsize=%d\n",L.elem,L.length,L.listsize); } 程序运行结果: 初始化L后:L.elem=2558 L.length=0 L.listsize=10 在L的表头依次插入1~5后:*L.elem=5 4 3 2 1 L.elem=2558 L.length=5 L.listsize=10 L是否空:i=0(1:是 0:否) 清空L后:L.elem=2558 L.length=0 L.listsize=10 L是否空:i=1(1:是 0:否) 在L的表尾依次插入1~10后:*L.elem=1 2 3 4 5 6 7 8 9 10 L.elem=2558 L.length=10 L.listsize=10 在L的表头插入0后:*L.elem=0 1 2 3 4 5 6 7 8 9 10 L.elem=2582(有可能改变) L.length=11(改变) L.listsize=12(改变) 第5个元素的值为4 第11个元素的值为10 没有值为11的元素 第10个元素的值为3的平方 没有值为4的平方的元素 元素0无前驱 元素1的前驱为0 元素9的后继为10 元素10无后继 删除第12个元素失败 删除第11个元素成功,其值为10 依次输出L的元素:0 1 2 3 4 5 6 7 8 9 L的元素值加倍后: 0 2 4 6 8 10 12 14 16 18 销毁L后:L.elem=0 L.length=0 L.listsize=0 第 2 章 线 性 表 · 17· 以下是采用线性表的动态分配顺序存储结构(c2-1.h)调用几个不是基本操作的算法的 程序,它们都包含了 bo2-1.cpp 基本操作函数文件。 // algo2-1.cpp 实现算法2.1的程序 #include"c1.h" typedef int ElemType; #include"c2-1.h" // 采用线性表的动态分配顺序存储结构 #include"bo2-1.cpp" // 可以使用bo2-1.cpp中的基本操作 #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void Union(SqList &La,SqList Lb) // 算法2.1 { // 将所有在线性表Lb中但不在La中的数据元素插入到La中 ElemType e; int La_len,Lb_len; int i; La_len=ListLength(La); // 求线性表的长度 Lb_len=ListLength(Lb); for(i=1;i<=Lb_len;i++) { GetElem(Lb,i,e); // 取Lb中第i个数据元素赋给e if(!LocateElem(La,e,equal)) // La中不存在和e相同的元素,则插入之 ListInsert(La,++La_len,e); } } void main() { SqList La,Lb; int j; InitList(La); // 创建空表La。如不成功,则会退出程序的运行 for(j=1;j<=5;j++) // 在表La中插入5个元素,依次为1、2、3、4、5 ListInsert(La,j,j); printf("La= "); // 输出表La的内容 ListTraverse(La,print1); InitList(Lb); // 创建空表Lb for(j=1;j<=5;j++) // 在表Lb中插入5个元素,依次为2、4、6、8、10 ListInsert(Lb,j,2*j); printf("Lb= "); // 输出表Lb的内容 ListTraverse(Lb,print1); Union(La,Lb); // 调用Union(),将Lb中满足条件的元素插入La printf("new La= "); // 输出新表La的内容 ListTraverse(La,print1); } 程序运行结果: La= 1 2 3 4 5 Lb= 2 4 6 8 10 new La= 1 2 3 4 5 6 8 10 · 18· 《数据结构》算法实现及解析(第二版) // algo2-2.cpp 实现算法2.2的程序 #include"c1.h" typedef int ElemType; #include"c2-1.h" #include"bo2-1.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void MergeList(SqList La,SqList Lb,SqList &Lc) // 算法2.2 { // 已知线性表La和Lb中的数据元素按值非递减排列。 // 归并La和Lb得到新的线性表Lc,Lc的数据元素也按值非递减排列 int i=1,j=1,k=0; int La_len,Lb_len; ElemType ai,bj; InitList(Lc); // 创建空表Lc La_len=ListLength(La); Lb_len=ListLength(Lb); while(i<=La_len&&j<=Lb_len) // 表La和表Lb均非空 { GetElem(La,i,ai); GetElem(Lb,j,bj); if(ai<=bj) { ListInsert(Lc,++k,ai); ++i; } else { ListInsert(Lc,++k,bj); ++j; } } // 以下两个while循环只会有一个被执行 while(i<=La_len) // 表La非空且表Lb空 { GetElem(La,i++,ai); ListInsert(Lc,++k,ai); } while(j<=Lb_len) // 表Lb非空且表La空 { GetElem(Lb,j++,bj); ListInsert(Lc,++k,bj); } } void main() { SqList La,Lb,Lc; int j,a[4]={3,5,8,11},b[7]={2,6,8,9,11,15,20}; InitList(La); // 创建空表La for(j=1;j<=4;j++) // 在表La中插入4个元素 ListInsert(La,j,a[j-1]); printf("La= "); // 输出表La的内容 ListTraverse(La,print1); InitList(Lb); // 创建空表Lb 第 2 章 线 性 表 · 19· for(j=1;j<=7;j++) // 在表Lb中插入7个元素 ListInsert(Lb,j,b[j-1]); printf("Lb= "); // 输出表Lb的内容 ListTraverse(Lb,print1); MergeList(La,Lb,Lc); printf("Lc= "); // 输出表Lc的内容 ListTraverse(Lc,print1); } 程序运行结果: La= 3 5 8 11 Lb= 2 6 8 9 11 15 20 Lc= 2 3 5 6 8 8 9 11 11 15 20 // algo2-3.cpp 实现算法2.7的程序 #include"c1.h" typedef int ElemType; #include"c2-1.h" #include"bo2-1.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void MergeList(SqList La,SqList Lb,SqList &Lc) // 算法2.7 { // 已知顺序线性表La和Lb的元素按值非递减排列。 // 归并La和Lb得到新的顺序线性表Lc,Lc的元素也按值非递减排列 ElemType *pa,*pa_last,*pb,*pb_last,*pc; pa=La.elem; pb=Lb.elem; Lc.listsize=Lc.length=La.length+Lb.length; // 不用InitList()创建空表Lc pc=Lc.elem=(ElemType *)malloc(Lc.listsize*sizeof(ElemType)); if(!Lc.elem) // 存储分配失败 exit(OVERFLOW); pa_last=La.elem+La.length-1; pb_last=Lb.elem+Lb.length-1; while(pa<=pa_last&&pb<=pb_last) // 表La和表Lb均非空 { // 归并 if(*pa<=*pb) *pc++=*pa++; // 将pa所指单元的值赋给pc所指单元后,pa和pc分别+1(指向下一个单元) else *pc++=*pb++; // 将pb所指单元的值赋给pc所指单元后,pa和pc分别+1(指向下一个单元) } // 以下两个while循环只会有一个被执行 while(pa<=pa_last) // 表La非空且表Lb空 *pc++=*pa++; // 插入La的剩余元素 while(pb<=pb_last) // 表Lb非空且表La空 *pc++=*pb++; // 插入Lb的剩余元素 } void main() { SqList La,Lb,Lc; int j; InitList(La); // 创建空表La · 20· 《数据结构》算法实现及解析(第二版) for(j=1;j<=5;j++) // 在表La中插入5个元素,依次为1、2、3、4、5 ListInsert(La,j,j); printf("La= "); // 输出表La的内容 ListTraverse(La,print1); InitList(Lb); // 创建空表Lb for(j=1;j<=5;j++) // 在表Lb中插入5个元素,依次为2、4、6、8、10 ListInsert(Lb,j,2*j); printf("Lb= "); // 输出表Lb的内容 ListTraverse(Lb,print1); // 由按非递减排列的表La、Lb得到按非递减排列的表Lc MergeList(La,Lb,Lc); printf("Lc= "); // 输出表Lc的内容 ListTraverse(Lc,print1); } 程序运行结果: La= 1 2 3 4 5 Lb= 2 4 6 8 10 Lc= 1 2 2 3 4 4 5 6 8 10 // algo2-4.cpp 修改算法2.7的第一个循环语句中的条件语句为开关语句,且当 // *pa=*pb时,只将两者中之一插入Lc。此操作的结果和算法2.1相同 #include"c1.h" typedef int ElemType; #include"c2-1.h" #include"bo2-1.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void MergeList(SqList La,SqList Lb,SqList &Lc) { // 另一种合并线性表的方法(根据算法2.7下的要求修改算法2.7),La、Lb和Lc均为按递增排列的表 ElemType *pa,*pa_last,*pb,*pb_last,*pc; pa=La.elem; pb=Lb.elem; Lc.listsize=La.length+Lb.length; // 此句与算法2.7不同 pc=Lc.elem=(ElemType *)malloc(Lc.listsize*sizeof(ElemType)); if(!Lc.elem) exit(OVERFLOW); pa_last=La.elem+La.length-1; pb_last=Lb.elem+Lb.length-1; while(pa<=pa_last&&pb<=pb_last) // 表La和表Lb均非空 switch(comp(*pa,*pb)) // 此句与算法2.7不同 { case 0: pb++; case -1: *pc++=*pa++; break; case 1: *pc++=*pb++; } while(pa<=pa_last) // 表La非空且表Lb空 *pc++=*pa++; 第 2 章 线 性 表 · 21· while(pb<=pb_last) // 表Lb非空且表La空 *pc++=*pb++; Lc.length=pc-Lc.elem; // 加此句 } void main() { SqList La,Lb,Lc; int j; InitList(La); // 创建空表La for(j=1;j<=5;j++) // 在表La中插入5个元素,依次为1、2、3、4、5 ListInsert(La,j,j); printf("La= "); // 输出表La的内容 ListTraverse(La,print1); InitList(Lb); // 创建空表Lb for(j=1;j<=5;j++) // 在表Lb中插入5个元素,依次为2、4、6、8、10 ListInsert(Lb,j,2*j); printf("Lb= "); // 输出表Lb的内容 ListTraverse(Lb,print1); MergeList(La,Lb,Lc); // 由按递增排列的表La、Lb得到按递增排列的表Lc printf("Lc= "); // 输出表Lc的内容 ListTraverse(Lc,print1); } 程序运行结果: La= 1 2 3 4 5 Lb= 2 4 6 8 10 Lc= 1 2 3 4 5 6 8 10 2.3 线性表的链式表示和实现 和顺序表相比,链表存储结构在实现插入、删除的操作时,不需要移动大量数据元素 (但不容易实现随机存取线性表的第 i 个数据元素的操作)。所以,链表适用于经常需要进 行插入和删除操作的线性表,如飞机航班的乘客表等。 2.3.1  // c2-2.h 线性表的单链表存储结构(见图26) struct LNode { ElemType data; LNode *next; }; typedef LNode *LinkList; // 另一种定义LinkList的方法 图 26 线性表的单链表存储结构 data next LNodeLinkList LNode · 22· 《数据结构》算法实现及解析(第二版) 图 27 是根据 c2-2.h 定义的带 有头结点且具有两个结点的线性链表 的结构。bo2-2.cpp 是这种带有头结点 的线性链表基本操作。 // bo2-2.cpp 带有头结点的单链表(存储结构由c2-2.h定义)的基本操作(12个),包括算法2.8,2.9,2.10 void InitList(LinkList &L) { // 操作结果:构造一个空的线性表L(见图28) L=(LinkList)malloc(sizeof(LNode)); // 产生头结点,并使L指向此头结点 if(!L) // 存储分配失败 exit(OVERFLOW); L->next=NULL; // 指针域为空 } void DestroyList(LinkList &L) { // 初始条件:线性表L已存在。操作结果:销毁线性表L(见图29) LinkList q; while(L) { q=L->next; free(L); L=q; } } void ClearList(LinkList L) // 不改变L { // 初始条件:线性表L已存在。操作结果:将L重置为空表(见图28) LinkList p,q; p=L->next; // p指向第一个结点 while(p) // 没到表尾 { q=p->next; free(p); p=q; } L->next=NULL; // 头结点指针域为空 } Status ListEmpty(LinkList L) { // 初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE;否则返回FALSE if(L->next) // 非空 return FALSE; else return TRUE; } int ListLength(LinkList L) { // 初始条件:线性表L已存在。操作结果:返回L中数据元素的个数 int i=0; LinkList p=L->next; // p指向第一个结点 while(p) // 没到表尾 { i++; p=p->next; 图 29 线性表 L 被销毁 L NULL 图 27 带有头结点且具有两个结点(4,7)的线性链表 头指针 4 7 NULL 首元结点头结点 尾元结点 图 28 空(仅有头结点)的单链表 L NULL L 头结点 第 2 章 线 性 表 · 23· } return i; } Status GetElem(LinkList L,int i,ElemType &e) // 算法2.8 { // L为带头结点的单链表的头指针。当第i个元素存在时,其值赋给e并返回OK;否则返回ERROR int j=1; // j为计数器 LinkList p=L->next; // p指向第一个结点 while(p&&jnext; j++; } if(!p||j>i) // 第i个元素不存在 return ERROR; e=p->data; // 取第i个元素 return OK; } int LocateElem(LinkList L,ElemType e,Status(*compare)(ElemType,ElemType)) { // 初始条件: 线性表L已存在,compare()是数据元素判定函数(满足为1,否则为0) // 操作结果: 返回L中第1个与e满足关系compare()的数据元素的位序。 // 若这样的数据元素不存在,则返回值为0 int i=0; LinkList p=L->next; while(p) { i++; if(compare(p->data,e)) // 找到这样的数据元素 return i; p=p->next; } return 0; } Status PriorElem(LinkList L,ElemType cur_e,ElemType &pre_e) { // 初始条件: 线性表L已存在 // 操作结果: 若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱, // 返回OK;否则操作失败,pre_e无定义,返回INFEASIBLE LinkList q,p=L->next; // p指向第一个结点 while(p->next) // p所指结点有后继 { q=p->next; // q为p的后继 if(q->data==cur_e) { pre_e=p->data; return OK; } p=q; // p向后移 } return INFEASIBLE; } Status NextElem(LinkList L,ElemType cur_e,ElemType &next_e) { // 初始条件:线性表L已存在 · 24· 《数据结构》算法实现及解析(第二版) // 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, // 返回OK;否则操作失败,next_e无定义,返回INFEASIBLE LinkList p=L->next; // p指向第一个结点 while(p->next) // p所指结点有后继 { if(p->data==cur_e) { next_e=p->next->data; return OK; } p=p->next; } return INFEASIBLE; } Status ListInsert(LinkList L,int i,ElemType e) // 算法2.9。不改变L { // 在带头结点的单链线性表L中第i个位置之前插入元素e(见图210) int j=0; LinkList p=L,s; while(p&&jnext; j++; } if(!p||j>i-1) // i小于1或者大于表长 return ERROR; s=(LinkList)malloc(sizeof(LNode)); // 生成新结点 s->data=e; // 插入L中 s->next=p->next; p->next=s; return OK; } 图 210 在链表 L 的第 i 个位置之前插入元素 e p ii-11 ⋯L ⋯ (a) 初态,p 指向头结点 L ii-11 ⋯ p ⋯ s e (c) 在第 i 个结点之前插入值为 e 的结点 p ii-11 ⋯L ⋯ (b) p 指向第 i-1 个结点 第 2 章 线 性 表 · 25· Status ListDelete(LinkList L,int i,ElemType &e) // 算法2.10。不改变L { // 在带头结点的单链线性表L中,删除第i个元素,并由e返回其值(见图211) int j=0; LinkList p=L,q; while(p->next&&jnext; j++; } if(!p->next||j>i-1) // 删除位置不合理 return ERROR; q=p->next; // 删除并释放结点 p->next=q->next; e=q->data; free(q); return OK; } void ListTraverse(LinkList L,void(*vi)(ElemType)) // vi的形参类型为ElemType,与bo2-1.cpp中相应函数的形参类型ElemType&不同 { // 初始条件:线性表L已存在。操作结果:依次对L的每个数据元素调用函数vi() LinkList p=L->next; while(p) { vi(p->data); p=p->next; } printf("\n"); } main2-2.cpp 是验证基本操作 bo2-2.cpp 的主程序。注意到 bo2-2.cpp 中的 图 211 删除链表 L 的第 i 个结点 (a) 初态,p 指向头结点 L p ii-11 ⋯ ⋯ i+1 (b) p 指向第 i-1 个结点 L p ii-11 ⋯ ⋯ i+1 (c) 从链表上删除第 i 个结点 ⋯ q L p ii-11 ⋯ i+1 · 26· 《数据结构》算法实现及解析(第二版) ListTraverse()函数的形参 vi()函数的形参类型是 ElemType,不是引用类型。这与 bo2-1.cpp 中的 ListTraverse()函数不同。因此在主程序中替代 vi()的实参函数 print()的形 参类型也是 ElemType。由于 bo2-2.cpp 和 bo2-1.cpp 都是 12 个函数名、操作结果均相同 的基本操作函数,仅变量类型、实现过程不同,而验证基本操作的主函数 main2-1.cpp 和 main2-2.cpp 的作用又基本相同,所以程序 main2-2.cpp 和程序 main2-1.cpp 很相像。 // main2-2.cpp 检验bo2-2.cpp的主程序(与main2-1.cpp很像) #include"c1.h" typedef int ElemType; #include"c2-2.h" // 与main2-1.cpp不同 #include"bo2-2.cpp" // 与main2-1.cpp不同 #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void main() { // 除了几个输出语句外,主程序和main2-1.cpp很像 LinkList L; // 与main2-1.cpp不同 ElemType e,e0; Status i; int j,k; InitList(L); for(j=1;j<=5;j++) i=ListInsert(L,1,j); printf("在L的表头依次插入1~5后:L="); ListTraverse(L,print); // 依次对元素调用print(),输出元素的值 i=ListEmpty(L); printf("L是否空:i=%d(1:是 0:否)\n",i); ClearList(L); printf("清空L后:L="); ListTraverse(L,print); i=ListEmpty(L); printf("L是否空:i=%d(1:是 0:否)\n",i); for(j=1;j<=10;j++) ListInsert(L,j,j); printf("在L的表尾依次插入1~10后:L="); ListTraverse(L,print); GetElem(L,5,e); printf("第5个元素的值为%d\n",e); for(j=0;j<=1;j++) { k=LocateElem(L,j,equal); if(k) printf("第%d个元素的值为%d\n",k,j); else printf("没有值为%d的元素\n",j); } for(j=1;j<=2;j++) // 测试头两个数据 { GetElem(L,j,e0); // 把第j个数据赋给e0 i=PriorElem(L,e0,e); // 求e0的前驱 if(i==INFEASIBLE) 第 2 章 线 性 表 · 27· printf("元素%d无前驱\n",e0); else printf("元素%d的前驱为%d\n",e0,e); } for(j=ListLength(L)-1;j<=ListLength(L);j++) // 最后两个数据 { GetElem(L,j,e0); // 把第j个数据赋给e0 i=NextElem(L,e0,e); // 求e0的后继 if(i==INFEASIBLE) printf("元素%d无后继\n",e0); else printf("元素%d的后继为%d\n",e0,e); } k=ListLength(L); // k为表长 for(j=k+1;j>=k;j--) { i=ListDelete(L,j,e); // 删除第j个数据 if(i==ERROR) printf("删除第%d个元素失败\n",j); else printf("删除第%d个元素成功,其值为%d\n",j,e); } printf("依次输出L的元素:"); ListTraverse(L,print); DestroyList(L); printf("销毁L后:L=%u\n",L); } 程序运行结果: 在L的表头依次插入1~5后:L=5 4 3 2 1 L是否空:i=0(1:是 0:否) 清空L后:L= L是否空:i=1(1:是 0:否) 在L的表尾依次插入1~10后:L=1 2 3 4 5 6 7 8 9 10 第5个元素的值为5 没有值为0的元素 第1个元素的值为1 元素1无前驱 元素2的前驱为1 元素9的后继为10 元素10无后继 删除第11个元素失败 删除第10个元素成功,其值为10 依次输出L的元素:1 2 3 4 5 6 7 8 9 销毁L后:L=0 // algo2-12.cpp 用单链表实现算法2.1,仅有4句与algo2-1.cpp不同 #include"c1.h" · 28· 《数据结构》算法实现及解析(第二版) typedef int ElemType; #include"c2-2.h" // 此句与algo2-1.cpp不同(因为采用不同的结构) #include"bo2-2.cpp" // 此句与algo2-1.cpp不同(因为采用不同的结构) #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void Union(LinkList La,LinkList Lb) // 算法2.1,此句与algo2-1.cpp不同 { // 将所有在线性表Lb中但不在La中的数据元素插入到La中 ElemType e; int La_len,Lb_len; int i; La_len=ListLength(La); // 求线性表的长度 Lb_len=ListLength(Lb); for(i=1;i<=Lb_len;i++) { GetElem(Lb,i,e); // 取Lb中第i个数据元素赋给e if(!LocateElem(La,e,equal)) // La中不存在和e相同的元素,则插入之 ListInsert(La,++La_len,e); } } void main() { LinkList La,Lb; // 此句与algo2-1.cpp不同(因为采用不同的结构) int j; InitList(La); if(i==1) // 创建空表La成功 for(j=1;j<=5;j++) // 在表La中插入5个元素 ListInsert(La,j,j); printf("La= "); // 输出表La的内容 ListTraverse(La,print); InitList(Lb); // 也可不判断是否创建成功 for(j=1;j<=5;j++) // 在表Lb中插入5个元素 ListInsert(Lb,j,2*j); printf("Lb= "); // 输出表Lb的内容 ListTraverse(Lb,print); Union(La,Lb); printf("new La= "); // 输出新表La的内容 ListTraverse(La,print); } 程序运行结果: La= 1 2 3 4 5 Lb= 2 4 6 8 10 new La= 1 2 3 4 5 6 8 10 // algo2-13.cpp 采用单链表结构实现算法2.2的程序,仅有4句与algo2-2.cpp不同 #include"c1.h" typedef int ElemType; #include"c2-2.h" // 此句与algo2-2.cpp不同 #include"bo2-2.cpp" // 此句与algo2-2.cpp不同 第 2 章 线 性 表 · 29· #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void MergeList(LinkList La,LinkList Lb,LinkList &Lc) // 算法2.2,此句与algo2-2.cpp不同 { // 已知线性表La和Lb中的数据元素按值非递减排列。 // 归并La和Lb得到新的线性表Lc,Lc的数据元素也按值非递减排列 int i=1,j=1,k=0; int La_len,Lb_len; ElemType ai,bj; InitList(Lc); // 创建空表Lc La_len=ListLength(La); Lb_len=ListLength(Lb); while(i<=La_len&&j<=Lb_len) // 表La和表Lb均非空 { GetElem(La,i,ai); GetElem(Lb,j,bj); if(ai<=bj) { ListInsert(Lc,++k,ai); ++i; } else { ListInsert(Lc,++k,bj); ++j; } } while(i<=La_len) // 表La非空且表Lb空 { GetElem(La,i++,ai); ListInsert(Lc,++k,ai); } while(j<=Lb_len) // 表Lb非空且表La空 { GetElem(Lb,j++,bj); ListInsert(Lc,++k,bj); } } void main() { LinkList La,Lb,Lc; // 此句与algo2-2.cpp不同 int j,a[4]={3,5,8,11},b[7]={2,6,8,9,11,15,20}; // 教科书例22的数据 InitList(La); // 创建空表La for(j=1;j<=4;j++) // 在表La中插入4个元素 ListInsert(La,j,a[j-1]); printf("La= "); // 输出表La的内容 ListTraverse(La,print); InitList(Lb); // 创建空表Lb for(j=1;j<=7;j++) // 在表Lb中插入7个元素 ListInsert(Lb,j,b[j-1]); printf("Lb= "); // 输出表Lb的内容 ListTraverse(Lb,print); MergeList(La,Lb,Lc); · 30· 《数据结构》算法实现及解析(第二版) printf("Lc= "); // 输出表Lc的内容 ListTraverse(Lc,print); } 程序运行结果: La= 3 5 8 11 Lb= 2 6 8 9 11 15 20 Lc= 2 3 5 6 8 8 9 11 11 15 20 algo2-12.cpp 和 algo2-13.cpp 是采用链表结构实现算法 2.1 和 2.2 的程序,与采用顺 序表结构实现算法 2.1 和 2.2 的程序 algo2-1.cpp 和 algo2-2.cpp 相比,只有 4 条语句 不同: (1) 包含了不同的存储结构文件; (2) 包含了不同的基本操作函数文件; (3) 函数的形参类型不同,是否为引用参数也不一定相同; (4) 主程序中相应的变量类型不同。 原因是算法 2.1 和 2.2 是通过调用线性表的基本操作(如 ListLength()等)来完成的。 算法中不包括针对某种具体的存储结构所特有的变量的操作。这类算法的可移植性好,从 一种存储结构移植到另一种存储结构仅做少量修改即可。但由于没有考虑具体的存储结 构,这类算法往往不是最高效的。而算法 2.7 的程序(在 algo2-3.cpp 中)涉及到顺序存储 结构所特有的 L.length 等变量,不容易移植到其它存储结构中使用。但该程序可以充分考 虑顺序存储结构的特点,从而做到高效。 // algo2-5.cpp 实现算法2.11、2.12的程序 #include"c1.h" typedef int ElemType; #include"c2-2.h" #include"bo2-2.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void CreateList(LinkList &L,int n) // 算法2.11 { // 逆位序(插在表头)输入n个元素的值,建立带表头结构的单链线性表L int i; LinkList p; L=(LinkList)malloc(sizeof(LNode)); L->next=NULL; // 先建立一个带头结点的单链表 printf("请输入%d个数据\n",n); for(i=n;i>0;--i) { p=(LinkList)malloc(sizeof(LNode)); // 生成新结点 scanf("%d",&p->data); // 输入元素值 p->next=L->next; // 插入到表头 L->next=p; } } 第 2 章 线 性 表 · 31· void CreateList2(LinkList &L,int n) { // 正位序(插在表尾)输入n个元素的值,建立带表头结构的单链线性表L int i; LinkList p,q; L=(LinkList)malloc(sizeof(LNode)); // 生成头结点 L->next=NULL; q=L; printf("请输入%d个数据\n",n); for(i=1;i<=n;i++) { p=(LinkList)malloc(sizeof(LNode)); scanf("%d",&p->data); q->next=p; q=q->next; } p->next=NULL; } void MergeList(LinkList La,LinkList &Lb,LinkList &Lc) // 算法2.12 { // 已知单链线性表La和Lb的元素按值非递减排列。 // 归并La和Lb得到新的单链线性表Lc,Lc的元素也按值非递减排列 LinkList pa=La->next,pb=Lb->next,pc; Lc=pc=La; // 用La的头结点作为Lc的头结点 while(pa&&pb) if(pa->data<=pb->data) { pc->next=pa; pc=pa; pa=pa->next; } else { pc->next=pb; pc=pb; pb=pb->next; } pc->next=pa?pa:pb; // 插入剩余段 free(Lb); // 释放Lb的头结点 Lb=NULL; } void main() { int n=5; LinkList La,Lb,Lc; printf("按非递减顺序, "); CreateList2(La,n); // 正位序输入n个元素的值 printf("La="); // 输出链表La的内容 ListTraverse(La,print); printf("按非递增顺序, "); CreateList(Lb,n); // 逆位序输入n个元素的值 printf("Lb="); // 输出链表Lb的内容 ListTraverse(Lb,print); · 32· 《数据结构》算法实现及解析(第二版) MergeList(La,Lb,Lc); // 按非递减顺序归并La和Lb,得到新表Lc printf("Lc="); // 输出链表Lc的内容 ListTraverse(Lc,print); } 程序运行结果: 按非递减顺序, 请输入5个数据 1 2 2 3 7 La=1 2 2 3 7 按非递增顺序, 请输入5个数据 9 8 8 7 5 Lb=5 7 8 8 9 Lc=1 2 2 3 5 7 7 8 8 9 单链表也可以不设头结点,如图 212 所示。显 然,基于这种结构的基本操作和带有头结点的线性链 表基本操作是不同的。bo2-8.cpp 是不带头结点的线 性链表的基本操作。 // bo2-8.cpp 不带头结点的单链表(存储结构由c2-2.h定义)的部分基本操作(9个) #define DestroyList ClearList // DestroyList()和ClearList()的操作是一样的 void InitList(LinkList &L) { // 操作结果:构造一个空的线性表L(见图213) L=NULL; // 指针为空 } void ClearList(LinkList &L) { // 初始条件:线性表L已存在。操作结果:将L重置为空表(见图213) LinkList p; while(L) // L不空 { p=L; // p指向首元结点 L=L->next; // L指向第2个结点(新首元结点) free(p); // 释放首元结点 } } Status ListEmpty(LinkList L) { // 初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE;否则返回FALSE if(L) return FALSE; else return TRUE; } int ListLength(LinkList L) { // 初始条件:线性表L已存在。操作结果:返回L中数据元素的个数 int i=0; LinkList p=L; 图 212 不带头结点且具有 2 个 结点(4,7)的线性链表 头指针 4 7 NULL 首元结点 尾元结点 图 213 空的和被销毁的线性表 L L NULL 第 2 章 线 性 表 · 33· while(p) // p指向结点(没到表尾) { p=p->next; // p指向下一个结点 i++; } return i; } Status GetElem(LinkList L,int i,ElemType &e) { // L为不带头结点的单链表的头指针。当第i个元素存在时,其值赋给e并返回OK;否则返回ERROR int j=1; LinkList p=L; if(i<1) // i值不合法 return ERROR; while(jnext; } if(j==i) // 存在第i个元素 { e=p->data; return OK; } else return ERROR; } int LocateElem(LinkList L,ElemType e,Status(*compare)(ElemType,ElemType)) { // 初始条件:线性表L已存在,compare()是数据元素判定函数(满足为1;否则为0) // 操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。 // 若这样的数据元素不存在,则返回值为0 int i=0; LinkList p=L; while(p) { i++; if(compare(p->data,e)) // 找到这样的数据元素 return i; p=p->next; } return 0; } Status ListInsert(LinkList &L,int i,ElemType e) { // 在不带头结点的单链线性表L中第i个位置之前插入元素e int j=1; LinkList p=L,s; if(i<1) // i值不合法 return ERROR; s=(LinkList)malloc(sizeof(LNode)); // 生成新结点 s->data=e; // 给s的data域赋值 if(i==1) // 插在表头(见图214) { 图 214 在链表 L 的第 1 个位置之前插入元素 e 1 ⋯L s e (a) 初态 (b) 插在表头 1 ⋯L s e · 34· 《数据结构》算法实现及解析(第二版) s->next=L; L=s; // 改变L } else { // 插在表的其余处(见图215) while(p&&jnext; j++; } if(!p) // i大于表长+1 return ERROR; s->next=p->next; p->next=s; } return OK; } Status ListDelete(LinkList &L,int i,ElemType &e) { // 在不带头结点的单链线性表L中,删除第i个元素,并由e返回其值 int j=1; LinkList p=L,q; if(i==1) // 删除第1个结点(见图216) { L=p->next; // L由第2个结点开始 e=p->data; free(p); // 删除并释放第1个结点 } else(见图217) { while(p->next&&jnext; j++; } if(!p->next||j>i-1) // 删除位置不合理 return ERROR; q=p->next; // 删除并释放结点 p->next=q->next; e=q->data; free(q); } return OK; } void ListTraverse(LinkList L,void(*vi) (ElemType)) { // 初始条件:线性表L已存在 // 操作结果:依次对L的每个数据元素调用 // 函数vi() LinkList p=L; while(p) 图 215 在链表 L 的第 i 个位置之前插入元素 e ii-11 ⋯L p ⋯ s e (b) 在第 i 个结点之前插入值为 e 的结点 (a) 初态,p 指向第 1 个结点 p ii-11 ⋯ ⋯L s e 图 216 删除链表 L 的第 1 个结点 (a) p 指向第 1 个结点 p 21 ⋯L (b) L 指向第 2 个结点 p 21 ⋯L 图 217 删除链表 L 的第 i 个结点(i!=1) (a) 初态,p 指向第 1 个结点 p ii-11 ⋯ ⋯L i+1 (b) p 指向第 i-1 个结点 p ii-11 ⋯ ⋯L i+1 (c) 从链表上删除第 i 个结点 ⋯ qp ii-11 ⋯L i+1 第 2 章 线 性 表 · 35· { vi(p->data); p=p->next; } printf("\n"); } // bo2-9.cpp 不带头结点的单链表(存储结构由c2-2.h定义)的部分基本操作(2个) Status PriorElem(LinkList L,ElemType cur_e,ElemType &pre_e) { // 初始条件:线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱, // 返回OK;否则操作失败,pre_e无定义,返回INFEASIBLE LinkList q,p=L; // p指向第一个结点 while(p->next) // p所指结点有后继 { q=p->next; // q为p的后继 if(q->data==cur_e) { pre_e=p->data; return OK; } p=q; // p向后移 } return INFEASIBLE; } Status NextElem(LinkList L,ElemType cur_e,ElemType &next_e) { // 初始条件:线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, // 返回OK;否则操作失败,next_e无定义,返回INFEASIBLE LinkList p=L; // p指向第一个结点 while(p->next) // p所指结点有后继 { if(p->data==cur_e) { next_e=p->next->data; return OK; } p=p->next; } return INFEASIBLE; } // main2-8.cpp 检验bo2-8.cpp和bo2-9.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c2-2.h" #include"bo2-8.cpp" #include"bo2-9.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void main() { · 36· 《数据结构》算法实现及解析(第二版) LinkList L; ElemType e,e0; Status i; int j,k; InitList(L); for(j=1;j<=5;j++) { i=ListInsert(L,1,j); if(!i) // 插入失败 exit(ERROR); } printf("在L的表头依次插入1~5后:L="); ListTraverse(L,print); // 依次对元素调用print(),输出元素的值 i=ListEmpty(L); printf("L是否空:i=%d(1:是 0:否)\n",i); ClearList(L); printf("清空L后:L="); ListTraverse(L,print); i=ListEmpty(L); printf("L是否空:i=%d(1:是 0:否)\n",i); for(j=1;j<=10;j++) ListInsert(L,j,j); printf("在L的表尾依次插入1~10后:L="); ListTraverse(L,print); i=GetElem(L,5,e); if(i==OK) printf("第5个元素的值为%d\n",e); for(j=0;j<=1;j++) { k=LocateElem(L,j,equal); if(k) printf("第%d个元素的值为%d\n",k,j); else printf("没有值为%d的元素\n",j); } for(j=1;j<=2;j++) // 测试头两个数据 { GetElem(L,j,e0); // 把第j个数据赋给e0 i=PriorElem(L,e0,e); // 求e0的前驱 if(i==INFEASIBLE) printf("元素%d无前驱\n",e0); else printf("元素%d的前驱为%d\n",e0,e); } for(j=ListLength(L)-1;j<=ListLength(L);j++) // 最后两个数据 { GetElem(L,j,e0); // 把第j个数据赋给e0 i=NextElem(L,e0,e); // 求e0的后继 if(i==INFEASIBLE) printf("元素%d无后继\n",e0); else 第 2 章 线 性 表 · 37· printf("元素%d的后继为%d\n",e0,e); } k=ListLength(L); // k为表长 for(j=k+1;j>=k;j--) { i=ListDelete(L,j,e); // 删除第j个数据 if(i==ERROR) printf("删除第%d个元素失败\n",j); else printf("删除第%d个元素成功,其值为%d\n",j,e); } printf("依次输出L的元素:"); ListTraverse(L,print); DestroyList(L); printf("销毁L后:L=%u\n",L); } 程序运行结果: 在L的表头依次插入1~5后:L=5 4 3 2 1 L是否空:i=0(1:是 0:否) 清空L后:L= L是否空:i=1(1:是 0:否) 在L的表尾依次插入1~10后:L=1 2 3 4 5 6 7 8 9 10 第5个元素的值为5 没有值为0的元素 第1个元素的值为1 元素1无前驱 元素2的前驱为1 元素9的后继为10 元素10无后继 删除第11个元素失败 删除第10个元素成功,其值为10 依次输出L的元素:1 2 3 4 5 6 7 8 9 销毁L后:L=0 和带有头结点的单链表(见图 27)相比,不带头结点的单链表(见图 212)显得更直 观。但不带头结点的单链表在插入和删除第 1 个元素时与插入和删除其它元素时的操作不 一样,要改变链表头指针的值。而带有头结点的单链表无论插入和删除第几个元素,其操 作都是统一的。这从 bo2-2.cpp 和 bo2-8.cpp 两文件中的 ListInsert()及 ListDelete()函数 的区别可看出。 algo2-6.cpp 是以结构体为数据元素(ElemType 类型),利用不带头结点的单链表对学 生健康登记表进行操作的一个实例。 // func2-1.cpp 不带头结点的单链表(存储结构由c2-2.h定义)的扩展操作(3个) // algo2-6.cpp和bo7-2.cpp用到 · 38· 《数据结构》算法实现及解析(第二版) void InsertAscend(LinkList &L,ElemType e,int(*compare)(ElemType,ElemType)) { // 按关键字非降序将e插入表L。函数compare()返回值为形参1的关键字-形参2的关键字 LinkList q=L; if(!L||compare(e,L->data)<=0) // 链表空或e的关键字小于等于首结点的关键字 ListInsert(L,1,e); // 将e插在表头,在bo2-8.cpp中 else { while(q->next&&compare(q->next->data,e)<0) // q不是尾结点且q的下一结点关键字next; ListInsert(q,2,e); // 插在q所指结点后(将q作为头指针) } } LinkList Point(LinkList L,ElemType e,Status(*equal)(ElemType,ElemType),LinkList &p) { // 查找表L中满足条件的结点。如找到,返回指向该结点的指针,p指向该结点的前驱(若该结点是 // 首元结点,则p=NULL)。如表L中无满足条件的结点,则返回NULL,p无定义。 // 函数equal()的两形参的关键字相等,返回OK;否则返回ERROR int i,j; i=LocateElem(L,e,equal); if(i) // 找到 { if(i==1) // 是首元结点 { p=NULL; return L; } p=L; for(j=2;jnext; return p->next; } return NULL; // 没找到 } Status DeleteElem(LinkList &L,ElemType &e,Status(*equal)(ElemType,ElemType)) { // 删除表L中满足条件的结点,并返回TRUE;如无此结点,则返回FALSE。 // 函数equal()的两形参的关键字相等,返回OK;否则返回ERROR LinkList p,q; q=Point(L,e,equal,p); if(q) // 找到此结点,且q指向该结点 { if(p) // 该结点不是首元结点,p指向其前驱 ListDelete(p,2,e); // 将p作为头指针,删除第2个结点 else // 该结点是首元结点 ListDelete(L,1,e); return TRUE; } return FALSE; } // algo2-6.cpp 利用无头结点的单链表结构处理教科书图2.1(学生健康登记表) #include"c1.h" #define NAMELEN 8 // 姓名最大长度 #define CLASSLEN 4 // 班级名最大长度 第 2 章 线 性 表 · 39· struct stud // 记录的结构 { char name[NAMELEN+1]; // 包括′\0′ long num; char sex; int age; char Class[CLASSLEN+1]; // 包括′\0′ int health; }; typedef stud ElemType; // 链表结点元素类型为结构体(见图218) #include"c2-2.h" #include"bo2-8.cpp" // 无头结点单链表的部分基本操作 #include"func2-1.cpp" // 无头结点单链表的扩展操作 char sta[3][9]={"健康 ","一般 ","神经衰弱"}; // 健康状况(3类)(见图219) FILE *fp; // 全局变量 void Print(stud e) { // 显示记录e的内容 printf("%-8s %6ld",e.name,e.num); if(e.sex==’m’) printf(" 男"); else printf(" 女"); printf("%5d %-4s",e.age,e.Class); printf("%9s\n",sta[e.health]); } void ReadIn(stud &e) { // 由键盘输入结点信息 printf("请输入姓名(<=%d个字符): ",NAMELEN); scanf("%s",e.name); printf("请输入学号: "); scanf("%ld",&e.num); printf("请输入性别(m:男 f:女): "); scanf("%*c%c",&e.sex); printf("请输入年龄: "); scanf("%d",&e.age); printf("请输入班级(<=%d个字符): ",CLASSLEN); scanf("%s",e.Class); printf("请输入健康状况(0:%s 1:%s 2:%s):",sta[0],sta[1],sta[2]); scanf("%d",&e.health); } void WriteToFile(stud e) { // 将结点信息写入fp指定的文件 fwrite(&e,sizeof(stud),1,fp); } Status ReadFromFile(stud &e) { // 由fp指定的文件读取结点信息到e int i; i=fread(&e,sizeof(stud),1,fp); if(i==1) // 读取文件成功 return OK; else return ERROR; 图 219 sta 类型 sta "健康 " "一般 " "神经衰弱" [0] [1] [2] 图 218 ElemType 和 LNode 类型 stud 和 ElemType name num sex age Class health LNode name num sex age Class health next data · 40· 《数据结构》算法实现及解析(第二版) } int cmp(ElemType c1,ElemType c2) { return (int)(c1.num-c2.num); } void Modify(LinkList &L,ElemType e) { // 修改结点内容,并按学号将结点非降序插入链表L char s[80]; Print(e); // 显示原内容 printf("请输入待修改项的内容,不修改的项按回车键保持原值:\n"); printf("请输入姓名(<=%d个字符): ",NAMELEN); gets(s); if(strlen(s)) strcpy(e.name,s); printf("请输入学号: "); gets(s); if(strlen(s)) e.num=atol(s); printf("请输入性别(m:男 f:女): "); gets(s); if(strlen(s)) e.sex=s[0]; printf("请输入年龄: "); gets(s); if(strlen(s)) e.age=atoi(s); printf("请输入班级(<=%d个字符): ",CLASSLEN); gets(s); if(strlen(s)) strcpy(e.Class,s); printf("请输入健康状况(0:%s 1:%s 2:%s):",sta[0],sta[1],sta[2]); gets(s); if(strlen(s)) e.health=atoi(s); // 修改完毕 InsertAscend(L,e,cmp); // 把q所指结点的内容按学号非降序插入L(func2-1.cpp) } #define N 4 // student记录的个数 Status EqualNum(ElemType c1,ElemType c2) { if(c1.num==c2.num) return OK; else return ERROR; } Status EqualName(ElemType c1,ElemType c2) { if(strcmp(c1.name,c2.name)) return ERROR; else return OK; } void main() 第 2 章 线 性 表 · 41· { // 表的初始记录 stud student[N]={{"王小林",790631, ′m′,18,"计91",0},{"陈红",790632, ′f′,20,"计91",1}, {"刘建平",790633, ′m′,21,"计91",0},{"张立立",790634, ′m′,17,"计91",2}}; int i,j,flag=1; char filename[13]; ElemType e; LinkList T,p,q; InitList(T); // 初始化链表 while(flag) { printf("1:将结构体数组student中的记录按学号非降序插入链表\n"); printf("2:将文件中的记录按学号非降序插入链表\n"); printf("3:键盘输入新记录,并将其按学号非降序插入链表\n"); printf("4:删除链表中第一个有给定学号的记录\n"); printf("5:删除链表中第一个有给定姓名的记录\n"); printf("6:修改链表中第一个有给定学号的记录\n"); printf("7:修改链表中第一个有给定姓名的记录\n"); printf("8:查找链表中第一个有给定学号的记录\n"); printf("9:查找链表中第一个有给定姓名的记录\n"); printf("10:显示所有记录 11:将链表中的所有记录存入文件 12:结束\n"); printf("请选择操作命令: "); scanf("%d",&i); switch(i) { case 1: for(j=0;jdata); break; case 9: printf("请输入待查找记录的姓名: "); scanf("%*c%s",e.name); if(!(q=Point(T,e,EqualName,p))) // 在func2-1.cpp中 printf("没有姓名为%s的记录\n",e.name); else Print(q->data); break; case 10:printf(" 姓名 学号 性别 年龄 班级 健康状况\n"); ListTraverse(T,Print); break; case 11:printf("请输入文件名: "); scanf("%s",filename); if((fp=fopen(filename,"wb"))==NULL) printf("打开文件失败!\n"); else ListTraverse(T,WriteToFile); fclose(fp); break; case 12:flag=0; } } } 程序运行结果: 1:将结构体数组student中的记录按学号非降序插入链表 2:将文件中的记录按学号非降序插入链表 3:键盘输入新记录,并将其按学号非降序插入链表 4:删除链表中第一个有给定学号的记录 5:删除链表中第一个有给定姓名的记录 第 2 章 线 性 表 · 43· 6:修改链表中第一个有给定学号的记录 7:修改链表中第一个有给定姓名的记录 8:查找链表中第一个有给定学号的记录 9:查找链表中第一个有给定姓名的记录 10:显示所有记录 11:将链表中的所有记录存入文件 12:结束 请选择操作命令: 1(见图220) 1:将结构体数组student中的记录按学号非降序插入链表 2:将文件中的记录按学号非降序插入链表 3:键盘输入新记录,并将其按学号非降序插入链表 4:删除链表中第一个有给定学号的记录 5:删除链表中第一个有给定姓名的记录 6:修改链表中第一个有给定学号的记录 7:修改链表中第一个有给定姓名的记录 8:查找链表中第一个有给定学号的记录 9:查找链表中第一个有给定姓名的记录 10:显示所有记录 11:将链表中的所有记录存入文件 12:结束 请选择操作命令: 10 姓名 学号 性别 年龄 班级 健康状况 王小林 790631 男 18 计91 健康 陈红 790632 女 20 计91 一般 刘建平 790633 男 21 计91 健康 张立立 790634 男 17 计91 神经衰弱 1:将结构体数组student中的记录按学号非降序插入链表 2:将文件中的记录按学号非降序插入链表 3:键盘输入新记录,并将其按学号非降序插入链表 4:删除链表中第一个有给定学号的记录 5:删除链表中第一个有给定姓名的记录 6:修改链表中第一个有给定学号的记录 7:修改链表中第一个有给定姓名的记录 8:查找链表中第一个有给定学号的记录 9:查找链表中第一个有给定姓名的记录 10:显示所有记录 11:将链表中的所有记录存入文件 12:结束 请选择操作命令: 5 请输入待删除记录的姓名: 张立立(见图221) 1:将结构体数组student中的记录按学号非降序插入链表 2:将文件中的记录按学号非降序插入链表 3:键盘输入新记录,并将其按学号非降序插入链表 4:删除链表中第一个有给定学号的记录 5:删除链表中第一个有给定姓名的记录 6:修改链表中第一个有给定学号的记录 7:修改链表中第一个有给定姓名的记录 8:查找链表中第一个有给定学号的记录 9:查找链表中第一个有给定姓名的记录 10:显示所有记录 11:将链表中的所有记录存入文件 12:结束 请选择操作命令: 6 请输入待修改记录的学号: 790632 陈红 790632 女 20 计91 一般 请输入待修改项的内容,不修改的项按回车键保持原值: 请输入姓名(<=8个字符):  请输入学号:  图 220 链表此时内容 王小林 790631 男 18 计 91 健康 陈红 790632 女 20 计 91 一般 刘建平 790633 男 21 计 91 健康 张立立 790634 男 17 计 91 神经衰弱 NULL T 图 221 链表此时内容 王小林 790631 男 18 计 91 健康 陈红 790632 女 20 计 91 一般 刘建平 790633 男 21 计 91 健康 NULL T · 44· 《数据结构》算法实现及解析(第二版) 请输入性别(m:男 f:女):  请输入年龄:  请输入班级(<=4个字符): 计92 请输入健康状况(0:健康 1:一般 2:神经衰弱):  1:将结构体数组student中的记录按学号非降序插入链表 2:将文件中的记录按学号非降序插入链表 3:键盘输入新记录,并将其按学号非降序插入链表 4:删除链表中第一个有给定学号的记录 5:删除链表中第一个有给定姓名的记录 6:修改链表中第一个有给定学号的记录 7:修改链表中第一个有给定姓名的记录 8:查找链表中第一个有给定学号的记录 9:查找链表中第一个有给定姓名的记录 10:显示所有记录 11:将链表中的所有记录存入文件 12:结束 请选择操作命令: 10(见图222) 姓名 学号 性别 年龄 班级 健康状况 王小林 790631 男 18 计91 健康 陈红 790632 女 20 计92 一般 刘建平 790633 男 21 计91 健康 1:将结构体数组student中的记录按学号非降序插入链表 2:将文件中的记录按学号非降序插入链表 3:键盘输入新记录,并将其按学号非降序插入链表 4:删除链表中第一个有给定学号的记录 5:删除链表中第一个有给定姓名的记录 6:修改链表中第一个有给定学号的记录 7:修改链表中第一个有给定姓名的记录 8:查找链表中第一个有给定学号的记录 9:查找链表中第一个有给定姓名的记录 10:显示所有记录 11:将链表中的所有记录存入文件 12:结束 请选择操作命令: 11 请输入文件名: table.txt 1:将结构体数组student中的记录按学号非降序插入链表 2:将文件中的记录按学号非降序插入链表 3:键盘输入新记录,并将其按学号非降序插入链表 4:删除链表中第一个有给定学号的记录 5:删除链表中第一个有给定姓名的记录 6:修改链表中第一个有给定学号的记录 7:修改链表中第一个有给定姓名的记录 8:查找链表中第一个有给定学号的记录 9:查找链表中第一个有给定姓名的记录 10:显示所有记录 11:将链表中的所有记录存入文件 12:结束 请选择操作命令: 12 结构体 stud 的 health 域的类型是整型,其值的范围是 0~2,通过数组 sta(见图 219) 转换为 3 种健康状况。为直观,图 220~图 222 中显示的是转换后的结果。 图 222 的数据存到了当前目录下的 table.txt 文件中。在文本状态下打开 table.txt, 其内容如下: 王小林□□□g □m □计91□□□陈红□□□□□h □f □计92□ □刘建平□□□i □m □计 图 222 链表此时内容 王小林 790631 男 18 计 91 健康 陈红 790632 女 20 计 92 一般 刘建平 790633 男 21 计 91 健康 NULL T 第 2 章 线 性 表 · 45· 91□□□ 其中,姓名、性别和班级可正常显示,因为它们是字符或字符串,是以 ASCII 码格式 存储的。学号、年龄和健康状况显示不正常,因为它们是以整型或长整型数据的格式存储 的。虽然在文本状态下它们不能正常显示,但当读入计算机时,可正常识别。 某些高级语言如 BASIC,没有“指针”数据类型,但有“结构体”数据类型。在这类 高级语言中,若要使用链表结构,就需先开辟一个充分大的结构体数组。结构体的一个成 员存放数据,另一个成员(“游标”)存放下一个数的位置(下标),这称为静态链表。输出 链表不是按数组的下标顺序输出的,而是由一个指定的位置开始根据游标依次输出的。 c2-3.h 就是这样的一个数据结构。algo2-7.cpp 是以 c2-3.h 为数据结构,输出教科书中图 2.10 静态链表的示例程序: // c2-3.h 线性表的静态单链表存储结构(见图223) #define MAX_SIZE 100 // 链表的最大长度 typedef struct { ElemType data; int cur; }component,SLinkList[MAX_SIZE]; // algo2-7.cpp 教科书中图2.10静态链表示例 // 第1个结点的位置在[0].cur中。成员cur的值为0,则到链表尾 #include"c1.h" #define N 6 // 字符串长度 typedef char ElemType[N]; #include"c2-3.h" void main() { SLinkList s={{"",1},{"ZHAO",2},{"QIAN",3},{"SUN",4},{"LI",5},{"ZHOU",6},{"WU",7}, {"ZHENG",8},{"WANG",0}}; // 教科书中图2.10(a)的状态 int i; i=s[0].cur; // i指示第1个结点的位置 while(i) { // 输出教科书中图2.10(a)的状态 printf("%s ",s[i].data); // 输出链表的当前值 i=s[i].cur; // 找到下一个 } printf("\n"); s[4].cur=9; // 按教科书中图2.10(b)修改(在"LI"之后插入"SHI") s[9].cur=5; strcpy(s[9].data,"SHI"); s[6].cur=8; // 删除"ZHENG" i=s[0].cur; // i指示第1个结点的位置 while(i) { // 输出教科书中图2.10(b)的状态 printf("%s ",s[i].data); // 输出链表的当前值 i=s[i].cur; // 找到下一个 } printf("\n"); 图 223 静态单链表存储结构 data cur component [0] [1] [MAX_SIZE-2] [MAX_SIZE-1] SLinkList data cur    · 46· 《数据结构》算法实现及解析(第二版) } 程序运行结果: ZHAO QIAN SUN LI ZHOU WU ZHENG WANG ZHAO QIAN SUN LI SHI ZHOU WU WANG algo2-7.cpp 输出不是按数组下标的顺序输出的,而是像链表一样,由当前结点 cur 域 的值决定下一个结点的位置确定输出。这就是链表的特点。但 algo2-7.cpp 插入新结点是 完全靠人工判断该结点是否为空闲结点,而不是“自动地”找到空闲结点作为新结点,该 方法不能满足实际应用的需要。为了解决这个问题,将所有空闲结点链接形成一个备用链 表,数组下标为 0 的单元为备用链表的头结点(这时,链表的头结点就不能再是数组下标 为 0 的单元了,需要另外定义)。静态数组实际有 2 个链表,一个链表上链接的是线性表 的结点,另一个链表(备用链表)上链接的是所有没被使用的结点。静态数组的每一个元素 都链接在这 2 个链表中的一个上。当线性表需要新结点时,把备用链表中的首元结点(由 [0].cur 指示)从备用链表中删除,作为新结点,插入线性表。当删除线性表中的结点时, 被删除的结点插入备用链表中,成为备用链表的首元结点。之所以从备用链表删除结点或 向备用链表插入结点都在表头进行,是因为这样效率最高。开辟新结点和回收空闲结点的 操作如算法 2.15、2.16 所示,在程序 func2-2.cpp 中实现。 // func2-2.cpp 实现算法2.15、2.16的程序,main2-31.cpp和main2-32.cpp调用 int Malloc(SLinkList space) // 算法2.15(见图224) { // 若备用链表非空,则返回分配的结点下标(备用链表的第一个结点);否则返回0 int i=space[0].cur; if(i) // 备用链表非空 space[0].cur=space[i].cur; // 备用链表的头结点指向原备用链表的第二个结点 return i; // 返回新开辟结点的坐标 } void Free(SLinkList space,int k) // 算法2.16(见图225) { // 将下标为k的空闲结点回收到备用链表(成为备用链表的第一个结点) 图 224 调用 Malloc()示例 (a) 在调用 Malloc()之前,备用链 表上共有 2 个结点:[3]和[5] [0]备用链表头结点 [1] [2] [3] [4] [5] [MAX_SIZE-1] space 3 5 0 (b) 在调用 Malloc()之后,备用 链表上只有 1 个结点[5] [0]备用链表头结点 [1] [2] [3] [4] [5] [MAX_SIZE-1] space 5 5 0 i 新开辟结点     第 2 章 线 性 表 · 47· space[k].cur=space[0].cur; // 回收结点的"游标"指向备用链表的第一个结点 space[0].cur=k; // 备用链表的头结点指向新回收的结点 } 生成静态链表的方法可有两种:一种是在一个数组中只生成一个静态链表,这种情况 可以固定静态链表的头指针位置,如最后一个单元([MAX_SIZE-1]);另一种是在一个数 组中可根据需要生成若干个独立的链表,每个链表的头指针在生成链表时才指定。第一种 方法指定数组名就指定了链表,函数的形参简单。但若在一个程序中用到多个链表,就需 要定义多个数组,每个数组的备用链表不能互相调剂,空间浪费较大。第二种方法指定一 个链表必须在指定数组名的同时指定链表的头指针位置,函数要多一个形参。bo2-31.cpp 是第一种情况的基本操作,main2-31.cpp 是验证 bo2-31.cpp 的主函数。 // bo2-31.cpp 一个数组只生成一个静态链表(数据结构由c2-3.h定义)的基本操作(11个)包括算法2.13 #define DestroyList ClearList // DestroyList()和ClearList()的操作是一样的 void InitList(SLinkList L) { // 构造一个空的链表L,表头为L的最后一个单元L[MAX_SIZE-1],其余单元链成 // 一个备用链表,表头为L的第一个单元L[0],“0”表示空指针(见图226) int i; L[MAX_SIZE-1].cur=0; // L的最后一个单元为空链表的表头 for(i=0;iListLength(L)) return ERROR; for(l=1;l<=i;l++) // 移动到第i个元素处 k=L[k].cur; e=L[k].data; return OK; } int LocateElem(SLinkList L,ElemType e) // 算法2.13(有改动) { // 在静态单链线性表L中查找第1个值为e的元素。若找到,则返回它在L中的 // 位序;否则返回0。(与其它LocateElem()的定义不同) int i=L[MAX_SIZE-1].cur; // i指示表中第一个结点 while(i&&L[i].data!=e) // 在表中顺链查找(e不能是字符串) i=L[i].cur; return i; } Status PriorElem(SLinkList L,ElemType cur_e,ElemType &pre_e) { // 初始条件:线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱; // 否则操作失败,pre_e无定义 int j,i=L[MAX_SIZE-1].cur; // i指示链表第一个结点的位置 do { // 向后移动结点 j=i; i=L[i].cur; }while(i&&cur_e!=L[i].data); if(i) // 找到该元素 { pre_e=L[j].data; 第 2 章 线 性 表 · 49· return OK; } return ERROR; } Status NextElem(SLinkList L,ElemType cur_e,ElemType &next_e) { // 初始条件:线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继; // 否则操作失败,next_e无定义 int j,i=LocateElem(L,cur_e); // 在L中查找第一个值为cur_e的元素的位置 if(i) // L中存在元素cur_e { j=L[i].cur; // cur_e的后继的位置 if(j) // cur_e有后继 { next_e=L[j].data; return OK; // cur_e元素有后继 } } return ERROR; // L不存在cur_e元素,cur_e元素无后继 } Status ListInsert(SLinkList L,int i,ElemType e) { // 在L中第i个元素之前插入新的数据元素e(见图227) int l,j,k=MAX_SIZE-1; // k指向表头 if(i<1||i>ListLength(L)+1) return ERROR; j=Malloc(L); // 申请新单元 if(j) // 申请成功 { L[j].data=e; // 赋值给新单元 for(l=1;lListLength(L)) return ERROR; for(j=1;j=k;j--) { · 52· 《数据结构》算法实现及解析(第二版) i=ListDelete(L,j,e); // 删除第j个数据 if(i) printf("第%d个元素为%d,已删除。\n",j,e); else printf("删除第%d个元素失败(不存在此元素)。\n",j); } printf("依次输出L的元素:"); ListTraverse(L,print); // 依次对元素调用print(),输出元素的值 } 程序运行结果: 在L的表头依次插入1~5后:L=5 4 3 2 1 L是否空:i=0(1:是 0:否) 表L的长度=5 清空L后:L= L是否空:i=1(1:是 0:否) 表L的长度=0 在L的表尾依次插入1~10后:L=1 2 3 4 5 6 7 8 9 10 第5个元素的值为5 没有值为0的元素 值为1的元素在静态链表中的位序为5 元素1无前驱 元素2的前驱为1 元素9的后继为10 元素10无后继 删除第11个元素失败(不存在此元素)。 第10个元素为10,已删除。 依次输出L的元素:1 2 3 4 5 6 7 8 9 bo2-32.cpp 是第二种方法(在一个数组中可根据需要生成若干个独立的链表)的基本操 作,由整型形参 n 表示各个独立的链表表头的位序。main2-32.cpp 是验证 bo2-32.cpp 的 主函数。 // bo2-32.cpp 一个数组可生成若干静态链表(数据结构由c2-3.h定义)的基本操作(12个),包括算法2.14 #define DestroyList ClearList // DestroyList()和ClearList()的操作是一样的 void InitSpace(SLinkList L) // 算法2.14。另加(见图229) { // 将一维数组L中各分量链成一个备用链表,L[0].cur为头指针。“0”表示空指针。 int i; for(i=0;iListLength(L,n)) // i值不合理 return ERROR; for(l=1;l<=i;l++) // 移动i-1个元素 (a) 在调用 InitList()之前,L 中有 1 个 链表 La,上有 2 个元素:6 和 25 (b) 在调用 InitList()之后,增加了一个空表,它 的头结点是L[3]。现在,L中有2个链表 图 230 调用 InitList()示例 3 2 6 5 4 6 25 0 L [0]备用链表头结点 [1] La 链表头结点 [2] [3] [4] [5] [99] 4 2 6 5 0 6 25 0 L [0]备用链表头结点 [1] La 链表头结点 [2] 返回值: [3] i=3 新空表的头结点 [4] [5] [99]     · 54· 《数据结构》算法实现及解析(第二版) k=L[k].cur; e=L[k].data; return OK; } int LocateElem(SLinkList L,int n,ElemType e) // 算法2.13(有改动) { // 在L中表头位序为n的静态单链表中查找第1个值为e的元素。 // 若找到,则返回它在L中的位序;否则返回0。(与其它LocateElem()的定义不同) int i=L[n].cur; // i指示表中第一个结点 while(i&&L[i].data!=e) // 在表中顺链查找(e不能是字符串) i=L[i].cur; return i; } Status PriorElem(SLinkList L,int n,ElemType cur_e,ElemType &pre_e) { // 初始条件:在L中表头位序为n的静态单链表已存在 // 操作结果:若cur_e是此单链表的数据元素,且不是第一个, // 则用pre_e返回它的前驱;否则操作失败,pre_e无定义 int j,i=L[n].cur; // i为链表第一个结点的位置 do { // 向后移动结点 j=i; i=L[i].cur; }while(i&&cur_e!=L[i].data); if(i) // 找到该元素 { pre_e=L[j].data; return OK; } return ERROR; } Status NextElem(SLinkList L,int n,ElemType cur_e,ElemType &next_e) { // 初始条件:在L中表头位序为n的静态单链表已存在 // 操作结果:若cur_e是此单链表的数据元素,且不是最后一个, // 则用next_e返回它的后继;否则操作失败,next_e无定义 int i; i=LocateElem(L,n,cur_e); // 在链表中查找第一个值为cur_e的元素的位置 if(i) // 在静态单链表中存在元素cur_e { i=L[i].cur; // cur_e的后继的位置 if(i) // cur_e有后继 { next_e=L[i].data; return OK; // cur_e元素有后继 } } return ERROR; // L不存在cur_e元素,cur_e元素无后继 } Status ListInsert(SLinkList L,int n,int i,ElemType e) { // 在L中表头位序为n的链表的第i个元素之前插入新的数据元素e int l,j,k=n; // k指向表头 if(i<1||i>ListLength(L,n)+1) return ERROR; 第 2 章 线 性 表 · 55· j=Malloc(L); // 申请新单元 if(j) // 申请成功 { L[j].data=e; // 赋值给新单元 for(l=1;lListLength(L,n)) return ERROR; for(j=1;j=k;j--) 第 2 章 线 性 表 · 57· { i=ListDelete(L,Lb,j,e); // 删除第j个数据 if(i) printf("Lb表中第%d个元素为%d,已删除。\n",j,e); else printf("删除Lb表中第%d个元素失败(不存在此元素)。\n",j); } printf("依次输出Lb的元素:"); ListTraverse(L,Lb,print); // 依次对元素调用print(),输出元素的值 } 程序运行结果: La表空否?1(1:空 0:否) La的表长=0 在空表La的表头依次插入1~5后:La=5 4 3 2 1 在空表Lb的表尾依次插入1~5后:Lb=1 2 3 4 5 La表空否?0(1:空 0:否) La的表长=5 清空La后:La= La表空否?1(1:空 0:否) La的表长=0 Lb表的第2个元素的值为2 Lb表不存在第7个元素! Lb表中没有值为0的元素 Lb表中值为1的元素在静态链表中的位序为8 Lb表中的元素1无前驱 Lb表中元素2的前驱为1 Lb表中元素4的后继为5 Lb表中元素5无后继 删除Lb表中第6个元素失败(不存在此元素)。 Lb表中第5个元素为5,已删除。 依次输出Lb的元素:1 2 3 4 // algo2-8.cpp 实现算法2.17的程序 #include"c1.h" #define N 2 typedef char ElemType; #include"c2-3.h" #include"func2-2.cpp" #include"bo2-32.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void difference(SLinkList space,int &S) // 算法2.17 { // 依次输入集合A和B的元素,在一维数组space中建立表示集合(A-B)∪(B-A) // 的静态链表,S为其头指针。假设备用空间足够大,space[0].cur为备用空间的头指针 int r,p,m,n,i,j,k; ElemType b; InitSpace(space); // 初始化备用空间 S=Malloc(space); // 生成S的头结点 r=S; // r指向S的当前最后结点 printf("请输入集合A和B的元素个数m,n:"); scanf("%d,%d%*c",&m,&n); // %*c吃掉回车符 · 58· 《数据结构》算法实现及解析(第二版) printf("请输入集合A的元素(共%d个):",m); for(j=1;j<=m;j++) // 建立集合A的链表 { i=Malloc(space); // 分配结点 scanf("%c",&space[i].data); // 输入A的元素值 space[r].cur=i; // 插入到表尾 r=i; } scanf("%*c"); // %*c吃掉回车符 space[r].cur=0; // 尾结点的指针为空 printf("请输入集合B的元素(共%d个):",n); for(j=1;j<=n;j++) { // 依次输入B的元素,若不在当前表中,则插入;否则删除 scanf("%c",&b); p=S; k=space[S].cur; // k指向集合A中的第一个结点 while(k!=space[r].cur&&space[k].data!=b) { // 在当前表中查找 p=k; k=space[k].cur; } if(k==space[r].cur) { // 当前表中不存在该元素,插入在r所指结点之后,且r的位置不变 i=Malloc(space); space[i].data=b; space[i].cur=space[r].cur; space[r].cur=i; } else // 该元素已在表中,删除之 { space[p].cur=space[k].cur; Free(space,k); if(r==k) r=p; // 若删除的是尾元素,则需修改尾指针 } } } void main() { int k; SLinkList s; difference(s,k); ListTraverse(s,k,print2); } 程序运行结果(以教科书图 2.11 为例): 请输入集合A和B的元素个数m,n:6,4 请输入集合A的元素(共6个):cbegfd 请输入集合B的元素(共4个):abnf 第 2 章 线 性 表 · 59· c e g d n a algo2-9.cpp 是用静态链表的基本操作来实现算法 2.17 功能的。由于只用到 1 个链 表,故采用 bo2-31.cpp 中的基本操作;又由于集合是与顺序无关的,而链表的插入以插 在表头效率最高,故在 algo2-9.cpp 中插入元素时均插在表头。这只影响集合中元素的输 出顺序。将 algo2-9.cpp 与 algo2-8.cpp 对比可见,采用基本操作可使程序简洁明了,思路 清晰,因此在编程时应尽量采用已有的基本操作,以提高效率。 // algo2-9.cpp 尽量采用bo2-31.cpp中的基本操作实现算法2.17的功能 #include"c1.h" #define N 2 typedef char ElemType; #include"c2-3.h" #include"func2-2.cpp" #include"bo2-31.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void difference(SLinkList space) // 改进算法2.17(尽量利用基本操作实现) { // 依次输入集合A和B的元素,在一维数组space中建立表示集合(A-B)∪(B-A)的静态链表 int m,n,i,j; ElemType b,c; InitList(space); // 构造空链表 printf("请输入集合A和B的元素个数m,n:"); scanf("%d,%d%*c",&m,&n); // %*c吃掉回车符 printf("请输入集合A的元素(共%d个):",m); for(j=1;j<=m;j++) // 建立集合A的链表 { scanf("%c",&b); // 输入A的元素值 ListInsert(space,1,b); // 插入到表头 } scanf("%*c"); // 吃掉回车符 printf("请输入集合B的元素(共%d个):",n); for(j=1;j<=n;j++) { // 依次输入B的元素,若不在当前表中,则插入;否则删除 scanf("%c",&b); for(i=1;i<=ListLength(space);i++) { GetElem(space,i,c); // 依次求得表中第i个元素的值,并将其赋给c if(c==b) // 表中存在b,且其是第i个元素 { ListDelete(space,i,c); // 删除第i个元素 break; // 跳出i循环 } } if(i>ListLength(space)) // 表中不存在b ListInsert(space,1,b); // 将b插在表头 } } void main() { · 60· 《数据结构》算法实现及解析(第二版) SLinkList s; difference(s); ListTraverse(s,print2); } 程序运行结果(以教科书图 2.11 为例): 请输入集合A和B的元素个数m,n:6,4 请输入集合A的元素(共6个):cbegfd 请输入集合B的元素(共4个):abnf n a d g e c 2.3.2  单链的循环链表结点的存储结构和单链表的存储结构一样,所不同的是:最后一个结 点的 next 域指向头结点,而不是“空”。这样,由表尾很容易找到表头。但若链表较长, 则由表头找到表尾较费时,因而,单循环链表往往设立尾指针而不是头指针,如图 231 所示。这在两个链表首尾相连合并成一个链表时非常方便。Bo2-4.cpp 是设立尾指针的单 循环链表的基本操作。 // bo2-4.cpp 设立尾指针的单循环链表(存储结构由c2-2.h定义)的12个基本操作 void InitList(LinkList &L) { // 操作结果:构造一个空的线性表L(见图232) L=(LinkList)malloc(sizeof(LNode)); // 产生头结点,并使L指向此头结点 if(!L) // 存储分配失败 exit(OVERFLOW); L->next=L; // 指针域指向头结点 } void DestroyList(LinkList &L) { // 操作结果:销毁线性表L(见图233) LinkList q,p=L->next; // p指向头结点 while(p!=L) // 没到表尾 { q=p->next; free(p); p=q; } free(L); L=NULL; } 图 231 设立尾指针且具有 2 个结点(4,7)的单循环链表 尾指针 4 7 头结点 图 233 线性表 L 被销毁 L NULL 图 232 空(仅有头结点)的单循环链表 L L头结点 第 2 章 线 性 表 · 61· void ClearList(LinkList &L) // 改变L { // 初始条件:线性表L已存在。操作结果:将L重置为空表(见图232) LinkList p,q; L=L->next; // L指向头结点 p=L->next; // p指向第一个结点 while(p!=L) // 没到表尾 { q=p->next; free(p); p=q; } L->next=L; // 头结点指针域指向自身 } Status ListEmpty(LinkList L) { // 初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE;否则返回FALSE if(L->next==L) // 空 return TRUE; else return FALSE; } int ListLength(LinkList L) { // 初始条件:L已存在。操作结果:返回L中数据元素个数 int i=0; LinkList p=L->next; // p指向头结点 while(p!=L) // 没到表尾 { i++; p=p->next; } return i; } Status GetElem(LinkList L,int i,ElemType &e) { // 当第i个元素存在时,其值赋给e并返回OK;否则返回ERROR int j=1; // 初始化,j为计数器 LinkList p=L->next->next; // p指向第一个结点 if(i<=0||i>ListLength(L)) // 第i个元素不存在 return ERROR; while(jnext; j++; } e=p->data; // 取第i个元素 return OK; } int LocateElem(LinkList L,ElemType e,Status(*compare)(ElemType,ElemType)) { // 初始条件:线性表L已存在,compare()是数据元素判定函数 // 操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。 // 若这样的数据元素不存在,则返回值为0 int i=0; · 62· 《数据结构》算法实现及解析(第二版) LinkList p=L->next->next; // p指向第一个结点 while(p!=L->next) { i++; if(compare(p->data,e)) // 满足关系 return i; p=p->next; } return 0; } Status PriorElem(LinkList L,ElemType cur_e,ElemType &pre_e) { // 初始条件:线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱; // 否则操作失败,pre_e无定义 LinkList q,p=L->next->next; // p指向第一个结点 q=p->next; while(q!=L->next) // p没到表尾 { if(q->data==cur_e) { pre_e=p->data; return TRUE; } p=q; q=q->next; } return FALSE; // 操作失败 } Status NextElem(LinkList L,ElemType cur_e,ElemType &next_e) { // 初始条件:线性表L已存在 // 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继; // 否则操作失败,next_e无定义 LinkList p=L->next->next; // p指向第一个结点 while(p!=L) // p没到表尾 { if(p->data==cur_e) { next_e=p->next->data; return TRUE; } p=p->next; } return FALSE; // 操作失败 } Status ListInsert(LinkList &L,int i,ElemType e) // 改变L { // 在L的第i个位置之前插入元素e LinkList p=L->next,s; // p指向头结点 int j=0; if(i<=0||i>ListLength(L)+1) // 无法在第i个元素之前插入 return ERROR; while(jnext; j++; } s=(LinkList)malloc(sizeof(LNode)); // 生成新结点 s->data=e; // 插入L中 s->next=p->next; p->next=s; if(p==L) // 改变尾结点(见图234) L=s; return OK; } Status ListDelete(LinkList &L,int i,ElemType &e) // 改变L { // 删除L的第i个元素,并由e返回其值 LinkList p=L->next,q; // p指向头结点 int j=0; if(i<=0||i>ListLength(L)) // 第i个元素不存在 return ERROR; while(jnext; j++; } q=p->next; // q指向待删除结点 p->next=q->next; e=q->data; if(L==q) // 删除的是表尾元素(见图235) L=p; free(q); // 释放待删除结点 return OK; } void ListTraverse(LinkList L,void(*vi)(ElemType)) { // 初始条件:L已存在 // 操作结果:依次对L的每个数据元素调用函数vi() LinkList p=L->next->next; // p指向首元结点 while(p!=L->next) // p不指向头结点 { vi(p->data); p=p->next; } printf("\n"); } // main2-4.cpp 单循环链表,检验bo2-4.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c2-2.h" #include"bo2-4.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void main() 图 234 在链表 L 的表尾插入元素 e (a) 初态,p 指向头结点 L i-11 ⋯ p (b) p 指向第 i-1 个结点(尾结点) L i-11 ⋯ p se (c) 插入 s 为尾结点 L i-11 ⋯ p se 图 235 删除链表 L 的表尾结点 (a) 初态,p 指向头结点 ii-11 ⋯ p L (b) p 指向第 i-1 个结点 ii-11 ⋯ p L q (c) 从链表上删除第 i 个结点 ii-11 ⋯ p L q · 64· 《数据结构》算法实现及解析(第二版) { LinkList L; ElemType e; int j; Status i; InitList(L); // 初始化单循环链表L i=ListEmpty(L); printf("L是否空 i=%d(1:空 0:否)\n",i); ListInsert(L,1,3); // 在L中依次插入3,5 ListInsert(L,2,5); i=GetElem(L,1,e); j=ListLength(L); printf("L中数据元素个数=%d,第1个元素的值为%d。\n",j,e); printf("L中的数据元素依次为"); ListTraverse(L,print); PriorElem(L,5,e); // 求元素5的前驱 printf("5前面的元素的值为%d。\n",e); NextElem(L,3,e); // 求元素3的后继 printf("3后面的元素的值为%d。\n",e); printf("L是否空 %d(1:空 0:否)\n",ListEmpty(L)); j=LocateElem(L,5,equal); if(j) printf("L的第%d个元素为5。\n",j); else printf("不存在值为5的元素\n"); i=ListDelete(L,2,e); printf("删除L的第2个元素:\n"); if(i) { printf("删除的元素值为%d,现在L中的数据元素依次为",e); ListTraverse(L,print); } else printf("删除不成功!\n"); ClearList(L); printf("清空L后,L是否空:%d(1:空 0:否)\n",ListEmpty(L)); DestroyList(L); } 程序运行结果: L是否空 i=1(1:空 0:否) L中数据元素个数=2,第1个元素的值为3。 L中的数据元素依次为3 5 5前面的元素的值为3。 3后面的元素的值为5。 L是否空 0(1:空 0:否) L的第2个元素为5。 删除L的第2个元素: 删除的元素值为5,现在L中的数据元素依次为3 第 2 章 线 性 表 · 65· 清空L后,L是否空:1(1:空 0:否) // algo2-10.cpp 两个仅设表尾指针的循环链表的合并(教科书图2.13) #include"c1.h" typedef int ElemType; #include"c2-2.h" #include"bo2-4.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void MergeList(LinkList &La,LinkList Lb) { // 将Lb合并到La的表尾,由La指示新表 LinkList p=Lb->next; Lb->next=La->next; La->next=p->next; free(p); La=Lb; } void main() { int n=5,i; LinkList La,Lb; InitList(La); for(i=1;i<=n;i++) ListInsert(La,i,i); printf("La="); // 输出链表La的内容 ListTraverse(La,print); InitList(Lb); for(i=1;i<=n;i++) ListInsert(Lb,1,i*2); printf("Lb="); // 输出链表Lb的内容 ListTraverse(Lb,print); MergeList(La,Lb); printf("La+Lb="); // 输出合并后的链表的内容 ListTraverse(La,print); } 程序运行结果: La=1 2 3 4 5 Lb=10 8 6 4 2 La+Lb=1 2 3 4 5 10 8 6 4 2 2.3.3  // c2-4.h 线性表的双向链表存储结构(见图236) typedef struct DuLNode { ElemType data; DuLNode *prior,*next; }DuLNode,*DuLinkList; 图 236 线性表的双向链表存储结构 DuLNode prior data next DuLinkList DuLNode DuLNode · 66· 《数据结构》算法实现及解析(第二版) 双向链表(见图 237)每个结点有两个指针,一 个指向结点的前驱,另一个指向结点的后继。所 以,从链表的每一个结点出发,都可到达任意一个 结点,有利于链表的查找。单链表的找前驱函数, 除了有指向当前结点的指针外,还有一个紧跟其 后,一直指向其前驱的指针。在双向链表中,不需 要这个指向前驱的指针。 // bo2-5.cpp 带头结点的双向循环链表(存储结构由c2-4.h定义)的基本操作(14个),包括算法2.18,2.19 void InitList(DuLinkList &L) { // 产生空的双向循环链表L(见图238) L=(DuLinkList)malloc(sizeof(DuLNode)); if(L) L->next=L->prior=L; else exit(OVERFLOW); } void DestroyList(DuLinkList &L) { // 操作结果:销毁双向循环链表L(见图239) DuLinkList q,p=L->next; // p指向第一个结点 while(p!=L) // p没到表头 { q=p->next; free(p); p=q; } free(L); L=NULL; } void ClearList(DuLinkList L) // 不改变L { // 初始条件:L已存在。操作结果:将L重置为空表(见图238) DuLinkList q,p=L->next; // p指向第一个结点 while(p!=L) // p没到表头 { q=p->next; free(p); p=q; } L->next=L->prior=L; // 头结点的两个指针域均指向自身 } Status ListEmpty(DuLinkList L) { // 初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE;否则返回FALSE if(L->next==L&&L->prior==L) return TRUE; else return FALSE; } 图 239 销毁双向循环链表 L NULL L 图 238 空的双向循环链表 头结点L 图 237 带有头结点且具有 2 个结点 (4,7)的双向循环链表 L L 头结点 4 7 第 2 章 线 性 表 · 67· int ListLength(DuLinkList L) { // 初始条件:L已存在。操作结果:返回L中数据元素个数 int i=0; DuLinkList p=L->next; // p指向第1个结点 while(p!=L) // p没到表头 { i++; p=p->next; } return i; } Status GetElem(DuLinkList L,int i,ElemType &e) { // 当第i个元素存在时,其值赋给e并返回OK;否则返回ERROR int j=1; // j为计数器 DuLinkList p=L->next; // p指向第1个结点 while(p!=L&&jnext; j++; } if(p==L||j>i) // 第i个元素不存在 return ERROR; e=p->data; // 取第i个元素 return OK; } int LocateElem(DuLinkList L,ElemType e,Status(*compare)(ElemType,ElemType)) { // 初始条件:L已存在,compare()是数据元素判定函数 // 操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。 // 若这样的数据元素不存在,则返回值为0 int i=0; DuLinkList p=L->next; // p指向第1个元素 while(p!=L) { i++; if(compare(p->data,e)) // 找到这样的数据元素 return i; p=p->next; } return 0; } Status PriorElem(DuLinkList L,ElemType cur_e,ElemType &pre_e) { // 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱; // 否则操作失败,pre_e无定义 DuLinkList p=L->next->next; // p指向第2个元素 while(p!=L) // p没到表头 { if(p->data==cur_e) { pre_e=p->prior->data; return TRUE; } · 68· 《数据结构》算法实现及解析(第二版) p=p->next; } return FALSE; } Status NextElem(DuLinkList L,ElemType cur_e,ElemType &next_e) { // 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继; // 否则操作失败,next_e无定义 DuLinkList p=L->next->next; // p指向第2个元素 while(p!=L) // p没到表头 { if(p->prior->data==cur_e) { next_e=p->data; return TRUE; } p=p->next; } return FALSE; } DuLinkList GetElemP(DuLinkList L,int i) // 另加 { // 在双向链表L中返回第i个元素的地址。i为0,返回头结点的地址。若第i个元素不存在, // 返回NULL(算法2.18、2.19要调用的函数) int j; DuLinkList p=L; // p指向头结点 if(i<0||i>ListLength(L)) // i值不合法 return NULL; for(j=1;j<=i;j++) p=p->next; return p; } Status ListInsert(DuLinkList L,int i,ElemType e) { // 在带头结点的双链循环线性表L中第i个位置之前插入元素e,i的合法值为1≤ i≤表长 +1 // 改进算法2.18;否则无法在第表长+1个结点之前插入元素 DuLinkList p,s; if(i<1||i>ListLength(L)+1) // i值不合法 return ERROR; p=GetElemP(L,i-1); // 在L中确定第i个元素前驱的位置指针p if(!p) // p=NULL,即第i个元素的前驱不存在(设头结点为第1个元素的前驱) return ERROR; s=(DuLinkList)malloc(sizeof(DuLNode)); if(!s) return OVERFLOW; s->data=e; s->prior=p; // 在第i-1个元素之后插入 s->next=p->next; p->next->prior=s; p->next=s; return OK; } Status ListDelete(DuLinkList L,int i,ElemType &e) // 算法2.19 { // 删除带头结点的双链循环线性表L的第i个元素,i的合法值为1≤ i≤表长 第 2 章 线 性 表 · 69· DuLinkList p; if(i<1) // i值不合法 return ERROR; p=GetElemP(L,i); // 在L中确定第i个元素的位置指针p if(!p) // p=NULL,即第i个元素不存在 return ERROR; e=p->data; p->prior->next=p->next; p->next->prior=p->prior; free(p); return OK; } void ListTraverse(DuLinkList L,void(*visit)(ElemType)) { // 由双链循环线性表L的头结点出发,正序对每个数据元素调用函数visit() DuLinkList p=L->next; // p指向头结点 while(p!=L) { visit(p->data); p=p->next; } printf("\n"); } void ListTraverseBack(DuLinkList L,void(*visit)(ElemType)) { // 由双链循环线性表L的头结点出发,逆序对每个数据元素调用函数visit()。另加 DuLinkList p=L->prior; // p指向尾结点 while(p!=L) { visit(p->data); p=p->prior; } printf("\n"); } // main2-5.cpp 检验bo2-5.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c2-4.h" #include"bo2-5.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void main() { DuLinkList L; int i,n; Status j; ElemType e; InitList(L); for(i=1;i<=5;i++) ListInsert(L,i,i); // 在第i个结点之前插入i printf("正序输出链表:"); ListTraverse(L,print); // 正序输出 printf("逆序输出链表:"); · 70· 《数据结构》算法实现及解析(第二版) ListTraverseBack(L,print); // 逆序输出 n=2; ListDelete(L,n,e); // 删除并释放第n个结点 printf("删除第%d个结点,值为%d,其余结点为",n,e); ListTraverse(L,print); // 正序输出 printf("链表的元素个数为%d\n",ListLength(L)); printf("链表是否空:%d(1:是 0:否)\n",ListEmpty(L)); ClearList(L); // 清空链表 printf("清空后,链表是否空:%d(1:是 0:否)\n",ListEmpty(L)); for(i=1;i<=5;i++) ListInsert(L,i,i); // 重新插入5个结点 ListTraverse(L,print); // 正序输出 n=3; j=GetElem(L,n,e); // 将链表的第n个元素赋值给e if(j) printf("链表的第%d个元素值为%d\n",n,e); else printf("不存在第%d个元素\n",n); n=4; i=LocateElem(L,n,equal); if(i) printf("等于%d的元素是第%d个\n",n,i); else printf("没有等于%d的元素\n",n); j=PriorElem(L,n,e); if(j) printf("%d的前驱是%d\n",n,e); else printf("不存在%d的前驱\n",n); j=NextElem(L,n,e); if(j) printf("%d的后继是%d\n",n,e); else printf("不存在%d的后继\n",n); DestroyList(L); } 程序运行结果: 正序输出链表:1 2 3 4 5 逆序输出链表:5 4 3 2 1 删除第2个结点,值为2,其余结点为1 3 4 5 链表的元素个数为4 链表是否空:0(1:是 0:否) 清空后,链表是否空:1(1:是 0:否) 1 2 3 4 5 链表的第3个元素值为3 等于4的元素是第4个 4的前驱是3 第 2 章 线 性 表 · 71· 4的后继是5 前面介绍的线性链表结构较简单,还不能满足实际应用的需要。其主要存在 3 个问 题:第一,只有头指针,没有尾指针。如要在表尾插入结点,则效率很低。第二,求表长 要从表头找到表尾效率也很低。第三,基本操作函数太少。c2-5.h 是从实际应用角度出发 重新定义的线性链表类型,LinkList 类型增加了尾指针和表长 2 个成员,成为结构体类 型。bo2-6.cpp 是基于 c2-5.h 的基本操作。 // c2-5.h 带头结点的线性链表类型 typedef struct LNode // 结点类型(见图240) { ElemType data; LNode *next; }*Link,*Position; struct LinkList // 链表类型(见图241) { Link head,tail; // 分别指向线性链表中的头结点和最后一个结点 int len; // 指示线性链表中数据元素的个数 }; 图 242 是根据 c2-5.h 定义的具有 2 个结点的线性链表的结构。 // bo2-6.cpp 具有实用意义的线性链表(存储结构由c2-5.h定义)的24个基本操作 void MakeNode(Link &p,ElemType e) { // 分配由p指向的值为e的结点。若分配失败,则退出 p=(Link)malloc(sizeof(LNode)); if(!p) exit(ERROR); p->data=e; } void FreeNode(Link &p) { // 释放p所指结点 free(p); p=NULL; } void InitList(LinkList &L) { // 构造一个空的线性链表L(见图243) Link p; p=(Link)malloc(sizeof(LNode)); // 生成头结点 if(p) { p->next=NULL; L.head=L.tail=p; L.len=0; } else exit(ERROR); 图 240 结点类型 Link 和 Position data next LNode LNode 图 241 链表类型 LinkList head len tail LNode LNode 图 242 具有 2 个结点且带头结点的线性链表 2 7 NULL4 图 243 空的线性链表 L NULL L 0 · 72· 《数据结构》算法实现及解析(第二版) } void ClearList(LinkList &L) { // 将线性链表L重置为空表,并释放原链表的结点空间 Link p,q; if(L.head!=L.tail) // 不是空表 { p=q=L.head->next; L.head->next=NULL; while(p!=L.tail) { p=q->next; free(q); q=p; } free(q); L.tail=L.head; L.len=0; } } void DestroyList(LinkList &L) { // 销毁线性链表L,L不再存在(见图244) ClearList(L); // 清空链表 FreeNode(L.head); L.tail=NULL; L.len=0; } void InsFirst(LinkList &L,Link h,Link s) // 形参增加L,因为需修改L { // h指向L的一个结点,把h当做头结点,将s所指结点插入在第一个结点之前 s->next=h->next; h->next=s; if(h==L.tail) // h指向尾结点 L.tail=h->next; // 修改尾指针 L.len++; } Status DelFirst(LinkList &L,Link h,Link &q) // 形参增加L,因为需修改L { // h指向L的一个结点,把h当做头结点,删除链表中的第一个结点并以q返回。 // 若链表为空(h指向尾结点),q=NULL,返回FALSE q=h->next; if(q) // 链表非空 { h->next=q->next; if(!h->next) // 删除尾结点 L.tail=h; // 修改尾指针 L.len--; return OK; } else return FALSE; // 链表空 } void Append(LinkList &L,Link s) { // 将指针s(s->data为第一个数据元素)所指(彼此以指针相链,以NULL结尾)的 NULL 0 NULL L 图 244 销毁后的线性链表 L 第 2 章 线 性 表 · 73· // 一串结点链接在线性链表L的最后一个结点之后,并改变链表L的尾指针指向新的尾结点 int i=1; L.tail->next=s; while(s->next) { s=s->next; i++; } L.tail=s; L.len+=i; } Position PriorPos(LinkList L,Link p) { // 已知p指向线性链表L中的一个结点,返回p所指结点的直接前驱的位置。若无前驱,则返回NULL Link q; q=L.head->next; if(q==p) // 无前驱 return NULL; else { while(q->next!=p) // q不是p的直接前驱 q=q->next; return q; } } Status Remove(LinkList &L,Link &q) { // 删除线性链表L中的尾结点并以q返回,改变链表L的尾指针指向新的尾结点 Link p=L.head; if(L.len==0) // 空表 { q=NULL; return FALSE; } while(p->next!=L.tail) p=p->next; q=L.tail; p->next=NULL; L.tail=p; L.len--; return OK; } void InsBefore(LinkList &L,Link &p,Link s) { // 已知p指向线性链表L中的一个结点,将s所指结点插入在p所指结点之前, // 并修改指针p指向新插入的结点 Link q; q=PriorPos(L,p); // q是p的前驱 if(!q) // p无前驱 q=L.head; s->next=p; q->next=s; p=s; L.len++; · 74· 《数据结构》算法实现及解析(第二版) } void InsAfter(LinkList &L,Link &p,Link s) { // 已知p指向线性链表L中的一个结点,将s所指结点插入在p所指结点之后, // 并修改指针p指向新插入的结点 if(p==L.tail) // 修改尾指针 L.tail=s; s->next=p->next; p->next=s; p=s; L.len++; } void SetCurElem(Link p,ElemType e) { // 已知p指向线性链表中的一个结点,用e更新p所指结点中数据元素的值 p->data=e; } ElemType GetCurElem(Link p) { // 已知p指向线性链表中的一个结点,返回p所指结点中数据元素的值 return p->data; } Status ListEmpty(LinkList L) { // 若线性链表L为空表,则返回TRUE;否则返回FALSE if(L.len) return FALSE; else return TRUE; } int ListLength(LinkList L) { // 返回线性链表L中元素个数 return L.len; } Position GetHead(LinkList L) { // 返回线性链表L中头结点的位置 return L.head; } Position GetLast(LinkList L) { // 返回线性链表L中最后一个结点的位置 return L.tail; } Position NextPos(Link p) { // 已知p指向线性链表L中的一个结点,返回p所指结点的直接后继的位置。若无后继,则返回NULL return p->next; } Status LocatePos(LinkList L,int i,Link &p) { // 返回p指示线性链表L中第i个结点的位置,并返回OK,i值不合法时返回ERROR。i=0为头结点 int j; if(i<0||i>L.len) return ERROR; else { p=L.head; for(j=1;j<=i;j++) 第 2 章 线 性 表 · 75· p=p->next; return OK; } } Position LocateElem(LinkList L,ElemType e,Status (*compare)(ElemType,ElemType)) { // 返回线性链表L中第1个与e满足函数compare()判定关系的元素的位置, // 若不存在这样的元素,则返回NULL Link p=L.head; do p=p->next; while(p&&!(compare(p->data,e))); // 没到表尾且没找到满足关系的元素 return p; } void ListTraverse(LinkList L,void(*visit)(ElemType)) { // 依次对L的每个数据元素调用函数visit() Link p=L.head->next; int j; for(j=1;j<=L.len;j++) { visit(p->data); p=p->next; } printf("\n"); } void OrderInsert(LinkList &L,ElemType e,int (*comp)(ElemType,ElemType)) { // 已知L为有序线性链表,将元素e按非降序插入在L中。(用于一元多项式) Link o,p,q; q=L.head; p=q->next; while(p!=NULL&&comp(p->data,e)<0) // p不是表尾且元素值小于e { q=p; p=p->next; } o=(Link)malloc(sizeof(LNode)); // 生成结点 o->data=e; // 赋值 q->next=o; // 插入 o->next=p; L.len++; // 表长加1 if(!p) // 插在表尾 L.tail=o; // 修改尾结点 } Status LocateElem(LinkList L,ElemType e,Position &q,int(*compare)(ElemType,ElemType)) { // 若升序链表L中存在与e满足判定函数compare()取值为0的元素,则q指示L中 // 第一个值为e的结点的位置,并返回TRUE;否则q指示第一个与e满足判定函数 // compare()取值>0的元素的前驱的位置。并返回FALSE。(用于一元多项式) Link p=L.head,pp; do { pp=p; p=p->next; · 76· 《数据结构》算法实现及解析(第二版) }while(p&&(compare(p->data,e)<0)); // 没到表尾且p->data.expndata,e)>0) // 到表尾或compare(p->data,e)>0 { q=pp; return FALSE; } else // 找到 { q=p; return TRUE; } } // main2-6.cpp 检验bo2-6.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c2-5.h" #include"bo2-6.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 void main() { Link p,h; LinkList L; Status i; int j,k; InitList(L); // 初始化空的线性表L for(j=1;j<=2;j++) { MakeNode(p,j); // 生成由p指向、值为j的结点 InsFirst(L,L.tail,p); // 插在表尾 } OrderInsert(L,0,comp); // 按升序插在有序表头 for(j=0;j<=3;j++) { i=LocateElem(L,j,p,comp); if(i) printf("链表中有值为%d的元素。\n",p->data); else printf("链表中没有值为%d的元素。\n",j); } printf("输出链表:"); ListTraverse(L,print); // 输出L for(j=1;j<=4;j++) { printf("删除表头结点:"); DelFirst(L,L.head,p); // 删除L的首结点,并以p返回 if(p) printf("%d\n",GetCurElem(p)); else printf("表空,无法删除 p=%u\n",p); } 第 2 章 线 性 表 · 77· printf("L中结点个数=%d L是否空 %d(1:空 0:否)\n",ListLength(L),ListEmpty(L)); MakeNode(p,10); p->next=NULL; // 尾结点 for(j=4;j>=1;j--) { MakeNode(h,j*2); h->next=p; p=h; } // h指向一串5个结点,其值依次是2 4 6 8 10 Append(L,h); // 把结点h链接在线性链表L的最后一个结点之后 OrderInsert(L,12,comp); // 按升序插在有序表尾头 OrderInsert(L,7,comp); // 按升序插在有序表中间 printf("输出链表:"); ListTraverse(L,print); // 输出L for(j=1;j<=2;j++) { p=LocateElem(L,j*5,equal); if(p) printf("L中存在值为%d的结点。\n",j*5); else printf("L中不存在值为%d的结点。\n",j*5); } for(j=1;j<=2;j++) { LocatePos(L,j,p); // p指向L的第j个结点 h=PriorPos(L,p); // h指向p的前驱 if(h) printf("%d的前驱是%d。\n",p->data,h->data); else printf("%d没前驱。\n",p->data); } k=ListLength(L); for(j=k-1;j<=k;j++) { LocatePos(L,j,p); // p指向L的第j个结点 h=NextPos(p); // h指向p的后继 if(h) printf("%d的后继是%d。\n",p->data,h->data); else printf("%d没后继。\n",p->data); } printf("L中结点个数=%d L是否空 %d(1:空 0:否)\n",ListLength(L),ListEmpty(L)); p=GetLast(L); // p指向最后一个结点 SetCurElem(p,15); // 将最后一个结点的值变为15 printf("第1个元素为%d 最后1个元素为%d\n",GetCurElem(GetHead(L)->next),GetCurElem(p)); MakeNode(h,10); InsBefore(L,p,h); // 将10插到尾结点之前,p指向新结点 p=p->next; // p恢复为尾结点 MakeNode(h,20); InsAfter(L,p,h); // 将20插到尾结点之后 k=ListLength(L); · 78· 《数据结构》算法实现及解析(第二版) printf("依次删除表尾结点并输出其值:"); for(j=0;j<=k;j++) if(!(i=Remove(L,p))) // 删除不成功 printf("删除不成功 p=%u\n",p); else printf("%d ",p->data); MakeNode(p,29); // 重建具有1个结点(29)的链表 InsFirst(L,L.head,p); DestroyList(L); // 销毁线性链表L printf("销毁线性链表L之后: L.head=%u L.tail=%u L.len=%d\n",L.head,L.tail,L.len); } 程序运行结果: 链表中有值为0的元素。 链表中有值为1的元素。 链表中有值为2的元素。 链表中没有值为3的元素。 输出链表:0 1 2 删除表头结点:0 删除表头结点:1 删除表头结点:2 删除表头结点:表空,无法删除 p=0 L中结点个数=0 L是否空 1(1:空 0:否) 输出链表:2 4 6 7 8 10 12 L中不存在值为5的结点。 L中存在值为10的结点。 2没前驱。 4的前驱是2。 10的后继是12。 12没后继。 L中结点个数=7 L是否空 0(1:空 0:否) 第1个元素为2 最后1个元素为15 依次删除表尾结点并输出其值:20 15 10 10 8 7 6 4 2 删除不成功 p=0 销毁线性链表L之后: L.head=0 L.tail=0 L.len=0 // algo2-11.cpp 实现算法2.20、2.21的程序 #include"c1.h" typedef int ElemType; #include"c2-5.h" #include"bo2-6.cpp" #include"func2-3.cpp" // 包括equal()、comp()、print()、print2()和print1()函数 Status ListInsert_L(LinkList &L,int i,ElemType e) // 算法2.20 { // 在带头结点的单链线性表L的第i个元素之前插入元素e Link h,s; if(!LocatePos(L,i-1,h)) return ERROR; // i值不合法 MakeNode(s,e); // 结点分配失败则退出 InsFirst(L,h,s); // 对于从第i个结点开始的链表,第i-1个结点是它的头结点 第 2 章 线 性 表 · 79· return OK; } void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc,int(*compare)(ElemType,ElemType)) { // 已知单链线性表La和Lb的元素按值非递减排列。归并La和Lb得到新的单链 // 线性表Lc,Lc的元素也按值非递减排列。算法2.21 Link ha,hb,pa,pb,q; ElemType a,b; InitList(Lc); // 存储空间分配失败则退出 ha=GetHead(La); // ha和hb分别指向La和Lb的头结点 hb=GetHead(Lb); pa=NextPos(ha); // pa和pb分别指向La和Lb的首元结点 pb=NextPos(hb); while(pa&&pb) // La和Lb均非空 { a=GetCurElem(pa); // a和b为两表中当前比较元素(第1个元素) b=GetCurElem(pb); if(compare(a,b)<=0) // a<=b { DelFirst(La,ha,q); // 移去La的首元结点并以q返回 q->next=NULL; // 将q的next域赋值NULL,以便调用Append() Append(Lc,q); // 将q结点接在Lc的尾部 pa=NextPos(ha); // pa指向La新的首元结点 } else // a>b { DelFirst(Lb,hb,q); // 移去Lb的首元结点并以q返回 q->next=NULL; // 将q的next域赋值NULL,以便调用Append() Append(Lc,q); // 将q结点接在Lc的尾部 pb=NextPos(hb); // pb指向Lb新的首元结点 } } if(pa) // La非空 Append(Lc,pa); // 链接La中剩余结点 else // Lb非空 Append(Lc,pb); // 链接Lb中剩余结点 free(ha); // 销毁La和Lb La.head=La.tail=NULL; La.len=0; free(hb); Lb.head=Lb.tail=NULL; Lb.len=0; } int diff(ElemType c1,ElemType c2) { return c1-c2; } void main() { LinkList La,Lb,Lc; int j; InitList(La); · 80· 《数据结构》算法实现及解析(第二版) for(j=1;j<=5;j++) ListInsert_L(La,j,j); // 顺序插入 1、2、3、4、5 printf("La="); ListTraverse(La,print); InitList(Lb); for(j=1;j<=5;j++) ListInsert_L(Lb,j,2*j); // 顺序插入 2、4、6、8、10 printf("Lb="); ListTraverse(Lb,print); MergeList_L(La,Lb,Lc,diff); // 归并La和Lb,产生Lc printf("Lc="); ListTraverse(Lc,print); DestroyList(Lc); } 程序运行结果: La=1 2 3 4 5 Lb=2 4 6 8 10 Lc=1 2 2 3 4 4 5 6 8 10 2.4 一元多项式的表示及相加 // c2-6.h 抽象数据类型Polynomial的实现(见图245) typedef struct // 项的表示,多项式的项作为LinkList的数据元素 { float coef; // 系数 int expn; // 指数 }term,ElemType; // 两个类型名:term用于本ADT,ElemType为LinkList的数据对象名 图 246 是根据 c2-5.h 和 c2-6.h 定义的多项式 7.3+22X7 的存储结构。 // bo2-7.cpp 多项式(存储结构由c2-6.h定义)的基本操作及算法2.22,2.23等(8个) #include"c2-5.h" #include"bo2-6.cpp" typedef LinkList polynomial; #define DestroyPolyn DestroyList // 与bo2-6.cpp中的函数同义不同名 #define PolynLength ListLength // 与bo2-6.cpp中的函数同义不同名 图 245 多项式的存储结构 coef expn term 和 ElemType 图 246 多项式 7.3+22X7 的存储结构 2 7.3 0 22 7 NULL 第 2 章 线 性 表 · 81· void OrderInsertMerge(LinkList &L,ElemType e,int(* compare)(term,term)) { // 按有序判定函数compare()的约定,将值为e的结点插入或合并到升序链表L的适当位置 Position q,s; if(LocateElem(L,e,q,compare)) // L中存在该指数项 { q->data.coef+=e.coef; // 改变当前结点系数的值 if(!q->data.coef) // 系数为0 { // 删除多项式L中当前结点 s=PriorPos(L,q); // s为当前结点的前驱 if(!s) // q无前驱 s=L.head; DelFirst(L,s,q); FreeNode(q); } } else // 生成该指数项并插入链表 { MakeNode(s,e); // 生成结点 InsFirst(L,q,s); } } int cmp(term a,term b) // CreatPolyn()的实参 { // 依a的指数值<、=或>b的指数值,分别返回-1、0或+1 if(a.expn==b.expn) return 0; else return (a.expn-b.expn)/abs(a.expn-b.expn); } void CreatPolyn(polynomial &P,int m) // 算法2.22 { // 输入m项的系数和指数,建立表示一元多项式的有序链表P Position q,s; term e; int i; InitList(P); printf("请依次输入%d个系数,指数:\n",m); for(i=1;i<=m;++i) { // 依次输入m个非零项(可按任意顺序) scanf("%f,%d",&e.coef,&e.expn); if(!LocateElem(P,e,q,cmp)) // 当前链表中不存在该指数项,cmp是实参 { MakeNode(s,e); // 生成结点并插入链表 InsFirst(P,q,s); } } } void PrintPolyn(polynomial P) { // 打印输出一元多项式P Link q; q=P.head->next; // q指向第1个结点 printf(" 系数 指数\n"); while(q) · 82· 《数据结构》算法实现及解析(第二版) { printf("%f %d\n",q->data.coef,q->data.expn); q=q->next; } } void AddPolyn(polynomial &Pa,polynomial &Pb) // 算法2.23 { // 多项式加法:Pa=Pa+Pb,并销毁一元多项式Pb Position ha,hb,qa,qb; term a,b; ha=GetHead(Pa); hb=GetHead(Pb); // ha和hb分别指向Pa和Pb的头结点 qa=NextPos(ha); qb=NextPos(hb); // qa和qb分别指向Pa和Pb中当前结点(现为第1个结点) while(!ListEmpty(Pa)&&!ListEmpty(Pb)&&qa) { // Pa和Pb均非空且ha没指向尾结点(qa!=0) a=GetCurElem(qa); b=GetCurElem(qb); // a和b为两表中当前比较元素 switch(cmp(a,b)) { case -1:ha=qa; // 多项式Pa中当前结点的指数值小 qa=NextPos(ha); // ha和qa均向后移1个结点 break; case 0: qa->data.coef+=qb->data.coef; // 两者的指数值相等,修改Pa当前结点的系数值 if(qa->data.coef==0) // 删除多项式Pa中当前结点 { DelFirst(Pa,ha,qa); FreeNode(qa); } else ha=qa; DelFirst(Pb,hb,qb); FreeNode(qb); qb=NextPos(hb); qa=NextPos(ha); break; case 1: DelFirst(Pb,hb,qb); // 多项式Pb中当前结点的指数值小 InsFirst(Pa,ha,qb); ha=ha->next; qb=NextPos(hb); } } if(!ListEmpty(Pb)) { Pb.tail=hb; Append(Pa,qb); // 链接Pb中剩余结点 } DestroyPolyn(Pb); // 销毁Pb } void AddPolyn1(polynomial &Pa,polynomial &Pb) { // 另一种多项式加法的算法:Pa=Pa+Pb,并销毁一元多项式Pb Position qb; 第 2 章 线 性 表 · 83· term b; qb=GetHead(Pb); // qb指向Pb的头结点 qb=qb->next; // qb指向Pb的第1个结点 while(qb) { b=GetCurElem(qb); OrderInsertMerge(Pa,b,cmp); qb=qb->next; } DestroyPolyn(Pb); // 销毁Pb } void Opposite(polynomial Pa) { // 一元多项式Pa系数取反 Position p; p=Pa.head; while(p->next) { p=p->next; p->data.coef*=-1; } } void SubtractPolyn(polynomial &Pa,polynomial &Pb) { // 多项式减法:Pa=Pa-Pb,并销毁一元多项式Pb Opposite(Pb); AddPolyn(Pa,Pb); } void MultiplyPolyn(polynomial &Pa,polynomial &Pb) { // 多项式乘法:Pa=Pa×Pb,并销毁一元多项式Pb polynomial Pc; Position qa,qb; term a,b,c; InitList(Pc); qa=GetHead(Pa); qa=qa->next; while(qa) { a=GetCurElem(qa); qb=GetHead(Pb); qb=qb->next; while(qb) { b=GetCurElem(qb); c.coef=a.coef*b.coef; c.expn=a.expn+b.expn; OrderInsertMerge(Pc,c,cmp); qb=qb->next; } qa=qa->next; } DestroyPolyn(Pb); // 销毁Pb ClearList(Pa); // 将Pa重置为空表 · 84· 《数据结构》算法实现及解析(第二版) Pa.head=Pc.head; Pa.tail=Pc.tail; Pa.len=Pc.len; } // main2-7.cpp 检验bo2-7.cpp的主程序 #include"c1.h" #include"c2-6.h" #include"bo2-7.cpp" void main() { polynomial p,q; int m; printf("请输入第1个一元多项式的非零项的个数:"); scanf("%d",&m); CreatPolyn(p,m); printf("请输入第2个一元多项式的非零项的个数:"); scanf("%d",&m); CreatPolyn(q,m); AddPolyn(p,q); printf("2个一元多项式相加的结果:\n"); PrintPolyn(p); printf("请输入第3个一元多项式的非零项的个数:"); scanf("%d",&m); CreatPolyn(q,m); AddPolyn1(p,q); printf(2个一元多项式相加的结果(另一种方法):\n"); PrintPolyn(p); printf("请输入第4个一元多项式的非零项的个数:"); scanf("%d",&m); CreatPolyn(q,m); SubtractPolyn(p,q); printf("2个一元多项式相减的结果:\n"); PrintPolyn(p); printf("请输入第5个一元多项式的非零项的个数:"); scanf("%d",&m); CreatPolyn(q,m); MultiplyPolyn(p,q); printf("2个一元多项式相乘的结果:\n"); PrintPolyn(p); DestroyPolyn(p); } 程序运行结果: 请输入第1个一元多项式的非零项的个数:3 请依次输入3个系数,指数: 1,2 5,4 第 2 章 线 性 表 · 85· 3,3 请输入第2个一元多项式的非零项的个数:3 请依次输入3个系数,指数: -3,3 4,2 7,1 2个一元多项式相加的结果: 系数 指数 7.000000 1 5.000000 2 5.000000 4 请输入第3个一元多项式的非零项的个数:3 请依次输入3个系数,指数: -5,2 3,3 -3,1 2个一元多项式相加的结果(另一种方法): 系数 指数 4.000000 1 3.000000 3 5.000000 4 请输入第4个一元多项式的非零项的个数:3 请依次输入3个系数,指数: 4,1 2,3 6,6 2个一元多项式相减的结果: 系数 指数 1.000000 3 5.000000 4 -6.000000 6 请输入第5个一元多项式的非零项的个数:2 请依次输入2个系数,指数: 1,1 2,2 2个一元多项式相乘的结果: 系数 指数 1.000000 4 7.000000 5 10.000000 6 -6.000000 7 -12.000000 8 · 86· 《数据结构》算法实现及解析(第二版) 第 3 章 栈 和 队 列 3.1 栈 3.1.1  3.1.2   // c3-1.h 栈的顺序存储结构(见图31) #define STACK_INIT_SIZE 10 // 存储空间初始分配量 #define STACK_INCREMENT 2 // 存储空间分配增量 struct SqStack { SElemType *base; // 在栈构造之前和销毁之后,base的值为NULL SElemType *top; // 栈顶指针 int stacksize; // 当前已分配的存储空间,以元素为单位 }; // 顺序栈 // bo3-1.cpp 顺序栈(存储结构由c3-1.h定义)的基本操作(9个) void InitStack(SqStack &S) { // 构造一个空栈S(见图32) if(!(S.base=(SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType)))) exit(OVERFLOW); // 存储分配失败 S.top=S.base; S.stacksize=STACK_INIT_SIZE; } void DestroyStack(SqStack &S) { // 销毁栈S,S不再存在(见图33) free(S.base); S.base=NULL; S.top=NULL; S.stacksize=0; } void ClearStack(SqStack &S) { // 把S置为空栈 S.top=S.base; } Status StackEmpty(SqStack S) { // 若栈S为空栈,则返回TRUE;否则返回FALSE if(S.top==S.base) 图 31 顺序栈存储结构 SqStack stacksize top base SElemType SElemType 图 32 构造一个空的顺序栈 S [9] [8] [1] [0] 10 S 0 NULL NULL 图 33 销毁顺序栈 S S   第 3 章 栈 和 队 列 · 87· return TRUE; else return FALSE; } int StackLength(SqStack S) { // 返回S的元素个数,即栈的长度 return S.top-S.base; } Status GetTop(SqStack S,SElemType &e) { // 若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR if(S.top>S.base) { e=*(S.top-1); return OK; } else return ERROR; } void Push(SqStack &S,SElemType e) { // 插入元素e为新的栈顶元素(见图34) if(S.top-S.base>=S.stacksize) // 栈满,追加存储空间 { S.base=(SElemType *)realloc(S.base,(S.stacksize+STACK_INCREMENT)*sizeof(SElemType)); if(!S.base) exit(OVERFLOW); // 存储分配失败 S.top=S.base+S.stacksize; S.stacksize+=STACK_INCREMENT; } *(S.top)++=e; } Status Pop(SqStack &S,SElemType &e) { // 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK; // 否则返回ERROR(见图35) if(S.top==S.base) return ERROR; e=*--S.top; return OK; } void StackTraverse(SqStack S,void(*visit)(SElemType)) { // 从栈底到栈顶依次对栈中每个元素调用函数visit() while(S.top>S.base) visit(*S.base++); printf("\n"); } // main3-1.cpp 检验bo3-1.cpp的主程序 #include"c1.h" typedef int SElemType; // 定义栈元素类型,此句要在c3-1.h的前面 #include"c3-1.h" #include"bo3-1.cpp" void print(SElemType c) 图 34 调用 Push()示例 (a) 调用 Push()之前 (b) 调用 Push()之后 s10 s9 s8 s7 s6 s5 s4 s3 s2 s1 10 S S e s10 s9 s8 s7 s6 s5 s4 s3 s2 s1 12 图 35 调用 Pop()示例 (a) 调用 Pop()之前 (b) 调用 Pop()之后 s11 s10 s9 s8 s7 s6 s5 s4 s3 s2 s1 12 S S 返回参数: e=s11 s11 s10 s9 s8 s7 s6 s5 s4 s3 s2 s1 12 · 88· 《数据结构》算法实现及解析(第二版) { printf("%d ",c); } void main() { int j; SqStack s; SElemType e; InitStack(s); for(j=1;j<=12;j++) Push(s,j); printf("栈中元素依次为"); StackTraverse(s,print); Pop(s,e); printf("弹出的栈顶元素 e=%d\n",e); printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s)); GetTop(s,e); printf("栈顶元素 e=%d 栈的长度为%d\n",e,StackLength(s)); ClearStack(s); printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s)); DestroyStack(s); printf("销毁栈后,s.top=%u s.base=%u s.stacksize=%d\n",s.top,s.base, s.stacksize); } 程序运行结果: 栈中元素依次为1 2 3 4 5 6 7 8 9 10 11 12 弹出的栈顶元素 e=12 栈空否:0(1:空 0:否) 栈顶元素 e=11 栈的长度为11 清空栈后,栈空否:1(1:空 0:否) 销毁栈后,s.top=0 s.base=0 s.stacksize=0 栈也是线性表,是操作受限的线性表。栈的操作是线性表操作的子集。因此,也可以 将线性表的结构作为栈的结构。例如,可把不带头结点的线性单链表结构(见图 212)作 为链栈的结构,如图 36 所示。这样,线性单链表的一些基本操作(在 bo2-8.cpp 中)就 可以直接用于链栈的操作了。例如,初始化链表 和初始化链栈的操作是一样的,就不必定义 InitStack() 函 数 , 可 通 过 “ #define InitStack InitList ” 命 令 直 接 把 InitList() 函 数 当 作 InitStack()函数使用。同时把栈元素 SElemType 定义为线性表元素 ElemType。线性表的另一些 基本操作,如 ListInsert()也可以作为栈的基本操作 Push()来使用(取特例 i=1,即在第 1 个 元素之前插入)。由于栈的操作被限定仅在栈顶进行,显然,令表头为栈顶可简化栈的操 作。教科书对栈的定义是:限定仅在表尾进行插入或删除操作的线性表(教科书 44 页)。 图 36 具有 3 个栈元素(4,7,2)的链栈 S S 栈顶 栈底2 NULL 4 7 第 3 章 栈 和 队 列 · 89· 许多书上也都是这样定义的。对链栈,这样定义就不恰当。更准确的定义是:限定仅在表 的一端进行插入或删除操作的线性表。bo3-5.cpp 是链栈的基本操作,其中有部分函数是 通过 bo2-8.cpp 中的相关函数改名得来,还有部分函数是通过在特例下(如令形参 i=1,处 理首元结点)调用 bo2-8.cpp 中的相关函数得来。在文件 main3-5.cpp 中,调用栈的基本操 作函数仍可使用 StackLength()、Pop()等函数名。这就把线性表的一些操作函数的应用范 围扩展到了栈的领域,减少了编写栈的基本操作函数的工作量。栈是一种线性表,因此对 栈的操作是在一定条件下对线性表的操作。 // bo3-5.cpp 链栈(存储结构由c2-2.h定义)的基本操作(4个) // 部分基本操作是由bo2-8.cpp中的函数改名得来 // 另一部分基本操作是由调用bo2-8.cpp中的函数(取特例)得来 typedef SElemType ElemType; // 栈结点类型和链表结点类型一致 #include"c2-2.h" // 单链表存储结构 typedef LinkList LinkStack; // LinkStack是指向栈结点的指针类型 #define InitStack InitList // InitStack()与InitList()作用相同,下同 #define DestroyStack DestroyList #define ClearStack ClearList #define StackEmpty ListEmpty #define StackLength ListLength #include"bo2-8.cpp" // 无头结点单链表的基本操作 Status GetTop(LinkStack S,SElemType &e) { // 若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR return GetElem(S,1,e); } Status Push(LinkStack &S,SElemType e) { // 插入元素e为新的栈顶元素 return ListInsert(S,1,e); } Status Pop(LinkStack &S,SElemType &e) { // 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR return ListDelete(S,1,e); } void StackTraverse(LinkStack S,void(*visit)(SElemType)) { // 从栈底到栈顶依次对栈中每个元素调用函数visit() LinkStack temp,p=S; // p指向栈顶元素 InitStack(temp); // 初始化临时栈temp while(p) { Push(temp,p->data); // 由S栈顶到栈底,依次将栈元素入栈到temp栈 p=p->next; } ListTraverse(temp,visit); // 遍历temp线性表 } // main3-5.cpp 检验bo3-5.cpp的主程序 #include"c1.h" typedef int SElemType; // 定义栈元素的类型 #include"bo3-5.cpp" void print(SElemType c) · 90· 《数据结构》算法实现及解析(第二版) { printf("%d ",c); } void main() { int j; LinkStack s; SElemType e; InitStack(s); // 初始化栈s for(j=1;j<=5;j++) // 将2,4,6,8,10入栈 Push(s,2*j); printf("栈中的元素从栈底到栈顶依次为"); StackTraverse(s,print); Pop(s,e); printf("弹出的栈顶元素为%d\n",e); printf("栈空否: %d(1:空 0:否)\n",StackEmpty(s)); GetTop(s,e); printf("当前栈顶元素为%d,栈的长度为%d\n",e,StackLength(s)); ClearStack(s); printf("清空栈后,栈空否: %d(1:空 0:否),栈的长度为%d\n",StackEmpty(s),StackLength(s)); DestroyStack(s); } 程序运行结果: 栈中的元素从栈底到栈顶依次为 2 4 6 8 10 弹出的栈顶元素为10 栈空否: 0(1:空 0:否) 当前栈顶元素为8,栈的长度为4 清空栈后,栈空否: 1(1:空 0:否),栈的长度为0 由于栈只在表的一端进行插入和删除的操作,采用顺序存储结构(c3-1.h 定义),在入 栈和出栈时也不需要移动栈中元素。故顺序栈比链栈的效率要高一些。 3.2 栈的应用举例 3.2.1  // algo3-1.cpp 调用算法3.1的程序 #define N 8 // 定义待转换的进制N(二进制~九进制) typedef int SElemType; // 定义栈元素类型为整型 #include"c1.h" #include"c3-1.h" // 采用顺序栈 #include"bo3-1.cpp" // 利用顺序栈的基本操作 void conversion() // 算法3.1 { // 对于输入的任意一个非负十进制整数,打印输出与其等值的N进制数 SqStack s; unsigned n; // 非负整数 第 3 章 栈 和 队 列 · 91· SElemType e; InitStack(s); // 初始化栈 printf("将十进制整数n转换为%d进制数,请输入:n(>=0)=",N); scanf("%u",&n); // 输入非负十进制整数n while(n) // 当n不等于0 { Push(s,n%N); // 入栈n除以N的余数(N进制的低位) n=n/N; } while(!StackEmpty(s)) // 当栈不空 { Pop(s,e); // 弹出栈顶元素且赋值给e printf("%d",e); // 输出e } printf("\n"); } void main() { conversion(); } 程序运行结果(见图 37): 将十进制整数 n 转换为 8 进制数,请输入:n(>=0)=1348 2504 如果将 N 定义为 2,algo3-1.cpp 就是将十进制数转换为二进制数的程序。 程序运行结果: 将十进制整数 n 转换为 2 进制数,请输入:n(>=0)=13 1101 algo3-1.cpp 能不能用于十进制到十六进制的转换呢?存在一个问题,要将余数 10~15 转换为 A~F 输出。algo3-2.cpp 实现了十进制到十六进制的转换。 // algo3-2.cpp 改算法3.1,十进制→十六进制 typedef int SElemType; // 定义栈元素类型为整型 #include"c1.h" #include"c3-1.h" // 采用顺序栈 #include"bo3-1.cpp" // 利用顺序栈的基本操作 void conversion() { // 对于输入的任意一个非负十进制整数,打印输出与其等值的十六进制数 SqStack s; unsigned n; // 非负整数 图 37 栈 s 在元素最多时的状态 2 5 0 4 10 s · 92· 《数据结构》算法实现及解析(第二版) SElemType e; InitStack(s); // 初始化栈 printf("将十进制整数n转换为十六进制数,请输入:n(>=0)="); scanf("%u",&n); // 输入非负十进制整数n while(n) // 当n不等于0 { Push(s,n%16); // 入栈n除以16的余数(十六进制的低位) n=n/16; } while(!StackEmpty(s)) // 当栈不空 { Pop(s,e); // 弹出栈顶元素且赋值给e if(e<=9) printf("%d",e); else printf("%c",e+55); // 大于9的余数,输出相应的字符 } printf("\n"); } void main() { conversion(); } 程序运行结果(见图 38): 将十进制整数n转换为十六进制数,请输入:n(>=0)=164 A4 3.2.2  检验括号匹配的方法,就是对给定的字符串依次检验:若是左括号,入栈;若是右括 号,出栈一个左括号判断是否与之相匹配;是其它字符,不检验。检验到字符串尾,还要 检查栈是否空。只有栈空,整个字符串才匹配完。 // algo3-3.cpp 括号(()、[]和{})匹配的检验 typedef char SElemType; #include"c1.h" #include"c3-1.h" #include"bo3-1.cpp" void check() { // 对于输入的任意一个字符串,检验括号是否配对 SqStack s; SElemType ch[80],*p,e; InitStack(s); // 初始化栈成功 printf("请输入带括号(()、[]和{})的表达式\n"); 图 38 栈 s 在元素最多时的状态 10 4 10 s 第 3 章 栈 和 队 列 · 93· gets(ch); p=ch; // p指向字符串的首字符 while(*p) // 没到串尾 switch(*p) { case ′(′: case ′[′: case ′{′:Push(s,*p++); // 左括号入栈,且p++ break; case ′)′: case ′]′: case ′}′:if(!StackEmpty(s)) // 栈不空 { Pop(s,e); // 弹出栈顶元素 if(!(e==′(′&&*p==′)′||e==′[′&&*p==′]′||e==′{′&&*p==′}′)) { // 出现3种匹配情况之外的情况 printf("左右括号不配对\n"); exit(ERROR); } } else // 栈空 { printf("缺乏左括号\n"); exit(ERROR); } default: p++; // 其它字符不处理,指针向后移 } if(StackEmpty(s)) // 字符串结束时栈空 printf("括号匹配\n"); else printf("缺乏右括号\n"); } void main() { check(); } 程序运行结果: 请输入带括号(()、[]和{})的表达式 {[(5-2)*(7-3)+2]*4+8}*6 括号匹配 3.2.3 // algo3-4.cpp 行编辑程序,实现算法3.2 typedef char SElemType; · 94· 《数据结构》算法实现及解析(第二版) #include"c1.h" #include"c3-1.h" #include"bo3-1.cpp" FILE *fp; void copy(SElemType c) { // 将字符c送至fp所指的文件中 fputc(c,fp); } void LineEdit() { // 利用字符栈s,从终端接收一行并送至调用过程的数据区。算法3.2 SqStack s; char ch; InitStack(s); printf("请输入一个文本文件,^Z结束输入:\n"); ch=getchar(); while(ch!=EOF) { // 当全文没结束(EOF为^Z键,全文结束符) while(ch!=EOF&&ch!=′\n′) { // 当全文没结束且没到行末(不是换行符) switch(ch) { case ′#′:if(!StackEmpty(s)) Pop(s,ch); // 仅当栈非空时退栈,c可由ch替代 break; case ′@′:ClearStack(s); // 重置s为空栈 break; default :Push(s,ch); // 其它字符进栈 } ch=getchar(); // 从终端接收下一个字符 } StackTraverse(s,copy); // 将从栈底到栈顶的栈内字符传送至文件 fputc(′\n′,fp); // 向文件输入一个换行符 ClearStack(s); // 重置s为空栈 if(ch!=EOF) ch=getchar(); } DestroyStack(s); } void main() { fp=fopen("ed.txt","w"); // 在当前目录下建立ed.txt文件,用于写数据, if(fp) // 如已有同名文件则先删除原文件 { LineEdit(); fclose(fp); // 关闭fp所指的文件 } else printf("建立文件失败!\n"); } 第 3 章 栈 和 队 列 · 95· 程序运行结果(以教科书 49 页下的输入为例): 请输入一个文本文件,^Z结束输入: whli##ilr#e(s#*s) outcha@putchar(*s=#++); ^Z 文件 ed.txt 的内容: while(*s) putchar(*s++); 3.2.4 // func3-1.cpp、algo3-5.cpp、algo3-9.cpp和algo3-11.cpp要调用的函数、结构和全局变量 struct PosType // 迷宫坐标位置类型(见图39) { int x; // 行值 int y; // 列值 }; #define MAXLENGTH 25 // 设迷宫的最大行列为25 typedef int MazeType[MAXLENGTH][MAXLENGTH]; // 迷宫数组类型[行][列] // 全局变量 MazeType m; // 迷宫数组 int x,y; // 迷宫的行数,列数 PosType begin,end; // 迷宫的入口坐标,出口坐标 void Print() { // 输出迷宫的解(m数组) int i,j; for(i=0;i′; // t1>t2 break; case ′*′: case ′/′:if(t1==′*′||t1==′/′||t1==′)′) f=′>′; // t1>t2 else f=′<′; // t1′; // t1>t2 } break; case′\n′:switch(t1) { case′\n′:f=′=′; // t1=t2 break; case′(′:printf("缺乏右括号\n"); exit(ERROR); default :f=′>′; // t1>t2 } } return f; } Status In(SElemType c) { // 判断c是否为7种运算符之一 switch(c) { case′+′: case′-′: case′*′: case′/′: case′(′: case′)′: case′\n′:return TRUE; default :return FALSE; } } SElemType Operate(SElemType a,SElemType theta,SElemType b) { // 做四则运算a theta b,返回运算结果 switch(theta) { case′+′:return a+b; case′-′:return a-b; case′*′:return a*b; } return a/b; } // algo3-6.cpp 表达式求值(输入的值在0~9之间,中间结果和输出的值在-128~127之间),算法3.4 typedef char SElemType; // 栈元素为字符型 #include"c1.h" #include"c3-1.h" #include"bo3-1.cpp" #include"func3-2.cpp" · 102· 《数据结构》算法实现及解析(第二版) SElemType EvaluateExpression() // 算法3.4,有改动 { // 算术表达式求值的算符优先算法。设OPTR和OPND分别为运算符栈和运算数栈 SqStack OPTR,OPND; SElemType a,b,c,x; InitStack(OPTR); // 初始化运算符栈OPTR和运算数栈OPND InitStack(OPND); Push(OPTR,′\n′); // 将换行符压入运算符栈OPTR的栈底(改) c=getchar(); // 由键盘读入1个字符到c GetTop(OPTR,x); // 将运算符栈OPTR的栈顶元素赋给x while(c!=′\n′||x!=′\n′) // c和x不都是换行符 { if(In(c)) // c是7种运算符之一 switch(Precede(x,c)) // 判断x和c的优先权 { case′<′:Push(OPTR,c); // 栈顶元素x的优先权低,入栈c c=getchar(); // 由键盘读入下一个字符到c break; case′=′:Pop(OPTR,x); // x=′(′且c=′)′情况,弹出′(′给x(后又扔掉) c=getchar(); // 由键盘读入下一个字符到c(扔掉′)′) break; case′>′:Pop(OPTR,x); // 栈顶元素x的优先权高,弹出运算符栈OPTR的栈顶元素给x(改) Pop(OPND,b); // 依次弹出运算数栈OPND的栈顶元素给b,a Pop(OPND,a); Push(OPND,Operate(a,x,b)); // 做运算a x b,并将运算结果入运算数栈 } else if(c>=′0′&&c<=′9′) // c是操作数 { Push(OPND,c-48); // 将该操作数的值(不是ASCII码)压入运算数栈OPND c=getchar(); // 由键盘读入下一个字符到c } else // c是非法字符 { printf("出现非法字符\n"); exit(ERROR); } GetTop(OPTR,x); // 将运算符栈OPTR的栈顶元素赋给x } Pop(OPND,x); // 弹出运算数栈OPND的栈顶元素(运算结果)给x(改此处) if(!StackEmpty(OPND)) // 运算数栈OPND不空(运算符栈OPTR仅剩′\n′) { printf("表达式不正确\n"); exit(ERROR); } return x; } void main() { printf("请输入算术表达式(输入的值要在0~9之间、中间运算值和输出结果在-128~127之间)\n"); printf("%d\n",EvaluateExpression()); // 返回值(8位二进制,1个字节)按整型格式输出 } 第 3 章 栈 和 队 列 · 103· 程序运行结果(以教科书例 31 为例): 请输入算术表达式(输入的值要在0~9之间、中间运算值和输出结果在-128~127之间) 3*(7-2) 15 algo3-7.cpp 对 algo3-6.cpp 做了些改进,把连续输入的几个数值型字符作为一个整数 处理。使数值范围扩大为整型,更具有实用性。 // algo3-7.cpp 表达式求值(范围为int类型,输入负数要用(0-正数)表示) typedef int SElemType; // 栈元素类型为整型,改algo3-6.cpp #include"c1.h" #include"c3-1.h" // 顺序栈的存储结构 #include"bo3-1.cpp" // 顺序栈的基本操作 #include"func3-2.cpp" SElemType EvaluateExpression() { // 算术表达式求值的算符优先算法。设OPTR和OPND分别为运算符栈和运算数栈 SqStack OPTR,OPND; SElemType a,b,d,x; // 改algo3-6.cpp char c; // 存放由键盘接收的字符,改algo3-6.cpp char z[11]; // 存放整数字符串,改algo3-6.cpp int i; // 改algo3-6.cpp InitStack(OPTR); // 初始化运算符栈OPTR和运算数栈OPND InitStack(OPND); Push(OPTR,′\n′); // 将换行符压入运算符栈OPTR的栈底(改) c=getchar(); // 由键盘读入1个字符到c GetTop(OPTR,x); // 将运算符栈OPTR的栈顶元素赋给x while(c!=′\n′||x!=′\n′) // c和x不都是换行符 { if(In(c)) // c是7种运算符之一 switch(Precede(x,c)) // 判断x和c的优先权 { case′<′:Push(OPTR,c); // 栈顶元素x的优先权低,入栈c c=getchar(); // 由键盘读入下一个字符到c break; case′=′:Pop(OPTR,x); // x=′(′且c=′)′情况,弹出′(′给x(后又扔掉) c=getchar(); // 由键盘读入下一个字符到c(扔掉′)′) break; case′>′ :Pop(OPTR,x); // 栈顶元素x的优先权高,弹出运算符栈OPTR的栈顶元素给x(改) Pop(OPND,b); // 依次弹出运算数栈OPND的栈顶元素给b,a Pop(OPND,a); Push(OPND,Operate(a,x,b)); // 做运算a x b,并将运算结果入运算数栈 } else if(c>=′0′&&c<=′9′) // c是操作数,此语句改algo3-6.cpp { i=0; while(c>=′0′&&c<=′9′) // 是连续数字 · 104· 《数据结构》算法实现及解析(第二版) { z[i++]=c; c=getchar(); } z[i]=0; // 字符串结束符 d=atoi(z); // 将z中保存的数值型字符串转为整型存于d Push(OPND,d); // 将d压入运算数栈OPND } else // c是非法字符,以下同algo3-6.cpp { printf("出现非法字符\n"); exit(ERROR); } GetTop(OPTR,x); // 将运算符栈OPTR的栈顶元素赋给x } Pop(OPND,x); // 弹出运算数栈OPND的栈顶元素(运算结果)给x(改此处) if(!StackEmpty(OPND)) // 运算数栈OPND不空(运算符栈OPTR仅剩’\n’) { printf("表达式不正确\n"); exit(ERROR); } return x; } void main() { printf("请输入算术表达式,负数要用(0-正数)表示\n"); printf("%d\n",EvaluateExpression()); } 程序运行结果: 请输入算术表达式,负数要用(0-正数)表示 (0-12)*((5-3)*3+2)/(2+2) -24 3.3 栈与递归的实现 函数中有直接或间接地调用自身函数的语句,这样的函数称为递归函数。递归函数用 得好,可简化编程工作。但函数自己调用自己,有可能造成死循环。为了避免死循环,要 做到两点: (1) 降阶。递归函数虽然调用自身,但并不是简单地重复。它的实参值每次是不一样 的。一般逐渐减小,称为降阶。如教科书式(33)的 Ackerman 函数,当 m≠0 时,求 Ack(m,n)可由 Ack(m-1,⋯ )得到,Ack()函数的第 1 个参数减小了。 (2) 有出口。即在某种条件下,不再进行递归调用。仍以教科书式(33)的 Ackerman 第 3 章 栈 和 队 列 · 105· 函数为例,当 m=0 时,Ack(0,n)=n+1,终止了递归调用。所以,递归函数总有条件语 句。m=0 的条件是由逐渐降阶形成的。如取 Ack(m,n)函数的实参 m=-1,即使通过降阶也 不会出现 m=0 的情况,这就造成了死循环。 编译软件在遇到函数调用时,会将调用函数的实参、返回地址等信息存入软件自身所 带的栈中,再去运行被调函数。从被调函数返回时,再将调用函数的信息出栈,接着运行 调用函数。编译软件开辟的栈空间是有限的,当递归调用时,嵌套的层次往往很多,就有 可能使栈发生溢出现象,从而出现不可预料的后果。运行 algo3-8.cpp 时,m、n 的取值就 不可过大。 // algo3-8.cpp 用递归调用求Ackerman(m,n)的值 #include int ack(int m,int n) { int z; if(m==0) z=n+1; // 出口 else if(n==0) z=ack(m-1,1); // 对形参m降阶 else z=ack(m-1,ack(m,n-1)); // 对形参m、n降阶 return z; } void main() { int m,n; printf("Please input m,n:"); scanf("%d,%d",&m,&n); printf("Ack(%d,%d)=%d\n",m,n,ack(m,n)); } 程序运行结果(m、n 不可取值过大): Please input m,n:3,9 Ack(3,9)=4093 // algo3-9.cpp 用递归函数求解迷宫问题(求出所有解) #include"c1.h" // 根据《PASCAL程序设计》(郑启华编著)中的程序改编 #include"func3-1.cpp" // 定义墙元素值为0,可通过路径为-1,通过路径为足迹 void Try(PosType cur,int curstep) { // 由当前位置cur、当前步骤curstep试探下一点 int i; PosType next; // 下一个位置 PosType direc[4]={{0,1},{1,0},{0,-1},{-1,0}}; // {行增量,列增量},移动方向,依次为东南西北 for(i=0;i<=3;i++) // 依次试探东南西北四个方向 { next.x=cur.x+direc[i].x; // 根据移动方向,给下一位置赋值 · 106· 《数据结构》算法实现及解析(第二版) next.y=cur.y+direc[i].y; if(m[next.x][next.y]==-1) // 下一个位置是通路 { m[next.x][next.y]=++curstep; // 将下一个位置设为足迹 if(next.x!=end.x||next.y!=end.y) // 没到终点 Try(next,curstep); // 由下一个位置继续试探(降阶递归调用,离终点更近) else // 到终点 { Print(); // 输出结果(出口,不再递归调用) printf("\n"); } m[next.x][next.y]=-1; // 恢复为通路,以便在另一个方向试探另一条路 curstep--; // 足迹也减1 } } } void main() { Init(-1); // 初始化迷宫,通道值为-1 printf("此迷宫从入口到出口的路径如下:\n"); m[begin.x][begin.y]=1; // 入口的足迹为1 Try(begin,1); // 由第1步入口试探起 } 程序运行结果: 请输入迷宫的行数,列数(包括外墙):5,5 请输入迷宫内墙单元数:1 请依次输入迷宫内墙每个单元的行数,列数: 2,2 迷宫结构如下: 0 0 0 0 0 0 -1 -1 -1 0 0 -1 0 -1 0 0 -1 -1 -1 0 0 0 0 0 0 请输入入口的行数,列数:1,1 请输入出口的行数,列数:3,3 此迷宫从入口到出口的路径如下: 0 0 0 0 0 0 1 2 3 0 0 -1 0 4 0 0 -1 -1 5 0 0 0 0 0 0 0 0 0 0 0 0 1 -1 -1 0 0 2 0 -1 0 0 3 4 5 0 0 0 0 0 0 第 3 章 栈 和 队 列 · 107· 此迷宫从入口到出口有 2 条路径。由入口(1 行 1 列足迹 1)处向 4 个方向试探,只有 2 个方向(东、南)是通路(-1)。首先朝东继续试探。足迹 2~4 都只有 1 个方向是通路,足 迹 5 到达出口。输出路径且逐一恢复足迹为-1(通路)。恢复后的情况是除入口为 1 外,迷 宫其它各点与初态相同,以便第 2 条路径(由入口向南)继续试探。 与 algo3-5.cpp 相比,algo3-9.cpp 程序简短,且没使用栈(递归函数使用了编译软件 内设的栈)。 // algo3-10.cpp Hanoi塔问题,调用算法3.5的程序 #include int c=0; // 全局变量,搬动次数 void move(char x,int n,char z) { // 第n个圆盘从塔座x搬到塔座z printf("第%i步: 将%i号盘从%c移到%c\n",++c,n,x,z); } void hanoi(int n,char x,char y,char z) // 算法3.5 { // 将塔座x上按直径由小到大且自上而下编号为1至n的n个圆盘 // 按规则搬到塔座z上。y可用作辅助塔座 if(n==1) // (出口) move(x,1,z); // 将编号为1的圆盘从x移到z else { hanoi(n-1,x,z,y); // 将x上编号为1至n-1的圆盘移到y,z作辅助塔(降阶递归调用) move(x,n,z); // 将编号为n的圆盘从x移到z hanoi(n-1,y,x,z); // 将y上编号为1至n-1的圆盘移到z,x作辅助塔(降阶递归调用) } } void main() { int n; printf("3个塔座为a、b、c,圆盘最初在a座,借助b座移到c座。请输入圆盘数:"); scanf("%d",&n); hanoi(n,′a′,′b′,′c′); } 程序运行结果: 3个塔座为a、b、c,圆盘最初在a座,借助b座移到c座。请输入圆盘数:3 第1步: 将1号盘从a移到c 第2步: 将2号盘从a移到b 第3步: 将1号盘从c移到b 第4步: 将3号盘从a移到c 第5步: 将1号盘从b移到a 第6步: 将2号盘从b移到c 第7步: 将1号盘从a移到c · 108· 《数据结构》算法实现及解析(第二版) 3.4 队 列 3.4.1  3.4.2 %&’((&’%#  // c3-2.h 单链队列--队列的链式存储结构 typedef struct QNode // (见图312) { QElemType data; QNode *next; }*QueuePtr; struct LinkQueue // (见图313) { QueuePtr front,rear; // 队头、队尾指针 }; 和栈一样,队列也是操作受限的线性表,只允许在队尾插入元素,在队头删除元素。 对于链队列结构,为了便于插入元素,设立了队尾指针。这样,插入元素的操作与队列长 度无关。图 314 是具有两个元素的链队列示例。 // bo3-2.cpp 链队列(存储结构由c3-2.h定义)的基本操作(9个) void InitQueue(LinkQueue &Q) { // 构造一个空队列Q(见图315) if(!(Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode)))) exit(OVERFLOW); Q.front->next=NULL; } void DestroyQueue(LinkQueue &Q) { // 销毁队列Q(无论空否均可)(见图316) while(Q.front) { Q.rear=Q.front->next; free(Q.front); Q.front=Q.rear; } 图 312 单链队列的结点类型 data next QNodeQueuePtr QNode 图 313 LinkQueue 类型 LinkQueue front rear QNode QNode 图 316 销毁后的队列 Q Q NULLNULL 图 315 空队列 Q NULL Q 图 314 具有两个元素(4,7)的链队列 LinkQueue 7 NULL4 第 3 章 栈 和 队 列 · 109· } void ClearQueue(LinkQueue &Q) { // 将Q清为空队列 QueuePtr p,q; Q.rear=Q.front; p=Q.front->next; Q.front->next=NULL; while(p) { q=p; p=p->next; free(q); } } Status QueueEmpty(LinkQueue Q) { // 若Q为空队列,则返回TRUE;否则返回FALSE if(Q.front->next==NULL) return TRUE; else return FALSE; } int QueueLength(LinkQueue Q) { // 求队列的长度 int i=0; QueuePtr p; p=Q.front; while(Q.rear!=p) { i++; p=p->next; } return i; } Status GetHead(LinkQueue Q,QElemType &e) { // 若队列不空,则用e返回Q的队头元素,并返回OK;否则返回ERROR QueuePtr p; if(Q.front==Q.rear) return ERROR; p=Q.front->next; e=p->data; return OK; } void EnQueue(LinkQueue &Q,QElemType e) { // 插入元素e为Q的新的队尾元素(见图317) QueuePtr p; if(!(p=(QueuePtr)malloc(sizeof(QNode)))) // 存储分配失败 exit(OVERFLOW); p->data=e; p->next=NULL; Q.rear->next=p; 图 317 在队列 Q 的尾部插入元素 e (a) p 生成队尾结点 p e NULL⋯ Q NULL (b) 将队尾结点插入队列 p e NULL⋯ Q · 110· 《数据结构》算法实现及解析(第二版) Q.rear=p; } Status DeQueue(LinkQueue &Q,QElemType &e) { // 若队列不空,删除Q的队头元素,用e返回其值, // 并返回OK;否则返回ERROR(见图318) QueuePtr p; if(Q.front==Q.rear) return ERROR; p=Q.front->next; e=p->data; Q.front->next=p->next; if(Q.rear==p) Q.rear=Q.front; free(p); return OK; } void QueueTraverse(LinkQueue Q,void(*vi)(QElemType)) { // 从队头到队尾依次对队列Q中每个元素调用函数vi() QueuePtr p; p=Q.front->next; while(p) { vi(p->data); p=p->next; } printf("\n"); } // main3-2.cpp 检验bo3-2.cpp的主程序 #include"c1.h" typedef int QElemType; #include"c3-2.h" #include"bo3-2.cpp" void print(QElemType i) { printf("%d ",i); } void main() { int i; QElemType d; LinkQueue q; InitQueue(q); printf("成功地构造了一个空队列!\n"); printf("是否空队列?%d(1:空 0:否) ",QueueEmpty(q)); printf("队列的长度为%d\n",QueueLength(q)); EnQueue(q,-5); EnQueue(q,5); EnQueue(q,10); printf("插入3个元素(-5,5,10)后,队列的长度为%d\n",QueueLength(q)); printf("是否空队列?%d(1:空 0:否) ",QueueEmpty(q)); 图 318 删除队列 Q 的第 1 个元素 (a) p 指向队列 Q 的第 1 个结点 p ⋯ Q NULL (b) 队列 Q 头结点的指针域指向第 2 个结点 p ⋯ Q NULL 第 3 章 栈 和 队 列 · 111· printf("队列的元素依次为"); QueueTraverse(q,print); i=GetHead(q,d); if(i==OK) printf("队头元素是:%d\n",d); DeQueue(q,d); printf("删除了队头元素%d\n",d); i=GetHead(q,d); if(i==OK) printf("新的队头元素是:%d\n",d); ClearQueue(q); printf("清空队列后,q.front=%u q.rear=%u q.front->next=%u\n",q.front,q.rear,q.front->next); DestroyQueue(q); printf("销毁队列后,q.front=%u q.rear=%u\n",q.front, q.rear); } 程序运行结果: 成功地构造了一个空队列! 是否空队列?1(1:空 0:否) 队列的长度为0 插入3个元素(-5,5,10)后,队列的长度为3 是否空队列?0(1:空 0:否) 队列的元素依次为-5 5 10 队头元素是:-5 删除了队头元素-5 新的队头元素是:5 清空队列后,q.front=2216 q.rear=2216 q.front->next=0 销毁队列后,q.front=0 q.rear=0 由 c3-2.h 和 c2-2.h 对比可见,单链队列和单链表的结构有相同之处。单链队列也是 带有头结点的单链表,它的队头指针相当于单链表的头指针。因为队列操作是线性表操作 的子集,所以 bo3-2.cpp 中的基本操作也可以用单链表的基本操作来代替。这样既可以充 分利用现有资源,减小编程工作量,又可以更清楚地看出队列和线性表的内在联系和共 性。bo3-6.cpp 是利用单链表的基本操作实现单链队列基本操作的程序。 // bo3-6.cpp 用单链表的基本操作实现链队列(存储结构由c3-2.h定义)的基本操作(9个) typedef QElemType ElemType; #define LinkList QueuePtr // 定义单链表的类型与相应的链队列的类型相同 #define LNode QNode #include"bo2-2.cpp" // 单链表的基本操作 void InitQueue(LinkQueue &Q) { // 构造一个空队列Q InitList(Q.front); // 调用单链表的基本操作 Q.rear=Q.front; } void DestroyQueue(LinkQueue &Q) { // 销毁队列Q(无论空否均可) DestroyList(Q.front); · 112· 《数据结构》算法实现及解析(第二版) Q.rear=Q.front; } void ClearQueue(LinkQueue &Q) { // 将Q清为空队列 ClearList(Q.front); Q.rear=Q.front; } Status QueueEmpty(LinkQueue Q) { // 若Q为空队列,则返回TRUE;否则返回FALSE return ListEmpty(Q.front); } int QueueLength(LinkQueue Q) { // 求队列的长度 return ListLength(Q.front); } Status GetHead(LinkQueue Q,QElemType &e) { // 若队列不空,则用e返回Q的队头元素,并返回OK;否则返回ERROR return GetElem(Q.front,1,e); } void EnQueue(LinkQueue &Q,QElemType e) { // 插入元素e为Q的新的队尾元素 QueuePtr p; if(!(p=(QueuePtr)malloc(sizeof(QNode)))) // 存储分配失败 exit(OVERFLOW); p->data=e; p->next=NULL; Q.rear->next=p; Q.rear=p; } Status DeQueue(LinkQueue &Q,QElemType &e) { // 若队列不空,删除Q的队头元素,用e返回其值,并返回OK;否则返回ERROR if(Q.front->next==Q.rear) // 队列仅有1个元素(删除的也是队尾元素) Q.rear=Q.front; // 令队尾指针指向头结点 return ListDelete(Q.front,1,e); } void QueueTraverse(LinkQueue Q,void(*vi)(QElemType)) { // 从队头到队尾依次对队列Q中每个元素调用函数vi() ListTraverse(Q.front,vi); } 用单链表的操作代替单链队列的操作又和代替链栈的操作情况不同。链栈和单链表的 结构完全相同,许多栈的基本操作仅是单链表基本操作改了个名。而单链队列和单链表的 结构并不完全相同,只能是在单链队列的基本操作中调用单链表的基本操作。 main3-6.cpp 是检验 bo3-6.cpp 的程序。只有 1 句和 main3-2.cpp 不同。程序运行结 果也和 main3-2.cpp 的完全相同。从 main3-6.cpp 中完全看不出 bo3-6.cpp 和 bo3-2.cpp 的区别。 // main3-6.cpp 检验bo3-6.cpp的主程序 #include"c1.h" 第 3 章 栈 和 队 列 · 113· typedef int QElemType; #include"c3-2.h" #include"bo3-6.cpp" // 仅此句与main3-2.cpp不同 void print(QElemType i) { printf("%d ",i); } void main() { int i; QElemType d; LinkQueue q; InitQueue(q); printf("成功地构造了一个空队列!\n"); printf("是否空队列?%d(1:空 0:否) ",QueueEmpty(q)); printf("队列的长度为%d\n",QueueLength(q)); EnQueue(q,-5); EnQueue(q,5); EnQueue(q,10); printf("插入3个元素(-5,5,10)后,队列的长度为%d\n",QueueLength(q)); printf("是否空队列?%d(1:空 0:否) ",QueueEmpty(q)); printf("队列的元素依次为"); QueueTraverse(q,print); i=GetHead(q,d); if(i==OK) printf("队头元素是:%d\n",d); DeQueue(q,d); printf("删除了队头元素%d\n",d); i=GetHead(q,d); if(i==OK) printf("新的队头元素是:%d\n",d); ClearQueue(q); printf("清空队列后,q.front=%u q.rear=%u q.front->next=%u\n",q.front,q.rear,q.front->next); DestroyQueue(q); printf("销毁队列后,q.front=%u q.rear=%u\n",q.front, q.rear); } 程序运行结果: 成功地构造了一个空队列! 是否空队列?1(1:空 0:否) 队列的长度为0 插入3个元素(-5,5,10)后,队列的长度为3 是否空队列?0(1:空 0:否) 队列的元素依次为-5 5 10 队头元素是:-5 删除了队头元素-5 新的队头元素是:5 清空队列后,q.front=2216 q.rear=2216 q.front->next=0 销毁队列后,q.front=0 q.rear=0 · 114· 《数据结构》算法实现及解析(第二版) 3.4.3 )*&’((&’+  队列的顺序表示为什么要采用循环方式?首先分析两种非循环顺序队列的表示和实现 以及它们存在的问题。 // c3-5.h 队列的顺序存储结构(非循环队列,队列头元素在[0]单元) #define QUEUE_INIT_SIZE 10 // 队列存储空间的初始分配量 #define QUEUE_INCREMENT 2 // 队列存储空间的分配增量 struct SqQueue1(见图319) { QElemType *base; // 初始化的动态分配存储空间 int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置 int queuesize; // 当前分配的存储容量(以sizeof(QElemType)为单位) }; 图 320 是根据 c3-5.h 定义的有两个元素的 非循环顺序队列。 // bo3-7.cpp 顺序非循环队列(存储结构由c3-5.h定义)的基本操作(9个) void InitQueue(SqQueue1 &Q) { // 构造一个空队列Q(见图321) if(!(Q.base=(QElemType*)malloc (QUEUE_INIT_SIZE*sizeof(QElemType)))) exit(ERROR); // 存储分配失败 Q.rear=0; // 空队列,尾指针为0 Q.queuesize=QUEUE_INIT_SIZE; // 初始存储容量 } void DestroyQueue(SqQueue1 &Q) { // 销毁队列Q,Q不再存在(见图322) free(Q.base); // 释放存储空间 Q.base=NULL; Q.rear=Q.queuesize=0; } void ClearQueue(SqQueue1 &Q) { // 将Q清为空队列 Q.rear=0; } Status QueueEmpty(SqQueue1 Q) { // 若队列Q为空队列,则返回TRUE;否则返回FALSE if(Q.rear==0) return TRUE; else return FALSE; } int QueueLength(SqQueue1 Q) { // 返回Q的元素个数,即队列的长度 return Q.rear; } 图 319 队列的顺序存储结构 SqQueue1 base rear queuesize QElemType 图 321 构造一个空的顺序队列 Q Q [0] [1] [8] [9] 0 10 图 320 有两个元素(Q1,Q2)的顺序队列 Q Q [0] [1] [8] [9] Q1 Q22 10 图 322 销毁顺序队列 Q NULL 0 0 Q     第 3 章 栈 和 队 列 · 115· Status GetHead(SqQueue1 Q,QElemType &e) { // 若队列不空,则用e返回Q的队头元素,并返回OK;否则返回ERROR if(Q.rear) { e=*Q.base; return OK; } else return ERROR; } void EnQueue(SqQueue1 &Q,QElemType e) { // 插入元素e为Q的新的队尾元素(见图323) if(Q.rear==Q.queuesize) // 当前存储空间已满 { // 增加分配 Q.base=(QElemType*)realloc(Q.base,(Q.queuesize+QUEUE_INCREMENT)*sizeof(QElemType)); if(!Q.base) // 分配失败 exit(ERROR); Q.queuesize+=QUEUE_INCREMENT; // 增加存储容量 } Q.base[Q.rear++]=e; // 入队新元素,队尾指针+1 } Status DeQueue(SqQueue1 &Q,QElemType &e) { // 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK;否则返回ERROR(见图324) int i; if(Q.rear) // 队列不空 { e=*Q.base; for(i=1;i0) printf("现在由队头删除%d个元素: \n",l-2); while(QueueLength(Q)>2) { DeQueue(Q,d); printf("删除的元素值为%d\n",d); } j=GetHead(Q,d); if(j) · 124· 《数据结构》算法实现及解析(第二版) printf("现在队头元素为%d\n",d); ClearQueue(Q); printf("清空队列后, 队列空否?%u(1:空 0:否)\n",QueueEmpty(Q)); DestroyQueue(Q); } 程序运行结果: 初始化队列后,队列空否?1(1:空 0:否) 请输入整型队列元素(不超过4个),-1为提前结束符: 1 2 3 -1 队列长度为3 现在队列空否?0(1:空 0:否) 连续5次由队头删除元素,队尾插入元素: 删除的元素是1,请输入待插入的元素: 4 删除的元素是2,请输入待插入的元素: 5 删除的元素是3,请输入待插入的元素: 6 删除的元素是4,请输入待插入的元素: 7 删除的元素是5,请输入待插入的元素: 8 现在队列中的元素为 6 7 8 共向队尾插入了8个元素 现在由队头删除1个元素: 删除的元素值为6 现在队头元素为7 清空队列后, 队列空否?1(1:空 0:否) c3-3.h 所采用的循环顺序队列存储结构,当队列长度大于 MAX_QSIZE-1 时,无法动 态地增加存储空间,原因是 MAX_QSIZE 是固定于 c3-3.h 中的常量。为了使循环队列也 能动态地增加存储空间,不固定队列长度,把队列长度也作为结构体的一个成员。前述 c3-4.h 就可以作为这种循环顺序队列的存储结构,bo3-8.cpp 是基于 c3-4.h 结构的循环顺 序队列的基本操作。 // bo3-8.cpp 循环队列(存储结构由c3-4.h定义)的基本操作(4个) int QueueLength(SqQueue2 Q) { // 返回Q的元素个数,即队列的长度 return(Q.rear-Q.front+Q.queuesize)%Q.queuesize; } void EnQueue(SqQueue2 &Q,QElemType e) { // 插入元素e为Q的新的队尾元素(见图336) int i; if((Q.rear+1)%Q.queuesize==Q.front) { // 队列满,增加存储单元 Q.base=(QElemType *)realloc(Q.base,(Q.queuesize+QUEUE_INCREMENT)*sizeof(QElemType)); if(!Q.base) // 增加单元失败 exit(ERROR); if(Q.front>Q.rear) // 形成循环 { 第 3 章 栈 和 队 列 · 125· for(i=Q.queuesize-1;i>=Q.front;i--) Q.base[i+QUEUE_INCREMENT]=Q.base[i]; // 移动高端元素到新的高端 Q.front+=QUEUE_INCREMENT; // 移动队头指针 } Q.queuesize+=QUEUE_INCREMENT; // 增加队列长度 } Q.base[Q.rear]=e; // 将e插入队尾 Q.rear=++Q.rear%Q.queuesize; // 移动队尾指针 } Status DeQueue(SqQueue2 &Q,QElemType &e) { // 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK;否则返回ERROR(见图337) if(Q.front==Q.rear) // 队列空 return ERROR; e=Q.base[Q.front]; // 用e返回队头元素 Q.front=++Q.front%Q.queuesize; // 移动队头指针 return OK; } void QueueTraverse(SqQueue2 Q,void(*vi)(QElemType)) { // 从队头到队尾依次对队列Q中每个元素调用函数vi() int i=Q.front; // i指向队头 while(i!=Q.rear) // 没到队尾 { vi(Q.base[i]); // 调用函数vi() i=++i%Q.queuesize; // 向后移动i指针 } 图 337 调用 DeQueue()示例 (a) Q 调用函数之前的状态 (b) Q 调用函数之后的状态 Q2 Q3 Q4 Q5 Q1 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] Q 9 4 10 返回参数: e=Q1 Q2 Q3 Q4 Q5 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] Q 0 4 10 图 336 调用 EnQueue()示例 (a) Q 调用函数之前的状态 (b) Q 增加存储容量 (c) 移动高端元素到新的高端 (d) Q 调用函数之后的状态 Q6 Q7 Q8 Q9 Q1 Q2 Q3 Q4 Q5 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] Q 5 4 10 Q6 Q7 Q8 Q9 e Q1 Q2 Q3 Q4 Q5 Q 7 5 12 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] Q6 Q7 Q8 Q9 Q1 Q2 Q3 Q4 Q5 Q 7 4 12 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] Q6 Q7 Q8 Q9 Q1 Q2 Q3 Q4 Q5 Q 5 4 10 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] · 126· 《数据结构》算法实现及解析(第二版) printf("\n"); } // main3-8.cpp 循环且可增加存储空间的顺序队列,检验bo3-8.cpp的主程序 #include"c1.h" typedef int QElemType; #include"c3-4.h" #include"bo3-4.cpp" // 基本操作(1),与非循环同 #include"bo3-8.cpp" // 基本操作(2),与非循环不同 void print(QElemType i) { printf("%d ",i); } void main() { Status j; int i,n=11; QElemType d; SqQueue2 Q; InitQueue(Q); printf("初始化队列后,队列空否?%u(1:空 0:否)\n",QueueEmpty(Q)); printf("队列长度为%d\n",QueueLength(Q)); printf("请输入%d个整型队列元素:\n",n); for(i=0;i=0) // 没到入口 { Push(s,q.base[i]); // 将队列中的路径入栈(栈底为出口,栈顶为入口) i=q.base[i].pre; // i为前一元素在队列中的位置 } i=0; // i为走出迷宫的足迹 while(!StackEmpty(s)) { Pop(s,qf); // 依照由入口到出口的顺序弹出路径 i++; m[qf.seat.x][qf.seat.y]=i; // 标记路径为足迹(标记前的值为-1) } printf("走出迷宫的一个方案:\n"); Print(); // 输出m数组 第 3 章 栈 和 队 列 · 129· } } void main() { Init(1); // 初始化迷宫,通道值为1 Path(); // 求一条迷宫路径 } 程序运行结果(见图 339): 请输入迷宫的行数,列数(包括外墙):5,5 请输入迷宫内墙单元数:2 请依次输入迷宫内墙每个单元的行数,列数: 2,2 2,3 迷宫结构如下: 0 0 0 0 0 0 1 1 1 0 0 1 0 0 0 0 1 1 1 0 0 0 0 0 0 请输入入口的行数,列数:1,1 请输入出口的行数,列数:3,3 走出迷宫的一个方案: 0 0 0 0 0 0 1 -1 -1 0 0 2 0 0 0 0 -1 3 4 0 0 0 0 0 0 algo3-11.cpp 也是一种求迷宫的算法。该算法先将入口坐标及-1 入队,-1 表示入口坐 标是第 1 点(无前驱)。出队入口点(1,1,-1),找 m[1][1]周边值为 1 的点(m[1][2]和 m[2][1]),入队这 2 点,且令它们的 pre 成员值为 0,因为它们的前一步是 m[1][1],而 m[1][1] 在 队 列 中 的 序 号 为 [0] ; 再 出 队 (1,2,0) , 找 m[1][2] 周 边 值 为 1 的 点 (m[1][3]),入队该点,且令它的 pre 成员值为 1;⋯⋯;直到入队 (3,3,4)。而 m[3][3] 恰是出口,说明找到了一条由入口到出口的路径。为了输出这条路径,将(3,3,4)入栈 s, 由(3,3,4)得知,出口的前一步在队列中序号为[4]处。再入栈队列中序号为[4]的元素 (3,2,2)。依此类推,再入栈队列中序号为[2]的元素(2,1,0)、序号为[0]的元素(1,1,-1)。 出栈 s 时,依次将 m[1][1]、m[2][1]、m[3][2]和 m[3][3]赋值 1、2、3 和 4(足迹)。 在到达终点时,队列的状态如图 339(a)所示。这时队列中只有 2 个元素,队头元素 的序号为[5],队尾元素的序号为[6]。序号从[0]到[4]的那些元素虽然出队,但它们在队 列中的存储状态并没有遭到破坏,仍完好地保存。所以才可以调出队列的“历史记录”。 这也算是这种存储结构的一个优点吧。 图 339 程序运行期间栈和队列的状态 (a) 到达终点时队列 q 的状态 (b) 栈 s 在元素最多时的状态 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] q 5 7 10 1 1 1 1 2 0 2 1 0 1 3 1 3 2 2 3 1 2 3 3 4 10 s 1 1 -1 2 1 0 3 2 2 3 3 4 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] · 130· 《数据结构》算法实现及解析(第二版) 在 algo3-11.cpp 的第 4 行,我们定义 D 为 8,它可以斜行(从足迹 2 到足迹 3)。如果 定义 D 为 4,输入相同,则需 4 步走到出口。运行结果如下(省略相同部分): 走出迷宫的一个方案: 0 0 0 0 0 0 1 -1 -1 0 0 2 0 0 0 0 3 4 5 0 0 0 0 0 0 由于队列要在表的一端插入元素,在表的另一端删除元素,故采用链式存储结构比较 好。可以从根本上免除移动队列元素的操作,且节约存储空间。 3.5 离散事件模拟 // func3-3.cpp、algo3-12.cpp和algo3-13.cpp用到的函数及变量等 #include"c1.h" typedef struct // 定义ElemType为结构体类型 { int OccurTime; // 事件发生时刻 int NType; // 事件类型,Qu表示到达事件,0至Qu-1表示Qu个窗口的离开事件 }Event,ElemType; // 事件类型,有序链表LinkList的数据元素类型(见图340) #include"c2-5.h" // 从实际应用角度出发重新定义的线性链表结构 typedef LinkList EventList; // 事件链表指针类型,定义为有序链表 #include"bo2-6.cpp" // 基于c2-5.h存储结构的基本操作 typedef struct { int ArrivalTime; // 到达时刻 int Duration; // 办理事务所需时间 }QElemType; // 定义队列的数据元素类型QElemType为结构体类型(见图341) #include"c3-2.h" // 链队列存储结构 #include"bo3-2.cpp" // 链队列基本操作 // 程序中用到的主要变量(全局) EventList ev; // 事件表头指针 Event en,et; // 事件,临时变量 //FILE *fp; // 文件型指针,用于指向b.txt或d.txt文件 long int TotalTime=0; // 累计客户逗留时间(初值为0) int CloseTime,CustomerNum=0; // 银行营业时间(单位是分),客户数(初值为0) int cmp(Event a,Event b) { // 依事件a的发生时刻<、=或>事件b的发生时刻分别返回-1、0或1 if(a.OccurTime==b.OccurTime) return 0; else return (a.OccurTime-b.OccurTime)/abs(a.OccurTime-b.OccurTime); } void Random(int &d,int &i) { // 生成两个随机数 d=rand()%Blsj+1; // 1到Blsj之间的随机数(办理业务的时间) 图 340 Event 和 ElemType 结构 OccurTime NType Event ElemType 图 341 QElemType 结构 ArrivalTime Duration QElemType 第 3 章 栈 和 队 列 · 131· i=rand()%(Khjg+1); // 0到Khjg之间的随机数(客户到达的时间间隔) } void OpenForDay(); void CustomerArrived(); void CustomerDeparture(); void Bank_Simulation() { // 银行业务模拟函数 Link p; OpenForDay(); // 初始化事件表ev且插入第1个到达事件,初始化队列 while(!ListEmpty(ev)) // 事件表ev不空 { DelFirst(ev,ev.head,p); // 删除事件表ev的第1个结点,并由p返回其指针,在bo2-6.cpp中 // if(p->data.OccurTime<50) // 输出前50分钟内发生的事件到文件d.txt中 // fprintf(fp,"p->data.OccurTime=%3d p->data.NType=%d\n",p->data.OccurTime,p->data.NType); en.OccurTime=GetCurElem(p).OccurTime; // GetCurElem()在bo2-6.cpp中,返回p->data(ElemType类型) en.NType=GetCurElem(p).NType; if(en.NType==Qu) // 到达事件 CustomerArrived(); // 处理客户到达事件 else // 由某窗口离开的事件 CustomerDeparture(); // 处理客户离开事件 } // 计算并输出平均逗留时间 printf("窗口数=%d 两相邻到达的客户的时间间隔=0~%d分钟 每个客户办理业务的时间= 1~%d分钟\n",Qu,Khjg,Blsj); printf("客户总数:%d, 所有客户共耗时:%ld分钟,平均每人耗时:%d分钟,",CustomerNum,TotalTime, TotalTime/CustomerNum); printf("最后一个客户离开的时间:%d分\n",en.OccurTime); } // algo3-12.cpp 银行业务模拟。实现算法3.6、3.7的程序 #define Qu 4 // 客户队列数 #define Khjg 5 // 两相邻到达的客户的时间间隔最大值 #define Blsj 30 // 每个客户办理业务的时间最大值 #include"func3-3.cpp" // 包含algo3-12.cpp和algo3-13.cpp共同用到的函数和变量等 LinkQueue q[Qu]; // Qu个客户队列 QElemType customer; // 客户记录,临时变量 //FILE *fq; // 文件型指针,用于指向a.txt文件 void OpenForDay() { // 初始化事件链表ev且插入第1个到达事件,初始化Qu个队列 int i; InitList(ev); // 初始化事件链表ev为空(见图342) en.OccurTime=0; // 设定第1位客户到达时间为0 // (银行一开门,就有客户来) //fprintf(fq,"首位客户到达时刻=%3d,",en.OccurTime); en.NType=Qu; // 到达 OrderInsert(ev,en,cmp); // 将第1个到达事件en有序插入事件表ev中,在bo2-6.cpp中(见图343) for(i=0;idata.OccurTime= 0 p->data.NType=4 p->data.OccurTime= 4 p->data.NType=4 p->data.OccurTime= 7 p->data.NType=1 p->data.OccurTime= 8 p->data.NType=4 p->data.OccurTime= 9 p->data.NType=4 p->data.OccurTime= 10 p->data.NType=4 p->data.OccurTime= 14 p->data.NType=4 p->data.OccurTime= 16 p->data.NType=4 p->data.OccurTime= 17 p->data.NType=4 p->data.OccurTime= 17 p->data.NType=0 p->data.OccurTime= 21 p->data.NType=4 p->data.OccurTime= 22 p->data.NType=0 p->data.OccurTime= 24 p->data.NType=4 p->data.OccurTime= 25 p->data.NType=2 p->data.OccurTime= 25 p->data.NType=1 p->data.OccurTime= 27 p->data.NType=1 p->data.OccurTime= 27 p->data.NType=4 p->data.OccurTime= 28 p->data.NType=4 p->data.OccurTime= 33 p->data.NType=4 p->data.OccurTime= 35 p->data.NType=0 p->data.OccurTime= 38 p->data.NType=4 p->data.OccurTime= 38 p->data.NType=2 p->data.OccurTime= 39 p->data.NType=3 p->data.OccurTime= 41 p->data.NType=4 p->data.OccurTime= 45 p->data.NType=4 p->data.OccurTime= 46 p->data.NType=4 p->data.OccurTime= 47 p->data.NType=4 p->data.OccurTime= 49 p->data.NType=4 文件 a.txt 的内容是每位客户的到达时刻和他办理业务所需的时间(二者根据随机函数 依次生成)以及他所排的队列等信息。排队的原则是排在人数最少的队列,如果有不止一 队的人数都同为最少,则排在序号最小的队列。由文件 a.txt 的第 3 条记录可知,第 8 分 钟来了一个办理业务需要 17 分钟的客户,这时除 0 队有 1 人外(1 队在第 4 分钟来的人, 已在第 7 分钟离去),1~3 队均无人,客户排在了序号最小的 1 队。 文件 b.txt 的内容是事件表 ev 的历史记录。ev 就象一个安装在门口的监视器,按照时 间顺序,记录客户的到达和离去。对于离去的客户,还要记录是由哪个窗口离去。文件 b.txt 的第 3 条记录记下了第 7 分钟在 1 队(1 号窗口)发生了一个离去事件。这和由文件 图 346 事件表的历史记录 0 4 4 4 7 1 8 4 9 4 10 4 33 4 38 4 39 3 35 0 38 2 27 4 28 4 14 4 16 4 17 4 17 0 21 4 22 0 24 4 25 2 25 1 27 1 49 4 ^ 47 4 46 4 45 4 41 4 ev 第 3 章 栈 和 队 列 · 135· a.txt 推算的结果(见图 345)相吻合。 主程序 main()的第 5 行语句的作用是根据程序运行当前时间的不同,产生不同的随机 函数。否则,随机数总是一样的。在调试程序时,希望每次产生相同的随机数,以便分析 程序运行结果。而在实际应用时,则希望每次产生不同的随机数。 在运行 algo3-12.cpp 时不会产生 a.txt 和 b.txt 文件,因为有关产生这两个文件的语句 已标为注释。在调试程序时,可加一些输出语句帮助分析,最后提交的程序应只输出需要 的结果。实际上,迷宫求解问题中,图 311(到达终点时栈 S 的内容)的数据也是根据增 加的输出语句得出的。 以上是银行开 4 个服务窗口,两相邻客户到达的时间间隔为 0~5 分钟,每个客户办 理业务所需的时间为 1~30 分钟的模拟结果。模拟结果显示,客户等候耗时太多。最后一 个客户离开时,已 14 个半小时了。如果开 6 个窗口(修改程序 algo3-12.cpp 的第 1 行,定 义 Qu 为 6,6 个队列),程序运行结果如下: 请输入银行营业时间长度(单位:分): 480 窗口数=6 两相邻到达的客户的时间间隔=0~5分钟 每个客户办理业务的时间=1~30分钟 客户总数:209, 所有客户共耗时:14311分钟,平均每人耗时:68分钟,最后一个客户离开的时间:602分 平均每位客户等候耗时大大缩短。 目前银行大多使用排队机,即 1 个队列,多个窗口。algo3-13.cpp 是根据这种情况编 制的程序。其中 OpenForDay()、CustomerArrived()和 CustomerDeparture()等 3 个函数与 algo3-12.cpp 中的相应函数是同名函数,但内容不同。而 Bank_Simulation(),甚至 main() 函数(除了打开的文件名不同之外)都是相同的。 // algo3-13.cpp 使用排队机的银行业务模拟 #define Qu 4 // 窗口数 #define Khjg 5 // 两相邻到达的客户的时间间隔最大值 #define Blsj 30 // 每个客户办理业务的时间最大值 #include"func3-3.cpp" // 包含algo3-12.cpp和algo3-13.cpp共同用到的函数和变量等 LinkQueue q; // 排队机队列q QElemType customer[Qu]; // Qu个客户队列元素, 存放正在窗口办理业务的客户的信息 //FILE *fq; // 文件型指针,用于指向c.txt文件 //int j=0; // 计数器,产生c.txt文件用到 Boolean chk[Qu]; // 窗口状态,1为闲,0为忙 void OpenForDay() { // 初始化事件链表ev且插入第1个到达事件,初始化排队机q,初始化Qu个窗口为1(空闲) int i; InitList(ev); // 初始化事件链表ev为空(见图342) en.OccurTime=0; // 设定第1位客户到达时间为0(银行一开门,就有客户来) en.NType=Qu; // 到达 OrderInsert(ev,en,cmp); // 将第1个到达事件en有序插入事件表ev中,在bo2-6.cpp中(见图343) InitQueue(q); // 初始化排队机队列q(见图347) for(i=0;idata.OccurTime= 0 p->data.NType=4 p->data.OccurTime= 4 p->data.NType=4 p->data.OccurTime= 7 p->data.NType=1 p->data.OccurTime= 8 p->data.NType=4 p->data.OccurTime= 9 p->data.NType=4 p->data.OccurTime= 10 p->data.NType=4 p->data.OccurTime= 14 p->data.NType=4 p->data.OccurTime= 16 p->data.NType=4 p->data.OccurTime= 17 p->data.NType=4 p->data.OccurTime= 17 p->data.NType=0 p->data.OccurTime= 21 p->data.NType=4 p->data.OccurTime= 22 p->data.NType=0 p->data.OccurTime= 24 p->data.NType=0 p->data.OccurTime= 24 p->data.NType=4 p->data.OccurTime= 25 p->data.NType=2 p->data.OccurTime= 25 p->data.NType=1 p->data.OccurTime= 27 p->data.NType=4 p->data.OccurTime= 28 p->data.NType=4 p->data.OccurTime= 33 p->data.NType=4 p->data.OccurTime= 37 p->data.NType=0 p->data.OccurTime= 38 p->data.NType=4 p->data.OccurTime= 38 p->data.NType=2 p->data.OccurTime= 39 p->data.NType=3 p->data.OccurTime= 41 p->data.NType=4 p->data.OccurTime= 45 p->data.NType=4 p->data.OccurTime= 46 p->data.NType=4 p->data.OccurTime= 47 p->data.NType=4 p->data.OccurTime= 49 p->data.NType=4 p->data.OccurTime= 49 p->data.NType=1 和 algo3-12.cpp 的运行结果相比,在营业时间、营业窗口数和客户总数相同条件下, algo3-13.cpp 的平均每人耗时和最后一个客户离开的时间比 algo3-12.cpp 要略小。因为没 有调用 srand()函数,两程序产生的随机数是一样的(通过比较文件 a.txt 和 c.txt 的内容可 看出)。不同的是,客户所排队列或窗口不完全相同。algo3-12.cpp 在关门后(不再有客户 进入)可能出现某队已空,而其他队还有若干人的情况。这就延长了最后一个客户离开的 时间,也增大了平均每人耗时。 图 350 事件表的历史记录 0 4 4 4 7 1 8 4 9 4 10 4 33 4 38 4 39 3 37 0 38 2 27 4 28 4 14 4 16 4 17 4 17 0 21 4 22 0 24 0 24 4 25 2 25 1 49 1 ^ 49 4 47 4 46 4 45 4 41 4 ev 注:用^表示 NULL 第 3 章 栈 和 队 列 · 139· 修改程序 algo3-13.cpp 的第 1 行,定义 Qu 为 6,6 个队列,程序运行结果如下: 请输入银行营业时间长度(单位:分): 480 窗口数=6 两相邻到达的客户的时间间隔=0~5分钟 每个客户办理业务的时间=1~30分钟 客户总数:209, 所有客户共耗时:13946分钟,平均每人耗时:66分钟,最后一个客户离开的时间:566分 · 140· 《数据结构》算法实现及解析(第二版) 第 4 章 串 4.1 串类型的定义 在 C 语言中,字符串存于字符型数组中。无论数组有多大,用数值 0 表示串结束。图 41 表示了“but”字符串在 C 语言中的存储结构。 其中数组 a 的定义为 char a[10]; C 语言还在库函数 string.h 中提供了许多串处理 的基本操作,如求串长函数 strlen()、串拷贝函数 strcpy()等。 算法语言本身提供的字符串存储结构及其基本操作不一定能满足实际应用的需要,我 们往往还要根据具体情况另外定义字符串的存储结构及基于该存储结构的基本操作。 4.2 串的表示和实现 4.2.1  // c4-1.h 串的定长顺序存储结构(见图42) #define MAX_STR_LEN 40 // 用户可在255(1个字节)以内定义最大串长 typedef char SString[MAX_STR_LEN+1]; // 0号单元存放串的长度 // bo4-1.cpp 串采用定长顺序存储结构(由c4-1.h定义)的基本操作(13个),包括算法4.2,4.3,4.5 // SString是数组,故不需引用类型 #define DestroyString ClearString // DestroyString()与ClearString()作用相同 Status StrAssign(SString T,char *chars) { // 生成一个其值等于chars的串T int i; if(strlen(chars)>MAX_STR_LEN) return ERROR; else { T[0]=strlen(chars); for(i=1;i<=T[0];i++) T[i]=*(chars+i-1); return OK; } a 图 41 在 语言中的存储结构 [0][1][2][3][4][5][6][7][8][9] 有效字符 but\0?????? 串长 [0] 有效字符 [40] 3 b u t ? ? ? ⋯ ? SString 图  类型 第 4 章 串 · 141· } void StrCopy(SString T,SString S) { // 由串S复制得串T int i; for(i=0;i<=S[0];i++) T[i]=S[i]; } Status StrEmpty(SString S) { // 若S为空串,则返回TRUE;否则返回FALSE if(S[0]==0) return TRUE; else return FALSE; } int StrCompare(SString S,SString T) {// 初始条件:串S和T存在。操作结果:若S>T,则返回值>0;若S=T,则返回值=0;若SS[0]||len<0||len>S[0]-pos+1) return ERROR; for(i=1;i<=len;i++) Sub[i]=S[pos+i-1]; Sub[0]=len; return OK; } int Index(SString S,SString T,int pos) { // 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0。 // 其中,T非空,1≤ pos≤ StrLength(S)。算法4.5 int i,j; if(1<=pos&&pos<=S[0]) { i=pos; j=1; while(i<=S[0]&&j<=T[0]) if(S[i]==T[j]) // 继续比较后继字符 { ++i; ++j; } else // 指针后退重新开始匹配 { i=i-j+2; j=1; } if(j>T[0]) return i-T[0]; else return 0; } else return 0; } Status StrInsert(SString S,int pos,SString T) { // 初始条件:串S和T存在,1≤ pos≤ StrLength(S)+1 // 操作结果:在串S的第pos个字符之前插入串T。完全插入返回TRUE,部分插入返回FALSE int i; if(pos<1||pos>S[0]+1) return ERROR; if(S[0]+T[0]<=MAX_STR_LEN) { // 完全插入 for(i=S[0];i>=pos;i--) S[i+T[0]]=S[i]; for(i=pos;i=pos+T[0];i--) S[i]=S[i-T[0]]; for(i=pos;iS[0]-len+1||len<0) return ERROR; for(i=pos+len;i<=S[0];i++) S[i-len]=S[i]; S[0]-=len; return OK; } Status Replace(SString S,SString T,SString V) // 此函数与串的存储结构无关 { // 初始条件:串S,T和V存在,T是非空串 // 操作结果:用V替换主串S中出现的所有与T相等的不重叠的子串 int i=1; // 从串S的第一个字符起查找串T Status k; if(StrEmpty(T)) // T是空串 return ERROR; do { i=Index(S,T,i); // 结果i为从上一个i之后找到的子串T的位置 if(i) // 串S中存在串T { StrDelete(S,i,StrLength(T)); // 删除该串T k=StrInsert(S,i,V); // 在原串T的位置插入串V if(!k) // 不能完全插入 return ERROR; i+=StrLength(V); // 在插入的串V后面继续查找串T } }while(i); return OK; } void StrPrint(SString T) { // 输出字符串T。另加 int i; for(i=1;i<=T[0];i++) printf("%c",T[i]); printf("\n"); } · 144· 《数据结构》算法实现及解析(第二版) // main4-1.cpp 检验bo4-1.cpp的主程序 #include"c1.h" #include"c4-1.h" #include"bo4-1.cpp" void main() { int i,j; Status k; char s,c[MAX_STR_LEN+1]; SString t,s1,s2; printf("请输入串s1: "); gets(c); k=StrAssign(s1,c); if(!k) { printf("串长超过MAX_STR_LEN(=%d)\n",MAX_STR_LEN); exit(0); } printf("串长为%d 串空否?%d(1:是 0:否)\n",StrLength(s1),StrEmpty(s1)); StrCopy(s2,s1); printf("拷贝s1生成的串为"); StrPrint(s2); printf("请输入串s2: "); gets(c); k=StrAssign(s2,c); if(!k) { printf("串长超过MAX_STR_LEN(%d)\n",MAX_STR_LEN); exit(0); } i=StrCompare(s1,s2); if(i<0) s=’<’; else if(i==0) s=’=’; else s=’>’; printf("串s1%c串s2\n",s); k=Concat(t,s1,s2); printf("串s1联接串s2得到的串t为"); StrPrint(t); if(k==FALSE) printf("串t有截断\n"); ClearString(s1); printf("清为空串后,串s1为"); StrPrint(s1); printf("串长为%d 串空否?%d(1:是 0:否)\n",StrLength(s1),StrEmpty(s1)); printf("求串t的子串,请输入子串的起始位置,子串长度: "); scanf("%d,%d",&i,&j); k=SubString(s2,t,i,j); if(k) { 第 4 章 串 · 145· printf("子串s2为"); StrPrint(s2); } printf("从串t的第pos个字符起,删除len个字符,请输入pos,len: "); scanf("%d,%d",&i,&j); StrDelete(t,i,j); printf("删除后的串t为"); StrPrint(t); i=StrLength(s2)/2; StrInsert(s2,i,t); printf("在串s2的第%d个字符之前插入串t后,串s2为\n",i); StrPrint(s2); i=Index(s2,t,1); printf("s2的第%d个字母起和t第一次匹配\n",i); SubString(t,s2,1,1); printf("串t为"); StrPrint(t); Concat(s1,t,t); printf("串s1为"); StrPrint(s1); k=Replace(s2,t,s1); if(k) // 替换成功 { printf("用串s1取代串s2中和串t相同的不重叠的串后,串s2为"); StrPrint(s2); } DestroyString(s2); // 销毁操作同清空 } 程序运行结果: 请输入串s1: ABCD 串长为4 串空否?0(1:是 0:否) 拷贝s1生成的串为ABCD 请输入串s2: 123456 串s1>串s2 串s1联接串s2得到的串t为ABCD123456 清为空串后,串s1为 串长为0 串空否?1(1:是 0:否) 求串t的子串,请输入子串的起始位置,子串长度: 3,7 子串s2为CD12345 从串t的第pos个字符起,删除len个字符,请输入pos,len: 4,4 删除后的串t为ABC456 在串s2的第3个字符之前插入串t后,串s2为 CDABC45612345 s2的第3个字母起和t第一次匹配 串t为C 串s1为CC 用串s1取代串s2中和串t相同的不重叠的串后,串s2为CCDABCC45612345 · 146· 《数据结构》算法实现及解析(第二版) 4.2.2  // c4-2.h 串的堆分配存储(见图44) struct HString { char *ch; // 若是非空串,则按串长分配存储区;否则ch为NULL int length; // 串长度 }; // bo4-2.cpp 串采用堆分配存储结构(由c4-2.h定义)的基本操作(14个)。包括算法4.1,4.4 #define DestroyString ClearString // DestroyString()与ClearString()作用相同 void StrAssign(HString &T,char *chars) { // 生成一个其值等于串常量chars的串T(见图45) int i,j; if(T.ch) free(T.ch); // 释放T原有空间 i=strlen(chars); // 求chars的长度i if(!i) { // chars的长度为0 T.ch=NULL; T.length=0; } else { // chars的长度不为0 T.ch=(char*)malloc(i*sizeof(char)); // 分配串空间 if(!T.ch) // 分配串空间失败 exit(OVERFLOW); for(j=0;jT,则返回值>0;若S=T,则返回值=0;若SS.length||len<0||len>S.length-pos+1) return ERROR; if(Sub.ch) free(Sub.ch); // 释放旧空间 if(!len) // 空子串 { Sub.ch=NULL; Sub.length=0; } else { // 完整子串 Sub.ch=(char*)malloc(len*sizeof(char)); if(!Sub.ch) 图 46 空串 S 的结构 NULL 0 S · 148· 《数据结构》算法实现及解析(第二版) exit(OVERFLOW); for(i=0;i<=len-1;i++) Sub.ch[i]=S.ch[pos-1+i]; Sub.length=len; } return OK; } void InitString(HString &T) { // 初始化(产生空串)字符串T。另加 T.length=0; T.ch=NULL; } int Index(HString S,HString T,int pos) // 算法4.1 { // T为非空串。若主串S中第pos个字符之后存在与T相等的子串, // 则返回第一个这样的子串在S中的位置;否则返回0 int n,m,i; HString sub; InitString(sub); if(pos>0) { n=StrLength(S); m=StrLength(T); i=pos; while(i<=n-m+1) { SubString(sub,S,i,m); if(StrCompare(sub,T)!=0) ++i; else return i; } } return 0; } Status StrInsert(HString &S,int pos,HString T) // 算法4.4 { // 1≤ pos≤ StrLength(S)+1。在串S的第pos个字符之前插入串T int i; if(pos<1||pos>S.length+1) // pos不合法 return ERROR; if(T.length) // T非空,则重新分配空间,插入T { S.ch=(char*)realloc(S.ch,(S.length+T.length)*sizeof(char)); if(!S.ch) exit(OVERFLOW); for(i=S.length-1;i>=pos-1;--i) // 为插入T而腾出位置 S.ch[i+T.length]=S.ch[i]; for(i=0;i’; printf("串s%c串t\n",c); Concat(r,t,s); printf("串t联接串s产生的串r为"); StrPrint(r); StrAssign(s,"oo"); printf("串s为"); StrPrint(s); StrAssign(t,"o"); printf("串t为"); StrPrint(t); Replace(r,t,s); printf("把串r中和串t相同的子串用串s代替后,串r为"); StrPrint(r); ClearString(s); printf("串s清空后,串长为%d 空否?%d(1:空 0:否)\n",StrLength(s),StrEmpty(s)); SubString(s,r,6,4); printf("串s为从串r的第6个字符起的4个字符,长度为%d 串s为",s.length); StrPrint(s); StrCopy(t,r); printf("复制串t为串r,串t为"); StrPrint(t); StrInsert(t,6,s); printf("在串t的第6个字符前插入串s后,串t为"); StrPrint(t); StrDelete(t,1,5); printf("从串t的第1个字符起删除5个字符后,串t为"); StrPrint(t); printf("%d是从串t的第1个字符起,和串s相同的第1个子串的位置\n",Index(t,s,1)); printf("%d是从串t的第2个字符起,和串s相同的第1个子串的位置\n",Index(t,s,2)); DestroyString(t); // 销毁操作同清空 程序运行结果: 串t为God bye! 串长为8 串空否?0(1:空 0:否) 串s为God luck! 第 4 章 串 · 151· 串s>串t 串t联接串s产生的串r为God bye!God luck! 串s为oo 串t为o 把串r中和串t相同的子串用串s代替后,串r为Good bye!Good luck! 串s清空后,串长为0 空否?1(1:空 0:否) 串s为从串r的第6个字符起的4个字符,长度为4 串s为bye! 复制串t为串r,串t为Good bye!Good luck! 在串t的第6个字符前插入串s后,串t为Good bye!bye!Good luck! 从串t的第1个字符起删除5个字符后,串t为bye!bye!Good luck! 1是从串t的第1个字符起,和串s相同的第1个子串的位置 5是从串t的第2个字符起,和串s相同的第1个子串的位置 串的堆分配存储结构(由 c4-2.h 定义)根据串的长度,动态地分配存储空间。这样既 保证满足需要,又不浪费空间,对于串长也没有限制。串的定长存储结构(由 c4-1.h 定义) 就没有这样灵活了。在 Concat()、StrInsert()和 Replace()中,总要检查串是否被截断,且 对于短串的情况,空间浪费较大。故堆分配存储结构较好。 4.2.3  // c4-3.h 串的块链存储结构(见图47) #define CHUNK_SIZE 4 // 可由用户定义的块大小 struct Chunk { char ch[CHUNK_SIZE]; Chunk *next; }; struct LString { Chunk *head,*tail; // 串的头和尾指针 int curlen; // 串的当前长度 }; 图 48 是根据 c4-3.h 定义的串“ABCDEFGHI”的一种可能的存储形式(“#”作为 填补空余的字符,不计为串的字符)。 图 47 串的块链存储结构 Chunk ch[0] ch[1] ch[2] ch[3] next Chunk LString head curlen tail ChunkChunk 图 48 串“ABCDEFGHI”的一种块链存储结 9 ABCDHI##NULLE#FG · 152· 《数据结构》算法实现及解析(第二版) // bo4-3.cpp 串采用块链存储结构(由c4-3.h定义)的基本操作(15个) #define DestroyString ClearString // DestroyString()与ClearString()作用相同 void InitString(LString &T) { // 初始化(产生空串)字符串T。另加(见图49) T.curlen=0; T.head= T.tail=NULL; } Status StrAssign(LString &T,char *chars) { // 生成一个其值等于chars的串T(要求chars中不包含填补空余的字符)。成功返回OK;否则返回ERROR int i,j,k,m; Chunk *p,*q; i=strlen(chars); // i为串的长度 if(!i||strchr(chars,blank)) // 串长为0或chars中包含填补空余的字符 return ERROR; T.curlen=i; j=i/CHUNK_SIZE; // j为块链的结点数 if(i%CHUNK_SIZE) j++; for(k=0;kch+m)=*chars++; if(k==0) // 第一个链块 T.head=q=p; // 头指针指向第一个链块 else { q->next=p; q=p; } if(!*chars) // 最后一个链块 { T.tail=q; q->next=NULL; for(;mch+m)=blank; } } return OK; } Status ToChars(LString T,char* &chars) { // 将串T的内容转换为字符串,chars为其头指针。成功返回OK;否则返回ERROR。另加 Chunk *p=T.head; // p指向第1个块结点 int i; char *q; chars=(char*)malloc((T.curlen+1)*sizeof(char)); if(!chars||!T.curlen) // 生成字符串数组失败或串T长为0 return ERROR; q=chars; // q指向chars的第1个字符 图 49 空串结构 NULL 0 NULL 第 4 章 串 · 153· while(p) // 块链没结束 { for(i=0;ich[i]!=blank) // 当前字符不是填补空余的字符 *q++=(p->ch[i]); // 赋给q所指字符空间 p=p->next; } chars[T.curlen]=0; // 串结束符 return OK; } Status StrCopy(LString &T,LString S) { // 初始条件:串S存在 // 操作结果:由串S复制得串T,去掉填补空余的字符。成功返回OK;否则返回ERROR char *c; Status i; if(!ToChars(S,c)) // c为串S的内容 return ERROR; i=StrAssign(T,c); // 将串S的内容赋给T free(c); // 释放c的空间 return i; } Status StrEmpty(LString S) { // 初始条件:串S存在。操作结果:若S为空串,则返回TRUE;否则返回FALSE if(S.curlen) // 非空 return FALSE; else return TRUE; } int StrCompare(LString S,LString T) { // 若S>T,则返回值>0;若S=T,则返回值=0;若Snext; free(p); p=q; } S.head=S.tail=NULL; S.curlen=0; } Status Concat(LString &T,LString S1,LString S2) { // 用T返回由S1和S2联接而成的新串(中间可能有填补空余的字符) LString a1,a2; Status i,j; InitString(a1); InitString(a2); i=StrCopy(a1,S1); j=StrCopy(a2,S2); if(!i||!j) // 至少有1个串拷贝不成功 return ERROR; T.curlen=S1.curlen+S2.curlen; // 生成串T T.head=a1.head; a1.tail->next=a2.head; // a1,a2两串首尾相连 T.tail=a2.tail; return OK; } Status SubString(LString &Sub, LString S,int pos,int len) { // 用Sub返回串S的第pos个字符起长度为len的子串。 // 其中,1≤ pos≤ StrLength(S)且0≤ len≤ StrLength(S)-pos+1 char *b,*c; Status i; if(pos<1||pos>S.curlen||len<0||len>S.curlen-pos+1) // pos或len值不合法 return ERROR; if(!ToChars(S,c)) // c为串S的内容 return ERROR; b=c+pos-1; // b指向串c中串Sub内容的首地址 b[len]=0; // Sub结束处赋0(字符串结束符) i=StrAssign(Sub,b); // 将串b的内容赋给Sub free(c); return i; } int Index(LString S,LString T,int pos) { // T为非空串。若主串S中第pos个字符之后存在与T相等的子串, // 则返回第一个这样的子串在S中的位置,否则返回0 int i,n,m; LString sub; if(pos>=1&&pos<=StrLength(S)) // pos满足条件 { n=StrLength(S); // 主串长度 m=StrLength(T); // 串T长度 i=pos; while(i<=n-m+1) { SubString(sub,S,i,m); // sub为从S的第i个字符起,长度为m的子串 第 4 章 串 · 155· if(StrCompare(sub,T)) // sub不等于T ++i; else return i; } } return 0; } Status StrInsert(LString &S, int pos,LString T) { // 1≤ pos≤ StrLength(S)+1。在串S的第pos个字符之前插入串T char *b,*c; int i,j; Status k; if(pos<1||pos>S.curlen+1) // pos的值超出范围 return ERROR; if(!ToChars(S,c)) // c为串S的内容 return ERROR; if(!ToChars(T,b)) // b为串T的内容 return ERROR; j=(int)strlen(c); // j为串S的最初长度 c=(char*)realloc(c,(j+strlen(b)+1)*sizeof(char)); // 增加c的存储空间 for(i=j;i>=pos-1;i--) c[i+strlen(b)]=c[i]; // 为插入串b腾出空间 for(i=0;i<(int)strlen(b);i++) // 在串c中插入串b c[pos+i-1]=b[i]; InitString(S); // 释放S的原有存储空间 k=StrAssign(S,c); // 由c生成新的串S free(b); free(c); return k; } Status StrDelete(LString &S,int pos,int len) { // 从串S中删除第pos个字符起长度为len的子串 char *c; int i; Status k; if(pos<1||pos>S.curlen-len+1||len<0) // pos,len的值超出范围 return ERROR; if(!ToChars(S,c)) // c为串S的内容 return ERROR; for(i=pos+len-1;i<=(int)strlen(c);i++) c[i-len]=c[i]; // c为删除后串S的内容 InitString(S); // 释放S的原有存储空间 k=StrAssign(S,c); // 由c生成新的串S free(c); return k; } Status Replace(LString &S,LString T,LString V) // 此函数与串的存储结构无关 { // 初始条件:串S,T和V存在,T是非空串 // 操作结果:用V替换主串S中出现的所有与T相等的不重叠的子串 int i=1; // 从串S的第一个字符起查找串T · 156· 《数据结构》算法实现及解析(第二版) if(StrEmpty(T)) // T是空串 return ERROR; do { i=Index(S,T,i); // 结果i为从上一个i之后找到的子串T的位置 if(i) // 串S中存在串T { StrDelete(S,i,StrLength(T)); // 删除该串T StrInsert(S,i,V); // 在原串T的位置插入串V i+=StrLength(V); // 在插入的串V后面继续查找串T } }while(i); return OK; } void StrPrint(LString T) { // 输出字符串T。另加 int i=0,j; Chunk *h; h=T.head; while(ich+j)!=blank) // 不是填补空余的字符 { printf("%c",*(h->ch+j)); i++; } h=h->next; } printf("\n"); } // main4-3.cpp 检验bo4-3.cpp的主程序 char blank='#'; // 全局变量,用于填补空余 #include"c1.h" #include"c4-3.h" #include"bo4-3.cpp" void main() { char *s1="ABCDEFGHI",*s2="12345",*s3="",*s4="asd#tr",*s5="ABCD"; Status k; int pos,len; LString t1,t2,t3,t4; InitString(t1); InitString(t2); printf("初始化串t1后,串t1空否?%d(1:空 0:否) 串长=%d\n",StrEmpty(t1),StrLength(t1)); k=StrAssign(t1,s3); if(k==ERROR) printf("出错\n"); // 不能生成空串 k=StrAssign(t1,s4); if(k==ERROR) 第 4 章 串 · 157· printf("出错\n"); // 不能生成含有变量blank所代表的字符的串 k=StrAssign(t1,s1); if(k==OK) { printf("串t1为"); StrPrint(t1); } else printf("出错\n"); printf("串t1空否?%d(1:空 0:否) 串长=%d\n",StrEmpty(t1),StrLength(t1)); StrAssign(t2,s2); printf("串t2为"); StrPrint(t2); StrCopy(t3,t1); printf("由串t1拷贝得到串t3,串t3为"); StrPrint(t3); InitString(t4); StrAssign(t4,s5); printf("串t4为"); StrPrint(t4); Replace(t3,t4,t2); printf("用t2取代串t3中的t4串后,串t3为"); StrPrint(t3); ClearString(t1); printf("清空串t1后,串t1空否?%d(1:空 0:否) 串长=%d\n",StrEmpty(t1),StrLength(t1)); Concat(t1,t2,t3); printf("串t1(=t2+t3)为"); StrPrint(t1); pos=Index(t1,t3,1); printf("pos=%d\n",pos); printf("在串t1的第pos个字符之前插入串t2,请输入pos: "); scanf("%d",&pos); k=StrInsert(t1,pos,t2); if(k) { printf("插入串t2后,串t1为"); StrPrint(t1); } else printf("插入失败!\n"); printf("求从t1的第pos个字符起,长度为len的子串t2,请输入pos,len: "); scanf("%d,%d",&pos,&len); SubString(t2,t1,pos,len); printf("串t2为"); StrPrint(t2); printf("StrCompare(t1,t2)=%d\n",StrCompare(t1,t2)); printf("删除串t1中的子字符串:从第pos个字符起删除len个字符。请输入pos,len:"); scanf("%d,%d",&pos,&len); k=StrDelete(t1,pos,len); if(k) { · 158· 《数据结构》算法实现及解析(第二版) printf("从第%d位置起删除%d个元素后串t1为",pos,len); StrPrint(t1); } DestroyString(t1); // 销毁操作同清空 } 程序运行结果: 初始化串t1后,串t1空否?1(1:空 0:否) 串长=0 出错 出错 串t1为ABCDEFGHI 串t1空否?0(1:空 0:否) 串长=9 串t2为12345 由串t1拷贝得到串t3,串t3为ABCDEFGHI 串t4为ABCD 用t2取代串t3中的t4串后,串t3为12345EFGHI 清空串t1后,串t1空否?1(1:空 0:否) 串长=0 串t1(=t2+t3)为1234512345EFGHI pos=6 在串t1的第pos个字符之前插入串t2,请输入pos: 1 插入串t2后,串t1为123451234512345EFGHI 求从t1的第pos个字符起,长度为len的子串t2,请输入pos,len: 3,2 串t2为34 StrCompare(t1,t2)=-2 删除串t1中的子字符串:从第pos个字符起删除len个字符。请输入pos,len:6,15 从第6位置起删除15个元素后串t1为12345 由 bo4-3.cpp 可见,串的块链存储结构优势很少,一般不用。 4.3 串的模式匹配算法 4.3.1    Index(S,T,pos) 串的模式匹配的一般方法如算法 4.5(在 bo4-1.cpp 中)所示:由主串 S 的第 pos 个字 符起,检验是否存在子串 T。首先令 i 等于 pos(i 为 S 中当前待比较字符的位序),j 等于 1(j 为 T 中当前待比较字符的位序),如果 S 的 第 i 个字符与 T 的第 j 个字符相同,则 i、j 各 加 1 继续比较,直至 T 的最后一个字符(找 到)。如果还没到 T 的最后一个字符,比较就 出现了不同(没找到),则令 i 等于 pos+1,j 等 于 1,由 pos 的下一个位置起,继续查找是否 存在子串 T。这个过程如图 410 所示。 图 410 模式匹配的一般方法 ABCDEFGHIJK ⋯T j(初值) ⋯ j(终值) j(回溯后的新初值) i(初值) ⋯ i(终值) ⋯ ABCDEFGHIJJ ⋯S i(回溯后的新初值)pos 第 4 章 串 · 159· 4.3.2   在算法 4.5 中,主串 S 的指针 i 总要回溯,特别是在如图 410 所示的有较多字符匹 配而又不完全匹配的情况下,回溯得更多。这时,主串 S 的一个字符要进行多次比较,显 然效率较低。 如果能使主串 S 的指针 i 不回溯,在有些情况下效率则会大为提高。这是可以做到 的,因为主串 S 中位于 i-1,i-2,⋯ 的字符恰和子串 T 中位于 j-1,j-2,⋯ 的字符相 等,如图 410 所示。仍以图 410 为例,当 S 和 T 在第 i(终值)个字符处字符不相符 时,i 仍保持在终值处不动,j 回溯到第 1 个字符与 i 的当前字符继续进行比较。j 回溯到第 几个字符是由子串 T 的模式决定的。算法 4.7 根据子串 T 生成的 next 数组指示 j 回溯到第 几个字符。next 数组的意义是这样的:如果 next[j]=k,当子串 T 的第 j 个字符与主串 S 的 第 i 个字符“失配”时,S 的第 i 个字符继续与 T 的第 k 个字符进行比较,T 的第 k 个字符 之前的那些字符均与 S 的第 i 个字符之前的字符匹配。以教科书中图 4.5 为例,设子串 T 为“abaabcac”。当 T 的第 5 个字符与 S 的第 i 个字符失配时,S 的第 i-1 个字符一定是 a, 和 T 的第 4 个字符相等。它和 T 的第 1 个字 符相等。这样,S 的第 i 个字符和 T 的第 2 个 字符开始比较即可。所以,对于模式串 “abaabcac”,next[5]=2,详见图 411。 算法 4.7 求子串的数组 next[]还有可改 进之处。以图 411 为例:如果 T 的第 5 个 字符与 S 的第 i 个字符失配,则 S 的第 i 个字 符一定不是 b。这样,尽管 S 的第 i-1 个字符 是 a,和 T 的第 1 个字符相等,但 S 的第 i 个字符肯定和 T 的第 2 个字符 b 不相等。所以 可令 next[5]=1,使 S 的第 i 个字符和 T 的第 1 个字符开始比较。这样使得模式串又向右 移了一位,提高了匹配的效率。算法 4.8 是改进的求数组 next[](在算法 4.8 中的形参是 nextval[])的算法。 算法 4.6 是改进的模式匹配算法。它利用算法 4.7 或算法 4.8 求得的数组 next[],提 高了算法的效率。algo4-1.cpp 是实现改进的模式匹配算法的程序。函数 get_next()和 get_nextval()分别求得给定的模式串的数组 next[]和 nextval[],函数 Index_KMP()利用数 组 next[]或 nextval[]求出模式串在主串中的位置。其中,next[j]=0,并不是将主串的当前 字符与模式串的第 0 个字符进行比较(模式串也没有第 0 个字符),而是主串当前字符的下 一个字符与模式串的第 1 个字符进行比较。 // algo4-1.cpp 实现算法4.6、4.7、4.8的程序 #include"c1.h" #include"c4-1.h" #include"bo4-1.cpp" void get_next(SString T,int next[]) { // 求模式串T的next函数值并存入数组next。算法4.7 int i=1,j=0; next[1]=0; a b a a b c a cT j(初值) j(终值) j(回溯后的新初值) ⋯ a b a a c ⋯ i(初值) i(终值也是新初值) S 图 411 串"abaabcac"的数组 next[5]=2 的由来 · 160· 《数据结构》算法实现及解析(第二版) while(iT[0]) // 匹配成功 return i-T[0]; else return 0; } void main() { int i,*p; SString s1,s2; // 以教科书算法4.8之上的数据为例 StrAssign(s1,"aaabaaaab"); printf("主串为"); StrPrint(s1); 第 4 章 串 · 161· StrAssign(s2,"aaaab"); printf("子串为"); StrPrint(s2); p=(int*)malloc((StrLength(s2)+1)*sizeof(int)); // 生成s2的next数组空间 get_next(s2,p); // 利用算法4.7,求得next数组,存于p中 printf("子串的next数组为"); for(i=1;i<=StrLength(s2);i++) printf("%d ",*(p+i)); printf("\n"); i=Index_KMP(s1,s2,1,p); // 利用算法4.6求得串s2在s1中首次匹配的位置i if(i) printf("主串和子串在第%d个字符处首次匹配\n",i); else printf("主串和子串匹配不成功\n"); get_nextval(s2,p); // 利用算法4.8,求得next数组,存于p中 printf("子串的nextval数组为"); for(i=1;i<=StrLength(s2);i++) printf("%d ",*(p+i)); printf("\n"); printf("主串和子串在第%d个字符处首次匹配\n",Index_KMP(s1,s2,1,p)); } 程序运行结果: 主串为aaabaaaab 子串为aaaab 子串的next数组为0 1 2 3 4 主串和子串在第5个字符处首次匹配 子串的nextval数组为0 0 0 0 4 主串和子串在第5个字符处首次匹配 4.4 串操作应用举例 4.4.1 // algo4-2.cpp 文本行编辑 #include"c1.h" #include"c4-2.h" // 采用串的堆分配存储结构 #include"bo4-2.cpp" // 串的堆分配基本操作 #define MAX_LEN 25 // 文件最大行数 #define LINE_LEN 75 // 每行字符数最大值+1 #define NAME_LEN 20 // 文件名最大长度(包括盘符、路径)+1 // 全局变量(见图412) HString T[MAX_LEN]; char str[LINE_LEN],filename[NAME_LEN]=""; FILE *fp; 图 412 一个 3 行文本存储结构示例 n1 n2 n3 第 1 行内容(n1 个字符) 第 2 行内容(n2 个字符) 第 3 行内容(n3 个字符) [0] [1] [2] [3] [24] 全局变量 n=3 T   · 162· 《数据结构》算法实现及解析(第二版) int n=0; // 文本行数 void Open() { // 打开文件(新或旧) if(filename[0]) // 文件已打开 printf("已存在打开的文件\n"); else { printf("请输入文件名(可包括盘符、路径,不超过%d个字符): ",NAME_LEN-1); scanf("%s",filename); fp=fopen(filename,"r"); // 以读的方式打开文件 if(fp) // 已存在此文件 { while(fgets(str,LINE_LEN,fp)) // 由文件读入1行字符成功 { str[strlen(str)-1]=0; // 将换行符10强制改为串结束符0 if(n>=MAX_LEN) { printf("文件太大\n"); return; } StrAssign(T[n],str); n++; } fclose(fp); // 关闭文件 } else printf("新文件\n"); } } void List() { // 显示文本内容 int i; for(i=0;iMAX_LEN) { printf("插入行太多\n"); return; } if(n>=l-1&&l>0) { 第 4 章 串 · 163· for(i=n-1;i>=l-1;i--) T[i+m]=T[i]; n+=m; printf("请顺序输入待插入内容:\n"); for(i=l-1;i=l+m-1&&l>0) { for(i=l-1+m;iMAX_LEN) { printf("拷贝行太多\n"); return; } if(n>=k-1&&n>=l-1+m&&(k>=l+m||k<=l)) { for(i=n-1;i>=k-1;i--) T[i+m]=T[i]; n+=m; if(k<=l) l+=m; · 164· 《数据结构》算法实现及解析(第二版) for(i=l-1;i0&&i<=n) // 行号合法 { printf("%d: ",i); StrPrint(T[i-1]); printf("请输入新内容: "); gets(str); StrAssign(T[i-1],str); } else printf("行号超出范围\n"); } void Search() { // 查找字符串 int i,k,f=1; // f为继续查找标志 char b[2]; HString s; printf("请输入待查找的字符串: "); scanf("%s%*c",str); InitString(s); StrAssign(s,str); for(i=0;i= ′A′&&s1[0]<=′Z′) // 单词首字母为大写 { // 写入词表 wdlist.item[wdlist.last]=(char *)malloc((l+1)*sizeof(char)); // 生成串空间(包括′\0′) for(i=0;i0;i++); // 查找是否为非索引词 if(!l) // 是非索引词 { free(wdlist.item[wdlist.last]); // 从词表中删除该词 wdlist.item[wdlist.last]=NULL; } else wdlist.last++; // 词表长度+1 } s1=s2+1; // s1移动到下一个单词的首字符处 }; } void GetWord(int i,HString &wd) { // 用wd返回词表wdlist中第i个关键词,算法4.11 StrAssign(wd,wdlist.item[i]); // 生成关键字字符串 bo4-2.cpp } int Locate(IdxListType &idxlist,HString wd,Status &b) { // 在索引表idxlist中查询是否存在与wd相等的关键词。若存在,则返回其 // 在索引表中的位置,且b取值TRUE;否则返回插入位置,且b取值FALSE,算法4.12 int i,m; for(i=idxlist.last;(m=StrCompare(idxlist.item[i].key,wd))>0;--i); // bo4-2.cpp if(m==0) // 找到 { · 172· 《数据结构》算法实现及解析(第二版) b=TRUE; return i; } else { b=FALSE; return i+1; } } void InsertNewKey(IdxListType &idxlist,int i,HString wd) { // 在索引表idxlist的第i项上插入新关键词wd,并初始化书号索引的链表为空表,算法4.13 int j; for(j=idxlist.last;j>=i;--j) // 后移索引项 idxlist.item[j+1]=idxlist.item[j]; InitString(idxlist.item[i].key); // bo4-2.cpp StrCopy(idxlist.item[i].key,wd); // 串拷贝插入新的索引项 bo4-2.cpp InitList(idxlist.item[i].bnolist); // 初始化书号索引表为空表 bo2-6.cpp idxlist.last++; } void InsertBook(IdxListType &idxlist,int i,int bno) { // 在索引表idxlist的第i项中插入书号为bno的索引,算法4.14 Link p; MakeNode(p,bno); // 分配结点 bo2-6.cpp p->next=NULL; Append(idxlist.item[i].bnolist,p); // 插入新的书号索引 bo2-6.cpp } void InsIdxList(IdxListType &idxlist,int bno) { // 将书号为bno的关键词插入索引表,算法4.10 int i,j; Status b; HString wd; InitString(wd); // bo4-2.cpp for(i=0;inext; fprintf(f,"%d ",p->data); } fprintf(f,"\n"); } } void main() { // 算法4.9 FILE *f; // 任何时间最多打开一个文件 IdxListType idxlist; // 索引表 int BookNo; // 书号变量 int k; if(!(f=fopen("NoIdx.txt","r"))) // 打开非索引词文件 exit(OVERFLOW); fscanf(f,"%d",&noidx.last); // 读取非索引词个数 for(k=0;knext=NULL; // 给书号结点的指针域赋值 Append(idxlist.item[k].bnolist,p); // 在表尾插入新的书号结点,bo2-6.cpp } } fclose(f); if(!(f=fopen("BookInfo.txt","r"))) // 打开书目文件 exit(FALSE); i=0; while(fgets(buf,MaxLineLen,f)) { // 把书目文件的内容拷到booklist中 booklist.item[i].bookno=atoi(buf); // 前几位数字为书号 strcpy(booklist.item[i++].bookname,&buf[4]); // 将buf由书名开始的字符串拷贝到booklist中 } booklist.last=i; while(flag) { printf("请输入书目的关键词(一个):"); scanf("%s",buf); i=0; while(buf[i]) buf[i++]=tolower(buf[i]); // 字母转为小写 StrAssign(ch,buf); i=0; do { k=StrCompare(ch,idxlist.item[i++].key); // bo4-2.cpp }while(k&&i<=idxlist.last); if(!k) // 索引表中有此关键词 { · 176· 《数据结构》算法实现及解析(第二版) p=idxlist.item[--i].bnolist.head->next; // p指向索引表中此关键词相应链表的首元结点 while(p) { j=0; while(jdata!=booklist.item[j].bookno) //在booklist中找相应的书号 j++; if(jnext; // 继续向后找 } } else printf("没找到\n"); printf("继续查找请输入1,退出查找请输入0:"); scanf("%d",&flag); } } 程序运行结果: 请输入书目的关键词(一个):DATA 5 Computer Data Structures 10 Introduction to Data Structures 23 Fundamentals of Data Structures 继续查找请输入1,退出查找请输入0:1 请输入书目的关键词(一个):structure 没找到 继续查找请输入1,退出查找请输入0:0 第 5 章 数组和广义表 · 177· 第 5 章 数组和广义表 5.1 数组的定义 5.2 数组的顺序表示和实现 // c5-1.h 数组的顺序存储表示(见图51) #include // 标准头文件,提供宏va_start,va_arg和va_end,用于存取变长参数表 #define MAX_ARRAY_DIM 8 // 假设数组维数的最大值为8 struct Array { ElemType *base; // 数组元素基址,由InitArray分配 int dim; // 数组维数 int *bounds; // 数组维界基址,由InitArray分配 int *constants; // 数组映象函数常量基址,由InitArray分配 }; // bo5-1.cpp 顺序存储数组(存储结构由c5-1.h定义)的基本操作(5个) Status InitArray(Array &A,int dim,...) { // 若维数dim和各维长度合法,则构造相应的数组A,并返回OK(见图52) int elemtotal=1,i; // elemtotal是数组元素总数,初值为1(累乘器) va_list ap; if(dim<1||dim>MAX_ARRAY_DIM) return ERROR; A.dim=dim; A.bounds=(int *)malloc(dim*sizeof(int)); if(!A.bounds) exit(OVERFLOW); va_start(ap,dim); for(i=0;i=0;--i) A.constants[i]=A.bounds[i+1]*A.constants[i+1]; return OK; } void DestroyArray(Array &A) { // 销毁数组A(见图53) if(A.base) free(A.base); if(A.bounds) free(A.bounds); if(A.constants) free(A.constants); A.base= A.bounds=A.constants=NULL; A.dim=0; } Status Locate(Array A,va_list ap,int &off) // Value()、Assign()调用此函数 { // 若ap指示的各下标值合法,则求出该元素在A中的相对地址off int i,ind; off=0; for(i=0;i=A.bounds[i]) return OVERFLOW; off+=A.constants[i]*ind; } return OK; } Status Value(ElemType &e,Array A,...) // 在VC++中,...之前的形参不能是引用类型 { // ...依次为各维的下标值,若各下标合法,则e被赋值为A的相应的元素值 va_list ap; int off; va_start(ap,A); if(Locate(A,ap,off)==OVERFLOW) // 调用Locate() return ERROR; e=*(A.base+off); return OK; } Status Assign(Array A,ElemType e,...) // 变量A的值不变,故不需要& { // ...依次为各维的下标值,若各下标合法,则将e的值赋给A的指定的元素 va_list ap; int off; va_start(ap,e); if(Locate(A,ap,off)==OVERFLOW) // 调用Locate() return ERROR; 图 53 销毁数组 A A NULL 0 NULL NULL 第 5 章 数组和广义表 · 179· *(A.base+off)=e; return OK; } // main5-1.cpp 检验bo5-1.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c5-1.h" #include"bo5-1.cpp" void main() { Array A; int i,j,k,*p,dim=3,bound1=3,bound2=4,bound3=2; // A[3][4][2]数组 ElemType e,*p1; InitArray(A,dim,bound1,bound2,bound3); // 构造3×4×2的3维数组A(见图52) p=A.bounds; printf("A.bounds="); for(i=0;i // 实现变长参数表要包括的头文件 typedef int ElemType; ElemType Max(int num,...) // ...表示变长参数表,位于形参表的最后,前面必须至少有一个固定参数 { // 函数功能:返回num个数中的最大值 va_list ap; // 定义ap是变长参数表类型(C语言的数据类型) int i; ElemType m,n; if(num<1) exit(ERROR); va_start(ap,num); // ap指向固定参数num后面的实参表 第 5 章 数组和广义表 · 181· m=va_arg(ap,ElemType);//依次读取ap所指的实参(以逗号为分隔符)作为ElemType类型实参,ap向后移 for(i=1;iMAX_SIZE) return ERROR; M.data[0].i=0; // 为以下比较顺序做准备 for(i=1;i<=M.tu;i++) { do { printf("请按行序顺序输入第%d个非零元素所在的行(1~%d),列(1~%d),元素值:",i,M.mu,M.nu); scanf("%d,%d,%d",&m,&n,&e); k=0; if(m<1||m>M.mu||n<1||n>M.nu) // 行或列超出范围 k=1; if(mi==i&&p->j==j) // p指向非零元,且p所指元素为当前处理元素 { printf("%3d",p->e); // 输出p所指元素的值 p++; // p指向下一个元素 k++; // 计数器+1 } else // p所指元素不是当前处理元素 printf("%3d",0); // 输出0 printf("\n"); } } void CopySMatrix(TSMatrix M,TSMatrix &T) { // 由稀疏矩阵M复制得到T T=M; } int comp(int c1,int c2) { // AddSMatrix函数要用到,另加 if(c1MAX_SIZE) // 非零元素个数太多 return ERROR; return OK; } Status SubtSMatrix(TSMatrix M,TSMatrix N,TSMatrix &Q) { // 求稀疏矩阵的差Q=M-N int i; for(i=1;i<=N.tu;i++) N.data[i].e*=-1; return AddSMatrix(M,N,Q); } void TransposeSMatrix(TSMatrix M,TSMatrix &T) { // 求稀疏矩阵M的转置矩阵T。算法5.1改 int p,q,col; T.mu=M.nu; T.nu=M.mu; T.tu=M.tu; if(T.tu) { q=1; for(col=1;col<=M.nu;++col) for(p=1;p<=M.tu;++p) if(M.data[p].j==col) { T.data[q].i=M.data[p].j; T.data[q].j=M.data[p].i; T.data[q].e=M.data[p].e; ++q; } } } Status MultSMatrix(TSMatrix M,TSMatrix N,TSMatrix &Q) { // 求稀疏矩阵的乘积Q=M×N int i,j; ElemType *Nc,*Tc; TSMatrix T; // 临时矩阵 if(M.nu!=N.mu) return ERROR; T.nu=M.mu; // 临时矩阵T是Q的转秩矩阵 T.mu=N.nu; T.tu=0; Nc=(ElemType*)malloc((N.mu+1)*sizeof(ElemType));//Nc为矩阵N一列的临时数组(非压缩,[0]不用) Tc=(ElemType*)malloc((M.nu+1)*sizeof(ElemType));//Tc为矩阵T一行的临时数组(非压缩,[0]不用) if(!Nc||!Tc) // 创建临时数组不成功 第 5 章 数组和广义表 · 185· exit(ERROR); for(i=1;i<=N.nu;i++) // 对于N的每一列 { for(j=1;j<=N.mu;j++) Nc[j]=0; // 矩阵Nc的初值为0 for(j=1;j<=M.mu;j++) Tc[j]=0; // 临时数组Tc的初值为0,[0]不用 for(j=1;j<=N.tu;j++) // 对于N的每一个非零元素 if(N.data[j].j==i) // 属于第i列 Nc[N.data[j].i]=N.data[j].e; // 根据其所在行将其元素值赋给相应的Nc for(j=1;j<=M.tu;j++) // 对于M的每一个值 Tc[M.data[j].i]+=M.data[j].e*Nc[M.data[j].j]; // Tc中存N的第i列与M相乘的结果 for(j=1;j<=M.mu;j++) if(Tc[j]!=0) { T.data[++T.tu].e=Tc[j]; T.data[T.tu].i=i; T.data[T.tu].j=j; } } if(T.tu>MAX_SIZE) // 非零元素个数太多 return ERROR; TransposeSMatrix(T,Q); // 将T的转秩赋给Q DestroySMatrix(T); // 销毁临时矩阵T free(Tc); // 释放动态数组Tc和Nc free(Nc); return OK; } // main5-2.cpp 检验bo5-2.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c5-2.h" #include"bo5-2.cpp" void main() { TSMatrix A,B,C; printf("创建矩阵A: "); CreateSMatrix(A); PrintSMatrix(A); printf("由矩阵A复制矩阵B:\n"); CopySMatrix(A,B); PrintSMatrix1(B); DestroySMatrix(B); printf("销毁矩阵B后:\n"); PrintSMatrix1(B); printf("创建矩阵B2:(与矩阵A的行、列数相同,行、列分别为%d,%d)\n",A.mu,A.nu); CreateSMatrix(B); PrintSMatrix1(B); AddSMatrix(A,B,C); printf("矩阵C1(A+B):\n"); · 186· 《数据结构》算法实现及解析(第二版) PrintSMatrix1(C); SubtSMatrix(A,B,C); printf("矩阵C2(A-B):\n"); PrintSMatrix1(C); TransposeSMatrix(A,C); printf("矩阵C3(A的转置):\n"); PrintSMatrix1(C); printf("创建矩阵A2: "); CreateSMatrix(A); PrintSMatrix1(A); printf("创建矩阵B3:(行数应与矩阵A2的列数相同=%d)\n",A.nu); CreateSMatrix(B); PrintSMatrix1(B); MultSMatrix(A,B,C); printf("矩阵C5(A×B):\n"); PrintSMatrix1(C); } 程序运行结果: 创建矩阵A: 请输入矩阵的行数,列数,非零元素数:3,3,2 请按行序顺序输入第1个非零元素所在的行(1~3),列(1~3),元素值:1,2,1 请按行序顺序输入第2个非零元素所在的行(1~3),列(1~3),元素值:2,2,2 3行3列2个非零元素。(见图57) 行 列 元素值 1 2 1 2 2 2 由矩阵A复制矩阵B: 0 1 0 0 2 0 0 0 0 销毁矩阵B后: 创建矩阵B2:(与矩阵A的行、列数相同,行、列分别为3,3) 请输入矩阵的行数,列数,非零元素数:3,3,1 请按行序顺序输入第1个非零元素所在的行(1~3),列(1~3),元素值:1,2,1 0 1 0 0 0 0 0 0 0 矩阵C1(A+B): 0 2 0 0 2 0 0 0 0 矩阵C2(A-B): 0 0 0 0 2 0 0 0 0 矩阵C3(A的转置): 0 0 0 1 2 0 图 57 矩阵 A 图示           000 020 010 第 5 章 数组和广义表 · 187· 0 0 0 创建矩阵A2: 请输入矩阵的行数,列数,非零元素数:2,3,2 请按行序顺序输入第1个非零元素所在的行(1~2),列(1~3),元素值:1,1,1 请按行序顺序输入第2个非零元素所在的行(1~2),列(1~3),元素值:2,3,2 1 0 0 0 0 2 创建矩阵B3:(行数应与矩阵A2的列数相同=3) 请输入矩阵的行数,列数,非零元素数:3,2,2 请按行序顺序输入第1个非零元素所在的行(1~3),列(1~2),元素值:2,2,1 请按行序顺序输入第2个非零元素所在的行(1~3),列(1~2),元素值:3,1,2 0 0 0 1 2 0 矩阵C5(A×B): 0 0 4 0 // algo5-1.cpp 实现算法5.2的程序 #include"c1.h" typedef int ElemType; #include"c5-2.h" #include"bo5-2.cpp" void FastTransposeSMatrix(TSMatrix M,TSMatrix &T) { // 快速求稀疏矩阵M的转置矩阵T。算法5.2改 int p,q,t,col,*num,*cpot; num=(int *)malloc((M.nu+1)*sizeof(int)); // 存M每列(T每行)非零元素个数([0]不用) cpot=(int *)malloc((M.nu+1)*sizeof(int)); // 存T每行的下1个非零元素的存储位置([0]不用) T.mu=M.nu; // 给T的行、列数与非零元素个数赋值 T.nu=M.mu; T.tu=M.tu; if(T.tu) // 是非零矩阵 { for(col=1;col<=M.nu;++col) num[col]=0; // 计数器初值设为0 for(t=1;t<=M.tu;++t) // 求M中每一列含非零元素个数 ++num[M.data[t].j]; cpot[1]=1; // T的第1行的第1个非零元在T.data中的序号为1 for(col=2;col<=M.nu;++col) cpot[col]=cpot[col-1]+num[col-1]; // 求T的第col行的第1个非零元在T.data中的序号 for(p=1;p<=M.tu;++p) // 从M的第1个元素开始 { col=M.data[p].j; // 求得在M中的列数 q=cpot[col]; // q指示M当前的元素在T中的序号 T.data[q].i=M.data[p].j; T.data[q].j=M.data[p].i; T.data[q].e=M.data[p].e; ++cpot[col]; // T第col行的下1个非零元在T.data中的序号 } } free(num); free(cpot); · 188· 《数据结构》算法实现及解析(第二版) } void main() { TSMatrix A,B; printf("创建矩阵A: "); CreateSMatrix(A); PrintSMatrix1(A); FastTransposeSMatrix(A,B); printf("矩阵B(A的快速转置):\n"); PrintSMatrix1(B); } 程序运行结果: 创建矩阵A: 请输入矩阵的行数,列数,非零元素数:3,2,4 请按行序顺序输入第1个非零元素所在的行(1~3),列(1~2),元素值:1,1,1 请按行序顺序输入第2个非零元素所在的行(1~3),列(1~2),元素值:2,1,2 请按行序顺序输入第3个非零元素所在的行(1~3),列(1~2),元素值:3,1,3 请按行序顺序输入第4个非零元素所在的行(1~3),列(1~2),元素值:3,2,4 1 0 2 0 3 4 矩阵B(A的快速转置): 1 2 3 0 0 4 由于稀疏矩阵的三元组顺序表存储结构要求先按行、同行再按列顺序存储非零元素, 算法 5.1(在 bo5-2.cpp 中)采用了双重循环求转置矩阵,对于外层循环 col(列)的每一个 值,对所有的非零元素,如果其列数与 col 相等,则按顺序存入转秩矩阵。这就保证了转 秩矩阵也是先按行、同行再按列顺序存储非零元素。所以它的时间复杂度为 O(列数×非 零元素数)。而算法 5.2(在 algo5-1.cpp 中)采用了 2 个单循环。第 1 个循环,对所有的非 零元素,计算其所在列并计数,得到每列的非零元素数 num[col]及每列第 1 个非零元素在 转秩矩阵中的存储位置 cpot[col]。第 2 个循环,对所有的非零元素,根据其列数和 cpot[col]的当前值,存入转秩矩阵。由于还要给 num[col]和 cpot[col]赋初值,它的时间复 杂度为 O(列数+非零元素数)。 // c5-3.h 稀疏矩阵的三元组行逻辑链接的顺序表存储表示(见图58) #define MAX_SIZE 100 // 非零元个数的最大值 #define MAX_RC 20 // 最大行列数 struct Triple // 同c5-2.h { int i,j; // 行下标,列下标 ElemType e; // 非零元素值 }; struct RLSMatrix 第 5 章 数组和广义表 · 189· { Triple data[MAX_SIZE+1]; // 非零元三元组表,data[0]未用 int rpos[MAX_RC+1]; // 各行第一个非零元素的位置表,比c5-2.h增加的项 int mu,nu,tu; // 矩阵的行数、列数和非零元个数 }; 三元组行逻辑链接的顺序表存储表示(c5-3.h)比三元组顺序表存储表示(c5-2.h)增加 了 rpos 数组,用以存放各行的第一个非零元素在 data 数组中的位置。这样,就可以迅速地 找到某一行的元素。图 59 是采用三元组行逻辑链接的顺序表存储稀疏矩阵的实例。和 c5-2.h 存储结构一样,创建稀疏矩阵输入非零元时,也要按行、列的顺序由小到大输入。 // bo5-3.cpp 行逻辑链接稀疏矩阵(存储结构由c5-3.h定义)的基本操作(8个),包括算法5.3 Status CreateSMatrix(RLSMatrix &M) { // 创建稀疏矩阵M int i,j; Triple T; Status k; printf("请输入矩阵的行数,列数,非零元素数:"); scanf("%d,%d,%d",&M.mu,&M.nu,&M.tu); if(M.tu>MAX_SIZE||M.mu>MAX_RC) return ERROR; M.data[0].i=0; // 为以下比较做准备 for(i=1;i<=M.tu;i++) { do { printf("请按行序顺序输入第%d个非零元素所在的行(1~%d),列(1~%d),元素值:",i,M.mu,M.nu); 图 59 采用三元组行逻辑链接 存储稀疏矩阵的实例 3 4 5 1 3 5 [0] [1] [2] [3] [4] [MAX_RC] 1 1 1 1 3 2 2 2 3 2 4 4 3 3 5 [0] [1] [2] [3] [4] [5] [MAX_SIZE] (a) 稀疏矩阵 (b) 存储表示           0500 4030 0201     图 58 三元组行逻辑链接 顺序表存储结构 行数 列数 非 0 元个数 mu nu tu [0] [1] [mu] [mu+1] [MAX_RC] rpos 有值 空 i j e TripleRLSMatrix [0] [1] [tu] [tu+1] [MAX_SIZE] 有值 空 i j e         · 190· 《数据结构》算法实现及解析(第二版) scanf("%d,%d,%d",&T.i,&T.j,&T.e); k=0; if(T.i<1||T.i>M.mu||T.j<1||T.j>M.nu) // 行、列超出范围 k=1; if(T.i=1;i--) // 计算rpos[] { M.rpos[i]=1; // 赋初值1 for(j=i-1;j>=1;j--) M.rpos[i]+=M.rpos[j]; } return OK; } void DestroySMatrix(RLSMatrix &M) { // 销毁稀疏矩阵M(使M为0行0列0个非零元素的矩阵) M.mu=M.nu=M.tu=0; } void PrintSMatrix(RLSMatrix M) { // 输出稀疏矩阵M int i; printf("%d行%d列%d个非零元素。\n",M.mu,M.nu,M.tu); printf("行 列 元素值\n"); for(i=1;i<=M.tu;i++) printf("%2d%4d%8d\n",M.data[i].i,M.data[i].j,M.data[i].e); for(i=1;i<=M.mu;i++) printf("第%d行的第一个非零元素是本矩阵第%d个元素\n",i,M.rpos[i]); } void PrintSMatrix1(RLSMatrix M) { // 按矩阵形式输出M int i,j,k=1; Triple *p=M.data; p++; // p指向第1个非零元素 for(i=1;i<=M.mu;i++) { for(j=1;j<=M.nu;j++) if(k<=M.tu&&p->i==i&&p->j==j) // p指向非零元,且p所指元素为当前处理元素 { printf("%3d",p->e); // 输出p所指元素的值 p++; // p指向下一个元素 k++; // 计数器+1 } else // p所指元素不是当前处理元素 printf("%3d",0); // 输出0 第 5 章 数组和广义表 · 191· printf("\n"); } } void CopySMatrix(RLSMatrix M,RLSMatrix &T) { // 由稀疏矩阵M复制得到T T=M; } Status AddSMatrix(RLSMatrix M,RLSMatrix N,RLSMatrix &Q) { // 求稀疏矩阵的和Q=M+N int k,p,q,tm,tn; if(M.mu!=N.mu||M.nu!=N.nu) return ERROR; Q.mu=M.mu; // Q矩阵行数 Q.nu=M.nu; // Q矩阵列数 Q.tu=0; // Q矩阵非零元素数初值 for(k=1;k<=M.mu;++k) // 对于每一行,k指示行号 { Q.rpos[k]=Q.tu+1; // Q矩阵第k行的第1个元素的位置 p=M.rpos[k]; // p指示M矩阵第k行当前元素的序号 q=N.rpos[k]; // q指示N矩阵第k行当前元素的序号 if(k==M.mu) // 是最后一行 { tm=M.tu+1; // tm,tn分别是p,q的上界 tn=N.tu+1; } else { tm=M.rpos[k+1]; tn=N.rpos[k+1]; } while(pN矩阵当前元素的列 Q.data[++Q.tu]=N.data[q++]; // 将N的当前值赋给Q while(pMAX_SIZE) · 192· 《数据结构》算法实现及解析(第二版) return ERROR; else return OK; } Status SubtSMatrix(RLSMatrix M,RLSMatrix N,RLSMatrix &Q) { // 求稀疏矩阵的差Q=M-N int i; if(M.mu!=N.mu||M.nu!=N.nu) return ERROR; for(i=1;i<=N.tu;++i) // 对于N的每一元素,其值乘以-1 N.data[i].e*=-1; AddSMatrix(M,N,Q); // Q=M+(-N) return OK; } Status MultSMatrix(RLSMatrix M,RLSMatrix N,RLSMatrix &Q) { // 求稀疏矩阵乘积Q=M×N。算法5.3改 int arow,brow,p,q,ccol,ctemp[MAX_RC+1],t,tp; if(M.nu!=N.mu) // 矩阵M的列数应和矩阵N的行数相等 return ERROR; Q.mu=M.mu; // Q初始化 Q.nu=N.nu; Q.tu=0; if(M.tu*N.tu==0) // M和N至少有一个是零矩阵 return ERROR; for(arow=1;arow<=M.mu;++arow) { // 从M的第一行开始,到最后一行,arow是M的当前行 for(ccol=1;ccol<=Q.nu;++ccol) ctemp[ccol]=0; // Q的当前行的各列元素累加器清零 Q.rpos[arow]=Q.tu+1; // Q当前行的第1个元素位于上1行最后1个元素之后 if(arowMAX_SIZE) return ERROR; 第 5 章 数组和广义表 · 193· Q.data[Q.tu].i=arow; Q.data[Q.tu].j=ccol; Q.data[Q.tu].e=ctemp[ccol]; } } return OK; } void TransposeSMatrix(RLSMatrix M,RLSMatrix &T) { // 求稀疏矩阵M的转置矩阵T int p,q,t,col,*num; num=(int *)malloc((M.nu+1)*sizeof(int)); T.mu=M.nu; T.nu=M.mu; T.tu=M.tu; if(T.tu) { for(col=1;col<=M.nu;++col) num[col]=0; // 设初值 for(t=1;t<=M.tu;++t) // 求M中每一列非零元个数 ++num[M.data[t].j]; T.rpos[1]=1; for(col=2;col<=M.nu;++col) // 求M中第col中第一个非零元在T.data中的序号 T.rpos[col]=T.rpos[col-1]+num[col-1]; for(col=1;col<=M.nu;++col) num[col]=T.rpos[col]; for(p=1;p<=M.tu;++p) { col=M.data[p].j; q=num[col]; T.data[q].i=M.data[p].j; T.data[q].j=M.data[p].i; T.data[q].e=M.data[p].e; ++num[col]; } } free(num); } // main5-3.cpp 检验bo5-3.cpp的主程序(与main5-2.cpp很相像) #include"c1.h" typedef int ElemType; #include"c5-3.h" // 此行与main5-2.cpp不同 #include"bo5-3.cpp" // 此行与main5-2.cpp不同 void main() { RLSMatrix A,B,C; // 此行与main5-2.cpp不同 printf("创建矩阵A: "); CreateSMatrix(A); PrintSMatrix(A); printf("由矩阵A复制矩阵B:\n"); CopySMatrix(A,B); · 194· 《数据结构》算法实现及解析(第二版) PrintSMatrix1(B); DestroySMatrix(B); printf("销毁矩阵B后:\n"); PrintSMatrix1(B); printf("创建矩阵B2:(与矩阵A的行、列数相同,行、列分别为%d,%d)\n",A.mu,A.nu); CreateSMatrix(B); PrintSMatrix1(B); AddSMatrix(A,B,C); printf("矩阵C1(A+B):\n"); PrintSMatrix1(C); SubtSMatrix(A,B,C); printf("矩阵C2(A-B):\n"); PrintSMatrix1(C); TransposeSMatrix(A,C); printf("矩阵C3(A的转置):\n"); PrintSMatrix1(C); printf("创建矩阵A2:\n"); CreateSMatrix(A); PrintSMatrix1(A); printf("创建矩阵B3:(行数应与矩阵A2的列数相同=%d)\n",A.nu); CreateSMatrix(B); PrintSMatrix1(B); MultSMatrix(A,B,C); printf("矩阵C5(A×B):\n"); PrintSMatrix1(C); } 程序运行结果: 创建矩阵A: 请输入矩阵的行数,列数,非零元素数:3,3,2 请按行序顺序输入第1个非零元素所在的行(1~3),列(1~3),元素值:1,2,1 请按行序顺序输入第2个非零元素所在的行(1~3),列(1~3),元素值:2,2,2 3行3列2个非零元素。(见图57) 行 列 元素值 1 2 1 2 2 2 第1行的第一个非零元素是本矩阵第1个元素 第2行的第一个非零元素是本矩阵第2个元素 第3行的第一个非零元素是本矩阵第3个元素 由矩阵A复制矩阵B: 0 1 0 0 2 0 0 0 0 销毁矩阵B后: 创建矩阵B2:(与矩阵A的行、列数相同,行、列分别为3,3) 请输入矩阵的行数,列数,非零元素数:3,3,1 请按行序顺序输入第1个非零元素所在的行(1~3),列(1~3),元素值:1,2,1 0 1 0 0 0 0 第 5 章 数组和广义表 · 195· 0 0 0 矩阵C1(A+B): 0 2 0 0 2 0 0 0 0 矩阵C2(A-B): 0 0 0 0 2 0 0 0 0 矩阵C3(A的转置): 0 0 0 1 2 0 0 0 0 创建矩阵A2: 请输入矩阵的行数,列数,非零元素数:2,3,2 请按行序顺序输入第1个非零元素所在的行(1~2),列(1~3),元素值:1,1,1 请按行序顺序输入第2个非零元素所在的行(1~2),列(1~3),元素值:2,3,2 1 0 0 0 0 2 创建矩阵B3:(行数应与矩阵A2的列数相同=3) 请输入矩阵的行数,列数,非零元素数:3,2,2 请按行序顺序输入第1个非零元素所在的行(1~3),列(1~2),元素值:2,2,1 请按行序顺序输入第2个非零元素所在的行(1~3),列(1~2),元素值:3,1,2 0 0 0 1 2 0 矩阵C5(A×B): 0 0 4 0 // c5-4.h 稀疏矩阵的十字链表存储表示(见图510) struct OLNode { int i,j; // 该非零元的行和列下标 ElemType e; // 非零元素值 OLNode *right,*down; // 该非零元所在行表和列表的后继链域 }; typedef OLNode *OLink; struct CrossList { OLink *rhead,*chead; // 行和列链表头指针向量基址,由CreatSMatrix_OL()分配 int mu,nu,tu; // 稀疏矩阵的行数、列数和非零元个数 }; 图 511 是采用十字链表存储稀疏矩阵的实例(教科书图 5.6)。由于十字链表存储结 构中的非零元素是按其所在行、列插入相应的链表的,所以,在创建稀疏矩阵输入非零元 时,可以按任意顺序输入非零元素。每个非零元结点按升序被插入到两个没有头结点的单 链表中:一个是所在行链表;另一个是所在列链表。当插入或删除结点时,只要修改相关 图 510 稀疏矩阵的十字链表存储结构 CrossList rhead chead mu nu tu OLink OLink OLink i j e down right OLNode OLNode OLNode · 196· 《数据结构》算法实现及解析(第二版) 的行、列链表即可,比较灵活。 // bo5-4.cpp 稀疏矩阵的十字链表存储(存储结构由c5-4.h定义)的基本操作(9个),包括算法5.4 void InitSMatrix(CrossList &M) { // 初始化M(CrossList类型的变量必须初始化,否则创建、复制矩阵将出错)。加(见图512) M.rhead=M.chead=NULL; M.mu=M.nu=M.tu=0; } void InitSMatrixList(CrossList &M) { // 初始化十字链表表头指针向量。加(见图513) int i; if(!(M.rhead=(OLink*)malloc((M.mu+1)*sizeof(OLink)))) // 生成行表头指针向量 exit(OVERFLOW); if(!(M.chead=(OLink*)malloc((M.nu+1)*sizeof(OLink)))) // 生成列表头指针向量 exit(OVERFLOW); for(i=1;i<=M.mu;i++) // 初始化矩阵T的行表头指针向量,各行链表为空 M.rhead[i]=NULL; for(i=1;i<=M.nu;i++) // 初始化矩阵T的列表头指针向量,各列链表为空 M.chead[i]=NULL; } void InsertAscend(CrossList &M,OLink p) { // 初始条件:稀疏矩阵M存在,p指向的结点存在。 // 操作结果:按行列升序将p所指结点插入M(见图514) 图 511 采用十字链表存储稀疏矩阵的示例 (a) 稀疏矩阵 (b) 存储表示           − 0002 0010 5003 NULL M 3 4 4 chead rhead mu nu tu 1 4 5 NULLNULL 2 2 -1 NULLNULL 3 1 2 NULLNULL 1 1 3 图 512 初始化和销 毁稀疏矩阵 NULL NULL 0 0 0 图 513 调用 InitSMatrixList()的示例 NULLNULLNULLNULL M 3 4 chead rhead mu nu tu NULLNULLNULL 第 5 章 数组和广义表 · 197· OLink q=M.rhead[p->i]; // q指向待插行表 if(!q||p->jj) // 待插的行表空或p所指结点的列值小于首结点的列值 { p->right=M.rhead[p->i]; // 插在表头 M.rhead[p->i]=p; } else { while(q->right&&q->right->jj)//q所指不是尾结点且q的下一结点的列值小于p所指结点的列值 q=q->right; // q向后移 p->right=q->right; // 将p插在q所指结点之后 q->right=p; } q=M.chead[p->j]; // q指向待插列表 if(!q||p->ii) // 待插的列表空或p所指结点的行值小于首结点的行值 { p->down=M.chead[p->j]; // 插在表头 M.chead[p->j]=p; } else { while(q->down&&q->down->ii) //q所指不是尾结点且q的下一结点的行值小于p所指结点的行值 q=q->down; // q向下移 p->down=q->down; // 将p插在q所指结点之下 图 514 按升序将 p 所指结点插入十字链表存储稀疏矩阵 M 示例 (a) 插入前 NULL M 3 4 4 chead rhead mu nu tu 1 4 5 NULLNULL 2 2 -1 NULLNULL 3 1 2 NULLNULL 1 1 3 p 1 3 7 (b) 插入后 M 3 4 4 chead rhead mu nu tu 1 4 5 NULLNULL 2 2 -1 NULLNULL 3 1 2 NULLNULL 1 1 3 1 3 7 NULL · 198· 《数据结构》算法实现及解析(第二版) q->down=p; } M.tu++; } void DestroySMatrix(CrossList &M) { // 初始条件:稀疏矩阵M存在。操作结果:销毁稀疏矩阵M(见图512) int i; OLink p,q; for(i=1;i<=M.mu;i++) // 按行释放结点 { p=*(M.rhead+i); while(p) { q=p; p=p->right; free(q); } } free(M.rhead); free(M.chead); InitSMatrix(M); } void CreateSMatrix(CrossList &M) { // 创建稀疏矩阵M,采用十字链表存储表示。算法5.4改 int i,k; OLink p; if(M.rhead) DestroySMatrix(M); printf("请输入稀疏矩阵的行数 列数 非零元个数: "); scanf("%d%d%d",&M.mu,&M.nu,&i); InitSMatrixList(M); // 初始化M的表头指针向量 printf("请按任意次序输入%d个非零元的行 列 元素值:\n",M.tu); for(k=0;ki,&p->j,&p->e); // 给结点的3个成员赋值 InsertAscend(M,p); // 将结点p按行列值升序插到矩阵M中 } } void PrintSMatrix(CrossList M) { // 初始条件:稀疏矩阵M存在。操作结果:按行或按列输出稀疏矩阵M int i,j; OLink p; printf("%d行%d列%d个非零元素\n",M.mu,M.nu,M.tu); printf("请输入选择(1.按行输出 2.按列输出): "); scanf("%d",&i); switch(i) { 第 5 章 数组和广义表 · 199· case 1: for(j=1;j<=M.mu;j++) { p=M.rhead[j]; while(p) { cout<i<<"行"<j<<"列值为"<e<right; } } break; case 2: for(j=1;j<=M.nu;j++) { p=M.chead[j]; while(p) { cout<i<<"行"<j<<"列值为"<e<down; } } } } void PrintSMatrix1(CrossList M) { // 按矩阵形式输出M int i,j; OLink p; for(i=1;i<=M.mu;i++) { // 从第1行到最后1行 p=M.rhead[i]; // p指向该行的第1个非零元素 for(j=1;j<=M.nu;j++) // 从第1列到最后1列 if(!p||p->j!=j) // 已到该行表尾或当前结点的列值不等于当前列值 printf("%-3d",0); // 输出0 else { printf("%-3d",p->e); p=p->right; } printf("\n"); } } void CopySMatrix(CrossList M,CrossList &T) { // 初始条件:稀疏矩阵M存在。操作结果:由稀疏矩阵M复制得到T int i; OLink p,q; if(T.rhead) // 矩阵T存在 DestroySMatrix(T); T.mu=M.mu; T.nu=M.nu; InitSMatrixList(T); // 初始化T的表头指针向量 for(i=1;i<=M.mu;i++) // 按行复制 { · 200· 《数据结构》算法实现及解析(第二版) p=M.rhead[i]; // p指向i行链表头 while(p) // 没到行尾 { if(!(q=(OLNode*)malloc(sizeof(OLNode)))) // 生成结点q exit(OVERFLOW); *q=*p; // 给结点q赋值 InsertAscend(T,q); // 将结点q按行列值升序插到矩阵T中 p=p->right; } } } int comp(int c1,int c2) { // AddSMatrix函数要用到,另加 if(c1j,pn->j)) { case -1: *pq=*pm; // M的列right; // 指针向后移 break; case 0: *pq=*pm; // M、N矩阵的列相等,元素值相加 pq->e+=pn->e; if(pq->e!=0) // 和为非零元素 InsertAscend(Q,pq); // 将结点pq按行列值升序插到矩阵Q中 else 第 5 章 数组和广义表 · 201· free(pq); // 释放结点 pm=pm->right; // 指针向后移 pn=pn->right; break; case 1: *pq=*pn; // M的列>N的列,将矩阵N的当前元素值赋给pq InsertAscend(Q,pq); // 将结点pq按行列值升序插到矩阵Q中 pn=pn->right; // 指针向后移 } } while(pm) // pn=NULL { pq=(OLink)malloc(sizeof(OLNode)); // 生成矩阵Q的结点 *pq=*pm; // M的列right; // 指针向后移 } while(pn) // pm=NULL { pq=(OLink)malloc(sizeof(OLNode)); // 生成矩阵Q的结点 *pq=*pn; // M的列>N的列,将矩阵N的当前元素值赋给pq InsertAscend(Q,pq); // 将结点pq按行列值升序插到矩阵Q中 pn=pn->right; // 指针向后移 } } if(Q.tu==0) // Q矩阵元素个数为0 DestroySMatrix(Q); // 销毁矩阵Q } void SubtSMatrix(CrossList M,CrossList N,CrossList &Q) { // 初始条件:稀疏矩阵M与N的行数和列数对应相等。操作结果:求稀疏矩阵的差Q=M-N int i; OLink pq,pm,pn; if(M.mu!=N.mu||M.nu!=N.nu) { printf("两个矩阵不是同类型的,不能相减\n"); exit(OVERFLOW); } Q.mu=M.mu; // 初始化Q矩阵 Q.nu=M.nu; Q.tu=0; // Q矩阵元素个数的初值为0 InitSMatrixList(Q); // 初始化Q的表头指针向量 for(i=1;i<=M.mu;i++) // 按行的顺序相减 { pm=M.rhead[i]; // pm指向矩阵M的第i行的第1个结点 pn=N.rhead[i]; // pn指向矩阵N的第i行的第1个结点 while(pm&&pn) // pm和pn均不空 { pq=(OLink)malloc(sizeof(OLNode)); // 生成矩阵Q的结点 switch(comp(pm->j,pn->j)) { case -1: *pq=*pm; // M的列right; // 指针向后移 break; case 0: *pq=*pm; // M、N矩阵的列相等,元素值相减 pq->e-=pn->e; if(pq->e!=0) // 差为非零元素 InsertAscend(Q,pq); // 将结点pq按行列值升序插到矩阵Q中 else free(pq); // 释放结点 pm=pm->right; // 指针向后移 pn=pn->right; break; case 1: *pq=*pn; // M的列>N的列,将矩阵N的当前元素值赋给pq pq->e*=-1; // 求反 InsertAscend(Q,pq); // 将结点pq按行列值升序插到矩阵Q中 pn=pn->right; // 指针向后移 } } while(pm) // pn=NULL { pq=(OLink)malloc(sizeof(OLNode)); // 生成矩阵Q的结点 *pq=*pm; // M的列right; // 指针向后移 } while(pn) // pm=NULL { pq=(OLink)malloc(sizeof(OLNode)); // 生成矩阵Q的结点 *pq=*pn; // M的列>N的列,将矩阵N的当前元素值赋给pq pq->e*=-1; // 求反 InsertAscend(Q,pq); // 将结点pq按行列值升序插到矩阵Q中 pn=pn->right; // 指针向后移 } } if(Q.tu==0) // Q矩阵元素个数为0 DestroySMatrix(Q); // 销毁矩阵Q } void MultSMatrix(CrossList M,CrossList N,CrossList &Q) { // 初始条件:稀疏矩阵M的列数等于N的行数。操作结果:求稀疏矩阵乘积Q=M×N int i,j,e; OLink pq,pm,pn; InitSMatrix(Q); Q.mu=M.mu; Q.nu=N.nu; Q.tu=0; InitSMatrixList(Q); // 初始化Q的表头指针向量 for(i=1;i<=Q.mu;i++) for(j=1;j<=Q.nu;j++) { pm=M.rhead[i]; 第 5 章 数组和广义表 · 203· pn=N.chead[j]; e=0; while(pm&&pn) switch(comp(pn->i,pm->j)) { case -1: pn=pn->down; // 列指针后移 break; case 0: e+=pm->e*pn->e; // 乘积累加 pn=pn->down; // 行列指针均后移 pm=pm->right; break; case 1: pm=pm->right; // 行指针后移 } if(e) // 值不为0 { pq=(OLink)malloc(sizeof(OLNode)); // 生成结点 if(!pq) // 生成结点失败 exit(OVERFLOW); pq->i=i; // 给结点赋值 pq->j=j; pq->e=e; InsertAscend(Q,pq); // 将结点pq按行列值升序插到矩阵Q中 } } if(Q.tu==0) // Q矩阵元素个数为0 DestroySMatrix(Q); // 销毁矩阵Q } void TransposeSMatrix(CrossList M,CrossList &T) { // 初始条件:稀疏矩阵M存在。操作结果:求稀疏矩阵M的转置矩阵T int u,i; OLink *head,p,q,r; CopySMatrix(M,T); // T=M u=T.mu; // 交换T.mu和T.nu T.mu=T.nu; T.nu=u; head=T.rhead; // 交换T.rhead和T.chead T.rhead=T.chead; T.chead=head; for(u=1;u<=T.mu;u++) // 对T的每一行 { p=T.rhead[u]; // p为行表头 while(p) // 没到表尾,对T的每一结点 { q=p->down; // q指向下一个结点 i=p->i; // 交换.i和.j p->i=p->j; p->j=i; r=p->down; // 交换.down和.right p->down=p->right; p->right=r; · 204· 《数据结构》算法实现及解析(第二版) p=q; // p指向下一个结点 } } } // main5-4.cpp 检验bo5-4.cpp的主程序 #include"c1.h" typedef int ElemType; #include"c5-4.h" #include"bo5-4.cpp" void main() { CrossList A,B,C; InitSMatrix(A); // CrossList类型的变量在初次使用之前必须初始化 InitSMatrix(B); printf("创建矩阵A: "); CreateSMatrix(A); PrintSMatrix(A); printf("由矩阵A复制矩阵B: "); CopySMatrix(A,B); PrintSMatrix(B); DestroySMatrix(B); // CrossList类型的变量在再次使用之前必须先销毁 printf("销毁矩阵B后,矩阵B为\n"); PrintSMatrix1(B); printf("创建矩阵B2:(与矩阵A的行、列数相同,行、列分别为%d,%d)\n",A.mu,A.nu); CreateSMatrix(B); PrintSMatrix1(B); printf("矩阵C1(A+B):\n"); AddSMatrix(A,B,C); PrintSMatrix1(C); DestroySMatrix(C); printf("矩阵C2(A-B):\n"); SubtSMatrix(A,B,C); PrintSMatrix1(C); DestroySMatrix(C); printf("矩阵C3(A的转置):\n"); TransposeSMatrix(A,C); PrintSMatrix1(C); DestroySMatrix(A); DestroySMatrix(B); DestroySMatrix(C); printf("创建矩阵A2: "); CreateSMatrix(A); PrintSMatrix1(A); printf("创建矩阵B3:(行数应与矩阵A2的列数相同=%d)\n",A.nu); CreateSMatrix(B); PrintSMatrix1(B); printf("矩阵C5(A×B):\n"); MultSMatrix(A,B,C); PrintSMatrix1(C); 第 5 章 数组和广义表 · 205· DestroySMatrix(A); DestroySMatrix(B); DestroySMatrix(C); } 程序运行结果: 创建矩阵A: 请输入稀疏矩阵的行数 列数 非零元个数: 3 3 2 请按任意次序输入2个非零元的行 列 元素值: 2 1 2 1 2 1 3行3列2个非零元素(见图515) 请输入选择(1.按行输出 2.按列输出): 1 1行2列值为1 2行1列值为2 由矩阵A复制矩阵B: 3行3列2个非零元素 请输入选择(1.按行输出 2.按列输出): 2 2行1列值为2 1行2列值为1 销毁矩阵B后,矩阵B为 创建矩阵B2:(与矩阵A的行、列数相同,行、列分别为3,3) 请输入稀疏矩阵的行数 列数 非零元个数: 3 3 2 请按任意次序输入2个非零元的行 列 元素值: 2 2 5 1 2 -1 0 -1 0 0 5 0 0 0 0 矩阵C1(A+B): 0 0 0 2 5 0 0 0 0 矩阵C2(A-B): 0 2 0 2 -5 0 0 0 0 矩阵C3(A的转置): 0 2 0 1 0 0 0 0 0 创建矩阵A2: 请输入稀疏矩阵的行数 列数 非零元个数: 2 3 2 请按任意次序输入2个非零元的行 列 元素值: 1 1 1 2 3 2 1 0 0 0 0 2 创建矩阵B3:(行数应与矩阵A2的列数相同=3) 请输入稀疏矩阵的行数 列数 非零元个数: 3 2 2 请按任意次序输入2个非零元的行 列 元素值: 图 515 矩阵 A 图示           000 002 010 · 206· 《数据结构》算法实现及解析(第二版) 2 2 1 3 1 2 0 0 0 1 2 0 矩阵C5(A×B): 0 0 4 0 5.4 广义表的定义 广义表,顾名思义,不是真正的线性表。它的结构更象第 6 章介绍的树结构,它的许 多基本操作都是采用递归算法的。另外,还要定义如何表示一个广义表,也就是广义表的 书写形式。本书采用的书写形式是字符串。算法 5.7(在 bo5-5.cpp 和 bo5-6.cpp 中)和算 法 5.8(在 func5-1.cpp 中)将广义表的字符串书写形式转换为广义表的存储结构。 5.5 广义表的存储结构 // c5-5.h 广义表的头尾链表存储结构(见图516) enum ElemTag{ATOM,LIST}; // ATOM==0:原子,LIST==1:子表 typedef struct GLNode { ElemTag tag; // 公共部分,用于区分原子结点和表结点 union // 原子结点和表结点的联合部分 { AtomType atom; // atom是原子结点的值域,AtomType由用户定义 struct { GLNode *hp,*tp; }ptr; // ptr是表结点的指针域,prt.hp和ptr.tp分别指向表头和表尾 }; }*GList,GLNode; // 广义表类型 图 517 是根据 c5-5.h 定义的广义表(a,(b,c,d))的存储结构。它的长度为 2,第 1 个元素为原子 a,第 2 个元素为子表(b,c,d)。 图 516 广义表的头尾链表存储结构 GLNode GLNode GLNode(子表) 1 ptr.hp ptr.tp GList GLNode(原子) 0 atom GList 图 517 广义表(a,(b,c,d))的头尾链表存储结构 GList 1 NULL 0 a 1 1 NULL 0 d0 b 1 0 c 1 第 5 章 数组和广义表 · 207· // c5-6.h 广义表的扩展线性链表存储表示(见图518) enum ElemTag{ATOM,LIST}; // ATOM==0:原子,LIST==1:子表 typedef struct GLNode1 { ElemTag tag; // 公共部分,用于区分原子结点和表结点 union // 原子结点和表结点的联合部分 { AtomType atom; // 原子结点的值域 GLNode1 *hp; // 表结点的表头指针 }; GLNode1 *tp; // 相当于线性链表的next,指向下一个元素结点 }*GList1,GLNode1; // 广义表类型GList1是一种扩展的线性链表 为了和 c5-5.h 定义的存储结构相区别,令 c5-6.h 定义的结构类型名为 GList1 和 GLNode1。 图 519 是根据 c5-6.h 定义的广义表(a,(b,c,d))的扩展线性链表存储结构。在这种 结构中,广义表的头指针所指结点的 tag 域值总是 1(表),其 tp 域总是 NULL。这样看 来,广义表的头指针所指结点相当于表的头结点。和图 517 相比,图 519 这种结构更 简洁些。 5.6 m 元多项式的表示 5.7 广义表的递归算法 5.7.1  算法 5.5 在 bo5-5.cpp 中。 图 518 广义表的扩展线性链表存储结构 GList1 GLNode1(原子) 0 atom tp GLNode1 GLNode1(子表) 1 hp tp GList1 GLNode1 GLNode1 图 519 广义表(a,(b,c,d))的扩展线性链表存储结构 1 NULL 0 a 1 NULL 0 d NULL0 c0 b GList1 相当于 头结点 · 208· 《数据结构》算法实现及解析(第二版) 5.7.2  算法 5.6 在 bo5-5.cpp 中。 5.7.3   // func5-1.cpp 广义表的书写形式串为SString类型,包括算法5.8。bo5-5.cpp和bo5-6.cpp调用 #include"c4-1.h" // 定义SString类型 #include"bo4-1.cpp" // SString类型的基本操作 void sever(SString str,SString hstr) // 算法5.8改。SString是数组,不需引用类型 { // 将非空串str分割成两部分:hstr为第一个′,′之前的子串,str为之后的子串 int n,k,i; // k记尚未配对的左括号个数 SString ch,c1,c2,c3; n=StrLength(str); // n为串str的长度 StrAssign(c1,","); // c1=′,′ StrAssign(c2,"("); // c2=′(′ StrAssign(c3,")"); // c3=′)′ SubString(ch,str,1,1); // ch为串str的第1个字符 for(i=1,k=0;i<=n&&StrCompare(ch,c1)||k!=0;++i) // i小于串长且ch不是’,’ { // 搜索最外层的第一个逗号 SubString(ch,str,i,1); // ch为串str的第i个字符 if(!StrCompare(ch,c2)) // ch=′(′ ++k; // 左括号个数+1 else if(!StrCompare(ch,c3)) // ch=′)′ --k; // 左括号个数-1 } if(i<=n) // 串str中存在′,′,它是第i-1个字符 { SubString(hstr,str,1,i-2); // hstr返回串str′,′前的字符 SubString(str,str,i,n-i+1); // str返回串str′,′后的字符 } else // 串str中不存在′,′ { StrCopy(hstr,str); // 串hstr就是串str ClearString(str); // ′,′后面是空串 } } // bo5-5.cpp 广义表的头尾链表存储(存储结构由c5-5.h定义)的基本操作(11个),包括算法5.5,5.6,5.7 #include"func5-1.cpp" // 算法5.8 void InitGList(GList &L) { // 创建空的广义表L L=NULL; } void CreateGList(GList &L,SString S) // 算法5.7 { // 采用头尾链表存储结构,由广义表的书写形式串S创建广义表L。设emp="()" SString sub,hsub,emp; GList p,q; StrAssign(emp,"()"); // 空串emp="()" if(!StrCompare(S,emp)) // S="()" L=NULL; // 创建空表 第 5 章 数组和广义表 · 209· else // S不是空串 { if(!(L=(GList)malloc(sizeof(GLNode)))) // 建表结点 exit(OVERFLOW); if(StrLength(S)==1) // S为单原子,只会出现在递归调用中 { L->tag=ATOM; L->atom=S[1]; // 创建单原子广义表 } else // S为表 { L->tag=LIST; p=L; SubString(sub,S,2,StrLength(S)-2); // 脱外层括号(去掉第1个字符和最后1个字符)给串sub do { // 重复建n个子表 sever(sub,hsub); // 从sub中分离出表头串hsub CreateGList(p->ptr.hp,hsub); q=p; if(!StrEmpty(sub)) // 表尾不空 { if(!(p=(GLNode *)malloc(sizeof(GLNode)))) exit(OVERFLOW); p->tag=LIST; q->ptr.tp=p; } }while(!StrEmpty(sub)); q->ptr.tp=NULL; } } } void DestroyGList(GList &L) { // 销毁广义表L GList q1,q2; if(L) { if(L->tag==LIST) // 删除表结点 { q1=L->ptr.hp; // q1指向表头 q2=L->ptr.tp; // q2指向表尾 DestroyGList(q1); // 销毁表头 DestroyGList(q2); // 销毁表尾 } free(L); L=NULL; } } void CopyGList(GList &T,GList L) { // 采用头尾链表存储结构,由广义表L复制得到广义表T。算法5.6 if(!L) // 复制空表 T=NULL; · 210· 《数据结构》算法实现及解析(第二版) else { T=(GList)malloc(sizeof(GLNode)); // 建表结点 if(!T) exit(OVERFLOW); T->tag=L->tag; if(L->tag==ATOM) T->atom=L->atom; // 复制单原子 else { CopyGList(T->ptr.hp,L->ptr.hp); // 递归复制子表 CopyGList(T->ptr.tp,L->ptr.tp); } } } int GListLength(GList L) { // 返回广义表的长度,即元素个数 int len=0; while(L) { L=L->ptr.tp; len++; } return len; } int GListDepth(GList L) { // 采用头尾链表存储结构,求广义表L的深度。算法5.5 int max,dep; GList pp; if(!L) return 1; // 空表深度为1 if(L->tag==ATOM) return 0; // 原子深度为0,只会出现在递归调用中 for(max=0,pp=L;pp;pp=pp->ptr.tp) { dep=GListDepth(pp->ptr.hp); // 递归求以pp->ptr.hp为头指针的子表深度 if(dep>max) max=dep; } return max+1; // 非空表的深度是各元素的深度的最大值加1 } Status GListEmpty(GList L) { // 判定广义表是否为空 if(!L) return TRUE; else return FALSE; } GList GetHead(GList L) { // 生成广义表L的表头元素,返回指向这个元素的指针 GList h,p; 第 5 章 数组和广义表 · 211· if(!L) // 空表无表头 return NULL; p=L->ptr.hp; // p指向L的表头元素 CopyGList(h,p); // 将表头元素复制给h return h; } GList GetTail(GList L) { // 将广义表L的表尾生成为广义表,返回指向这个新广义表的指针 GList t; if(!L) // 空表无表尾 return NULL; CopyGList(t,L->ptr.tp); // 将L的表尾拷给t return t; } void InsertFirst_GL(GList &L,GList e) { // 初始条件:广义表存在。操作结果:插入元素e(也可能是子表)作为广义表L的第1元素(表头) GList p=(GList)malloc(sizeof(GLNode)); // 生成新结点 if(!p) exit(OVERFLOW); p->tag=LIST; // 结点的类型是表 p->ptr.hp=e; // 表头指向e p->ptr.tp=L; // 表尾指向原表L L=p; // L指向新结点 } void DeleteFirst_GL(GList &L,GList &e) { // 初始条件:广义表L存在。操作结果:删除广义表L的第一元素,并用e返回其值 GList p=L; // p指向第1个结点 e=L->ptr.hp; // e指向L的表头 L=L->ptr.tp; // L指向原L的表尾 free(p); // 释放第1个结点 } void Traverse_GL(GList L,void(*v)(AtomType)) { // 利用递归算法遍历广义表L if(L) // L不空 if(L->tag==ATOM) // L为单原子 v(L->atom); else // L为广义表 { Traverse_GL(L->ptr.hp,v); // 递归遍历L的表头 Traverse_GL(L->ptr.tp,v); // 递归遍历L的表尾 } } // main5-5.cpp 检验bo5-5.cpp的主程序 #include"c1.h" typedef char AtomType; // 定义原子类型为字符型 #include"c5-5.h" // 定义广义表的头尾链表存储 #include"bo5-5.cpp" void visit(AtomType e) { printf("%c ", e); · 212· 《数据结构》算法实现及解析(第二版) } void main() { char p[80]; SString t; GList l,m; InitGList(l); InitGList(m); printf("空广义表l的深度=%d l是否空?%d(1:是 0:否)\n",GListDepth(l),GListEmpty(l)); printf("请输入广义表l(书写形式:空表:(),单原子:(a),其它:(a,(b),c)):\n"); gets(p); StrAssign(t,p); CreateGList(l,t); printf("广义表l的长度=%d\n",GListLength(l)); printf("广义表l的深度=%d l是否空?%d(1:是 0:否)\n",GListDepth(l),GListEmpty(l)); printf("遍历广义表l:\n"); Traverse_GL(l,visit); printf("\n复制广义表m=l\n"); CopyGList(m,l); printf("广义表m的长度=%d\n",GListLength(m)); printf("广义表m的深度=%d\n",GListDepth(m)); printf("遍历广义表m:\n"); Traverse_GL(m,visit); DestroyGList(m); m=GetHead(l); printf("\nm是l的表头元素,遍历m:\n"); Traverse_GL(m,visit); DestroyGList(m); m=GetTail(l); printf("\nm是由l的表尾形成的广义表,遍历广义表m:\n"); Traverse_GL(m,visit); InsertFirst_GL(m,l); printf("\n插入广义表l为m的表头,遍历广义表m:\n"); Traverse_GL(m,visit); printf("\n删除m的表头,遍历广义表m:\n"); DestroyGList(l); DeleteFirst_GL(m,l); Traverse_GL(m,visit); printf("\n"); DestroyGList(m); } 程序运行结果(以教科书图 5.7 为例): 空广义表l的深度=1 l是否空?1(1:是 0:否) 请输入广义表l(书写形式:空表:(),单原子:(a),其它:(a,(b),c)): ((),(e),(a,(b,c,d))) 广义表l的长度=3 广义表l的深度=3 l是否空?0(1:是 0:否) 第 5 章 数组和广义表 · 213· 遍历广义表l: e a b c d 复制广义表m=l 广义表m的长度=3 广义表m的深度=3 遍历广义表m: e a b c d m是l的表头元素,遍历m: m是由l的表尾形成的广义表,遍历广义表m: e a b c d 插入广义表l为m的表头,遍历广义表m: e a b c d e a b c d 删除m的表头,遍历广义表m: e a b c d // bo5-6.cpp 广义表的扩展线性链表存储(存储结构由c5-6.h定义)的基本操作(13个) #include"func5-1.cpp" // 算法5.8 void InitGList(GList1 &L) { // 创建空的广义表L L=NULL; } void CreateGList(GList1 &L,SString S) // 算法5.7改 { // 采用扩展线性链表存储结构,由广义表的书写形式串S创建广义表L。设emp="()" SString emp,sub,hsub; GList1 p; StrAssign(emp,"()"); // 设emp="()" if(!(L=(GList1)malloc(sizeof(GLNode1)))) // 建表结点不成功 exit(OVERFLOW); if(!StrCompare(S,emp)) // 创建空表 { L->tag=LIST; L->hp=L->tp=NULL; } else if(StrLength(S)==1) // 创建单原子广义表 { L->tag=ATOM; L->atom=S[1]; L->tp=NULL; } else // 创建一般表 { L->tag=LIST; L->tp=NULL; SubString(sub,S,2,StrLength(S)-2); // 脱外层括号(去掉第1个字符和最后1个字符)给串sub sever(sub,hsub); // 从sub中分离出表头串hsub CreateGList(L->hp,hsub); p=L->hp; while(!StrEmpty(sub)) // 表尾不空,则重复建n个子表 { sever(sub,hsub); // 从sub中分离出表头串hsub · 214· 《数据结构》算法实现及解析(第二版) CreateGList(p->tp,hsub); p=p->tp; }; } } void DestroyGList(GList1 &L) { // 初始条件:广义表L存在。操作结果:销毁广义表L GList1 ph,pt; if(L) // L不为空表 { // 由ph和pt接替L的两个指针 if(L->tag) // 是子表 ph=L->hp; else // 是原子 ph=NULL; pt=L->tp; DestroyGList(ph); // 递归销毁表ph DestroyGList(pt); // 递归销毁表pt free(L); // 释放L所指结点 L=NULL; // 令L为空 } } void CopyGList(GList1 &T,GList1 L) { // 初始条件:广义表L存在。操作结果:由广义表L复制得到广义表T T=NULL; if(L) // L不空 { T=(GList1)malloc(sizeof(GLNode1)); if(!T) exit(OVERFLOW); T->tag=L->tag; // 复制枚举变量 if(L->tag==ATOM) // 复制共用体部分 T->atom=L->atom; // 复制单原子 else CopyGList(T->hp,L->hp); // 复制子表 if(L->tp==NULL) // 到表尾 T->tp=L->tp; else CopyGList(T->tp,L->tp); // 复制子表 } } int GListLength(GList1 L) { // 初始条件:广义表L存在。操作结果:求广义表L的长度,即元素个数 int len=0; GList1 p=L->hp; // p指向第1个元素 while(p) { len++; p=p->tp; }; return len; } 第 5 章 数组和广义表 · 215· int GListDepth(GList1 L) { // 初始条件:广义表L存在。操作结果:求广义表L的深度 int max,dep; GList1 pp; if(L==NULL||L->tag==LIST&&!L->hp) return 1; // 空表深度为1 else if(L->tag==ATOM) return 0; // 单原子表深度为0,只会出现在递归调用中 else // 求一般表的深度 for(max=0,pp=L->hp;pp;pp=pp->tp) { dep=GListDepth(pp); // 求以pp为头指针的子表深度 if(dep>max) max=dep; } return max+1; // 非空表的深度是各元素的深度的最大值加1 } Status GListEmpty(GList1 L) { // 初始条件:广义表L存在。操作结果:判定广义表L是否为空 if(!L||L->tag==LIST&&!L->hp) return OK; else return ERROR; } GList1 GetHead(GList1 L) { // 生成广义表L的表头元素,返回指向这个元素的指针 GList1 h,p; if(!L||L->tag==LIST&&!L->hp) // 空表无表头 return NULL; p=L->hp->tp; // p指向L的表尾 L->hp->tp=NULL; // 截去L的表尾部分 CopyGList(h,L->hp); // 将表头元素复制给h L->hp->tp=p; // 恢复L的表尾(保持原L不变) return h; } GList1 GetTail(GList1 L) { // 将广义表L的表尾生成为广义表,返回指向这个新广义表的指针 GList1 t,p; if(!L||L->tag==LIST&&!L->hp) // 空表无表尾 return NULL; p=L->hp; // p指向表头 L->hp=p->tp; // 在L中删去表头 CopyGList(t,L); // 将L的表尾拷给t L->hp=p; // 恢复L的表头(保持原L不变) return t; } void InsertFirst_GL(GList1 &L,GList1 e) { // 初始条件:广义表存在。操作结果:插入元素e(也可能是子表)作为广义表L的第1元素(表头) GList1 p=L->hp; L->hp=e; e->tp=p; · 216· 《数据结构》算法实现及解析(第二版) } void DeleteFirst_GL(GList1 &L,GList1 &e) { // 初始条件:广义表L存在。操作结果:删除广义表L的第一元素,并用e返回其值 if(L&&L->hp) { e=L->hp; L->hp=e->tp; e->tp=NULL; } else e=L; } void Traverse_GL(GList1 L,void(*v)(AtomType)) { // 利用递归算法遍历广义表L GList1 hp; if(L) // L不空 { if(L->tag==ATOM) // L为单原子 { v(L->atom); hp=NULL; } else // L为子表 hp=L->hp; Traverse_GL(hp,v); Traverse_GL(L->tp,v); } } // main5-6.cpp 检验bo5-6.cpp的主程序 #include"c1.h" typedef char AtomType; // 定义原子类型为字符型 #include"c5-6.h" // 定义广义表的扩展线性链表存储结构 #include"bo5-6.cpp" // 广义表的扩展线性链表存储结构基本操作 void visit(AtomType e) { printf("%c ", e); } void main() { char p[80]; GList1 l,m; SString t; InitGList(l); // 建立空的广义表l printf("空广义表l的深度=%d l是否空?%d(1:是 0:否)\n",GListDepth(l),GListEmpty(l)); printf("请输入广义表l(书写形式:空表:(),单原子:(a),其它:(a,(b),c)):\n"); gets(p); StrAssign(t,p); CreateGList(l,t); printf("广义表l的长度=%d\n",GListLength(l)); printf("广义表l的深度=%d l是否空?%d(1:是 0:否)\n",GListDepth(l),GListEmpty(l)); 第 5 章 数组和广义表 · 217· printf("遍历广义表l:\n"); Traverse_GL(l,visit); printf("\n复制广义表m=l\n"); CopyGList(m,l); printf("广义表m的长度=%d\n",GListLength(m)); printf("广义表m的深度=%d\n",GListDepth(m)); printf("遍历广义表m:\n"); Traverse_GL(m,visit); DestroyGList(m); m=GetHead(l); printf("\nm是l的表头元素,遍历m:\n"); Traverse_GL(m,visit); DestroyGList(m); m=GetTail(l); printf("\nm是由l的表尾形成的广义表,遍历广义表m:\n"); Traverse_GL(m,visit); InsertFirst_GL(m,l); printf("\n插入广义表l为m的表头,遍历广义表m:\n"); Traverse_GL(m,visit); DeleteFirst_GL(m,l); printf("\n删除m的表头,遍历广义表m:\n"); Traverse_GL(m,visit); printf("\n"); DestroyGList(m); } 程序运行结果: 空广义表l的深度=1 l是否空?1(1:是 0:否) 请输入广义表l(书写形式:空表:(),单原子:(a),其它:(a,(b),c)): (a,(b),c) 广义表l的长度=3 广义表l的深度=2 l是否空?0(1:是 0:否) 遍历广义表l: a b c 复制广义表m=l 广义表m的长度=3 广义表m的深度=2 遍历广义表m: a b c m是l的表头元素,遍历广义表m: a m是由l的表尾形成的广义表,遍历广义表m: b c 插入广义表l为m的表头,遍历广义表m: a b c b c 删除m的表头,遍历广义表m: b c · 218· 《数据结构》算法实现及解析(第二版) 第 6 章 树 和 二 叉 树 6.1 树的定义和基本术语 6.2 二叉树 6.2.1  6.2.2  6.2.3  // c6-1.h 二叉树的顺序存储结构(见图61) #define MAX_TREE_SIZE 100 // 二叉树的最大结点数 typedef TElemType SqBiTree[MAX_TREE_SIZE]; // 0号单元存储根结点 struct position { int level,order; // 结点的层,本层序号(按满二叉树计算) }; 在顺序存储结构中,如图 62 所示,第 i 层结点的序号从 2iÁ1-1~2i-2;序号为 i 的结 点,其双亲序号为(i+1)/2-1,其左右孩子序号分别为 2i+1 和 2i+2;除了根结点,序号为 奇数的结点是其双亲的左孩子,它的右兄弟的序号是它的序号+1;序号为偶数的结点是其 双亲的右孩子,它的左兄弟的序号是它的序号-1;i 层的满二叉树,其结点总数为 2i-1。 图 61 二叉树的顺序存储结构 (a) 二叉树 (b) 存储表示 SqBiTree 空结点根结点 MAX_TREE_SIZE 1 2 0 3 4 0 0 ⋯ 0 1 2 3 4 图 62 顺序存储结构的二叉树序号图示 圈内数字为该结点 在数组中的序号 层 1⋯⋯⋯⋯⋯⋯⋯⋯⋯ 2⋯⋯⋯⋯ 3⋯⋯ 4 8 3 7 10 4 9 12 5 11 14 6 13 1 2 0 第 6 章 树 和 二 叉 树 · 219· 显然,在顺序存储结构中,按层序输入二叉树是最方便的。当最后一个结点的值输入 后,输入给定符号表示结束。二叉树的顺序存储结构适合存完全二叉树或近似完全二 叉树。 bo6-1.cpp 是采用顺序存储结构的基本操作程序,main6-1.cpp 是检验这些基本操作的 主程序。为了使这两个程序在结点类型为整型和字符型时都能使用,采用了编译预处理的 “#define”、“#if”等命令。这样,只要将 main6-1.cpp 的第 2 行或第 3 行改为注释行 即可。 // bo6-1.cpp 二叉树的顺序存储(存储结构由c6-1.h定义)的基本操作(23个) #define ClearBiTree InitBiTree // 在顺序存储结构中,两函数完全一样 #define DestroyBiTree InitBiTree // 在顺序存储结构中,两函数完全一样 void InitBiTree(SqBiTree T) { // 构造空二叉树T。因为T是数组名,故不需要& int i; for(i=0;i>T[i]; if(T[i]==999) { T[i]=Nil; break; } i++; } #endif for(i=1;i=0;i--) // 找到最后一个结点 if(T[i]!=Nil) break; i++; // 为了便于计算 do j++; while(i>=pow(2,j)); return j; } Status Root(SqBiTree T,TElemType &e) { // 初始条件:二叉树T存在。操作结果:当T不空,用e返回T的根,返回OK;否则返回ERROR,e无定义 if(BiTreeEmpty(T)) // T空 return ERROR; else { e=T[0]; return OK; } } TElemType Value(SqBiTree T,position e) { // 初始条件:二叉树T存在,e是T中某个结点(的位置) // 操作结果:返回处于位置e(层,本层序号)的结点的值 return T[int(pow(2,e.level-1)+e.order-2)]; } Status Assign(SqBiTree T,position e,TElemType value) { // 初始条件:二叉树T存在,e是T中某个结点(的位置) // 操作结果:给处于位置e(层,本层序号)的结点赋新值value int i=int(pow(2,e.level-1)+e.order-2); // 将层、本层序号转为矩阵的序号 if(value!=Nil&&T[(i+1)/2-1]==Nil) // 给叶子赋非空值但双亲为空 return ERROR; else if(value==Nil&&(T[i*2+1]!=Nil||T[i*2+2]!=Nil)) // 给双亲赋空值但有叶子(不空) return ERROR; T[i]=value; return OK; } TElemType Parent(SqBiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:若e是T的非根结点,则返回它的双亲;否则返回“空” int i; if(T[0]==Nil) // 空树 return Nil; 第 6 章 树 和 二 叉 树 · 221· for(i=1;i<=MAX_TREE_SIZE-1;i++) if(T[i]==e) // 找到e return T[(i+1)/2-1]; return Nil; // 没找到e } TElemType LeftChild(SqBiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点。操作结果:返回e的左孩子。若e无左孩子,则返回“空” int i; if(T[0]==Nil) // 空树 return Nil; for(i=0;i<=MAX_TREE_SIZE-1;i++) if(T[i]==e) // 找到e return T[i*2+1]; return Nil; // 没找到e } TElemType RightChild(SqBiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点。操作结果:返回e的右孩子。若e无右孩子,则返回“空” int i; if(T[0]==Nil) // 空树 return Nil; for(i=0;i<=MAX_TREE_SIZE-1;i++) if(T[i]==e) // 找到e return T[i*2+2]; return Nil; // 没找到e } TElemType LeftSibling(SqBiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回“空” int i; if(T[0]==Nil) // 空树 return Nil; for(i=1;i<=MAX_TREE_SIZE-1;i++) if(T[i]==e&&i%2==0) // 找到e且其序号为偶数(是右孩子) return T[i-1]; return Nil; // 没找到e } TElemType RightSibling(SqBiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回“空” int i; if(T[0]==Nil) // 空树 return Nil; for(i=1;i<=MAX_TREE_SIZE-1;i++) if(T[i]==e&&i%2) // 找到e且其序号为奇数(是左孩子) return T[i+1]; return Nil; // 没找到e } void Move(SqBiTree q,int j,SqBiTree T,int i) // InsertChild()用到。加 { // 把从q的j结点开始的子树移为从T的i结点开始的子树 if(q[2*j+1]!=Nil) // q的左子树不空 Move(q,(2*j+1),T,(2*i+1)); // 把q的j结点的左子树移为T的i结点的左子树 if(q[2*j+2]!=Nil) // q的右子树不空 · 222· 《数据结构》算法实现及解析(第二版) Move(q,(2*j+2),T,(2*i+2)); // 把q的j结点的右子树移为T的i结点的右子树 T[i]=q[j]; // 把q的j结点移为T的i结点 q[j]=Nil; // 把q的j结点置空 } void InsertChild(SqBiTree T,TElemType p,int LR,SqBiTree c) { //初始条件:二叉树T存在,p是T中某个结点的值,LR为0或1,非空二叉树c与T不相交且右子树为空 //操作结果:根据LR为0或1,插入c为T中p结点的左或右子树。p结点的原有左或右子树则成为c的右子树 int j,k,i=0; for(j=0;j>p.level>>p.order; e=Value(T,p); cout<<"待修改结点的原值为"<>e; Assign(T,p,e); cout<<"先序遍历二叉树:"<>e>>j; InsertChild(T,e,j,s); Print(T); cout<<"删除子树,请输入待删除子树根结点的层号 本层序号 左(0)或右(1)子树: "; cin>>p.level>>p.order>>j; DeleteChild(T,p,j); Print(T); ClearBiTree(T); cout<<"清除二叉树后,树空否?"<lchild) // 有左孩子 DestroyBiTree(T->lchild); // 销毁左孩子子树 if(T->rchild) // 有右孩子 DestroyBiTree(T->rchild); // 销毁右孩子子树 free(T); // 释放根结点 T=NULL; // 空指针赋0 } } void PreOrderTraverse(BiTree T,void(*Visit)(TElemType)) { // 初始条件:二叉树T存在,Visit是对结点操作的应用函数。算法6.1,有改动 // 操作结果:先序递归遍历T,对每个结点调用函数Visit一次且仅一次 if(T) // T不空 { Visit(T->data); // 先访问根结点 PreOrderTraverse(T->lchild,Visit); // 再先序遍历左子树 PreOrderTraverse(T->rchild,Visit); // 最后先序遍历右子树 } } 图 69 三棵先序遍历都为 abc 的二叉树 (b) a b c (a) a b c (c) a b c 图 68 二叉树(见图 61(a)所示) 的二叉链表存储结构 1 NULL 2 NULL 3 NULL NULL 4 NULL T NULL T 图 610 空的和销毁的二叉树 T · 228· 《数据结构》算法实现及解析(第二版) void InOrderTraverse(BiTree T,void(*Visit)(TElemType)) { // 初始条件:二叉树T存在,Visit是对结点操作的应用函数 // 操作结果:中序递归遍历T,对每个结点调用函数Visit一次且仅一次 if(T) { InOrderTraverse(T->lchild,Visit); // 先中序遍历左子树 Visit(T->data); // 再访问根结点 InOrderTraverse(T->rchild,Visit); // 最后中序遍历右子树 } } // bo6-2.cpp 二叉树的二叉链表存储(存储结构由c6-2.h定义)的基本操作(22个),包括算法6.1~6.4 #define ClearBiTree DestroyBiTree // 清空二叉树和销毁二叉树的操作一样 #include"func6-3.cpp" // 包括InitBiTree()、DestroyBiTree()、PreOrderTraverse()和InOrderTraverse()4函数 void CreateBiTree(BiTree &T) { // 算法6.4:按先序次序输入二叉树中结点的值(可为字符型或整型,在主程中定义), // 构造二叉链表表示的二叉树T。变量Nil表示空(子)树。有改动 TElemType ch; scanf(form,&ch); if(ch==Nil) // 空 T=NULL; else { T=(BiTree)malloc(sizeof(BiTNode)); // 生成根结点 if(!T) exit(OVERFLOW); T->data=ch; CreateBiTree(T->lchild); // 构造左子树 CreateBiTree(T->rchild); // 构造右子树 } } Status BiTreeEmpty(BiTree T) { // 初始条件:二叉树T存在。操作结果:若T为空二叉树,则返回TRUE;否则FALSE if(T) return FALSE; else return TRUE; } int BiTreeDepth(BiTree T) { // 初始条件:二叉树T存在。操作结果:返回T的深度 int i,j; if(!T) return 0; // 空树深度为0 if(T->lchild) i=BiTreeDepth(T->lchild); // i为左子树的深度 else i=0; if(T->rchild) j=BiTreeDepth(T->rchild); // j为右子树的深度 else 第 6 章 树 和 二 叉 树 · 229· j=0; return i>j?i+1:j+1; // T的深度为其左右子树的深度中的大者+1 } TElemType Root(BiTree T) { // 初始条件:二叉树T存在。操作结果:返回T的根 if(BiTreeEmpty(T)) return Nil; else return T->data; } TElemType Value(BiTree p) { // 初始条件:二叉树T存在,p指向T中某个结点。操作结果:返回p所指结点的值 return p->data; } void Assign(BiTree p,TElemType value) { // 给p所指结点赋值为value p->data=value; } typedef BiTree QElemType; // 设队列元素为二叉树的指针类型 #include"c3-2.h" // 链队列 #include"bo3-2.cpp" // 链队列的基本操作 TElemType Parent(BiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:若e是T的非根结点,则返回它的双亲,否则返回“空” LinkQueue q; QElemType a; if(T) // 非空树 { InitQueue(q); // 初始化队列 EnQueue(q,T); // 树根指针入队 while(!QueueEmpty(q)) // 队不空 { DeQueue(q,a); // 出队,队列元素赋给a if(a->lchild&&a->lchild->data==e||a->rchild&&a->rchild->data==e) // 找到e(是其左或右孩子) return a->data; // 返回e的双亲的值 else // 没找到e,则入队其左右孩子指针(如果非空) { if(a->lchild) EnQueue(q,a->lchild); if(a->rchild) EnQueue(q,a->rchild); } } } return Nil; // 树空或没找到e } BiTree Point(BiTree T,TElemType s) { // 返回二叉树T中指向元素值为s的结点的指针。另加 LinkQueue q; QElemType a; · 230· 《数据结构》算法实现及解析(第二版) if(T) // 非空树 { InitQueue(q); // 初始化队列 EnQueue(q,T); // 根指针入队 while(!QueueEmpty(q)) // 队不空 { DeQueue(q,a); // 出队,队列元素赋给a if(a->data==s) return a; if(a->lchild) // 有左孩子 EnQueue(q,a->lchild); // 入队左孩子 if(a->rchild) // 有右孩子 EnQueue(q,a->rchild); // 入队右孩子 } } return NULL; } TElemType LeftChild(BiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点。操作结果:返回e的左孩子。若e无左孩子,则返回“空” BiTree a; if(T) // 非空树 { a=Point(T,e); // a是结点e的指针 if(a&&a->lchild) // T中存在结点e且e存在左孩子 return a->lchild->data; // 返回e的左孩子的值 } return Nil; // 其余情况返回空 } TElemType RightChild(BiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点。操作结果:返回e的右孩子。若e无右孩子,则返回“空” BiTree a; if(T) // 非空树 { a=Point(T,e); // a是结点e的指针 if(a&&a->rchild) // T中存在结点e且e存在右孩子 return a->rchild->data; // 返回e的右孩子的值 } return Nil; // 其余情况返回空 } TElemType LeftSibling(BiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回“空” TElemType a; BiTree p; if(T) // 非空树 { a=Parent(T,e); // a为e的双亲 if(a!=Nil) // 找到e的双亲 { p=Point(T,a); // p为指向结点a的指针 if(p->lchild&&p->rchild&&p->rchild->data==e) // p存在左右孩子且右孩子是e 第 6 章 树 和 二 叉 树 · 231· return p->lchild->data; // 返回p的左孩子(e的左兄弟) } } return Nil; // 其余情况返回空 } TElemType RightSibling(BiTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回“空” TElemType a; BiTree p; if(T) // 非空树 { a=Parent(T,e); // a为e的双亲 if(a!=Nil) // 找到e的双亲 { p=Point(T,a); // p为指向结点a的指针 if(p->lchild&&p->rchild&&p->lchild->data==e) // p存在左右孩子且左孩子是e return p->rchild->data; // 返回p的右孩子(e的右兄弟) } } return Nil; // 其余情况返回空 } Status InsertChild(BiTree p,int LR,BiTree c) // 形参T无用 { // 初始条件:二叉树T存在,p指向T中某个结点,LR为0或1,非空二叉树c与T不相交且右子树为空 // 操作结果:根据LR为0或1,插入c为T中p所指结点的左或右子树。p所指结点的 // 原有左或右子树则成为c的右子树 if(p) // p不空 { if(LR==0) { c->rchild=p->lchild; p->lchild=c; } else // LR==1 { c->rchild=p->rchild; p->rchild=c; } return OK; } return ERROR; // p空 } Status DeleteChild(BiTree p,int LR) // 形参T无用 { // 初始条件:二叉树T存在,p指向T中某个结点,LR为0或1 // 操作结果:根据LR为0或1,删除T中p所指结点的左或右子树 if(p) // p不空 { if(LR==0) // 删除左子树 ClearBiTree(p->lchild); else // 删除右子树 ClearBiTree(p->rchild); · 232· 《数据结构》算法实现及解析(第二版) return OK; } return ERROR; // p空 } typedef BiTree SElemType; // 设栈元素为二叉树的指针类型 #include"c3-1.h" // 顺序栈 #include"bo3-1.cpp" // 顺序栈的基本操作 void InOrderTraverse1(BiTree T,void(*Visit)(TElemType)) { // 采用二叉链表存储结构,Visit是对数据元素操作的应用函数。算法6.3,有改动 // 中序遍历二叉树T的非递归算法(利用栈),对每个数据元素调用函数Visit SqStack S; InitStack(S); while(T||!StackEmpty(S)) { if(T) { // 根指针进栈,遍历左子树 Push(S,T); T=T->lchild; } else { // 根指针退栈,访问根结点,遍历右子树 Pop(S,T); Visit(T->data); T=T->rchild; } } printf("\n"); } void InOrderTraverse2(BiTree T,void(*Visit)(TElemType)) { // 采用二叉链表存储结构,Visit是对数据元素操作的应用函数。算法6.2,有改动 // 中序遍历二叉树T的非递归算法(利用栈),对每个数据元素调用函数Visit SqStack S; BiTree p; InitStack(S); Push(S,T); // 根指针进栈 while(!StackEmpty(S)) { while(GetTop(S,p)&&p) Push(S,p->lchild); // 向左走到尽头 Pop(S,p); // 空指针退栈 if(!StackEmpty(S)) { // 访问结点,向右一步 Pop(S,p); Visit(p->data); Push(S,p->rchild); } } printf("\n"); } void PostOrderTraverse(BiTree T,void(*Visit)(TElemType)) { // 初始条件:二叉树T存在,Visit是对结点操作的应用函数 第 6 章 树 和 二 叉 树 · 233· // 操作结果:后序递归遍历T,对每个结点调用函数Visit一次且仅一次 if(T) // T不空 { PostOrderTraverse(T->lchild,Visit); // 先后序遍历左子树 PostOrderTraverse(T->rchild,Visit); // 再后序遍历右子树 Visit(T->data); // 最后访问根结点 } } void LevelOrderTraverse(BiTree T,void(*Visit)(TElemType)) { // 初始条件:二叉树T存在,Visit是对结点操作的应用函数 // 操作结果:层序递归遍历T(利用队列),对每个结点调用函数Visit一次且仅一次 LinkQueue q; QElemType a; if(T) { InitQueue(q); // 初始化队列q EnQueue(q,T); // 根指针入队 while(!QueueEmpty(q)) // 队列不空 { DeQueue(q,a); // 出队元素(指针),赋给a Visit(a->data); // 访问a所指结点 if(a->lchild!=NULL) // a有左孩子 EnQueue(q,a->lchild); // 入队a的左孩子 if(a->rchild!=NULL) // a有右孩子 EnQueue(q,a->rchild); // 入队a的右孩子 } printf("\n"); } } // main6-2.cpp 检验bo6-2.cpp的主程序,利用条件编译选择数据类型(另一种方法) #define CHAR // 字符型 // #define INT // 整型(二者选一) #include"c1.h" #ifdef CHAR typedef char TElemType; TElemType Nil=′′; // 字符型以空格符为空 #define form "%c" // 输入输出的格式为%c #endif #ifdef INT typedef int TElemType; TElemType Nil=0; // 整型以0为空 #define form "%d" // 输入输出的格式为%d #endif #include"c6-2.h" #include"bo6-2.cpp" void visitT(TElemType e) { printf(form" ",e); } void main() · 234· 《数据结构》算法实现及解析(第二版) { int i; BiTree T,p,c; TElemType e1,e2; InitBiTree(T); printf("构造空二叉树后,树空否?%d(1:是 0:否)树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T)); e1=Root(T); if(e1!=Nil) printf("二叉树的根为 "form"\n",e1); else printf("树空,无根\n"); #ifdef CHAR printf("请先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n"); #endif #ifdef INT printf("请先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n"); #endif CreateBiTree(T); printf("建立二叉树后,树空否?%d(1:是 0:否) 树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T)); e1=Root(T); if(e1!=Nil) printf("二叉树的根为 "form"\n",e1); else printf("树空,无根\n"); printf("中序递归遍历二叉树:\n"); InOrderTraverse(T,visitT); printf("\n后序递归遍历二叉树:\n"); PostOrderTraverse(T,visitT); printf("\n请输入一个结点的值: "); scanf("%*c"form,&e1); p=Point(T,e1); // p为e1的指针 printf("结点的值为"form"\n",Value(p)); printf("欲改变此结点的值,请输入新值: "); scanf("%*c"form"%*c",&e2); // 后一个%*c吃掉回车符,为调用CreateBiTree()做准备 Assign(p,e2); printf("层序遍历二叉树:\n"); LevelOrderTraverse(T,visitT); e1=Parent(T,e2); if(e1!=Nil) printf("%c的双亲是"form"\n",e2,e1); else printf(form"没有双亲\n",e2); e1=LeftChild(T,e2); if(e1!=Nil) printf(form"的左孩子是"form"\n",e2,e1); else printf(form"没有左孩子\n",e2); e1=RightChild(T,e2); if(e1!=Nil) printf(form"的右孩子是"form"\n",e2,e1); else 第 6 章 树 和 二 叉 树 · 235· printf(form"没有右孩子\n",e2); e1=LeftSibling(T,e2); if(e1!=Nil) printf(form"的左兄弟是"form"\n",e2,e1); else printf(form"没有左兄弟\n",e2); e1=RightSibling(T,e2); if(e1!=Nil) printf(form"的右兄弟是"form"\n",e2,e1); else printf(form"没有右兄弟\n",e2); InitBiTree(c); printf("构造一个右子树为空的二叉树c:\n"); #ifdef CHAR printf("请先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n"); #endif #ifdef INT printf("请先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n"); #endif CreateBiTree(c); printf("先序递归遍历二叉树c:\n"); PreOrderTraverse(c,visitT); printf("\n层序遍历二叉树c:\n"); LevelOrderTraverse(c,visitT); printf("树c插到树T中,请输入树T中树c的双亲结点 c为左(0)或右(1)子树: "); scanf("%*c"form"%d",&e1,&i); p=Point(T,e1); // p是T中树c的双亲结点指针 InsertChild(p,i,c); printf("先序递归遍历二叉树:\n"); PreOrderTraverse(T,visitT); printf("\n中序非递归遍历二叉树:\n"); InOrderTraverse1(T,visitT); printf("删除子树,请输入待删除子树的双亲结点 左(0)或右(1)子树: "); scanf("%*c"form"%d",&e1,&i); p=Point(T,e1); DeleteChild(p,i); printf("先序递归遍历二叉树:\n"); PreOrderTraverse(T,visitT); printf("\n中序非递归遍历二叉树(另一种方法):\n"); InOrderTraverse2(T,visitT); DestroyBiTree(T); } 程序运行结果: 构造空二叉树后,树空否?1(1:是 0:否)树的深度=0 树空,无根 请先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树) · 236· 《数据结构》算法实现及解析(第二版) abdg e c f (见图611) 建立二叉树后,树空否?0(1:是 0:否) 树的深度=4 二叉树的根为 a 中序递归遍历二叉树: g d b e a c f 后序递归遍历二叉树: g d e b f c a 请输入一个结点的值: d 结点的值为d 欲改变此结点的值,请输入新值: m 层序遍历二叉树: a b c m e f g m的双亲是b m的左孩子是g m没有右孩子 m没有左兄弟 m的右兄弟是e 构造一个右子树为空的二叉树c: 请先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树) hijl k (见图612) 先序递归遍历二叉树c: h i j l k 层序遍历二叉树c: h i j k l 树c插到树T中,请输入树T中树c的双亲结点 c为左(0)或右(1)子树: b 1 先序递归遍历二叉树:(见图613) abmghijlkecf 中序非递归遍历二叉树: gmbljikheacf 删除子树,请输入待删除子树的双亲结点 左(0)或右(1)子树: h 0 先序递归遍历二叉树:(见图614) a b m g h e c f 中序非递归遍历二叉树(另一种方法): g m b h e a c f 图 612 右子树为空的树 c h i j k l 图 613 树 c 插到树 T 中作 为结点 b 的右子树 h i j k l a b m c f g e 图 614 删除 h 的左子 树后的二叉树 h a b m c f g e // c6-6.h 二叉树的三叉链表存储结构(见图615) typedef struct BiTPNode { 图 611 运行 main6-2.cpp 生成的二叉树 a b d e c f g 第 6 章 树 和 二 叉 树 · 237· TElemType data; BiTPNode *parent,*lchild,*rchild; // 双亲、左右孩子指针 }BiTPNode,*BiPTree; 二叉树的三叉链表存储结构比二叉链表多一个指向双亲结点的指针,因此,求双亲和 左右兄弟都很容易。但在构造二叉树时要另给双亲指针赋值,从而增加了复杂度。由于三 叉链表和二叉链表在结构上的相似性,它们有些相应的基本操作也很类似。图 616 是图 61(a)所示二叉树的三叉链表存储结构。bo6-6.cpp 是三叉链表存储结构的基本操作。 // bo6-6.cpp 二叉树的三叉链表存储(存储结构由c6-6.h定义)的基本操作(21个) #define ClearBiTree DestroyBiTree // 清空二叉树和销毁二叉树的操作一样 void InitBiTree(BiPTree &T) { // 操作结果:构造空二叉树T T=NULL; } void DestroyBiTree(BiPTree &T) { // 初始条件:二叉树T存在。操作结果:销毁二叉树T if(T) // 非空树 { if(T->lchild) // 有左孩子 DestroyBiTree(T->lchild); // 销毁左孩子子树 if(T->rchild) // 有右孩子 DestroyBiTree(T->rchild); // 销毁右孩子子树 free(T); // 释放根结点 T=NULL; // 空指针赋0 } } void CreateBiTree(BiPTree &T) { // 按先序次序输入二叉树中结点的值(可为字符型或整型,在主程中定义), 图 616 二叉树(见图 61(a)所示)的三叉链表存储结构 T 1 NULL NULL 2 NULL 3 NULL NULL 4 NULL 图 615 二叉树的三叉链表存储结构 BiPTree BiTPNode lchild data parent rchild BiTPNode BiTPNode BiTPNode · 238· 《数据结构》算法实现及解析(第二版) // 构造三叉链表表示的二叉树T TElemType ch; scanf(form,&ch); if(ch==Nil) // 空 T=NULL; else { T=(BiPTree)malloc(sizeof(BiTPNode)); // 动态生成根结点 if(!T) exit(OVERFLOW); T->data=ch; // 给根结点赋值 T->parent=NULL; // 根结点无双亲 CreateBiTree(T->lchild); // 构造左子树 if(T->lchild) // 有左孩子 T->lchild->parent=T; // 给左孩子的双亲域赋值 CreateBiTree(T->rchild); // 构造右子树 if(T->rchild) // 有右孩子 T->rchild->parent=T; // 给右孩子的双亲域赋值 } } Status BiTreeEmpty(BiPTree T) { // 初始条件:二叉树T存在。操作结果:若T为空二叉树,则返回TRUE;否则FALSE if(T) return FALSE; else return TRUE; } int BiTreeDepth(BiPTree T) { // 初始条件:二叉树T存在。操作结果:返回T的深度 int i,j; if(!T) return 0; // 空树深度为0 if(T->lchild) i=BiTreeDepth(T->lchild); // i为左子树的深度 else i=0; if(T->rchild) j=BiTreeDepth(T->rchild); // j为右子树的深度 else j=0; return i>j?i+1:j+1; // T的深度为其左右子树的深度中的大者+1 } TElemType Root(BiPTree T) { // 初始条件:二叉树T存在。操作结果:返回T的根 if(T) return T->data; else return Nil; } TElemType Value(BiPTree p) { // 初始条件:二叉树T存在,p指向T中某个结点。操作结果:返回p所指结点的值 第 6 章 树 和 二 叉 树 · 239· return p->data; } void Assign(BiPTree p,TElemType value) { // 给p所指结点赋值为value p->data=value; } typedef BiPTree QElemType; // 设队列元素为二叉树的指针类型 #include"c3-2.h" // 链队列 #include"bo3-2.cpp" // 链队列的基本操作 BiPTree Point(BiPTree T,TElemType e) { // 返回二叉树T中指向元素值为e的结点的指针。加 LinkQueue q; QElemType a; if(T) // 非空树 { InitQueue(q); // 初始化队列 EnQueue(q,T); // 根结点入队 while(!QueueEmpty(q)) // 队不空 { DeQueue(q,a); // 出队,队列元素赋给a if(a->data==e) return a; if(a->lchild) // 有左孩子 EnQueue(q,a->lchild); // 入队左孩子 if(a->rchild) // 有右孩子 EnQueue(q,a->rchild); // 入队右孩子 } } return NULL; } TElemType Parent(BiPTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:若e是T的非根结点,则返回它的双亲;否则返回“空” BiPTree a; if(T) // 非空树 { a=Point(T,e); // a是结点e的指针 if(a&&a!=T) // T中存在结点e且e是非根结点 return a->parent->data; // 返回e的双亲的值 } return Nil; // 其余情况返回空 } TElemType LeftChild(BiPTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点。操作结果:返回e的左孩子。若e无左孩子,则返回“空” BiPTree a; if(T) // 非空树 { a=Point(T,e); // a是结点e的指针 if(a&&a->lchild) // T中存在结点e且e存在左孩子 return a->lchild->data; // 返回e的左孩子的值 } · 240· 《数据结构》算法实现及解析(第二版) return Nil; // 其余情况返回空 } TElemType RightChild(BiPTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点。操作结果:返回e的右孩子。若e无右孩子,则返回“空” BiPTree a; if(T) // 非空树 { a=Point(T,e); // a是结点e的指针 if(a&&a->rchild) // T中存在结点e且e存在右孩子 return a->rchild->data; // 返回e的右孩子的值 } return Nil; // 其余情况返回空 } TElemType LeftSibling(BiPTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回“空” BiPTree a; if(T) // 非空树 { a=Point(T,e); // a是结点e的指针 if(a&&a!=T&&a->parent->lchild&&a->parent->lchild!=a) // T中存在结点e且e存在左兄弟 return a->parent->lchild->data; // 返回e的左兄弟的值 } return Nil; // 其余情况返回空 } TElemType RightSibling(BiPTree T,TElemType e) { // 初始条件:二叉树T存在,e是T中某个结点 // 操作结果:返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回“空” BiPTree a; if(T) // 非空树 { a=Point(T,e); // a是结点e的指针 if(a&&a!=T&&a->parent->rchild&&a->parent->rchild!=a) // T中存在结点e且e存在右兄弟 return a->parent->rchild->data; // 返回e的右兄弟的值 } return Nil; // 其余情况返回空 } Status InsertChild(BiPTree p,int LR,BiPTree c) // 形参T无用 { // 初始条件:二叉树T存在,p指向T中某个结点,LR为0或1,非空二叉树c与T不相交且右子树为空 // 操作结果:根据LR为0或1,插入c为T中p所指结点的左或右子树。p所指结点 // 的原有左或右子树则成为c的右子树 if(p) // p不空 { if(LR==0) { c->rchild=p->lchild; if(c->rchild) // c有右孩子(p原有左孩子) c->rchild->parent=c; p->lchild=c; c->parent=p; } 第 6 章 树 和 二 叉 树 · 241· else // LR==1 { c->rchild=p->rchild; if(c->rchild) // c有右孩子(p原有右孩子) c->rchild->parent=c; p->rchild=c; c->parent=p; } return OK; } return ERROR; // p空 } Status DeleteChild(BiPTree p,int LR) // 形参T无用 { // 初始条件:二叉树T存在,p指向T中某个结点,LR为0或1 // 操作结果:根据LR为0或1,删除T中p所指结点的左或右子树 if(p) // p不空 { if(LR==0) // 删除左子树 ClearBiTree(p->lchild); else // 删除右子树 ClearBiTree(p->rchild); return OK; } return ERROR; // p空 } void PreOrderTraverse(BiPTree T,void(*Visit)(BiPTree)) { // 先序递归遍历二叉树T if(T) { Visit(T); // 先访问根结点 PreOrderTraverse(T->lchild,Visit); // 再先序遍历左子树 PreOrderTraverse(T->rchild,Visit); // 最后先序遍历右子树 } } void InOrderTraverse(BiPTree T,void(*Visit)(BiPTree)) { // 中序递归遍历二叉树T if(T) { InOrderTraverse(T->lchild,Visit); // 中序遍历左子树 Visit(T); // 再访问根结点 InOrderTraverse(T->rchild,Visit); // 最后中序遍历右子树 } } void PostOrderTraverse(BiPTree T,void(*Visit)(BiPTree)) { // 后序递归遍历二叉树T if(T) { PostOrderTraverse(T->lchild,Visit); // 后序遍历左子树 PostOrderTraverse(T->rchild,Visit); // 后序遍历右子树 Visit(T); // 最后访问根结点 } · 242· 《数据结构》算法实现及解析(第二版) } void LevelOrderTraverse(BiPTree T,void(*Visit)(BiPTree)) { // 层序遍历二叉树T(利用队列) LinkQueue q; QElemType a; if(T) { InitQueue(q); EnQueue(q,T); while(!QueueEmpty(q)) { DeQueue(q,a); Visit(a); if(a->lchild!=NULL) EnQueue(q,a->lchild); if(a->rchild!=NULL) EnQueue(q,a->rchild); } } } // main6-6.cpp 检验bo6-6.cpp的主程序 #define CHAR // 字符型 // #define INT // 整型(二者选一) #ifdef CHAR typedef char TElemType; TElemType Nil=′′; // 字符型以空格符为空 #define form "%c" // 输入输出的格式为%c #endif #ifdef INT typedef int TElemType; TElemType Nil=0; // 整型以0为空 #define form "%d" // 输入输出的格式为%d #endif #include"c1.h" #include"c6-6.h" #include"bo6-6.cpp" void visitT(BiPTree T) { if(T) // T非空 printf(form" ",T->data); } void main() { int i; BiPTree T,c,q; TElemType e1,e2; InitBiTree(T); printf("构造空二叉树后,树空否?%d(1:是 0:否)树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T)); e1=Root(T); if(e1!=Nil) 第 6 章 树 和 二 叉 树 · 243· printf("二叉树的根为 "form"\n",e1); else printf("树空,无根\n"); #ifdef CHAR printf("请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n"); #endif #ifdef INT printf("请按先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n"); #endif CreateBiTree(T); printf("建立二叉树后,树空否?%d(1:是 0:否) 树的深度=%d\n",BiTreeEmpty(T),BiTreeDepth(T)); e1=Root(T); if(e1!=Nil) printf("二叉树的根为 "form"\n",e1); else printf("树空,无根\n"); printf("中序递归遍历二叉树:\n"); InOrderTraverse(T,visitT); printf("\n后序递归遍历二叉树:\n"); PostOrderTraverse(T,visitT); printf("\n层序遍历二叉树:\n"); LevelOrderTraverse(T,visitT); printf("\n请输入一个结点的值: "); scanf("%*c"form,&e1); c=Point(T,e1); // c为e1的指针 printf("结点的值为"form"\n",Value(c)); printf("欲改变此结点的值,请输入新值: "); scanf("%*c"form"%*c",&e2); Assign(c,e2); printf("层序遍历二叉树:\n"); LevelOrderTraverse(T,visitT); e1=Parent(T,e2); if(e1!=Nil) printf("\n"form"的双亲是"form"\n",e2,e1); else printf(form"没有双亲\n",e2); e1=LeftChild(T,e2); if(e1!=Nil) printf(form"的左孩子是"form"\n",e2,e1); else printf(form"没有左孩子\n",e2); e1=RightChild(T,e2); if(e1!=Nil) printf(form"的右孩子是"form"\n",e2,e1); else printf(form"没有右孩子\n",e2); e1=LeftSibling(T,e2); if(e1!=Nil) printf(form"的左兄弟是"form"\n",e2,e1); else printf(form"没有左兄弟\n",e2); · 244· 《数据结构》算法实现及解析(第二版) e1=RightSibling(T,e2); if(e1!=Nil) printf(form"的右兄弟是"form"\n",e2,e1); else printf(form"没有右兄弟\n",e2); InitBiTree(c); printf("构造一个右子树为空的二叉树c:\n"); #ifdef CHAR printf("请先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n"); #endif #ifdef INT printf("请先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n"); #endif CreateBiTree(c); printf("先序递归遍历二叉树c:\n"); PreOrderTraverse(c,visitT); printf("\n树c插到树T中,请输入树T中树c的双亲结点 c为左(0)或右(1)子树: "); scanf("%*c"form"%d",&e1,&i); q=Point(T,e1); InsertChild(q,i,c); printf("先序递归遍历二叉树:\n"); PreOrderTraverse(T,visitT); printf("\n删除子树,请输入待删除子树的双亲结点 左(0)或右(1)子树: "); scanf("%*c"form"%d",&e1,&i); q=Point(T,e1); DeleteChild(q,i); printf("先序递归遍历二叉树:\n"); PreOrderTraverse(T,visitT); printf("\n"); DestroyBiTree(T); } 程序运行结果: 构造空二叉树后,树空否?1(1:是 0:否)树的深度=0 树空,无根 请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树) abdg e c f (见图611) 建立二叉树后,树空否?0(1:是 0:否) 树的深度=4 二叉树的根为a 中序递归遍历二叉树: g d b e a c f 后序递归遍历二叉树: g d e b f c a 层序遍历二叉树: a b c d e f g 请输入一个结点的值: d 结点的值为d 欲改变此结点的值,请输入新值: m 第 6 章 树 和 二 叉 树 · 245· 层序遍历二叉树: a b c m e f g m的双亲是b m的左孩子是g m没有右孩子 m没有左兄弟 m的右兄弟是e 构造一个右子树为空的二叉树c: 请先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树) hijl k (见图612) 先序递归遍历二叉树c: h i j l k 树c插到树T中,请输入树T中树c的双亲结点 c为左(0)或右(1)子树: b 1(见图613) 先序递归遍历二叉树: abmghijlkecf 删除子树,请输入待删除子树的双亲结点 左(0)或右(1)子树: h 0(见图614) 先序递归遍历二叉树: a b m g h e c f 6.3 遍历二叉树和线索二叉树 6.3.1  遍历二叉树就是按某种规则,对二叉树的每个结点均访问一次,而且仅访问一次。这 实际上就是将非线性的二叉树结构线性化。遍历二叉树的方法有先序、中序、后序和层序 4 种,访问的顺序各不相同。以图 61(a)所示二叉树为例,先序遍历的顺序为 1 2 3 4; 中序遍历的顺序为 3 2 4 1;后序遍历的顺序为 3 4 2 1;层序遍历的顺序为 1 2 3 4。对于这 棵二叉树,层序遍历和先序遍历的顺序碰巧一致。有关二叉链表存储结构生成二叉树和遍 历二叉树的算法 6.1~6.4 在 bo6-2.cpp 中。 6.3.2  为了更方便、快捷地遍历二叉树,最好在二叉树的结点上增加 2 个指针,它们分别指 向遍历二叉树时该结点的前驱和后继结点。这样,从二叉树的任一结点都可以方便地找到 其它结点。但这样做大大降低了结构的存储密度。另外,根据二叉树的性质 3(教科书 124 页),有:n0=n2+1。空链域=2n0+n1(叶子结点有 2 个空链域,度为 1 的结点有 1 个空链 域)=n0+n1+n2+1=n+1。也就是说,在由 n 个结点组成的二叉树中,有 n+1 个指针是空指 针。如果能利用这 n+1 个空指针,使它们指向结点的前驱(当左孩子指针空)或后继(当右 孩子指针空),则既可不降低结构的存储密度,又可更方便、快捷地遍历二叉树。不过, 这样就无法区别左右孩子指针所指的到底是结点的左右孩子,还是结点的前驱后继了。为 了有所区别,另增加 2 个域 LTag 和 RTag。当所指的是孩子,其值为 0(Link);当所指的 是前驱后继,其值为 1(Thread)。这样做,结构的存储密度也有所降低,但不大。因为 LTag 和 RTag 分别只需要 1 个比特(二进制位)即可。c6-3.h 是二叉树的二叉线索存储结 · 246· 《数据结构》算法实现及解析(第二版) 构。它只是比二叉链表存储结构(在 c6-2.h 中)多了 LTag 和 RTag 个域。 // c6-3.h 二叉树的二叉线索存储结构(见图617) enum PointerTag // 枚举 {Link,Thread}; // Link(0):指针,Thread(1):线索 struct BiThrNode { TElemType data; BiThrNode *lchild,*rchild; // 左右孩子指针 PointerTag LTag,RTag; // 左右标志 }; typedef BiThrNode *BiThrTree; 构造线索二叉树的方法和构造以二叉链表存储的二叉树方法相似,都是按先序输入结 点的值来构造二叉树的。对比 bo6-2.cpp 中的 CreateBiTree()函数和 bo6-3.cpp 中的 CreateBiThrTree()函数可见,它们的区别有两点: (1) 二叉树结点的结构不同; (2) 构造线索二叉树时,若有左右孩子结点,还要给左右标志赋值 0(Link)。 图 61(a)所示二叉树调用 CreateBiThrTree()函数产生的二叉树结构如图 618 所 示。和调用 CreateBiTree()函数产生的二叉树结构(见图 68)相比,前者只是多了 LTag 和 RTag 两个域。并且当其相应孩子指针不空时,赋值 0。 调用 CreateBiThrTree()函数,只是构造了一棵可以线索化的二叉树,还没有完成线 索化。 因为对于一棵给定的二叉树,其先序、中序、后序和层序遍历的顺序是不同的。显 然,其线索化的操作和遍历的操作也是不同的。 图 619 是图 61(a)所示二叉树的中序线索二叉树存储结构示例。和二叉链表(见图 68)存储结构相比,第一,它多了一个头结点。其左孩子指针指向根结点,右孩子指针 (线索)指向中序遍历所访问的最后一个结点。第二,它每个结点的左右孩子指针都不是空 指针。在没有孩子的情况下,分别指向该结点中序遍历的前驱或后继。第三,中序遍历的 第 1 个结点(最左边的结点,它没有左孩子,本例是结点 3)的左孩子指针(线索)和最后 1 个结点(最右边的结点,它没有右孩子,本例是结点 1)的右孩子指针(线索)都指向头结 点。其目的是标志遍历的起点和终点。 图 617 二叉树的二叉线索存储结构 BiTNode lchild LTag data RTag rchild BiTNodeBiTNode BiThrTree 图 618 CreateBiThrTree()产生的 二叉树(以图 61(a)为例) 0 1 NULL T 0 2 0 NULL 3 NULL NULL 4 NULL 图 619 中序线索二叉树(以图 61(a)为例)存储结构 头结点 0 2 0 1 3 1 1 4 1 Thrt 0 1 0 1 1 根结点T 头指针 根指针 第 6 章 树 和 二 叉 树 · 247· 图 620 是空线索二叉树。 bo6-3.cpp 中的 InThreading()函数和 InOrderThreading()函数 共同完成了对二叉树的中序遍历线索化。其算法是:设置全局指 针 变 量 pre( 之所以设为全局变量,是因为在递归函数 InThreading()和 InOrderThreading()中都要用到,设为全局变量就 不必频繁传递变量的值),令 pre 总是指向遍历的前驱结点,p 指 向当前结点;在中序遍历过程中,如果 p 所指结点没有左孩子,则结点的左孩子指针指向 pre 所指结点,结点的 LTag 域的值为 1(Thread);如果 pre 所指结点没有右孩子,则结点的 右孩子指针指向 p,结点的 RTag 域的值为 1(Thread)。 对于图 619 所示的中序线索二叉树,我们能不能在找到遍历的第 1 个结点后,顺着 右孩子指针一直找到遍历的最后 1 个结点呢?这是不一定的。因为结点的右孩子指针并不 一定指向后继结点,它可能指向右孩子,而右孩子并不一定恰好是后继结点。 bo6-3.cpp 中的 InOrderTraverse_Thr()函数完成了对中序线索二叉树的中序遍历操 作。其算法是:当树不空时,由树根向左找,一直找到没有左孩子的结点(最左结点)。这 就是中序遍历的第 1 个结点。若该结点没有右孩子,则右孩子指针指向其后继结点;否 则,以其右孩子为子树的根,向左找,一直找到没有左孩子的结点。这就是后继结点。当 结点的右孩子指针指向头结点,遍历结束。 图 621 是图 61(a)所示二叉树的先序线索二叉树存储结构示例。bo6-3.cpp 中的先 序线索化的递归函数 PreThreading()与中序线索化的递归函数 InThreading()很相像,都是 利用递归进行线索化,只不过顺序不同。但由于 PreThreading()是先序线索化,所以判断 结点是否有左右孩子就不能由其左右孩子指针是否为空决定,而要由结点的 LTag 和 RTag 域是否为 0(Link)来决定。 对于先序线索化二叉树的先序遍历算法是这样的:根结点是遍历的第 1 个结点;如果 结点有左孩子,则左孩子是其后继;若结点没有左孩子,则右孩子指针所指的结点是其后 继(无论该结点有没有右孩子)。相关程序见 bo6-3.cpp 中的 PreOrderTraverse_Thr()函数。 bo6-3.cpp 中的后序线索化的递归函数 PostThreading()与中序线索化的递归函数 InThreading()也很相像,也是利用递归进行线索化,也只是顺序不同。图 622 是图 61(a)所示二叉树的后序线索二叉树存储结构示例。对于后序线索化二叉树的后序遍历 图 620 空线索二叉树 Thrt 头结点 0 1 图 621 先序线索二叉树(以图 61(a)为例)存储结构 头结点 0 2 0 1 3 1 1 4 1 Thrt 0 1 0 1 1 根结点T 头指针 根指针 · 248· 《数据结构》算法实现及解析(第二版) 算法较复杂。因为根结点是在最后遍历,所以要采用带有双亲指针的三叉链表结构才行。 本书没有给出它的算法。 // bo6-3.cpp 二叉树的二叉线索存储(存储结构由c6-3.h定义)的基本操作,包括算法6.5~6.7 void CreateBiThrTree(BiThrTree &T) // (见图618) { // 按先序输入线索二叉树中结点的值,构造线索二叉树T。0(整型)/空格(字符型)表示空结点 TElemType ch; scanf(form,&ch); if(ch==Nil) T=NULL; else { T=(BiThrTree)malloc(sizeof(BiThrNode)); // 生成根结点(先序) if(!T) exit(OVERFLOW); T->data=ch; // 给根结点赋植 CreateBiThrTree(T->lchild); // 递归构造左子树 if(T->lchild) // 有左孩子 T->LTag=Link; // 给左标志赋值(指针) CreateBiThrTree(T->rchild); // 递归构造右子树 if(T->rchild) // 有右孩子 T->RTag=Link; // 给右标志赋值(指针) } } BiThrTree pre; // 全局变量,始终指向刚刚访问过的结点 void InThreading(BiThrTree p) { // 通过中序遍历进行中序线索化,线索化之后pre指向最后一个结点。算法6.7 if(p) // 线索二叉树不空 { InThreading(p->lchild); // 递归左子树线索化 if(!p->lchild) // 没有左孩子 { p->LTag=Thread; // 左标志为线索(前驱) p->lchild=pre; // 左孩子指针指向前驱 } if(!pre->rchild) // 前驱没有右孩子 { 图 622 后序线索二叉树(以图 61(a)为例)存储结构 头结点 0 2 0 1 3 1 1 4 1 Thrt 0 1 0 1 1 根结点T 头指针 根指针 第 6 章 树 和 二 叉 树 · 249· pre->RTag=Thread; // 前驱的右标志为线索(后继) pre->rchild=p; // 前驱右孩子指针指向其后继(当前结点p) } pre=p; // 保持pre指向p的前驱 InThreading(p->rchild); // 递归右子树线索化 } } void InOrderThreading(BiThrTree &Thrt,BiThrTree T) { // 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点。算法6.6 if(!(Thrt=(BiThrTree)malloc(sizeof(BiThrNode)))) // 生成头结点不成功 exit(OVERFLOW); Thrt->LTag=Link; // 建头结点,左标志为指针 Thrt->RTag=Thread; // 右标志为线索 Thrt->rchild=Thrt; // 右指针回指 if(!T) // 若二叉树空,则左指针回指(见图620) Thrt->lchild=Thrt; else // (见图619) { Thrt->lchild=T; // 头结点的左指针指向根结点 pre=Thrt; // pre(前驱)的初值指向头结点 InThreading(T); // 中序遍历进行中序线索化,pre指向中序遍历的最后一个结点 pre->rchild=Thrt; // 最后一个结点的右指针指向头结点 pre->RTag=Thread; // 最后一个结点的右标志为线索 Thrt->rchild=pre; // 头结点的右指针指向中序遍历的最后一个结点 } } void InOrderTraverse_Thr(BiThrTree T,void(*Visit)(TElemType)) { // 中序遍历线索二叉树T(头结点)的非递归算法。算法6.5 BiThrTree p; p=T->lchild; // p指向根结点 while(p!=T) { // 空树或遍历结束时,p==T while(p->LTag==Link) // 由根结点一直找到二叉树的最左结点 p=p->lchild; Visit(p->data); // 访问此结点 while(p->RTag==Thread&&p->rchild!=T) // p->rchild是线索(后继),且不是遍历的最后一个结点 { p=p->rchild; Visit(p->data); // 访问后继结点 } p=p->rchild; // 若p->rchild不是线索(是右孩子),p指向右孩子,返回循环, } // 找这棵子树中序遍历的第1个结点 } void PreThreading(BiThrTree p) { // PreOrderThreading()调用的递归函数 if(!pre->rchild) // p的前驱没有右孩子 { pre->rchild=p; // p前驱的后继指向p pre->RTag=Thread; // pre的右孩子为线索 } if(!p->lchild) // p没有左孩子 · 250· 《数据结构》算法实现及解析(第二版) { p->LTag=Thread; // p的左孩子为线索 p->lchild=pre; // p的左孩子指向前驱 } pre=p; // 移动前驱 if(p->LTag==Link) // p有左孩子 PreThreading(p->lchild); // 对p的左孩子递归调用preThreading() if(p->RTag==Link) // p有右孩子 PreThreading(p->rchild); // 对p的右孩子递归调用preThreading() } void PreOrderThreading(BiThrTree &Thrt,BiThrTree T) { // 先序线索化二叉树T,头结点的右指针指向先序遍历的最后1个结点 if(!(Thrt=(BiThrTree)malloc(sizeof(BiThrNode)))) // 生成头结点 exit(OVERFLOW); Thrt->LTag=Link; // 头结点的左指针为孩子 Thrt->RTag=Thread; // 头结点的右指针为线索 Thrt->rchild=Thrt; // 头结点的右指针指向自身 if(!T) // 空树(见图620) Thrt->lchild=Thrt; // 头结点的左指针也指向自身 else { // 非空树(见图621) Thrt->lchild=T; // 头结点的左指针指向根结点 pre=Thrt; // 前驱为头结点 PreThreading(T); // 从头结点开始先序递归线索化 pre->rchild=Thrt; // 最后一个结点的后继指向头结点 pre->RTag=Thread; Thrt->rchild=pre; // 头结点的后继指向最后一个结点 } } void PreOrderTraverse_Thr(BiThrTree T,void(*Visit)(TElemType)) { // 先序遍历线索二叉树T(头结点)的非递归算法 BiThrTree p=T->lchild; // p指向根结点 while(p!=T) // p没指向头结点(遍历的最后1个结点的后继指向头结点) { Visit(p->data); // 访问根结点 if(p->LTag==Link) // p有左孩子 p=p->lchild; // p指向左孩子(后继) else // p无左孩子 p=p->rchild; // p指向右孩子或后继 } } void PostThreading(BiThrTree p) { // PostOrderThreading()调用的递归函数 if(p) // p不空 { PostThreading(p->lchild); // 对p的左孩子递归调用PostThreading() PostThreading(p->rchild); // 对p的右孩子递归调用PostThreading() if(!p->lchild) // p没有左孩子 { p->LTag=Thread; // p的左孩子为线索 p->lchild=pre; // p的左孩子指向前驱 第 6 章 树 和 二 叉 树 · 251· } if(!pre->rchild) // p的前驱没有右孩子 { pre->RTag=Thread; // p前驱的右孩子为线索 pre->rchild=p; // p前驱的后继指向p } pre=p; // 移动前驱 } } void PostOrderThreading(BiThrTree &Thrt,BiThrTree T) { // 后序递归线索化二叉树 if(!(Thrt=(BiThrTree)malloc(sizeof(BiThrNode)))) // 生成头结点 exit(OVERFLOW); Thrt->LTag=Link; // 头结点的左指针为孩子 Thrt->RTag=Thread; // 头结点的右指针为线索 if(!T) // 空树(见图620) Thrt->lchild=Thrt->rchild=Thrt; // 头结点的左右指针指向自身 else { // 非空树(见图622) Thrt->lchild=Thrt->rchild=T; // 头结点的左右指针指向根结点(最后一个结点) pre=Thrt; // 前驱为头结点 PostThreading(T); // 从头结点开始后序递归线索化 if(pre->RTag!=Link) // 最后一个结点没有右孩子 { pre->rchild=Thrt; // 最后一个结点的后继指向头结点 pre->RTag=Thread; } } } void DestroyBiTree(BiThrTree &T) { // DestroyBiThrTree调用的递归函数,T指向根结点 if(T) // 非空树 { if(T->LTag==0) // 有左孩子 DestroyBiTree(T->lchild); // 销毁左孩子子树 if(T->RTag==0) // 有右孩子 DestroyBiTree(T->rchild); // 销毁右孩子子树 free(T); // 释放根结点 T=NULL; // 空指针赋0 } } void DestroyBiThrTree(BiThrTree &Thrt) { // 初始条件:线索二叉树Thrt存在。操作结果:销毁线索二叉树Thrt if(Thrt) // 头结点存在 { if(Thrt->lchild) // 根结点存在 DestroyBiTree(Thrt->lchild); // 递归销毁头结点lchild所指二叉树 free(Thrt); // 释放头结点 Thrt=NULL; // 线索二叉树Thrt指针赋0 } } · 252· 《数据结构》算法实现及解析(第二版) // main6-3.cpp 检验bo6-3.cpp的主程序 #define CHAR 1 // 字符型 // #define CHAR 0 // 整型(二者选一) #if CHAR typedef char TElemType; TElemType Nil=′′; // 字符型以空格符为空 #define form "%c" // 输入输出的格式为%c #else typedef int TElemType; TElemType Nil=0; // 整型以0为空 #define form "%d" // 输入输出的格式为%d #endif #include"c1.h" #include"c6-3.h" #include"bo6-3.cpp" void vi(TElemType c) { printf(form" ",c); } void main() { BiThrTree H,T; #if CHAR printf("请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n"); #else printf("请按先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n"); #endif CreateBiThrTree(T); // 按先序产生二叉树 InOrderThreading(H,T); // 在中序遍历的过程中,中序线索化二叉树 printf("中序遍历(输出)线索二叉树:\n"); InOrderTraverse_Thr(H,vi); // 中序遍历(输出)线索二叉树 printf("\n"); DestroyBiThrTree(H); // 销毁线索二叉树 #if CHAR printf("请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n"); #else printf("请按先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n"); #endif scanf("%*c"); // 吃掉回车符 CreateBiThrTree(T); // 按先序产生二叉树T PreOrderThreading(H,T); // 在先序遍历的过程中,先序线索化二叉树 printf("先序遍历(输出)线索二叉树:\n"); PreOrderTraverse_Thr(H,vi); DestroyBiThrTree(H); // 销毁线索二叉树 #if CHAR printf("\n请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树)\n"); #else printf("\n请按先序输入二叉树(如:1 2 0 0 0表示1为根结点,2为左子树的二叉树)\n"); #endif scanf("%*c"); // 吃掉回车符 CreateBiThrTree(T); // 按先序产生二叉树T 第 6 章 树 和 二 叉 树 · 253· PostOrderThreading(H,T); // 在后序遍历的过程中,后序线索化二叉树 DestroyBiThrTree(H); // 销毁线索二叉树 } 程序运行结果: 请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树) abdg e c f (见图623) 中序遍历(输出)线索二叉树:(见图624) g d b e a c f 请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树) abdg e c f (见图623) 先序遍历(输出)线索二叉树:(见图625) a b d g e c f 请按先序输入二叉树(如:ab三个空格表示a为根结点,b为左子树的二叉树) abdg e c f (见图623) 图 626 是后序线索化的示意图。 图 623 生成的二叉树 a b d e c f g 图 624 中序线索化二叉树 H 头结点 0 1 T a b d e c f g 图 625 先序线索化二叉树 a b d e c f g H 头结点 0 1 T 图 626 后序线索化二叉树 H 头结点 0 1 T a b d e c f g · 254· 《数据结构》算法实现及解析(第二版) 6.4 树和森林 二叉树是最简单的树,还有多叉树。森林是由 2 棵以上的树组成的。本节介绍多叉树 和森林的存储方式。 6.4.1  c6-4.h(见图 627 所示)是用顺序结构存储树的。它是定长的(100 个结点),由 n 来 确定有效结点数。parent 域的值为-1 的是根结点。图 628 是教科书中图 6.13 所示之树 及其双亲表存储结构。 // c6-4.h 树的双亲表存储结构(见图627) #define MAX_TREE_SIZE 100 struct PTNode { TElemType data; int parent; // 双亲位置域 }; struct PTree { PTNode nodes[MAX_TREE_SIZE]; int n; // 结点数 }; // bo6-4.cpp 树的双亲表存储(存储结构由 c6-4.h 定义)的基本操作(14 个) #define ClearTree InitTree // 二者操作相同 #define DestroyTree InitTree // 二者操作相同 void InitTree(PTree &T) { // 操作结果:构造空树T T.n=0; } 图 627 树的双亲表存储结构 data parent PTNode PTree Data parent [0] [1] [98] [99] n     图 628 采用双亲表存储树的示例 (a) 教科书图 6.13 所示之树 (b) 存储表示 R BA C F HG K D E [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [99] 10 R 1 A 0 B 0 C 0 D 1 E 1 F 3 G 6 H 6 K 6 第 6 章 树 和 二 叉 树 · 255· typedef struct { int num; TElemType name; }QElemType; // 定义队列元素类型 #include"c3-2.h" // 定义LinkQueue类型(链队列) #include"bo3-2.cpp" // LinkQueue类型的基本操作 void CreateTree(PTree &T) { // 操作结果:构造树T LinkQueue q; QElemType p,qq; int i=1,j,l; char c[MAX_TREE_SIZE]; // 临时存放孩子结点数组 InitQueue(q); // 初始化队列 printf("请输入根结点(字符型,空格为空): "); scanf("%c%*c",&T.nodes[0].data); // 根结点序号为0,%*c吃掉回车符 if(T.nodes[0].data!=Nil) // 非空树 { T.nodes[0].parent=-1; // 根结点无双亲 qq.name=T.nodes[0].data; qq.num=0; EnQueue(q,qq); // 入队此结点 while(iMAX_TREE_SIZE) { printf("结点数超过数组容量\n"); exit(OVERFLOW); } T.n=i; } else T.n=0; } Status TreeEmpty(PTree T) { // 初始条件:树T存在。操作结果:若T为空树,则返回TRUE;否则返回FALSE if(T.n) · 256· 《数据结构》算法实现及解析(第二版) return FALSE; else return TRUE; } int TreeDepth(PTree T) { // 初始条件:树T存在。操作结果:返回T的深度 int k,m,def,max=0; for(k=0;k=0) // 有双亲 printf(" %c",Value(T,T.nodes[i].parent)); // 双亲 printf("\n"); } } Status InsertChild(PTree &T,TElemType p,int i,PTree c) { // 初始条件:树T存在,p是T中某个结点,1≤ i≤ p所指结点的度+1,非空树c与T不相交 // 操作结果:插入c为T中p结点的第i棵子树 int j,k,l,f=1,n=0; // 设交换标志f的初值为1,p的孩子数n的初值为0 PTNode t; if(!TreeEmpty(T)) // T不空 { · 258· 《数据结构》算法实现及解析(第二版) for(j=0;j1) // c不是p的第1棵子树 { for(k=j+1;k=l;k--) // 依次将序号l以后的结点向后移c.n个位置 { T.nodes[k+c.n]=T.nodes[k]; if(T.nodes[k].parent>=l) T.nodes[k+c.n].parent+=c.n; } for(k=0;kT.nodes[j+1].parent) { // 如果结点j的双亲排在结点j+1的双亲之后(树没有按层序排列),交换两结点 t=T.nodes[j]; T.nodes[j]=T.nodes[j+1]; T.nodes[j+1]=t; f=1; // 交换标志置1 for(k=j;kj) T.nodes[k-1].parent--; } j--; · 260· 《数据结构》算法实现及解析(第二版) } T.n-=n; // n为待删除结点数 } } void TraverseTree(PTree T,void(*Visit)(TElemType)) { // 初始条件:二叉树T存在,Visit是对结点操作的应用函数 // 操作结果:层序遍历树T,对每个结点调用函数Visit一次且仅一次 int i; for(i=0;idata); // 先访问根结点 PreOrderTraverse(T->firstchild,Visit); // 再先根遍历长子子树 PreOrderTraverse(T->nextsibling,Visit); // 最后先根遍历下一个兄弟子树 } } // bo6-5.cpp 树的二叉链表(孩子—兄弟 )存储(存储结构由c6-5.h定义)的基本操作(17个) #define ClearTree DestroyTree // 二者操作相同 #include"func6-2.cpp" // 包括PreOrderTraverse() void InitTree(CSTree &T) { // 操作结果:构造空树T T=NULL; } void DestroyTree(CSTree &T) { // 初始条件:树T存在。操作结果:销毁树T if(T) { if(T->firstchild) // T有长子 DestroyTree(T->firstchild); // 销毁T的长子为根结点的子树 if(T->nextsibling) // T有下一个兄弟 DestroyTree(T->nextsibling); // 销毁T的下一个兄弟为根结点的子树 free(T); // 释放根结点 T=NULL; } } typedef CSTree QElemType; // 定义队列元素类型 #include"c3-2.h" // 定义LinkQueue类型(链队列) #include"bo3-2.cpp" // LinkQueue类型的基本操作 void CreateTree(CSTree &T) { // 构造树T char c[20]; // 临时存放孩子结点(设不超过20个)的值 CSTree p,p1; LinkQueue q; int i,l; InitQueue(q); printf("请输入根结点(字符型,空格为空): "); scanf("%c%*c",&c[0]); if(c[0]!=Nil) // 非空树 { T=(CSTree)malloc(sizeof(CSNode)); // 建立根结点 T->data=c[0]; T->nextsibling=NULL; EnQueue(q,T); // 入队根结点的指针 while(!QueueEmpty(q)) // 队不空 { DeQueue(q,p); // 出队一个结点的指针 printf("请按长幼顺序输入结点%c的所有孩子: ",p->data); gets(c); · 264· 《数据结构》算法实现及解析(第二版) l=strlen(c); if(l>0) // 有孩子 { p1=p->firstchild=(CSTree)malloc(sizeof(CSNode)); // 建立长子结点 p1->data=c[0]; for(i=1;inextsibling=(CSTree)malloc(sizeof(CSNode)); // 建立下一个兄弟结点 EnQueue(q,p1); // 入队上一个结点 p1=p1->nextsibling; p1->data=c[i]; } p1->nextsibling=NULL; EnQueue(q,p1); // 入队最后一个结点 } else p->firstchild=NULL; // 长子指针为空 } } else T=NULL; // 空树 } Status TreeEmpty(CSTree T) { // 初始条件:树T存在。操作结果:若T为空树,则返回TURE;否则返回FALSE if(T) // T不空 return FALSE; else return TRUE; } int TreeDepth(CSTree T) { // 初始条件:树T存在。操作结果:返回T的深度 CSTree p; int depth,max=0; if(!T) // 树空 return 0; if(!T->firstchild) // 树无长子 return 1; for(p=T->firstchild;p;p=p->nextsibling) { // 求子树深度的最大值 depth=TreeDepth(p); if(depth>max) max=depth; } return max+1; // 树的深度=子树深度最大值+1 } TElemType Value(CSTree p) { // 返回p所指结点的值 return p->data; } TElemType Root(CSTree T) { // 初始条件:树T存在。操作结果:返回T的根 第 6 章 树 和 二 叉 树 · 265· if(T) return Value(T); else return Nil; } CSTree Point(CSTree T,TElemType s) { // 返回二叉链表(孩子—兄弟 )树T中指向元素值为s的结点的指针。另加 LinkQueue q; QElemType a; if(T) // 非空树 { InitQueue(q); // 初始化队列 EnQueue(q,T); // 根结点入队 while(!QueueEmpty(q)) // 队不空 { DeQueue(q,a); // 出队,队列元素赋给a if(a->data==s) return a; if(a->firstchild) // 有长子 EnQueue(q,a->firstchild); // 入队长子 if(a->nextsibling) // 有下一个兄弟 EnQueue(q,a->nextsibling); // 入队下一个兄弟 } } return NULL; } Status Assign(CSTree &T,TElemType cur_e,TElemType value) { // 初始条件:树T存在,cur_e是树T中结点的值。操作结果:改cur_e为value CSTree p; if(T) // 非空树 { p=Point(T,cur_e); // p为cur_e的指针 if(p) // 找到cur_e { p->data=value; // 赋新值 return OK; } } return ERROR ; // 树空或没找到 } TElemType Parent(CSTree T,TElemType cur_e) { // 初始条件:树T存在,cur_e是T中某个结点 // 操作结果:若cur_e是T的非根结点,则返回它的双亲;否则函数值为“空” CSTree p,t; LinkQueue q; InitQueue(q); if(T) // 树非空 { if(Value(T)==cur_e) // 根结点值为cur_e return Nil; EnQueue(q,T); // 根结点入队 · 266· 《数据结构》算法实现及解析(第二版) while(!QueueEmpty(q)) { DeQueue(q,p); if(p->firstchild) // p有长子 { if(p->firstchild->data==cur_e) // 长子为cur_e return Value(p); // 返回双亲 t=p; // 双亲指针赋给t p=p->firstchild; // p指向长子 EnQueue(q,p); // 入队长子 while(p->nextsibling) // 有下一个兄弟 { p=p->nextsibling; // p指向下一个兄弟 if(Value(p)==cur_e) // 下一个兄弟为cur_e return Value(t); // 返回双亲 EnQueue(q,p); // 入队下一个兄弟 } } } } return Nil; // 树空或没找到cur_e } TElemType LeftChild(CSTree T,TElemType cur_e) { // 初始条件:树T存在,cur_e是T中某个结点 // 操作结果:若cur_e是T的非叶子结点,则返回它的最左孩子;否则返回“空” CSTree f; f=Point(T,cur_e); // f指向结点cur_e if(f&&f->firstchild) // 找到结点cur_e且结点cur_e有长子 return f->firstchild->data; else return Nil; } TElemType RightSibling(CSTree T,TElemType cur_e) { // 初始条件:树T存在,cur_e是T中某个结点 // 操作结果:若cur_e有右兄弟,则返回它的右兄弟;否则返回“空” CSTree f; f=Point(T,cur_e); // f指向结点cur_e if(f&&f->nextsibling) // 找到结点cur_e且结点cur_e有右兄弟 return f->nextsibling->data; else return Nil; // 树空 } Status InsertChild(CSTree &T,CSTree p,int i,CSTree c) { // 初始条件:树T存在,p指向T中某个结点,1≤ i≤ p所指结点的度+1,非空树c与T不相交 // 操作结果:插入c为T中p结点的第i棵子树 // 因为p所指结点的地址不会改变,故p不需是引用类型 int j; if(T) // T不空 { if(i==1) // 插入c为p的长子 { 第 6 章 树 和 二 叉 树 · 267· c->nextsibling=p->firstchild; // p的原长子现是c的下一个兄弟(c本无兄弟) p->firstchild=c; } else // 找插入点 { p=p->firstchild; // 指向p的长子 j=2; while(p&&jnextsibling; j++; } if(j==i) // 找到插入位置 { c->nextsibling=p->nextsibling; p->nextsibling=c; } else // p原有孩子数小于i-1 return ERROR; } return OK; } else // T空 return ERROR; } Status DeleteChild(CSTree &T,CSTree p,int i) { // 初始条件:树T存在,p指向T中某个结点,1≤ i≤ p所指结点的度 // 操作结果:删除T中p所指结点的第i棵子树 // 因为p所指结点的地址不会改变,故p不需是引用类型 CSTree b; int j; if(T) // T不空 { if(i==1) // 删除长子 { b=p->firstchild; p->firstchild=b->nextsibling; // p的原次子现是长子 b->nextsibling=NULL; DestroyTree(b); } else // 删除非长子 { p=p->firstchild; // p指向长子 j=2; while(p&&jnextsibling; j++; } if(j==i) // 找到第i棵子树 { · 268· 《数据结构》算法实现及解析(第二版) b=p->nextsibling; p->nextsibling=b->nextsibling; b->nextsibling=NULL; DestroyTree(b); } else // p原有孩子数小于i return ERROR; } return OK; } else return ERROR; } void PostOrderTraverse(CSTree T,void(*Visit)(TElemType)) { // 后根遍历孩子—兄弟二叉链表结构的树 T CSTree p; if(T) { if(T->firstchild) // 有长子 { PostOrderTraverse(T->firstchild,Visit); // 后根遍历长子子树 p=T->firstchild->nextsibling; // p指向长子的下一个兄弟 while(p) { PostOrderTraverse(p,Visit); // 后根遍历下一个兄弟子树 p=p->nextsibling; // p指向再下一个兄弟 } } Visit(Value(T)); // 最后访问根结点 } } void LevelOrderTraverse(CSTree T,void(*Visit)(TElemType)) { // 层序遍历孩子—兄弟二叉链表结构的树 T CSTree p; LinkQueue q; InitQueue(q); if(T) { Visit(Value(T)); // 先访问根结点 EnQueue(q,T); // 入队根结点的指针 while(!QueueEmpty(q)) // 队不空 { DeQueue(q,p); // 出队一个结点的指针 if(p->firstchild) // 有长子 { p=p->firstchild; Visit(Value(p)); // 访问长子结点 EnQueue(q,p); // 入队长子结点的指针 while(p->nextsibling) // 有下一个兄弟 { p=p->nextsibling; 第 6 章 树 和 二 叉 树 · 269· Visit(Value(p)); // 访问下一个兄弟 EnQueue(q,p); // 入队兄弟结点的指针 } } } } } // main6-5.cpp 检验bo6-5.cpp的主程序 #include"c1.h" typedef char TElemType; TElemType Nil=′′; // 以空格符为空 #include"c6-5.h" #include"bo6-5.cpp" void vi(TElemType c) { printf("%c ",c); } void main() { int i; CSTree T,p,q; TElemType e,e1; InitTree(T); printf("构造空树后,树空否? %d(1:是 0:否) 树根为%c 树的深度为%d\n",TreeEmpty(T),Root(T), TreeDepth(T)); CreateTree(T); printf("构造树T后,树空否? %d(1:是 0:否) 树根为%c 树的深度为%d\n",TreeEmpty(T),Root(T), TreeDepth(T)); printf("先根遍历树T:\n"); PreOrderTraverse(T,vi); printf("\n请输入待修改的结点的值 新值: "); scanf("%c%*c%c%*c",&e,&e1); Assign(T,e,e1); printf("后根遍历修改后的树T:\n"); PostOrderTraverse(T,vi); printf("\n%c的双亲是%c,长子是%c,下一个兄弟是%c\n",e1,Parent(T,e1),LeftChild(T,e1), RightSibling(T,e1)); printf("建立树p:\n"); InitTree(p); CreateTree(p); printf("层序遍历树p:\n"); LevelOrderTraverse(p,vi); printf("\n将树p插到树T中,请输入T中p的双亲结点 子树序号: "); scanf("%c%d%*c",&e,&i); q=Point(T,e); InsertChild(T,q,i,p); printf("层序遍历树T:\n"); LevelOrderTraverse(T,vi); printf("\n删除树T中结点e的第i棵子树,请输入e i: "); scanf("%c%d",&e,&i); · 270· 《数据结构》算法实现及解析(第二版) q=Point(T,e); DeleteChild(T,q,i); printf("层序遍历树T:\n",e,i); LevelOrderTraverse(T,vi); printf("\n"); DestroyTree(T); } 程序运行结果: 构造空树后,树空否? 1(1:是 0:否) 树根为 树的深度为0 请输入根结点(字符型,空格为空): R 请按长幼顺序输入结点R的所有孩子: ABC 请按长幼顺序输入结点A的所有孩子: DE 请按长幼顺序输入结点B的所有孩子:  请按长幼顺序输入结点C的所有孩子:F 请按长幼顺序输入结点D的所有孩子:  请按长幼顺序输入结点E的所有孩子:  请按长幼顺序输入结点F的所有孩子: GHK 请按长幼顺序输入结点G的所有孩子:  请按长幼顺序输入结点H的所有孩子:  请按长幼顺序输入结点K的所有孩子:  构造树T后,树空否? 0(1:是 0:否) 树根为R 树的深度为4 先根遍历树T:(见图628(a)) RADEBCFGHK 请输入待修改的结点的值 新值: D d 后根遍历修改后的树T: d E A B G H K F C R d的双亲是A,长子是 ,下一个兄弟是E 建立树p: 请输入根结点(字符型,空格为空): f 请按长幼顺序输入结点f的所有孩子: ghk 请按长幼顺序输入结点g的所有孩子:  请按长幼顺序输入结点h的所有孩子:  请按长幼顺序输入结点k的所有孩子:  层序遍历树p:(见图629) f g h k 将树p插到树T中,请输入T中p的双亲结点 子树序号: R 3 层序遍历树T:(见图630) RABfCdEghkFGHK 删除树T中结点e的第i棵子树,请输入e i: C 1 层序遍历树T:(见图631) R A B f C d E g h k 6.4.2  6.4.3   第 6 章 树 和 二 叉 树 · 271· 6.5 树与等价问题 6.6 赫夫曼树及其应用 6.6.1  最优二叉树是带权路径长度最短的二叉树。根据结点的个数、权值的不同,最优二叉 树的形状也各不相同。图 634 是 3 棵最优二叉树的例子。它们的共同特点是:带权值的 结点都是叶子结点。权值越小的结点,其到根结点的路径越长。构造最优二叉树的方法 如下: (1) 将每个带有权值的结点作为一棵仅有根结点的二叉树,树的权值为结点的权值; (2) 将其中两棵权值最小的树组成一棵新二叉树,新树的权值为两棵树的权值之和; (3) 重复(2),直到所有结点都在一棵二叉树上。这棵二叉树就是最优二叉树。 最优二叉树的左右子树是可以互换的,因为这不影响树的带权路径长度。当结点的权 值差别大到一定程度,最优二叉树就形成了如图 634(b)所示的“一边倒”的形状。有些 书称最优二叉树都是这种“一边倒”的形状是不对的。这通过计算二叉树的带权路径长度 是否最短就可看出。当所有结点的权值一样,或其权值差别很小,最优二叉树就形成了如 图 634(c)所示的完全二叉树的形状。叶子结点的路径长度近似相等。 最优二叉树除了叶子结点就是度为 2 的结点,没有度为 1 的结点。这样才使得树的带 权路径长度最短。根据二叉树的性质 3,最优二叉树的结点数为叶子数的 2 倍减 1。 6.6.2  // c6-7.h 赫夫曼树和赫夫曼编码的存储结构(见图635) typedef struct { unsigned int weight; unsigned int parent,lchild,rchild; }HTNode,*HuffmanTree; // 动态分配数组存储赫夫曼树 typedef char **HuffmanCode; // 动态分配数组存储赫夫曼编码表 图 634 3 棵形状不同的最优二叉树 (b) 权值差别大的结点 构成的最优二叉树 9 2 1 5 (c) 权值相同的结点 构成的最优二叉树 2 2 22 (a) 最优二叉树 2 1 35 4 图 635 赫夫曼树和赫夫曼编码的存储结构 HuffmanTree HTNode weight parent lchild rchild charHuffmanCode char* · 272· 《数据结构》算法实现及解析(第二版) c6-7.h 定义的二叉树结构是我们在前边没有讨论过的,但它特别适合建立赫夫曼树。 赫夫曼树是由多棵二叉树(森林)组合成而的一棵树。这种二叉树结构既适合表示树,也适 合表示森林。赫夫曼树结点的结构包括权值、双亲及左右孩子,双亲值为 0 的是根结点, 左右孩子值均为 0 的是叶子结点。这种二叉树结构是动态生成的顺序结构。当叶子结点数 确定,赫夫曼树的结点数也确定。由图 636(d)可见,建成的赫夫曼树除 0 号结点空间不 用外,每个结点空间都没空置。 // func6-1.cpp 程序 algo6-1.cpp和algo6-2.cpp要调用 int min(HuffmanTree t,int i) { // 返回i个结点中权值最小的树的根结点序号,函数select()调用 int j,flag; unsigned int k=UINT_MAX; // 取k为不小于可能的值(无符号整型最大值) for(j=1;j<=i;j++) if(t[j].weights2) { j=s1; s1=s2; s2=j; } } // algo6-1.cpp 求赫夫曼编码。实现算法6.12的程序 #include"c1.h" #include"c6-7.h" #include"func6-1.cpp" void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n) // 算法6.12 { // w存放n个字符的权值(均>0),构造赫夫曼树HT,并求出n个字符的赫夫曼编码HC int m,i,s1,s2,start; unsigned c,f; HuffmanTree p; char *cd; if(n<=1) return; m=2*n-1; HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode)); // 0号单元未用 for(p=HT+1,i=1;i<=n;++i,++p,++w) { (*p).weight=*w; 第 6 章 树 和 二 叉 树 · 273· (*p).parent=0; (*p).lchild=0; (*p).rchild=0; } for(;i<=m;++i,++p) (*p).parent=0; for(i=n+1;i<=m;++i) // 建赫夫曼树 { // 在HT[1~i-1]中选择parent为0且weight最小的两个结点,其序号分别为s1和s2 select(HT,i-1,s1,s2); HT[s1].parent=HT[s2].parent=i; HT[i].lchild=s1; HT[i].rchild=s2; HT[i].weight=HT[s1].weight+HT[s2].weight; } // 从叶子到根逆向求每个字符的赫夫曼编码 HC=(HuffmanCode)malloc((n+1)*sizeof(char*)); // 分配n个字符编码的头指针向量([0]不用) cd=(char*)malloc(n*sizeof(char)); // 分配求编码的工作空间 cd[n-1]=′\0′; // 编码结束符 for(i=1;i<=n;i++) { // 逐个字符求赫夫曼编码 start=n-1; // 编码结束符位置 for(c=i,f=HT[i].parent;f!=0;c=f,f=HT[f].parent) // 从叶子到根逆向求编码 if(HT[f].lchild==c) cd[--start]=′0′; else cd[--start]=′1′; HC[i]=(char*)malloc((n-start)*sizeof(char)); // 为第i个字符编码分配空间 strcpy(HC[i],&cd[start]); // 从cd复制编码(串)到HC } free(cd); // 释放工作空间 } void main() { HuffmanTree HT; HuffmanCode HC; int *w,n,i; printf("请输入权值的个数(>1): "); scanf("%d",&n); w=(int*)malloc(n*sizeof(int)); printf("请依次输入%d个权值(整型):\n",n); for(i=0;i<=n-1;i++) scanf("%d",w+i); HuffmanCoding(HT,HC,w,n); for(i=1;i<=n;i++) puts(HC[i]); } · 274· 《数据结构》算法实现及解析(第二版) 程序运行结果(以教科书图 6.24 为例,如图 636 所示): 请输入权值的个数(>1): 4 请依次输入4个权值(整型): 7 5 2 4 0 10 110 111 图 636 是运行过程的图解。初始状态下(见图 636(b)),权值分别为 7、5、2、4 的 4 个结点是 4 棵独立的树(根结点)。它们没有双亲,也没有左右孩子。反复查找权值最 小的两棵树,并把它们合并成一棵树,其权值为两树的权值之和。最后,所有结点合并成 一棵赫夫曼树(见图 636(d))。 算法 6.12(在 algo6-1.cpp 中)在找到两个无双亲且权值最小的结点后,将序号小的结 点作为左子树,序号大的结点作为右子树,如果不按这个规则,赫夫曼编码的形式会改 变。但码长不会改变,仍然是赫夫曼编码。 algo6-2.cpp 与 algo6-1.cpp 仅是函数 HuffmanCoding()的后半部分不同,也就是在建 立好赫夫曼树后求赫夫曼编码的方法不同。algo6-1.cpp 是先找到叶子结点(左右孩子均为 0 的结点),再根据其双亲结点逐步找到根结点来求赫夫曼编码的;而 algo6-2.cpp 是由根 结点起,依次查找其左右孩子,直到找到叶子结点来求赫夫曼编码的。 // algo6-2.cpp 实现算法6.13的程序 #include"c1.h" 图 636 algo6-1.cpp 运行过程的图解 (b) 生成的 HT 数组赋初值后的状态 weight parent lchild rchild 7 0 0 0 5 0 0 0 2 0 0 0 4 0 0 0 0 0 0 HT [0] [1] [n] [2n-1] weight parent lchild rchild 7 7 0 0 5 6 0 0 2 5 0 0 4 5 0 0 6 6 3 4 11 7 2 5 18 0 1 6 HT [0] [1] [n] [2n-1] (d) HT 数组建赫夫曼树后的状态 w 7 5 2 4 (a) 生成的 w 数组 HC [1] [n] 0 10 110 111 (f) HC 数组元素是赫 夫曼编码的指针 7 2 45 (c) (b) 所对应的结点状况 (e) (d)所对应的结点状况 7 2 4 5      第 6 章 树 和 二 叉 树 · 275· #include"c6-7.h" #include"func6-1.cpp" void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n) // 前半部分为算法6.12 { // w存放n个字符的权值(均>0),构造赫夫曼树HT,并求出n个字符的赫夫曼编码HC int m,i,s1,s2; // 此句与algo6-1.cpp不同 unsigned c,cdlen; // 此句与algo6-1.cpp不同 HuffmanTree p; char *cd; if(n<=1) return; m=2*n-1; HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode)); // 0号单元未用 for(p=HT+1,i=1;i<=n;++i,++p,++w) { (*p).weight=*w; (*p).parent=0; (*p).lchild=0; (*p).rchild=0; } for(;i<=m;++i,++p) (*p).parent=0; for(i=n+1;i<=m;++i) // 建赫夫曼树 { // 在HT[1~i-1]中选择parent为0且weight最小的两个结点,其序号分别为s1和s2 select(HT,i-1,s1,s2); HT[s1].parent=HT[s2].parent=i; HT[i].lchild=s1; HT[i].rchild=s2; HT[i].weight=HT[s1].weight+HT[s2].weight; } // 以下为算法6.13,无栈非递归遍历赫夫曼树,求赫夫曼编码,以上同算法6.12 HC=(HuffmanCode)malloc((n+1)*sizeof(char*)); // 分配n个字符编码的头指针向量([0]不用) cd=(char*)malloc(n*sizeof(char)); // 分配求编码的工作空间 c=m; cdlen=0; for(i=1;i<=m;++i) HT[i].weight=0; // 遍历赫夫曼树时用作结点状态标志 while(c) { if(HT[c].weight==0) { // 向左 HT[c].weight=1; if(HT[c].lchild!=0) { c=HT[c].lchild; cd[cdlen++]=′0′; } else if(HT[c].rchild==0) { // 登记叶子结点的字符的编码 HC[c]=(char *)malloc((cdlen+1)*sizeof(char)); cd[cdlen]=′\0′; strcpy(HC[c],cd); // 复制编码(串) · 276· 《数据结构》算法实现及解析(第二版) } } else if(HT[c].weight==1) { // 向右 HT[c].weight=2; if(HT[c].rchild!=0) { c=HT[c].rchild; cd[cdlen++]=′1′; } } else { // HT[c].weight==2,退回 HT[c].weight=0; c=HT[c].parent; --cdlen; // 退到父结点,编码长度减1 } } free(cd); } void main() { // 主程序同algo6-1.cpp HuffmanTree HT; HuffmanCode HC; int *w,n,i; printf("请输入权值的个数(>1): "); scanf("%d",&n); w=(int *)malloc(n*sizeof(int)); printf("请依次输入%d个权值(整型):\n",n); for(i=0;i<=n-1;i++) scanf("%d",w+i); HuffmanCoding(HT,HC,w,n); for(i=1;i<=n;i++) puts(HC[i]); } 程序运行结果(以教科书例 62 为例): 请输入权值的个数(>1): 8 请依次输入8个权值(整型): 5 29 7 8 14 23 3 11 0110 10 1110 1111 110 00 0111 010 第 7 章 图 · 277· 第 7 章 图 7.1 图的定义和术语 7.2 图的存储结构 图是比较复杂的数据结构,它由顶点和顶点之间的弧或边组成。任何两个顶点之间都 可能存在弧或边。在计算机存储图时,只要能表示出顶点的个数及每个顶点的特征、每对 顶点之间是否存在弧(边)及弧(边)的特征,就能表示出图的所有信息,并作为图的一种存 储结构。本章介绍了 4 种图的存储结构,它们各有特点。 7.2.1  // c7-1.h 图的数组(邻接矩阵)存储结构(见图71) #define INFINITY INT_MAX // 用整型最大值代替∞ #define MAX_VERTEX_NUM 26 // 最大顶点个数 enum GraphKind{DG,DN,UDG,UDN}; // {有向图,有向网,无向图,无向网} typedef struct { VRType adj; // 顶点关系类型。对无权图, // 用1(是)或0(否)表示相邻否; // 对带权图,则为权值 InfoType *info; // 该弧相关信息的指针(可无) }ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 二维数组 struct MGraph { VertexType vexs[MAX_VERTEX_NUM]; // 顶点向量 AdjMatrix arcs; // 邻接矩阵 int vexnum,arcnum; // 图的当前顶点数和弧数 GraphKind kind; // 图的种类标志 }; c7-1.h 中,结构体 MGraph 中的顶点向量是 VertexType 类型。一般地,我们都在主程 序中定义 VertexType 类型为字符串类型,表示顶点名称(见 main7-1.cpp)。VertexType 类 型也可以是结构体。对于 AOV-网(见教科书 7.5.1 节),顶点不仅包括名称,还包括活 图 71 图的数组(邻接矩阵)存储结构 MGraph adj info ArcCell InfoType vexnum arcnum kind [0] [1] [28] [29] [0][1] ⋯ [28][29][0] [1] [28] [29] arcs vexs                   ...... ...... .... .... ...... ......        ⋯ · 278· 《数据结构》算法实现及解析(第二版) 动,所以要用结构体表示。 图 72 是根据 c7-1.h 定义的有向图的存储结构。vexs[]数组存放各顶点的信息, arcs[][]数组存放各顶点邻接关系信息(是否互为邻接点),如果 1 条弧从第 i 个顶点发出, 终止于第 j 个顶点,则 arcs[i][j]=1。如图 72(b)所示,arcs[0][1]=1,说明从 v1 到 v2 有 1 条弧。设对角元素(arcs[i][i])的邻接关系为 0,则 arcs[][]数组中值为 1 的元素的个 数等于有向图的弧数。图 73 是根据 c7-1.h 定义的无向网(网也称为带权图)的存储结 构。同图 72 一样,图 73 中的 vexs[]数组仍存放各顶点的信息,arcs[][]数组存放各 顶点邻接关系信息。对于网,顶点互为邻接点,则其值为权值;否则其值为∞。设对角元 素(arcs[i][i])的邻接关系为∞,如果在第 i 个顶点和第 j 个顶点之间有边(无向),则 arcs[i][j]= arcs[j][i]=权值。如图 73(b)所示,arcs[0][1]= arcs[1][0]=3,说明在 v1、 v2 之间有 1 条边,其权值为 3。无向图或网的二维数组是以主对角线为轴对称的,对称的 两个单元表示同一条边。arcs[][]数组中值不为∞的元素的个数等于无向网边数的 2 倍。 在这种数组(邻接矩阵)存储结构中,数组所占用的存储空间与图的弧或边数无关,故 适用于边数较多的稠密图。 // bo7-1.cpp 图的数组(邻接矩阵)存储(存储结构由c7-1.h定义)的基本操作(21个),包括算法7.1、 // 7.2和7.4~7.6 int LocateVex(MGraph G,VertexType u) { // 初始条件:图G存在,u和G中顶点有相同特征 // 操作结果:若G中存在顶点u,则返回该顶点在图中位置;否则返回-1 int i; for(i=0;i=G.vexnum||v<0) exit(ERROR); return G.vexs[v]; · 284· 《数据结构》算法实现及解析(第二版) } Status PutVex(MGraph &G,VertexType v,VertexType value) { // 初始条件:图G存在,v是G中某个顶点。操作结果:对v赋新值value int k; k=LocateVex(G,v); // k为顶点v在图G中的序号 if(k<0) return ERROR; strcpy(G.vexs[k],value); return OK; } int FirstAdjVex(MGraph G,VertexType v) { // 初始条件:图G存在,v是G中某个顶点 // 操作结果:返回v的第一个邻接顶点的序号。若顶点在G中没有邻接顶点,则返回-1 int i,j=0,k; k=LocateVex(G,v); // k为顶点v在图G中的序号 if(G.kind%2) // 网 j=INFINITY; for(i=0;i,若G是无向的,则还增添对称弧 int i,l,v1,w1; char s[MAX_INFO]; v1=LocateVex(G,v); // 尾 w1=LocateVex(G,w); // 头 if(v1<0||w1<0) return ERROR; G.arcnum++; // 弧或边数加1 if(G.kind%2) // 网 { printf("请输入此弧或边的权值: "); scanf("%d",&G.arcs[v1][w1].adj); } else // 图 G.arcs[v1][w1].adj=1; · 286· 《数据结构》算法实现及解析(第二版) printf("是否有该弧或边的相关信息(0:无 1:有): "); scanf("%d%*c",&i); if(i) { printf("请输入该弧或边的相关信息(<%d个字符):",MAX_INFO); gets(s); l=strlen(s); if(l) { G.arcs[v1][w1].info=(char*)malloc((l+1)*sizeof(char)); strcpy(G.arcs[v1][w1].info,s); } } if(G.kind>1) // 无向 { G.arcs[w1][v1].adj=G.arcs[v1][w1].adj; G.arcs[w1][v1].info=G.arcs[v1][w1].info; // 指向同一个相关信息 } return OK; } Status DeleteArc(MGraph &G,VertexType v,VertexType w) { // 初始条件:图G存在,v和w是G中两个顶点 // 操作结果:在G中删除弧,若G是无向的,则还删除对称弧 int v1,w1,j=0; if(G.kind%2) // 网 j=INFINITY; v1=LocateVex(G,v); // 尾 w1=LocateVex(G,w); // 头 if(v1<0||w1<0) // v1、w1的值不合法 return ERROR; G.arcs[v1][w1].adj=j; if(G.arcs[v1][w1].info) // 有其它信息 { free(G.arcs[v1][w1].info); G.arcs[v1][w1].info=NULL; } if(G.kind>=2) // 无向,删除对称弧 { G.arcs[w1][v1].adj=j; G.arcs[w1][v1].info=NULL; } G.arcnum--; // 弧数-1 return OK; } Boolean visited[MAX_VERTEX_NUM]; // 访问标志数组(全局量) void(*VisitFunc)(VertexType); // 函数变量 void DFS(MGraph G,int v) { // 从第v个顶点出发递归地深度优先遍历图G。算法7.5 int w; visited[v]=TRUE; // 设置访问标志为TRUE(已访问) VisitFunc(G.vexs[v]); // 访问第v个顶点 第 7 章 图 · 287· for(w=FirstAdjVex(G,G.vexs[v]);w>=0;w=NextAdjVex(G,G.vexs[v],G.vexs[w])) if(!visited[w]) DFS(G,w); // 对v的尚未访问的序号为w的邻接顶点递归调用DFS } void DFSTraverse(MGraph G,void(*Visit)(VertexType)) { // 初始条件:图G存在,Visit是顶点的应用函数。算法7.4 // 操作结果:从第1个顶点起,深度优先遍历图G,并对每个顶点调用函数Visit一次且仅一次 int v; VisitFunc=Visit; // 使用全局变量VisitFunc,使DFS不必设函数指针参数 for(v=0;v=0;w=NextAdjVex(G,G.vexs[u],G.vexs[w])) if(!visited[w]) // w为u的尚未访问的邻接顶点的序号 { visited[w]=TRUE; Visit(G.vexs[w]); EnQueue(Q,w); } } } printf("\n"); } void Display(MGraph G) { // 输出邻接矩阵存储结构的图G int i,j; char s[7]; · 288· 《数据结构》算法实现及解析(第二版) switch(G.kind) { case DG: strcpy(s,"有向图"); break; case DN: strcpy(s,"有向网"); break; case UDG:strcpy(s,"无向图"); break; case UDN:strcpy(s,"无向网"); } printf("%d个顶点%d条边或弧的%s。顶点依次是: ",G.vexnum,G.arcnum,s); for(i=0;i=2) // 无向图或网,产生第2个表结点,并插在第j个元素(入弧)的表头 { e.adjvex=i; // e.info不变,不必再赋值 · 296· 《数据结构》算法实现及解析(第二版) ListInsert(G.vertices[j].firstarc,1,e); // 插在第j个元素的表头,在bo2-8.cpp中 } } } void CreateGraphF(ALGraph &G) { // 采用邻接表存储结构,由文件构造没有相关信息图或网G(用一个函数构造4种图) int i,j,k,w; // w是权值 VertexType va,vb; // 连接边或弧的2顶点 ElemType e; char filename[13]; FILE *graphlist; printf("请输入数据文件名(f7-1.txt或f7-2.txt):"); scanf("%s",filename); printf("请输入图的类型(有向图:0,有向网:1,无向图:2,无向网:3): "); scanf("%d",&G.kind); graphlist=fopen(filename,"r"); // 以读的方式打开数据文件,并以graphlist表示 fscanf(graphlist,"%d",&G.vexnum); fscanf(graphlist,"%d",&G.arcnum); for(i=0;i=2) // 无向图或网,产生第2个表结点,并插在第j个元素(入弧)的表头 { e.adjvex=i; // e.info不变,不必再赋值 ListInsert(G.vertices[j].firstarc,1,e); // 插在第j个元素的表头,在bo2-8.cpp中 } } fclose(graphlist); // 关闭数据文件 } void DestroyGraph(ALGraph &G) { // 初始条件:图G存在。操作结果:销毁图G int i; ElemType e; 第 7 章 图 · 297· for(i=0;ii) // 顶点序号>i(保证动态生成的权值空间只释放1次) free(e.info); } else // 图 DestroyList(G.vertices[i].firstarc); // 销毁弧或边链表,在bo2-8.cpp中 G.vexnum=0; // 顶点数为0 G.arcnum=0; // 边或弧数为0 } VertexType& GetVex(ALGraph G,int v) { // 初始条件:图G存在,v是G中某个顶点的序号。操作结果:返回v的值 if(v>=G.vexnum||v<0) exit(ERROR); return G.vertices[v].data; } Status PutVex(ALGraph &G,VertexType v,VertexType value) { // 初始条件:图G存在,v是G中某个顶点。操作结果:对v赋新值value int i; i=LocateVex(G,v); if(i>-1) // v是G的顶点 { strcpy(G.vertices[i].data,value); return OK; } return ERROR; } int FirstAdjVex(ALGraph G,VertexType v) { // 初始条件:图G存在,v是G中某个顶点 // 操作结果:返回v的第一个邻接顶点的序号。若顶点在G中没有邻接顶点,则返回-1 ArcNode *p; int v1; v1=LocateVex(G,v); // v1为顶点v在图G中的序号 p=G.vertices[v1].firstarc; if(p) return p->data.adjvex; else return -1; } Status equalvex(ElemType a,ElemType b) { // DeleteArc()、DeleteVex()和NextAdjVex()要调用的函数 if(a.adjvex==b.adjvex) return OK; else return ERROR; } int NextAdjVex(ALGraph G,VertexType v,VertexType w) { // 初始条件:图G存在,v是G中某个顶点,w是v的邻接顶点 · 298· 《数据结构》算法实现及解析(第二版) // 操作结果:返回v的(相对于w的)下一个邻接顶点的序号。若w是v的最后一个邻接点,则返回-1 LinkList p,p1; // p1在Point()中用作辅助指针,Point()在func2-1.cpp中 ElemType e; int v1; v1=LocateVex(G,v); // v1为顶点v在图G中的序号 e.adjvex=LocateVex(G,w); // e.adjvex为顶点w在图G中的序号 p=Point(G.vertices[v1].firstarc,e,equalvex,p1); // p指向顶点v的链表中邻接顶点为w的结点 if(!p||!p->next) // 没找到w或w是最后一个邻接点 return -1; else // p->data.adjvex==w return p->next->data.adjvex; // 返回v的(相对于w的)下一个邻接顶点的序号 } void InsertVex(ALGraph &G,VertexType v) { // 初始条件:图G存在,v和图中顶点有相同特征 // 操作结果:在图G中增添新顶点v(不增添与顶点相关的弧,留待InsertArc()去做) strcpy(G.vertices[G.vexnum].data,v); // 构造新顶点向量 G.vertices[G.vexnum].firstarc=NULL; G.vexnum++; // 图G的顶点数加1 } Status DeleteVex(ALGraph &G,VertexType v) { // 初始条件:图G存在,v是G中某个顶点。操作结果:删除G中顶点v及其相关的弧 int i,j,k; ElemType e; LinkList p,p1; j=LocateVex(G,v); // j是顶点v的序号 if(j<0) // v不是图G的顶点 return ERROR; i=ListLength(G.vertices[j].firstarc); // 以v为出度的弧或边数,在bo2-8.cpp中 G.arcnum-=i; // 边或弧数-i if(G.kind%2) // 网 while(G.vertices[j].firstarc) // 对应的弧或边链表不空 { ListDelete(G.vertices[j].firstarc,1,e); // 删除链表的第1个结点,并将值赋给e free(e.info); // 释放动态生成的权值空间 } else // 图 DestroyList(G.vertices[j].firstarc); // 销毁弧或边链表,在bo2-8.cpp中 G.vexnum--; // 顶点数减1 for(i=j;inext=p->next; // 从链表中删除p所指结点 else // p指向首元结点 G.vertices[i].firstarc=p->next; // 头指针指向下一结点 if(G.kind<2) // 有向 第 7 章 图 · 299· { G.arcnum--; // 边或弧数-1 if(G.kind==1) // 有向网 free(p->data.info); // 释放动态生成的权值空间 } free(p); // 释放v为入度的结点 } for(k=j+1;k<=G.vexnum;k++) // 对于adjvex域>j的结点,其序号-1 { e.adjvex=k; p=Point(G.vertices[i].firstarc,e,equalvex,p1); // Point()在func2-1.cpp中 if(p) p->data.adjvex--; // 序号-1(因为前移) } } return OK; } Status InsertArc(ALGraph &G,VertexType v,VertexType w) { // 初始条件:图G存在,v和w是G中两个顶点 // 操作结果:在G中增添弧,若G是无向的,则还增添对称弧 ElemType e; int i,j; i=LocateVex(G,v); // 弧尾或边的序号 j=LocateVex(G,w); // 弧头或边的序号 if(i<0||j<0) return ERROR; G.arcnum++; // 图G的弧或边的数目加1 e.adjvex=j; e.info=NULL; // 初值 if(G.kind%2) // 网 { e.info=(int *)malloc(sizeof(int)); // 动态生成存放权值的空间 printf("请输入弧(边)%s→%s的权值: ",v,w); scanf("%d",e.info); } ListInsert(G.vertices[i].firstarc,1,e); // 将e插在弧尾的表头,在bo2-8.cpp中 if(G.kind>=2) // 无向,生成另一个表结点 { e.adjvex=i; // e.info不变 ListInsert(G.vertices[j].firstarc,1,e); // 将e插在弧头的表头 } return OK; } Status DeleteArc(ALGraph &G,VertexType v,VertexType w) { // 初始条件:图G存在,v和w是G中两个顶点 // 操作结果:在G中删除弧,若G是无向的,则还删除对称弧 int i,j; Status k; ElemType e; i=LocateVex(G,v); // i是顶点v(弧尾)的序号 j=LocateVex(G,w); // j是顶点w(弧头)的序号 · 300· 《数据结构》算法实现及解析(第二版) if(i<0||j<0||i==j) return ERROR; e.adjvex=j; k=DeleteElem(G.vertices[i].firstarc,e,equalvex); // 在func2-1.cpp中 if(k) // 删除成功 { G.arcnum--; // 弧或边数减1 if(G.kind%2) // 网 free(e.info); if(G.kind>=2) // 无向,删除对称弧 { e.adjvex=i; DeleteElem(G.vertices[j].firstarc,e,equalvex); } return OK; } else // 没找到待删除的弧 return ERROR; } Boolean visited[MAX_VERTEX_NUM]; // 访问标志数组(全局量) void(*VisitFunc)(char* v); // 函数变量(全局量) void DFS(ALGraph G,int v) { // 从第v个顶点出发递归地深度优先遍历图G。算法7.5 int w; visited[v]=TRUE; // 设置访问标志为TRUE(已访问) VisitFunc(G.vertices[v].data); // 访问第v个顶点 for(w=FirstAdjVex(G,G.vertices[v].data);w>=0;w=NextAdjVex(G,G.vertices[v].data, G.vertices[w].data)) if(!visited[w]) DFS(G,w); // 对v的尚未访问的邻接点w递归调用DFS } void DFSTraverse(ALGraph G,void(*Visit)(char*)) { // 对图G作深度优先遍历。算法7.4 int v; VisitFunc=Visit; // 使用全局变量VisitFunc,使DFS不必设函数指针参数 for(v=0;v=0;w=NextAdjVex(G,G.vertices[u].data, G.vertices[w].data)) if(!visited[w]) // w为u的尚未访问的邻接顶点 { visited[w]=TRUE; Visit(G.vertices[w].data); EnQueue(Q,w); // w入队 } } } printf("\n"); } void DFS1(ALGraph G,int v,void(*Visit)(char*)) { // 从第v个顶点出发递归地深度优先遍历图G。仅适用于邻接表存储结构 ArcNode *p; // p指向表结点 visited[v]=TRUE; // 设置访问标志为TRUE(已访问) Visit(G.vertices[v].data); // 访问该顶点 for(p=G.vertices[v].firstarc;p;p=p->next) // p依次指向v的邻接顶点 if(!visited[p->data.adjvex]) DFS1(G,p->data.adjvex,Visit); // 对v的尚未访问的邻接点递归调用DFS1 } void DFSTraverse1(ALGraph G,void(*Visit)(char*)) { // 对图G作深度优先遍历。DFS1设函数指针参数 int v; for(v=0;vnext) // p依次指向u的邻接顶点 if(!visited[p->data.adjvex]) // u的邻接顶点尚未被访问 { visited[p->data.adjvex]=TRUE; // 该邻接顶点设为已被访问 Visit(G.vertices[p->data.adjvex].data); // 访问该邻接顶点 EnQueue(Q,p->data.adjvex); // 入队该邻接顶点序号 } } } printf("\n"); } void Display(ALGraph G) { // 输出图的邻接矩阵G int i; ArcNode *p; switch(G.kind) { case DG: printf("有向图\n"); break; case DN: printf("有向网\n"); break; case UDG:printf("无向图\n"); break; case UDN:printf("无向网\n"); } printf("%d个顶点:\n",G.vexnum); for(i=0;idata.adjvex) // 有向或无向两次中的一次 { printf("%s→%s ",G.vertices[i].data,G.vertices[p->data.adjvex].data); if(G.kind%2) // 网 printf(":%d ",*(p->data.info)); } p=p->nextarc; } printf("\n"); } 第 7 章 图 · 303· } // main7-2.cpp 检验bo7-2.cpp的主程序 #include"c1.h" #define MAX_NAME 3 // 顶点字符串的最大长度+1 typedef int InfoType; // 网的权值类型 typedef char VertexType[MAX_NAME]; // 顶点类型为字符串 #include"c7-21.h" #include"bo7-2.cpp" void print(char *i) { printf("%s ",i); } void main() { int i,j,k,n; ALGraph g; VertexType v1,v2; printf("请顺序选择有向图,有向网,无向图,无向网\n"); for(i=0;i<4;i++) // 验证4种情况 { CreateGraph(g); Display(g); printf("插入新顶点,请输入顶点的值: "); scanf("%s",v1); InsertVex(g,v1); printf("插入与新顶点有关的弧或边,请输入弧或边数: "); scanf("%d",&n); for(k=0;kdata.tailvex=i; // 对弧结点赋值 p->data.headvex=j; p->data.hlink=G.xlist[j].firstin; // 完成在入弧和出弧链表表头的插入 p->tlink=G.xlist[i].firstout; G.xlist[j].firstin=(ArcBox1 *)p; // 强制类型转换 G.xlist[i].firstout=p; if(IncInfo) { // 是网 p->data.info=(InfoType *)malloc(sizeof(InfoType)); printf("请输入该弧的权值: "); scanf("%d",p->data.info); } else // 弧不含有相关信息 p->data.info=NULL; } } void DestroyGraph(OLGraph &G) { // 初始条件:有向图G存在。操作结果:销毁有向图G int i; ElemType e; for(i=0;i=G.vexnum||v<0) exit(ERROR); return G.xlist[v].data; } Status PutVex(OLGraph &G,VertexType v,VertexType value) { // 初始条件:有向图G存在,v是G中某个顶点。操作结果:对v赋新值value int i; i=LocateVex(G,v); if(i<0) // v不是G的顶点 return ERROR; strcpy(G.xlist[i].data,value); return OK; } int FirstAdjVex(OLGraph G,VertexType v) { // 初始条件:有向图G存在,v是G中某个顶点 // 操作结果:返回v的第一个邻接顶点的序号。若顶点在G中没有邻接顶点,则返回-1 第 7 章 图 · 311· int i; ArcBox *p; i=LocateVex(G,v); p=G.xlist[i].firstout; // p指向顶点v的第1个出弧 if(p) return p->data.headvex; else return -1; } int NextAdjVex(OLGraph G,VertexType v,VertexType w) { // 初始条件:有向图G存在,v是G中某个顶点,w是v的邻接顶点 // 操作结果:返回v的(相对于w的)下一个邻接顶点的序号,若w是v的最后一个邻接顶点,则返回-1 int i,j; ArcBox *p; i=LocateVex(G,v); // i是顶点v的序号 j=LocateVex(G,w); // j是顶点w的序号 p=G.xlist[i].firstout; // p指向顶点v的第1个出弧 while(p&&p->data.headvex!=j) p=p->tlink; if(p) // w不是v的最后一个邻接顶点 p=p->tlink; // p指向相对于w的下一个邻接顶点 if(p) // 有下一个邻接顶点 return p->data.headvex; else return -1; } void InsertVex(OLGraph &G,VertexType v) { // 初始条件:有向图G存在,v和有向图G中顶点有相同特征 // 操作结果:在有向图G中增添新顶点v(不增添与顶点相关的弧,留待InsertArc()去做) strcpy(G.xlist[G.vexnum].data,v); // 拷贝顶点名称 G.xlist[G.vexnum].firstin=NULL; // 初始化入弧链表 G.xlist[G.vexnum].firstout=NULL; // 初始化出弧链表 G.vexnum++; // 顶点数+1 } Status equal(ElemType c1,ElemType c2) { if(c1.headvex==c2.headvex) return TRUE; else return FALSE; } Status DeleteVex(OLGraph &G,VertexType v) { // 初始条件:有向图G存在,v是G中某个顶点。操作结果:删除G中顶点v及其相关的弧 int i,j,k; ElemType e1,e2; ArcBox *p; ArcBox1 *p1,*p2; k=LocateVex(G,v); // k是顶点v的序号 if(k<0) // v不是图G的顶点 return ERROR; // 以下删除顶点v的入弧 · 312· 《数据结构》算法实现及解析(第二版) e1.headvex=k; // e1作为LocateElem()的比较元素 for(j=0;jtailvex!=k) { p2=p1; p1=p1->hlink; } if(p1) // 找到顶点v的出弧 { if(p1==G.xlist[j].firstin) // 是首结点 G.xlist[j].firstin=p1->hlink; // 入弧指针指向下一个结点 else // 不是首结点 p2->hlink=p1->hlink; // 在链表中移去p1所指结点 if(p1->info) // 带权 free(p1->info); // 释放动态生成的权值空间 free(p1); // 释放p1所指结点 G.arcnum--; // 弧数-1 } } for(j=k+1;jk的顶点依次向前移 G.xlist[j-1]=G.xlist[j]; G.vexnum--; // 顶点数减1 for(j=0;jk的要减1 { p=G.xlist[j].firstout; // 处理出弧 while(p) { if(p->data.tailvex>k) p->data.tailvex--; // 序号-1 if(p->data.headvex>k) p->data.headvex--; // 序号-1 p=p->tlink; } } return OK; } Status InsertArc(OLGraph &G,VertexType v,VertexType w) 第 7 章 图 · 313· { // 初始条件:有向图G存在,v和w是G中两个顶点。操作结果:在G中增添弧 int i,j; int IncInfo; ArcBox *p; i=LocateVex(G,v); // 弧尾的序号 j=LocateVex(G,w); // 弧头的序号 if(i<0||j<0) return ERROR; p=(ArcBox *)malloc(sizeof(ArcBox)); // 生成新结点 p->data.tailvex=i; // 给新结点赋值 p->data.headvex=j; p->data.hlink=G.xlist[j].firstin; // 插在入弧和出弧的链头 p->tlink=G.xlist[i].firstout; G.xlist[j].firstin=(ArcBox1*)p; G.xlist[i].firstout=p; G.arcnum++; // 弧数加1 printf("要插入的弧是否带权(是: 1,否: 0): "); scanf("%d",&IncInfo); if(IncInfo) // 带权 { p->data.info=(InfoType *)malloc(sizeof(InfoType)); // 动态生成权值空间 printf("请输入该弧的权值: "); scanf("%d",p->data.info); } else p->data.info=NULL; return OK; } Status DeleteArc(OLGraph &G,VertexType v,VertexType w) { // 初始条件:有向图G存在,v和w是G中两个顶点。操作结果:在G中删除弧 int i,j,k; ElemType e; ArcBox1 *p1,*p2; i=LocateVex(G,v); // 弧尾的序号 j=LocateVex(G,w); // 弧头的序号 if(i<0||j<0||i==j) return ERROR; p1=G.xlist[j].firstin; // p1指向w的入弧链表 while(p1&&p1->tailvex!=i) // 使p1指向待删结点 { p2=p1; p1=p1->hlink; } if(p1==G.xlist[j].firstin) // 首结点是待删结点 G.xlist[j].firstin=p1->hlink; // 入弧指针指向下一个结点 else // 首结点不是待删结点 p2->hlink=p1->hlink; // 在链表中移去p1所指结点(该结点仍在出弧链表中) e.headvex=j; // 待删弧结点的弧头顶点序号为j,e作为LocateElem()的比较元素 k=LocateElem(G.xlist[i].firstout,e,equal); // 在出弧链表中的位序 ListDelete(G.xlist[i].firstout,k,e); // 在出弧链表中删除该结点,其值赋给e · 314· 《数据结构》算法实现及解析(第二版) if(e.info) // 带权 free(e.info); // 释放动态生成的权值空间 G.arcnum--; // 弧数-1 return OK; } Boolean visited[MAX_VERTEX_NUM]; // 访问标志数组 void(*VisitFunc)(VertexType); // 函数变量 void DFS(OLGraph G,int i) // DFSTraverse()调用 { ArcBox *p; visited[i]=TRUE; // 访问标志数组置1(已被访问) VisitFunc(G.xlist[i].data); // 遍历第i个顶点 p=G.xlist[i].firstout; // p指向第i个顶点的出度 while(p&&visited[p->data.headvex]) // p没到表尾且该弧的头顶点已被访问 p=p->tlink; // 查找下一个结点 if(p&&!visited[p->data.headvex]) // 该弧的头顶点未被访问 DFS(G,p->data.headvex); // 递归调用DFS() } void DFSTraverse(OLGraph G,void(*Visit)(VertexType)) { // 初始条件:有向图G存在,v是G中某个顶点,Visit是顶点的应用函数(算法7.4) // 操作结果:从第1个顶点起,按深度优先递归遍历有向图G,并对每个顶点调用函数Visit一次且仅一次 int v; VisitFunc=Visit; for(v=0;v=0;w=NextAdjVex(G,G.xlist[u].data, G.xlist[w].data)) if(!visited[w]) // w为u的尚未访问的邻接顶点的序号 { visited[w]=TRUE; Visit(G.xlist[w].data); EnQueue(Q,w); } } } printf("\n"); } void Display(OLGraph G) { // 输出有向图G int i; ArcBox *p; printf("共%d个顶点: ",G.vexnum); for(i=0;idata.headvex].data); if(p->data.info) // 该弧有相关信息(权值) printf("权值: %d ",*p->data.info); p=p->tlink; } printf("\n"); } } // main7-3.cpp 检验bo7-3.cpp的主程序 #include"c1.h" typedef int InfoType; // 权值类型 #define MAX_VERTEX_NAME 3 // 顶点字符串最大长度+1 typedef char VertexType[MAX_VERTEX_NAME]; #include"c7-31.h" #include"bo7-3.cpp" void visit(VertexType v) { printf("%s ",v); } void main() { int j,k,n; OLGraph g; · 316· 《数据结构》算法实现及解析(第二版) VertexType v1,v2; CreateDG(g); Display(g); printf("修改顶点的值,请输入原值 新值: "); scanf("%s%s",v1,v2); PutVex(g,v1,v2); printf("插入新顶点,请输入顶点的值: "); scanf("%s",v1); InsertVex(g,v1); printf("插入与新顶点有关的弧,请输入弧数: "); scanf("%d",&n); for(k=0;kmark=unvisited; // 设初值 p->ivex=i; p->ilink=G.adjmulist[i].firstedge; // 插在一端的表头 G.adjmulist[i].firstedge=p; p->jvex=j; p->jlink=G.adjmulist[j].firstedge; // 插在另一端的表头 G.adjmulist[j].firstedge=p; if(IncInfo) // 网 { p->info=(InfoType*)malloc(sizeof(InfoType)); printf("请输入该边的权值: "); scanf("%d",p->info); } else p->info=NULL; · 320· 《数据结构》算法实现及解析(第二版) } } VertexType& GetVex(AMLGraph G,int v) { // 初始条件:无向图G存在,v是G中某个顶点的序号。操作结果:返回v的值 if(v>=G.vexnum||v<0) exit(ERROR); return G.adjmulist[v].data; } Status PutVex(AMLGraph &G,VertexType v,VertexType value) { // 初始条件:无向图G存在,v是G中某个顶点。操作结果:对v赋新值value int i; i=LocateVex(G,v); if(i<0) // v不是G的顶点 return ERROR; strcpy(G.adjmulist[i].data,value); return OK; } int FirstAdjVex(AMLGraph G,VertexType v) { // 初始条件:无向图G存在,v是G中某个顶点 // 操作结果:返回v的第一个邻接顶点的序号。若顶点在G中没有邻接顶点,则返回-1 int i; i=LocateVex(G,v); if(i<0) // G中不存在顶点v return -1; if(G.adjmulist[i].firstedge) // v有邻接顶点 if(G.adjmulist[i].firstedge->ivex==i) return G.adjmulist[i].firstedge->jvex; else return G.adjmulist[i].firstedge->ivex; else return -1; } int NextAdjVex(AMLGraph G,VertexType v,VertexType w) { // 初始条件:无向图G存在,v是G中某个顶点,w是v的邻接顶点 // 操作结果:返回v的(相对于w的)下一个邻接顶点的序号。若w是v的最后一个邻接点,则返回-1 int i,j; EBox *p; i=LocateVex(G,v); // i是顶点v的序号 j=LocateVex(G,w); // j是顶点w的序号 if(i<0||j<0) // v或w不是G的顶点 return -1; p=G.adjmulist[i].firstedge; // p指向顶点v的第1条边 while(p) if(p->ivex==i&&p->jvex!=j) // 不是邻接顶点w(情况1) p=p->ilink; // 找下一个邻接顶点 else if(p->jvex==i&&p->ivex!=j) // 不是邻接顶点w(情况2) p=p->jlink; // 找下一个邻接顶点 else // 是邻接顶点w break; if(p&&p->ivex==i&&p->jvex==j) // 找到邻接顶点w(情况1) { 第 7 章 图 · 321· p=p->ilink; if(p&&p->ivex==i) return p->jvex; else if(p&&p->jvex==i) return p->ivex; } if(p&&p->ivex==j&&p->jvex==i) // 找到邻接顶点w(情况2) { p=p->jlink; if(p&&p->ivex==i) return p->jvex; else if(p&&p->jvex==i) return p->ivex; } return -1; } Status InsertVex(AMLGraph &G,VertexType v) { // 初始条件:无向图G存在,v和G中顶点有相同特征 // 操作结果:在G中增添新顶点v(不增添与顶点相关的弧,留待InsertArc()去做) if(G.vexnum==MAX_VERTEX_NUM) // 结点已满,不能插入 return ERROR; if(LocateVex(G,v)>=0) // 结点已存在,不能插入 return ERROR; strcpy(G.adjmulist[G.vexnum].data,v); G.adjmulist[G.vexnum++].firstedge=NULL; return OK; } Status DeleteArc(AMLGraph &G,VertexType v,VertexType w) { // 初始条件:无向图G存在,v和w是G中两个顶点。操作结果:在G中删除弧 int i,j; EBox *p,*q; i=LocateVex(G,v); j=LocateVex(G,w); if(i<0||j<0||i==j) return ERROR; // 图中没有该点或弧。以下使指向待删除边的第1个指针绕过这条边 p=G.adjmulist[i].firstedge; // p指向顶点v的第1条边 if(p&&p->jvex==j) // 第1条边即为待删除边(情况1) G.adjmulist[i].firstedge=p->ilink; else if(p&&p->ivex==j) // 第1条边即为待删除边(情况2) G.adjmulist[i].firstedge=p->jlink; else // 第1条边不是待删除边 { while(p) // 向后查找弧 if(p->ivex==i&&p->jvex!=j) // 不是待删除边 { q=p; p=p->ilink; // 找下一个邻接顶点 } else if(p->jvex==i&&p->ivex!=j) // 不是待删除边 { q=p; · 322· 《数据结构》算法实现及解析(第二版) p=p->jlink; // 找下一个邻接顶点 } else // 是邻接顶点w break; if(!p) // 没找到该边 return ERROR; if(p->ivex==i&&p->jvex==j) // 找到弧(情况1) if(q->ivex==i) q->ilink=p->ilink; else q->jlink=p->ilink; else if(p->ivex==j&&p->jvex==i) // 找到弧(情况2) if(q->ivex==i) q->ilink=p->jlink; else q->jlink=p->jlink; } // 以下由另一顶点起找待删除边且删除之 p=G.adjmulist[j].firstedge; // p指向顶点w的第1条边 if(p->jvex==i) // 第1条边即为待删除边(情况1) G.adjmulist[j].firstedge=p->ilink; else if(p->ivex==i) // 第1条边即为待删除边(情况2) G.adjmulist[j].firstedge=p->jlink; else // 第1条边不是待删除边 { while(p) // 向后查找弧 if(p->ivex==j&&p->jvex!=i) // 不是待删除边 { q=p; p=p->ilink; // 找下一个邻接顶点 } else if(p->jvex==j&&p->ivex!=i) // 不是待删除边 { q=p; p=p->jlink; // 找下一个邻接顶点 } else // 是邻接顶点v break; if(p->ivex==i&&p->jvex==j) // 找到弧(情况1) if(q->ivex==j) q->ilink=p->jlink; else q->jlink=p->jlink; else if(p->ivex==j&&p->jvex==i) // 找到弧(情况2) if(q->ivex==j) q->ilink=p->ilink; else q->jlink=p->ilink; } if(p->info) // 有相关信息(或权值) free(p->info); // 释放相关信息(或权值) free(p); // 释放结点 第 7 章 图 · 323· G.edgenum--; // 边数-1 return OK; } Status DeleteVex(AMLGraph &G,VertexType v) { // 初始条件:无向图G存在,v是G中某个顶点。操作结果:删除G中顶点v及其相关的边 int i,j; EBox *p; i=LocateVex(G,v); // i为待删除顶点的序号 if(i<0) return ERROR; for(j=0;jivex==j+1) { p->ivex--; p=p->ilink; } else { p->jvex--; p=p->jlink; } } return OK; } void DestroyGraph(AMLGraph &G) { // 初始条件:有向图G存在。操作结果:销毁有向图G int i; for(i=G.vexnum-1;i>=0;i--) // 由大到小依次删除顶点 DeleteVex(G,G.adjmulist[i].data); } Status InsertArc(AMLGraph &G,VertexType v,VertexType w) { // 初始条件:无向图G存在,v和W是G中两个顶点。操作结果:在G中增添弧 int i,j,IncInfo; EBox *p; i=LocateVex(G,v); // 一端 j=LocateVex(G,w); // 另一端 if(i<0||j<0||i==j) return ERROR; p=(EBox*)malloc(sizeof(EBox)); p->mark=unvisited; p->ivex=i; p->ilink=G.adjmulist[i].firstedge; // 插在表头 G.adjmulist[i].firstedge=p; · 324· 《数据结构》算法实现及解析(第二版) p->jvex=j; p->jlink=G.adjmulist[j].firstedge; // 插在表头 G.adjmulist[j].firstedge=p; printf("该边是否有权值(1:有 0:无): "); scanf("%d",&IncInfo); if(IncInfo) // 有权值 { p->info=(InfoType*)malloc(sizeof(InfoType)); printf("请输入该边的权值: "); scanf("%d",p->info); } else p->info=NULL; G.edgenum++; return OK; } Boolean visite[MAX_VERTEX_NUM]; // 访问标志数组(全局量) void(*VisitFunc)(VertexType v); void DFS(AMLGraph G,int v) { int j; EBox *p; VisitFunc(G.adjmulist[v].data); visite[v]=TRUE; p=G.adjmulist[v].firstedge; while(p) { j=p->ivex==v?p->jvex:p->ivex; if(!visite[j]) DFS(G,j); p=p->ivex==v?p->ilink:p->jlink; } } void DFSTraverse(AMLGraph G,void(*visit)(VertexType)) { // 初始条件:图G存在,Visit是顶点的应用函数。算法7.4 // 操作结果:从第1个顶点起,深度优先遍历图G,并对每个顶点调用函数Visit一次且仅一次 int v; VisitFunc=visit; for(v=0;v=0;w=NextAdjVex(G,G.adjmulist[u].data, G.adjmulist[w].data)) if(!visite[w]) // w为u的尚未访问的邻接顶点的序号 { visite[w]=TRUE; Visit(G.adjmulist[w].data); EnQueue(Q,w); } } } printf("\n"); } void MarkUnvizited(AMLGraph G) { // 置边的访问标记为未被访问 int i; EBox *p; for(i=0;imark=unvisited; if(p->ivex==i) p=p->ilink; else p=p->jlink; } } } void Display(AMLGraph G) { // 输出无向图的邻接多重表G int i; EBox *p; MarkUnvizited(G); // 置边的访问标记为未被访问 printf("%d个顶点:\n",G.vexnum); for(i=0;iivex==i) // 边的i端与该顶点有关 { if(!p->mark) // 只输出一次 { printf("%s-%s ",G.adjmulist[i].data,G.adjmulist[p->jvex].data); p->mark=visited; if(p->info) // 输出附带信息 printf("权值: %d ",*p->info); } p=p->ilink; } else // 边的j端与该顶点有关 { if(!p->mark) // 只输出一次 { printf("%s-%s ",G.adjmulist[p->ivex].data,G.adjmulist[i].data); p->mark=visited; if(p->info) // 输出附带信息 printf("权值: %d ",*p->info); } p=p->jlink; } printf("\n"); } } // main7-4.cpp 检验bo7-4.cpp的主程序 #include"c1.h" #define MAX_NAME 3 // 顶点字符串的最大长度+1 typedef int InfoType; // 权值类型 typedef char VertexType[MAX_NAME]; // 字符串类型 #include"c7-4.h" #include"bo7-4.cpp" void visit(VertexType v) { printf("%s ",v); } void main() { int k,n; AMLGraph g; VertexType v1,v2; CreateGraph(g); Display(g); printf("修改顶点的值,请输入原值 新值: "); 第 7 章 图 · 327· scanf("%s%s",v1,v2); PutVex(g,v1,v2); printf("插入新顶点,请输入顶点的值: "); scanf("%s",v1); InsertVex(g,v1); printf("插入与新顶点有关的边,请输入边数: "); scanf("%d",&n); for(k=0;knext 代替 FirstAdjVex()和 NextAdjVex()的作用。这样做效率高、直观,但仅适用于邻接表存储结 构。它们得到的结果是一样的。 数据文件 f7-2.txt 所表示的无向图与 f7-1.txt 的一样,如图 748 所示。只是边的输 入顺序不同。数据文件 f7-2.txt 的内容如下: 8 14 a b c d e f g h f g e f d h c h c g b h b e b d a h a g a f a e a c a b 图 749 根据数据文件 f7-1.txt 所产生的邻接表 [0] [1] [2] [3] [4] [5] [6] [7] [19] a b c d e f g h a NULLd c b a NULLbf a NULLeg b NULLh a NULLgh a NULLdeh b NULLcefh g a NULLcf 8 14 UDG   · 334· 《数据结构》算法实现及解析(第二版) 利用数据文件 f7-2.txt 运行 algo7-11.cpp,产生的邻接表如图 750 所示。 程序运行结果如下: 请输入数据文件名(f7-1.txt或f7-2.txt):f7-2.txt 请输入图的类型(有向图:0,有向网:1,无向图:2,无向网:3): 2 无向图 8个顶点: a b c d e f g h 14条弧(边): a→b a→c a→e a→f a→g a→h b→d b→e b→h c→g c→h d→h e→f f→g 深度优先搜索的结果: a b d h c g f e a b d h c g f e 广度优先搜索的结果: a b c e f g h d a b c e f g h d 其中,深度优先搜索的顺序与 algo7-10.cpp 的一样。algo7-10.cpp 利用数据文件 f7-2.txt 的运行结果与利用 f7-1.txt 的一样,读者可自行验证。 除了 c7-1.h 存储结构,c7-2.h~c7-4.h 中边或弧都是以链表形式存储的,且边或弧 总是插在表头。当边或弧的输入顺序不同时,其存储结构就不同,故搜索的顺序不同。 图 750 根据数据文件 f7-2.txt 所产生的邻接表 [0] [1] [2] [3] [4] [5] [6] [7] [19] a b c d e f g h d NULLa b c f NULLba g NULLea h NULLb h NULLga h NULLeda h NULLgfeb c f NULLca 8 14 UDG   第 7 章 图 · 335· 7.4 图的连通性问题 7.4.1 具有 n 个顶点的无向连通图至少有 n-1 条边,如果只有 n-1 条边,则不会形成环,这 样的图称为“生成树”。连通图可通过遍历构造生成树,非连通图的每个连通分量可构造 一棵生成树,整个非连通图构造为生成森林。algo7-1.cpp 调用算法 7.7、7.8,将无向图 构造为生成森林,并以孩子—兄弟二叉链表存储之。 // algo7-1.cpp 调用算法7.7、7.8 #include"c1.h" #define MAX_NAME 2 // 顶点字符串的最大长度+1 typedef char VertexType[MAX_NAME]; typedef VertexType TElemType; // 定义树的元素类型为图的顶点类型 #include"c6-5.h" // 孩子—兄弟二叉链表存储结构 #include"func6-2.cpp" // 孩子—兄弟二叉链表存储结构的先根遍历操作 typedef int InfoType; // 权值类型 #include"c7-21.h" // bo7-2.cpp采用的存储类型 #include"bo7-2.cpp" // 邻接表的基本操作 void DFSTree(ALGraph G,int v,CSTree &T) { // 从第v个顶点出发深度优先遍历图G,建立以T为根的生成树。算法7.8 Boolean first=TRUE; int w; CSTree p,q; visited[v]=TRUE; for(w=FirstAdjVex(G,G.vertices[v].data);w>=0;w=NextAdjVex(G,G.vertices[v].data, G.vertices[w].data)) // w依次为v的邻接顶点 if(!visited[w]) // w顶点不曾被访问 { p=(CSTree)malloc(sizeof(CSNode)); // 分配孩子结点 strcpy(p->data,G.vertices[w].data); p->firstchild=NULL; p->nextsibling=NULL; if(first) { // w是v的第一个未被访问的邻接顶点 T->firstchild=p; first=FALSE; // 是根的第一个孩子结点 } else // w是v的其它未被访问的邻接顶点 q->nextsibling=p; // 是上一邻接顶点的兄弟姐妹结点(第1次不通过此处,以后q已赋值) q=p; DFSTree(G,w,q); // 从第w个顶点出发深度优先遍历图G,建立子生成树q } } void DFSForest(ALGraph G,CSTree &T) { // 建立无向图G的深度优先生成森林的(最左)孩子(右)兄弟链表T。算法7.7 CSTree p,q; · 336· 《数据结构》算法实现及解析(第二版) int v; T=NULL; for(v=0;vdata,G.vertices[v].data); p->firstchild=NULL; p->nextsibling=NULL; if(!T) // 是第一棵生成树的根(T的根) T=p; else // 是其它生成树的根(前一棵的根的“兄弟”) q->nextsibling=p; // 第1次不通过此处,以后q已赋值 q=p; // q指示当前生成树的根 DFSTree(G,v,p); // 建立以p为根的生成树 } } void print(char *i) { printf("%s ",i); } void main() { ALGraph g; CSTree t; printf("请选择无向图\n"); CreateGraph(g); // 构造无向图g Display(g); // 输出无向图g DFSForest(g,t); // 建立无向图g的深度优先生成森林的孩子—兄弟链表 t printf("先序遍历生成森林:\n"); PreOrderTraverse(t,print); // 先序遍历生成森林的孩子—兄弟链表 t printf("\n"); } 程序运行结果(以教科书中图 7.3(a)的 G3 为例): 请选择无向图 请输入G的类型(有向图:0,有向网:1,无向图:2,无向网:3): 2 请输入G的顶点数,边数: 13,13(见图751) 请输入13个顶点的值(<2个字符): ABCDEFGHIJKLM 请顺序输入每条弧(边)的弧尾和弧头(以空格作为间隔): AB AC AF AL BM DE 第 7 章 图 · 337· GH GI GK HK JL JM LM 无向图(见图752) 13个顶点: ABCDEFGHIJKLM 13条弧(边): A-LA-FA-CA-B B-M D-E G-KG-IG-H H-K J-MJ-L L-M 先序遍历生成森林:(见图753) ALMJBFCDEGKHI 图 752 根据输入产生的邻接表 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [19] A B C D E F G H I J K L M BNULLCFL HNULLIK ANULL ANULLJM BNULLJL ANULLM GNULLK LNULLM GNULLH ENULL DNULL ANULL GNULL 13 13 UDG   图 751 非连通无向图 G HF D EC J KI BA ML · 338· 《数据结构》算法实现及解析(第二版) 以上对图的输入产生的邻接表如图 752 所示,仍略去网的权值指针域,并用顶点名 称代替顶点位置。调用算法 7.7 产生的生成森林如图 753 所示,此生成森林以孩子—兄 弟二叉链表存储的结构如图 754 所示。 7.4.2 7.4.3 // algo7-2.cpp 实现算法7.9的程序 #include"c1.h" typedef int VRType; typedef char InfoType; #define MAX_NAME 3 // 顶点字符串的最大长度+1 #define MAX_INFO 20 // 相关信息字符串的最大长度+1 typedef char VertexType[MAX_NAME]; #include"c7-1.h" #include"bo7-1.cpp" typedef struct { // 记录从顶点集U到V-U的代价最小的边的辅助数组定义(见图755) VertexType adjvex; VRType lowcost; }minside[MAX_VERTEX_NUM]; int minimum(minside SZ,MGraph G) { // 求SZ.lowcost的最小正值,并返回其在SZ中的序号 int i=0,j,k,min; while(!SZ[i].lowcost) i++; min=SZ[i].lowcost; // 第一个不为0的值 k=i; for(j=i+1;j0&&min>SZ[j].lowcost) // 找到新的大于0的最小值 { min=SZ[j].lowcost; k=j; } return k; } 图 753 生成森林 D E M J B A FL C H G K I 图 754 生成森林以孩子—兄弟 二叉链表存储的结构 G J D M L A B F E C IH K 图 755 minside 类型 [0] [1] [2] [25] adjvex lowcost minside    第 7 章 图 · 339· void MiniSpanTree_PRIM(MGraph G,VertexType u) { // 用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。算法7.9 int i,j,k; minside closedge; k=LocateVex(G,u); for(j=0;jnextarc) // 依次对v0的每个邻接顶点检查 { w=p->data.adjvex; // w为v0的邻接顶点位置 if(visited[w]==0) // w未曾访问,是v0的孩子 { DFSArticul(G,w); // 从第w个顶点出发深度优先遍历图G,查找并输出关节点。返回前求得low[w] if(low[w]=visited[v0]) // v0的孩子结点w只与v0相连,则v0是关节点 printf("%d %s\n",v0,G.vertices[v0].data); // 输出关节点v0 图 758 运行 algo7-8.cpp 过程图示 (a) (b) (c) (d) (e) (f) 3 4 5 1 6 V1 V2 V4 V5 V6 V3 5 5 6 6 2 0 1 2 3 4 5 [0] [1] [2] [3] [4] [5] set 3 4 5 6 V1 V2 V4 V5 V6 V3 5 5 6 6 2 0 1 0 3 4 5 [0] [1] [2] [3] [4] [5] set 3 4 5 6 V1 V2 V4 V5 V6 V3 5 5 6 6 0 1 0 3 4 3 [0] [1] [2] [3] [4] [5] set 4 5 6 V1 V2 V4 V5 V6 V3 5 5 6 6 0 1 0 3 1 3 [0] [1] [2] [3] [4] [5] set 5 6 V1 V2 V4 V5 V6 V3 5 5 6 6 0 1 0 0 1 0 [0] [1] [2] [3] [4] [5] set 6 V1 V2 V4 V5 V6 V3 5 6 6 1 1 1 1 1 1 [0] [1] [2] [3] [4] [5] set · 344· 《数据结构》算法实现及解析(第二版) } else if(visited[w]data.adjvex; // v是根结点的第1个邻接顶点的序号 DFSArticul(G,v); // 从第v顶点出发深度优先查找关节点 if(countnextarc) // 根有下一个邻接点 { p=p->nextarc; // p指向根的下一个邻接点 v=p->data.adjvex; if(visited[v]==0) // 此邻接点未被访问 DFSArticul(G,v); // 从此顶点出发深度优先查找关节点 } } } void main() { int i; ALGraph g; printf("请选择无向图\n"); CreateGraph(g); // 构造无向图g Display(g); // 输出无向图g printf("输出关节点:\n"); FindArticul(g); // 求连通图g的关节点 printf(" i G.vertices[i].data visited[i] low[i] lowOrder[i]\n"); // 输出辅助变量 for(i=0;idata.adjvex]++; p=p->nextarc; } } } // algo7-4.cpp 输出有向图的一个拓扑序列。实现算法7.12的程序 #include"c1.h" · 348· 《数据结构》算法实现及解析(第二版) #define MAX_NAME 5 // 顶点字符串的最大长度 typedef int InfoType; typedef char VertexType[MAX_NAME]; // 字符串类型 #include"c7-21.h" // 邻接表存储结构 #include"bo7-2.cpp" // 邻接表存储结构的基本操作 #include"func7-1.cpp" typedef int SElemType; // 栈元素类型 #include"c3-1.h" // 顺序栈的存储结构 #include"bo3-1.cpp" // 顺序栈的基本操作 Status TopologicalSort(ALGraph G) { // 有向图G采用邻接表存储结构。若G无回路,则输出G的顶点的一个拓扑序列并返回OK, // 否则返回ERROR。算法7.12 int i,k,count=0; // 已输出顶点数,初值为0 int indegree[MAX_VERTEX_NUM]; // 入度数组,存放各顶点当前入度数 SqStack S; ArcNode *p; FindInDegree(G,indegree); // 对各顶点求入度indegree[],在func7-1.cpp中 InitStack(S); // 初始化零入度顶点栈S for(i=0;inextarc) { // 对i号顶点的每个邻接顶点 k=p->data.adjvex; // 其序号为k if(!(--indegree[k])) // k的入度减1,若减为0,则将k入栈S Push(S,k); } } if(countnextarc) { // 对i号顶点的每个邻接点 k=p->data.adjvex; // 其序号为k if(--indegree[k]==0) // k的入度减1,若减为0,则将k入栈S Push(S,k); if(ve[i]+*(p->data.info)>ve[k]) // *(p->data.info)是的权值 ve[k]=ve[i]+*(p->data.info); // 顶点k事件的最早发生时间要受其直接前驱顶点i事件的 } // 最早发生时间和的权值约束。由于i已拓扑有序,故ve[i]不再改变 } if(countj) j=ve[i]; // j=Max(ve[]) 完成点的最早发生时间 for(i=0;inextarc) { // 弹出栈T的元素,赋给j,p指向j的后继事件k,事件k的最迟发生时间已确定(因为是逆拓扑排序) k=p->data.adjvex; dut=*(p->data.info); // dut=的权值 if(vl[k]-dut的权值约束。由于k已逆拓扑有序,故vl[k]不再改变 printf("\ni ve[i] vl[i]\n"); for(i=0;inextarc) { k=p->data.adjvex; dut=*(p->data.info); // dut=的权值 ee=ve[j]; // ee=活动的最早开始时间(在j点) el=vl[k]-dut; // el=活动的最迟开始时间(在j点) printf("%s→%s %3d %3d %3d ",G.vertices[j].data,G.vertices[k].data,dut,ee,el); // 输出各边的参数 if(ee==el) // 是关键活动 printf("关键活动"); printf("\n"); } return OK; } void main() { · 352· 《数据结构》算法实现及解析(第二版) ALGraph h; printf("请选择有向网\n"); CreateGraph(h); // 构造有向网h,在bo7-2.cpp中 Display(h); // 输出有向网h,在bo7-2.cpp中 CriticalPath(h); // 求h的关键路径 } 程序运行结果(以教科书中图 7.30 为例): 请选择有向网(见图765) 请输入图的类型(有向图:0,有向网:1,无向图:2,无向网:3): 1 请输入图的顶点数,边数: 6,8 请输入6个顶点的值(<5个字符): V1 V2 V3 V4 V5 V6 请输入每条弧(边)的权值、弧尾和弧头(以空格作为间隔): 3 V1 V2 2 V1 V3 2 V2 V4 3 V2 V5 4 V3 V4 3 V3 V6 2 V4 V6 1 V5 V6 有向网(见图766) 6个顶点: V1 V2 V3 V4 V5 V6 8条弧(边): V1→V3 :2 V1→V2 :3 V2→V5 :3 V2→V4 :2 V3→V6 :3 V3→V4 :4 V4→V6 :2 V5→V6 :1 拓扑序列:V1 V2 V5 V3 V4 V6 i ve[i] vl[i] (见图767) 0 0 0 关键路径经过的顶点 1 3 4 2 2 2 关键路径经过的顶点 3 6 6 关键路径经过的顶点 4 6 7 5 8 8 关键路径经过的顶点 j k 权值 ee el V1→V3 2 0 0 关键活动 V1→V2 3 0 1 V2→V5 3 3 4 V2→V4 2 3 4 V3→V6 3 2 5 V3→V4 4 2 2 关键活动 V4→V6 2 6 6 关键活动 V5→V6 1 6 7 图 765 有向网 V4V1 V6 V3 V5V2 3 4 3 1 2 2 3 2 图 766 邻接表的示意图 [0] [1] [2] [3] [4] [5] [19] V1 V2 V3 V4 V5 V6 NULL 6 8 DN V2 3 NULLV3 2 V6 2 NULL V6 1 NULL V4 2 NULLV5 3 V4 4 NULLV6 3   图 767 关键路径 V4V1 V6 V3 V5V2 3 4 3 1 2 2 3 2 第 7 章 图 · 353· 图 766 是邻接表的示意图。为直观和方便起见,表结点中邻接顶点的序号直接用其 名称代替,动态生成的权值也直接写在权值的指针域中。 运行 algo7-5.cpp 用到 2 个辅助数组:事件(顶点)最早发生时间 ve[]和事件最迟发生 时间 vl[]。 顶点 i 的事件(顶点)最早发生时间 ve[i]取决于其直接前驱事件的发生时间和二者之间 活动的持续时间(弧的权值)。如图 765 中,V2 事件的最早发生时间取决于 V1 的 ve[]和 的权值 3。即 V2 的 ve[]等于 V1 的 ve[]+3。如果顶点 i 有多个直接前驱事件则 ve[i] 取 最 大 值 , 如 V4 的 ve[] 取 决 于 V2 的 ve[]+ 的 权 值 2 与 V3 的 ve[]+的权值 4 这 2 者中的大值。没有直接前驱事件的顶点,其 ve[]=0(是最小 值),如顶点 V1。由于求顶点的 ve[]要求其直接前驱的 ve[]已知,故应先对有向网进行拓 扑排序。排序前设所有顶点的 ve[]初值=0(最小值),当出现较大的值,则用这个大值更新 ve[]。调用 TopologicalOrder()后,ve[]如以上程序运行结果所示。 所谓事件最迟发生时间是指在不影响工期的情况下,某事件可以最迟发生的时间。顶 点 i 的事件(顶点)最迟发生时间 vl[i]取决于其直接后继事件的最迟发生时间和二者之间活 动的持续时间(弧的权值)。如图 765 中,V4 事件的最迟发生时间取决于 V6 的 vl[]和 的权值 2。即 V4 的 vl[]等于 V6 的 vl[]-2。如果顶点 i 有多个直接后继事件则 vl[i]取最小值。如 V3 的 vl[]取决于 V4 的 vl[]-的权值 4 与 V6 的 vl[]- 的权值 3 这 2 者中的小值。没有直接后继事件的顶点,其 vl[]=ve[](是最大值,已先期求 出),如顶点 V6。由于求顶点的 vl[]时,要求其直接后继的 vl[]已知,故应先形成有向网 的逆拓扑序列。TopologicalOrder()将已拓扑排序的顶点入栈 T,形成逆拓扑序列。排序前 设所有顶点的 vl[]初值等于没有后继的那个顶点的 vl[](最大值),本例中这个顶点是 V6。当出现较小的值,则用这个小值更新 vl[]。调用 CriticalPath(),vl[]如以上程序运行 结果所示。 若对于顶点 i,有 ve[i]=vl[i],即事件最早发生时间等于事件最迟发生时间。说明为 保证工期,事件(顶点)i 的发生时间不可变更。如果变小,则前面的活动(入弧)还没完 成;如果变大,则影响后继事件按时完成。因此,顶点 i 是关键路径要经过的点。如以上 程序运行结果所示,V1、V3、V4 和 V6 是关键路径要经过的顶点。但光根据这些顶点, 还不能确定关键路径。如图 765 中,虽然 V3、V4 和 V6 是关键路径要经过的顶点,但 弧中哪个是关键路径还不清楚。 如果一个活动(弧),它的前端事件的最早发生时间 ve[j]+的权值等于 vl[k](后端事件的最迟发生时间),那么这个活动的发生时间就没有变更的余地,它就是整 个关键路径的一部分。求得 ve[]和 vl[]后,对于每一个弧,判断它的 ve[j]是否等于 它的 vl[k]+dut(弧的权值),可求出所有关键路径。图 767 中粗箭头弧是关键路径。 7.6 最短路径 7.6.1 // algo7-6.cpp 实现算法7.15的程序。迪杰斯特拉算法的实现 · 354· 《数据结构》算法实现及解析(第二版) #include"c1.h" #define MAX_NAME 5 // 顶点字符串的最大长度+1 #define MAX_INFO 20 // 相关信息字符串的最大长度+1 typedef int VRType; typedef char InfoType; typedef char VertexType[MAX_NAME]; #include"c7-1.h" // 邻接矩阵存储结构 #include"bo7-1.cpp" // 邻接矩阵存储结构的基本操作 typedef int PathMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 路径矩阵,二维数组 typedef int ShortPathTable[MAX_VERTEX_NUM]; // 最短距离表,一维数组 void ShortestPath_DIJ(MGraph G,int v0,PathMatrix P,ShortPathTable D) { // 用Dijkstra算法求有向网G的v0顶点到其余顶点v的最短路径P[v]及带权长度 // D[v]。若P[v][w]为TRUE,则w是从v0到v当前求得最短路径上的顶点。 // final[v]为TRUE当且仅当v∈S,即已经求得从v0到v的最短路径 算法7.15 int v,w,i,j,min; Status final[MAX_VERTEX_NUM]; // 辅助矩阵,为真表示该顶点到v0的最短距离已求出,初值为假 for(v=0;vsize-1 // 带参数的宏定义,指向p所指结点的底部(最后一个字) 图 82 是空闲块和占用块的表示。占用块的可利用区域是除头部域和尾部域之外的 区域。所有空闲块被链接在一个双循环结构的可利用空间表中,如图 83 所示。 图 81 边界标识法可利用空间表的结点结构 llink tag size rlink head(头部域,1 个 WORD) foot(底部域,1 个 WORD) uplink tag head foot (size-2)个 WORD llink tag size rlink uplink tag 图 82 空闲块和占用块的表示 (n-2)个 WORD 占用块可利用区域 1 n 1 (n-2)个 WORD 0 n 0 (m-2)个 WORD 0 m 0 (l-2)个 WORD 0 l 0 (a) 空闲块 (b) 占用块 第 8 章 动态存储管理 · 367· 8.3.2 // algo8-1.cpp 边界标识法。实现算法8.1的程序 #include"c1.h" #include"c8-1.h" #define MAX 1000 // 可利用空间的大小(以WORD的字节数为单位) #define e 10 // 块的最小尺寸-1(以WORD的字节数为单位) Space AllocBoundTag(Space &pav,int n) // 算法8.1(首次拟合法) { // 若可利用空间表pav中有不小于n的空闲块,则分配相应的存储块,并返回其首地址;否则返回NULL // 若分配后可利用空间表不空,则pav指向表中刚分配过的结点的后继结点 Space p,f; for(p=pav;p&&p->sizerlink!=pav;p=p->rlink); // 在pav中查找不小于n的空闲块 if(!p||p->sizerlink; // 移动pav,使其指向p所指结点的后继结点 if(p->size-n<=e) // 整块分配,不保留<=e的剩余量,删除该块 { if(pav==p) // 可利用空间表只有1个空闲块 pav=NULL; // 可利用空间表变为空表 else // 在表中删除该块 { pav->llink=p->llink; p->llink->rlink=pav; } p->tag=f->tag=1; // 修改分配结点的头部和底部标志为占用 } else // 分配该块的后n个字(高地址部分),不删除该块(见图84) { f->tag=1; // 修改分配块的底部标志 p->size-=n; // 置剩余块大小 f=FootLoc(p); // 指向剩余块底部 f->tag=0; // 设置剩余块底部 f->uplink=p; p=f+1; // 指向分配块头部 p->tag=1; // 设置分配块头部 p->size=n; } 图 83 可利用空间表图示 (a) 空的可利用空间表 (b) 有 3 个空闲块的可利用空间表 NULL pav (n-2)个 WORD 0 n 00 (m-2)个 WORD 0 m (l-2)个 WORD 0 l 0 pav · 368· 《数据结构》算法实现及解析(第二版) return p; // 返回分配块首地址 } } void Reclaim(Space &pav,Space &p) { // 边界标识法的回收算法,将p所指的释放块回收到可利用空间表pav中 Space s,t=p+p->size; // t指向释放块右邻块的首地址 int l=(p-1)->tag,r=(p+p->size)->tag; // l、r分别指示释放块的左、右邻块是否空闲 if(!pav) // 可利用空间表空 { // 将释放块加入到可利用空间表pav中 pav=p->llink=p->rlink=p; // 头部域的两个指针及pav均指向释放块 p->tag=0; // 修改头部域块标志为空闲 (FootLoc(p))->uplink=p; // 修改底部域指针,使其指向释放块的头部域 (FootLoc(p))->tag=0; // 修改底部域块标志为空闲 } else // 可利用空间表不空 if(l==1&&r==1) // 左右邻区均为占用块 { // 将释放块插入到可利用空间表pav中 p->tag=0; // 修改释放块头部域块标志为空闲 (FootLoc(p))->uplink=p; // 修改底部域指针,使其指向释放块的头部域 (FootLoc(p))->tag=0; // 修改底部域块标志为空闲 pav->llink->rlink=p; // 将p所指结点(刚释放的结点)插在pav所指结点之前 p->llink=pav->llink; p->rlink=pav; pav->llink=p; pav=p; // 修改pav,令刚释放的结点为下次分配时的最先查询的结点 } else if(l==0&&r==1) // 左邻区为空闲块,右邻区为占用块(见图85) { // 合并左邻块和释放块(将释放块“粘到”左邻块的下边,不改变可利用空间表pav) s=(p-1)->uplink; // s为左邻空闲块的头部域地址 s->size+=p->size; // 设置合并的空闲块大小 图 84 将空闲块的后 n 个 WORD 分配为占用块 (a) 分配前 (n1-2)个 WORD 0 n1 0 (m-2)个 WORD 0 m 0 (l-2)个 WORD 0 l 0 p f (b) 分配后 (n-2)个 WORD 占用 1 n 1 (n1-n-2)个 WORD 空闲 0 n1-n 0(m-2)个 WORD 0 m 0 (l-2)个 WORD 0 l 0p f 第 8 章 动态存储管理 · 369· t=FootLoc(p); // t指向合并的空闲块底部域(释放块的底部域) t->uplink=s; // 设置合并的空闲块底部域指针,使其指向合并的空闲块的头部域 t->tag=0; // 设置合并的空闲块底部域块标志为空闲 } else if(l==1&&r==0) // 右邻区为空闲块,左邻区为占用块(见图86) { // 合并右邻块和释放块,t为右邻空闲块的头部域地址,用合并块取代右邻空闲块在pav中的位置 p->tag=0; // P为合并后的结点头部域地址,设置其块标志为空闲 图 85 将释放块和左邻空闲块合并的图示 (b) 合并后 (n1+n-2)个 WORD 合并 0 n1+n 0 (m-2)个 WORD 0 m 0 (l-2)个 WORD 0 l 0 s t (a) 合并前 (m-2)个 WORD 0 m 0 (l-2)个 WORD 0 l 0(n-2)个 WORD 释放 1 n 1 (n1-2)个 WORD 空闲 0 n1 0 p s t (a) 合并前 (m-2)个 WORD 0 m 0 0 (l-2)个 WORD 0 l (n-2)个 WORD 释放 1 n 1 (n1-2)个 WORD 空闲 0 n1 0 p t 图 86 将释放块和右邻空闲块合并的图示 (b) 合并后 (n1+n-2)个 WORD 合并 0 n1+n 0 p FootLoc(t) 0 (l-2)个 WORD 0 l (m-2)个 WORD 0 m 0 · 370· 《数据结构》算法实现及解析(第二版) p->llink=t->llink; // p的前驱为原t的前驱 p->llink->rlink=p; // p的前驱的后继指向p p->rlink=t->rlink; // p的后继为原t的后继 p->rlink->llink=p; // p的后继的前驱指向p p->size+=t->size; // 新的合并块的大小 (FootLoc(t))->uplink=p; // 底部域(原t的底部域)指针指向新的合并块的头部域 if(pav==t) // 可利用空间表的头指针指向t(t已不是空闲结点首地址了) pav=p; // 修改pav,令刚释放的结点为下次分配时的最先查询的结点 } else // 左右邻区均为空闲块(见图87) { // 合并左右邻块和释放块,t为右邻空闲块的头部域地址 t->llink->rlink=t->rlink; // 在pav中删去右邻空闲块结点 t->rlink->llink=t->llink; s=(p-1)->uplink; // s为左邻空闲块的头部域地址,也是新的合并块的头部域地址 s->size+=p->size+t->size; // 设置新结点的大小(3块之和) (FootLoc(t))->uplink=s; // 新结点底部(原t的底部)指针指向其头部 if(pav==t) // 可利用空间表的头指针指向t(t已不是空闲结点首地址了) pav=s; // 修改pav,令刚释放的结点为下次分配时的最先查询的结点 } p=NULL; // 令刚释放的结点的指针为空 } void Print(Space p) { // 输出p所指的可利用空间表 Space h,f; if(p) // 可利用空间表不空 图 87 将释放块和左右邻空闲块合并的图示 (b) 合并后 (m-2)个 WORD 0 m 0 (l-2)个 WORD 0 l 0 (n1+n+n2-2)个 WORD 合并 0 n1+n+n2 0 s (a) 合并前(先将右邻块 t 从 pav 中删去) (m-2)个 WORD 0 m 0 (l-2)个 WORD 0 l 0 (n2-2)个 WORD 空闲 0 n2 0 (n-2)个 WORD 释放 1 n 1 (n1-2)个 WORD 空闲 0 n1 0 p s t 第 8 章 动态存储管理 · 371· { h=p; // h指向第一个结点的头部域(首地址) f=FootLoc(h); // f指向第一个结点的底部域 do { printf("块的大小=%d 块的首地址=%u ",h->size,f->uplink); // 输出结点信息 printf("块标志=%d(0:空闲 1:占用) 邻块首地址=%u\n",h->tag,f+1); h=h->rlink; // 指向下一个结点的头部域(首地址) f=FootLoc(h); // f指向下一个结点的底部域 }while(h!=p); // 没到循环链表的表尾 } } void PrintUser(Space p[]) { // 输出p数组所指的已分配空间 for(int i=0;isize,p[i]->tag); printf(" 块尾标志=%d\n",(FootLoc(p[i]))->tag); } } void main() { Space pav,p; // 空闲块指针 Space v[MAX/e]={NULL}; // 占用块指针数组(初始化为空) int n; printf("结构体WORD为%d个字节\n",sizeof(WORD)); p=new WORD[MAX+2]; // 申请大小为MAX*sizeof(WORD)个字节的空间(见图88) p->tag=1; // 设置低址边界,以防查找左邻块时出错 pav=p+1; // 可利用空间表的表头 pav->rlink=pav->llink=pav; // 初始化可利用空间(一个整块) pav->tag=0; pav->size=MAX; p=FootLoc(pav); // p指向底部域 p->uplink=pav; p->tag=0; (p+1)->tag=1; // 设置高址边界,以防查找右邻块时出错 printf("初始化后,可利用空间表为\n"); Print(pav); n=300; v[0]=AllocBoundTag(pav,n); printf("分配%u个存储空间后,可利用空间表为\n",n); Print(pav); PrintUser(v); n=450; v[1]=AllocBoundTag(pav,n); printf("分配%u个存储空间后,pav为\n",n); Print(pav); PrintUser(v); n=300; // 分配不成功 图 88 初始化可利用空间 1 1 (MAX-2)个 WORD 0 MAX 0 p pav · 372· 《数据结构》算法实现及解析(第二版) v[2]=AllocBoundTag(pav,n); printf("分配%u个存储空间后(不成功),pav为\n",n); Print(pav); PrintUser(v); n=242; // 分配整个块(250) v[2]=AllocBoundTag(pav,n); printf("分配%u个存储空间后(整块分配),pav为\n",n); Print(pav); PrintUser(v); printf("回收v[0](%d)后(当pav空时回收),pav为\n",v[0]->size); Reclaim(pav,v[0]); // pav为空 Print(pav); PrintUser(v); printf("1按回车键继续"); getchar(); printf("回收v[2](%d)后(左右邻区均为占用块),pav为\n",v[2]->size); Reclaim(pav,v[2]); // 左右邻区均为占用块 Print(pav); PrintUser(v); n=270; // 查找空间足够大的块 v[0]=AllocBoundTag(pav,n); printf("分配%u个存储空间后(查找空间足够大的块),pav为\n",n); Print(pav); PrintUser(v); n=30; // 在当前块上分配 v[2]=AllocBoundTag(pav,n); printf("分配%u个存储空间后(在当前块上分配),pav为\n",n); Print(pav); PrintUser(v); printf("回收v[1](%d)后(右邻区为空闲块,左邻区为占用块),pav为\n",v[1]->size); Reclaim(pav,v[1]); // 右邻区为空闲块,左邻区为占用块 Print(pav); PrintUser(v); printf("2按回车键继续"); getchar(); printf("回收v[0](%d)后(左邻区为空闲块,右邻区为占用块),pav为\n",v[0]->size); Reclaim(pav,v[0]); // 左邻区为空闲块,右邻区为占用块 Print(pav); PrintUser(v); printf("回收v[2](%d)后(左右邻区均为空闲块),pav为\n",v[2]->size); Reclaim(pav,v[2]); // 左右邻区均为空闲块 Print(pav); PrintUser(v); } 程序运行结果(见图 89): 结构体WORD为8个字节 初始化后,可利用空间表为(见图89(a)) 第 8 章 动态存储管理 · 373· 块的大小=1000 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=10812 分配300个存储空间后,可利用空间表为(见图89(b)) 块的大小=700 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=8412 块0的首地址=8412 块的大小=300 块头标志=1(0:空闲 1:占用) 块尾标志=1 分配450个存储空间后,pav为(见图89(c)) 块的大小=250 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=4812 块0的首地址=8412 块的大小=300 块头标志=1(0:空闲 1:占用) 块尾标志=1 块1的首地址=4812 块的大小=450 块头标志=1(0:空闲 1:占用) 块尾标志=1 分配300个存储空间后(不成功),pav为(见图89(c)) 块的大小=250 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=4812 块0的首地址=8412 块的大小=300 块头标志=1(0:空闲 1:占用) 块尾标志=1 块1的首地址=4812 块的大小=450 块头标志=1(0:空闲 1:占用) 块尾标志=1 分配242个存储空间后(整块分配),pav为(见图89(d)) 块0的首地址=8412 块的大小=300 块头标志=1(0:空闲 1:占用) 块尾标志=1 块1的首地址=4812 块的大小=450 块头标志=1(0:空闲 1:占用) 块尾标志=1 块2的首地址=2812 块的大小=250 块头标志=1(0:空闲 1:占用) 块尾标志=1 回收v[0](300)后(当pav空时回收),pav为(见图89(e)) 块的大小=300 块的首地址=8412 块标志=0(0:空闲 1:占用) 邻块首地址=10812 块1的首地址=4812 块的大小=450 块头标志=1(0:空闲 1:占用) 块尾标志=1 块2的首地址=2812 块的大小=250 块头标志=1(0:空闲 1:占用) 块尾标志=1 1按回车键继续 回收v[2](250)后(左右邻区均为占用块),pav为(见图89(f)) 块的大小=250 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=4812 块的大小=300 块的首地址=8412 块标志=0(0:空闲 1:占用) 邻块首地址=10812 块1的首地址=4812 块的大小=450 块头标志=1(0:空闲 1:占用) 块尾标志=1 分配270个存储空间后(查找空间足够大的块),pav为(见图89(g)) 块的大小=250 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=4812 块的大小=30 块的首地址=8412 块标志=0(0:空闲 1:占用) 邻块首地址=8652 块0的首地址=8652 块的大小=270 块头标志=1(0:空闲 1:占用) 块尾标志=1 块1的首地址=4812 块的大小=450 块头标志=1(0:空闲 1:占用) 块尾标志=1 分配30个存储空间后(在当前块上分配),pav为(见图89(h)) 块的大小=30 块的首地址=8412 块标志=0(0:空闲 1:占用) 邻块首地址=8652 块的大小=220 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=4572 块0的首地址=8652 块的大小=270 块头标志=1(0:空闲 1:占用) 块尾标志=1 块1的首地址=4812 块的大小=450 块头标志=1(0:空闲 1:占用) 块尾标志=1 块2的首地址=4572 块的大小=30 块头标志=1(0:空闲 1:占用) 块尾标志=1 回收v[1](450)后(右邻区为空闲块,左邻区为占用块),pav为(见图89(i)) 块的大小=480 块的首地址=4812 块标志=0(0:空闲 1:占用) 邻块首地址=8652 块的大小=220 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=4572 块0的首地址=8652 块的大小=270 块头标志=1(0:空闲 1:占用) 块尾标志=1 块2的首地址=4572 块的大小=30 块头标志=1(0:空闲 1:占用) 块尾标志=1 2按回车键继续 回收v[0](270)后(左邻区为空闲块,右邻区为占用块),pav为(见图89(j)) 块的大小=750 块的首地址=4812 块标志=0(0:空闲 1:占用) 邻块首地址=10812 块的大小=220 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=4572 块2的首地址=4572 块的大小=30 块头标志=1(0:空闲 1:占用) 块尾标志=1 回收v[2](30)后(左右邻区均为空闲块),pav为(见图89(a)) 块的大小=1000 块的首地址=2812 块标志=0(0:空闲 1:占用) 邻块首地址=10812 · 374· 《数据结构》算法实现及解析(第二版) 边界标识法对动态分配块的大小不做限制,在块的两头各设一个头部域和底部域,用 以标识该块是否被占用。设指针 p 指向动态分配块的头部域,则 p-1 指向其左邻块(低地 址紧邻区)的底部域,其底部域的 tag 成员指示该左邻块是否被占用。如果没被占用,其底 部域的 uplink 成员还指向该左邻块的头部域;p+p->size 指向其右邻块(高地址紧邻区)的头 部域,其头部域的 tag 成员指示该右邻块是否被占用。根据左右邻块被占用的情况,决定 该动态分配块是否与其邻块合并。 8.3.3  回收算法在 algo8-1.cpp 中。 8.4 伙伴系统 8.4.1  // c8-2.h 伙伴系统可利用空间表的结构(见图810) #define m 10 // 可利用空间总容量1024字的2的幂次,子表的个数为m+1 typedef struct WORD_b 图 89 运行 algo8-1.cpp 动态存储区的变化图示(阴影为占用块) 2812 10812 1000*8 (a) 300*8700*8 (b) 2812 108128412 300*8250*8 450*8 (c) 2812 1081284124812 300*8250*8 450*8 (d) 1081284122812 4812 300*8250*8 450*8 (e) 2812 1081284124812 300*8250*8 450*8 (f) 2812 1081284124812 (g) 30*8 8412 270*8250*8 450*8 10812865248122812 (h) 30*8 8412 270*8220*8 450*8 10812865248122812 30*8 4572 (i) 270*8220*8 480*8 10812865248122812 30*8 4572 (j) 220*8 750*8 1081248122812 30*8 4572 第 8 章 动态存储管理 · 375· { WORD_b *llink; // 指向前驱结点 int tag; // 块标志,0:空闲,1:占用 int kval; // 块大小,值为2的幂次k WORD_b *rlink; // 头部域,指向后继结点 }WORD_b,head,*Space; // WORD_b:内存字类型,结点的第一个字也称为head typedef struct HeadNode { int nodesize; // 该链表的空闲块的大小 WORD_b *first; // 该链表的表头指针 }FreeList[m+1]; // 表头向量类型(见图811) 8.4.2 // algo8-2.cpp 伙伴系统。实现算法8.2的程序 #include"c1.h" #include"c8-2.h" #define N 100 // 占用块个数的最大值 Space r; // r为生成空间的首地址,全局量 Space AllocBuddy(FreeList avail,int n) { // avail[0..m]为可利用空间表,n为申请分配量, // 若有不小于n的空闲块,则分配相应的存储块, // 并返回其首地址;否则返回NULL。算法8.2 int i,k; Space pa,pi,pre,suc; for(k=0;k<=m&&(avail[k].nodesizem) // 分配失败,返回NULL return NULL; else // 进行分配 { pa=avail[k].first; // pa指向可分配子表的第一个结点 pre=pa->llink; // pre和suc分别指向pa所指结点的前驱和后继 suc=pa->rlink; if(pa==suc) // 可分配子表只有1个结点 图 810 伙伴系统可利用空 间表的结点结构 llink tag kval rlink head(头部域,1 个 WORD_b) head (2ÁÂÃÄ-1)个 WORD_b 占用块可利用区域 llink tag kval rlink 图 811 伙伴系统可利用空间表的表头结构 [0] [1] [2] [3] [m] 2Å 2Æ 2Ç 2È 2É nodesize first FreeList 0 1 WORD_b 0 2 0 4 0 8 0 2 É    · 376· 《数据结构》算法实现及解析(第二版) avail[k].first=NULL; // 分配后该子表变成空表 else // 从子表中删去pa所指结点(链表的第1个结点) { pre->rlink=suc; suc->llink=pre; avail[k].first=suc; // 该子表的头指针指向pa所指结点的后继 } for(i=1;avail[k-i].nodesize>=n+1;++i) { // 从大到小将剩余块插入相应子表,约定将低地址(最前面)的块作为分配块 pi=pa+int(pow(2,k-i)); // pi指向再分割的后半块(剩余块) pi->rlink=pi; // pi是该链表的第1个结点,故左右指针都指向自身 pi->llink=pi; pi->tag=0; // 块标志为空闲 pi->kval=k-i; // 块大小 avail[k-i].first=pi; // 插入链表 } pa->tag=1; // 最后剩给pa的是分配块,令其块标志为占用 pa->kval=k-(--i); // 块大小 } return pa; // 返回分配块的地址 } Space buddy(Space p) { // 返回起始地址为p,块大小为pow(2,p->kval)的块的伙伴地址 if((p-r)%int(pow(2,p->kval+1))==0) // p为前块 return p+int(pow(2,p->kval)); // 返回后块地址 else // p为后块 return p-int(pow(2,p->kval)); // 返回前块地址 } void Reclaim(FreeList pav,Space &p) { // 伙伴系统的回收算法。将p所指的释放块回收到可利用空间表pav中 Space s; s=buddy(p); // 伙伴块的起始地址 while(s>=r&&stag==0&&s->kval==p->kval) // 归并伙伴块 { // 伙伴块起始地址在有效范围内且伙伴块空闲并与p块等大,从链表上删除该伙伴块结点 if(s->rlink==s) // 链表上仅此一个结点 pav[s->kval].first=NULL; // 置此链表为空 else // 链表上不止一个结点 { s->llink->rlink=s->rlink; // 前驱的后继为该结点的后继 s->rlink->llink=s->llink; // 后继的前驱为该结点的前驱 if(pav[s->kval].first==s) // s是链表的第一个结点 pav[s->kval].first=s->rlink; // 修改表头指向下一个结点 } // 以下修改结点头部 if((p-r)%int(pow(2,p->kval+1))==0) // p为前块 p->kval++; // 块大小加倍 else // p为后块(s为前块) { s->kval=p->kval+1; // 块大小加倍 p=s; // p指向新块首地址 } 第 8 章 动态存储管理 · 377· s=buddy(p); // 下一个伙伴块的起始地址 } // 以下将p插到可利用空间表中 p->tag=0; // 设块标志为空闲 if(pav[p->kval].first==NULL) // 该链表空 pav[p->kval].first=p->llink=p->rlink=p; // 左右指针及表头都指向自身 else // 该链表不空,插在表头 { p->rlink=pav[p->kval].first; p->llink=p->rlink->llink; p->rlink->llink=p; p->llink->rlink=p; pav[p->kval].first=p; } p=NULL; } void Print(FreeList p) { // 输出p中所有可利用空间表 int i; Space h; for(i=0;i<=m;i++) if(p[i].first) // 第i个可利用空间表不空 { h=p[i].first; // h指向链表的第一个结点的头部域(首地址) do { printf("块的大小=%d 块的首地址=%u ",int(pow(2,h->kval)),h); // 输出结点信息 printf("块标志=%d(0:空闲 1:占用)\n",h->tag); h=h->rlink; // 指向下一个结点的头部域(首地址) }while(h!=p[i].first); // 没到循环链表的表尾 } } void PrintUser(Space p[]) { // 输出p数组所指的已分配空间 for(int i=0;ikval))); printf(" 块标志=%d(0:空闲 1:占用)\n",p[i]->tag); } } void main() { int i,n; FreeList a; Space q[N]={NULL}; // q数组为占用块的首地址 printf("sizeof(WORD_b)=%u m=%u int(pow(2,m))=%u\n",sizeof(WORD_b),m,int(pow(2,m))); for(i=0;i<=m;i++) // 初始化a(见图812) { · 378· 《数据结构》算法实现及解析(第二版) a[i].nodesize=int(pow(2,i)); a[i].first=NULL; } r=a[m].first=new WORD_b[a[m].nodesize]; // 在最大链表中生成一个结点 if(r) // 生成结点成功 { r->llink=r->rlink=r; // 初始化该结点 r->tag=0; r->kval=m; Print(a); n=100; q[0]=AllocBuddy(a,n); // 向a申请100个WORD_b的内存(实际获得128个WORD_b) printf("申请%d个字后,可利用空间为\n",n); Print(a); PrintUser(q); n=200; q[1]=AllocBuddy(a,n); // 向a申请200个WORD_b的内存(实际获得256个WORD_b) printf("申请%d个字后,可利用空间为\n",n); Print(a); PrintUser(q); n=220; q[2]=AllocBuddy(a,n); // 向a申请220个WORD_b的内存(实际获得256个WORD_b) printf("申请%d个字后,可利用空间为\n",n); Print(a); PrintUser(q); Reclaim(a,q[1]); // 回收q[1],伙伴不空闲 printf("回收q[1]后,可利用空间为\n"); Print(a); PrintUser(q); Reclaim(a,q[0]); // 回收q[0],伙伴空闲 printf("回收q[0]后,可利用空间为\n"); Print(a); PrintUser(q); Reclaim(a,q[2]); // 回收q[2],伙伴空闲,生成一个大结点 printf("回收q[2]后,可利用空间为\n"); Print(a); PrintUser(q); } else printf("ERROR\n"); } 程序运行结果(见图 813): sizeof(WORD_b)=8 m=10 int(pow(2,m))=1024 块的大小=1024 块的首地址=3474 块标志=0(0:空闲 1:占用)(见图813(a)) 申请100个字后,可利用空间为(见图813(b)) 图 812 可利用空间表的初态 [0] [1] [2] [3] [m] 2ÅNULL 2ÆNULL 2ÇNULL 2ÈNULL 2É nodesize first FreeList (2É-1)个 WORD_b 占用块可利用区域 0 2É r(动态空间首地址)   第 8 章 动态存储管理 · 379· 块的大小=128 块的首地址=4498 块标志=0(0:空闲 1:占用) 块的大小=256 块的首地址=5522 块标志=0(0:空闲 1:占用) 块的大小=512 块的首地址=7570 块标志=0(0:空闲 1:占用) 占用块0的首地址=3474 块的大小=128 块标志=1(0:空闲 1:占用) 申请200个字后,可利用空间为(见图813(c)) 块的大小=128 块的首地址=4498 块标志=0(0:空闲 1:占用) 块的大小=512 块的首地址=7570 块标志=0(0:空闲 1:占用) 占用块0的首地址=3474 块的大小=128 块标志=1(0:空闲 1:占用) 占用块1的首地址=5522 块的大小=256 块标志=1(0:空闲 1:占用) 图 813 运行 algo8-2.cpp 内存区的变化图示(阴影为占用块) (a) 动态生成内存管理空间,r 为首地址,大小为 2m 个 WORD_b r=3474 11666 1024*8 (b) 申请 100 个单位,实际分配低地址的 27 个单位。剩余部分分为几块插入各自的表中 128*8 128*8 256*8 512*8 11666757055224498 q[0] 3474 (c) 申请 200 个单位,实际分配 28 个单位 q[1] 5522 q[0] 3474 116664498 7570 128*8 128*8 256*8 512*8 (d) 申请 220 个单位,实际分配 28 个单位。剩余部分插入相应的表中 11666 q[0] 3474 q[1] 5522 q[2] 7570 96184498 128*8 128*8 256*8 256*8256*8 (e) 回收 q[1]后的情况,不和左邻空闲块合并 q[2] 7570 q[0] 3474 11666961855224498 128*8 128*8 256*8 256*8256*8 (f) 回收 q[0]后的情况,与空闲的右邻块合并之后再合并 256*8256*8512*8 9618 q[2] 7570 116663474 · 380· 《数据结构》算法实现及解析(第二版) 申请220个字后,可利用空间为(见图813(d)) 块的大小=128 块的首地址=4498 块标志=0(0:空闲 1:占用) 块的大小=256 块的首地址=9618 块标志=0(0:空闲 1:占用) 占用块0的首地址=3474 块的大小=128 块标志=1(0:空闲 1:占用) 占用块1的首地址=5522 块的大小=256 块标志=1(0:空闲 1:占用) 占用块2的首地址=7570 块的大小=256 块标志=1(0:空闲 1:占用) 回收q[1]后,可利用空间为(见图813(e)) 块的大小=128 块的首地址=4498 块标志=0(0:空闲 1:占用) 块的大小=256 块的首地址=5522 块标志=0(0:空闲 1:占用) 块的大小=256 块的首地址=9618 块标志=0(0:空闲 1:占用) 占用块0的首地址=3474 块的大小=128 块标志=1(0:空闲 1:占用) 占用块2的首地址=7570 块的大小=256 块标志=1(0:空闲 1:占用) 回收q[0]后,可利用空间为(见图813(f)) 块的大小=256 块的首地址=9618 块标志=0(0:空闲 1:占用) 块的大小=512 块的首地址=3474 块标志=0(0:空闲 1:占用) 占用块2的首地址=7570 块的大小=256 块标志=1(0:空闲 1:占用) 回收q[2]后,可利用空间为(见图813(a)) 块的大小=1024 块的首地址=3474 块标志=0(0:空闲 1:占用) 8.4.3  回收算法在 algo8-2.cpp 中。伙伴系统的动态分配块只设头部域,因为它的大小不是 任意的。通过它的头部域信息,就可以找到其左右邻块,并可知道其左右邻块是否空闲, 所以不需要设底部域。伙伴系统在回收释放块时,也要合并它的左右邻块。和边界标识法 不同的是,在回收释放块时,其左右邻块即使是空闲块,也不一定合并,还要看它们是否 为伙伴。如图 814 所示,E 块和 D 块是伙伴,E 块和 F 块就不是伙伴。两相邻块是伙伴 的条件有两个:(1) 它们的块大小相等;(2) 由整个可利用内存区的起点 r 到合并块之前 的空间大小是这个合并块的整数倍。如 r 到 q 的空间和 D、E 合并后的空间大小相等,且 D、E 两块的大小相等,所以 D、E 是伙伴块,可以合并。而 r 到 p 的空间是 E、F 合并后 的空间大小的 1.5 倍,虽然 E、F 两块的大小相等且相邻,但不是伙伴块,不能合并。伙 伴块分前块和后块。从 r 到前块首地址的空间大小是前块大小的偶数倍;从 r 到后块首地 址的空间大小是后块大小的奇数倍。如 r 到 q 的空间是 D 的空间大小的 2 倍,故 D 是前 块;而 r 到 p 的空间是 E 的空间大小的 3 倍,故 E 是后块。知道某块是前块还是后块,又 知道它的大小,就能求得它的伙伴块的地址。如果它是前块,则它的伙伴块的地址是它的 地址加上它的大小,如 D 是前块,它的伙伴块 E 的地址 p 是 q+q->size;如果它是后块, 则它的伙伴块的地址是它的地址减去它的大小,如 E 是后块,它的伙伴块 D 的地址 q 是 p-p->size。algo8-2.cpp 中的函数 buddy()就是根据块地址判断其是前块还是后块,并返回 其伙伴块地址的函数。有了伙伴块地址,还要看其伙伴块的大小是否与其大小相等。如根 据函数 buddy()判断,F 是前块,其伙伴块地址是 t,但 G 并不是它的伙伴块,因为 G 的 图 814 伙伴关系图示 r ABCDEFGHIJ pq t 第 8 章 动态存储管理 · 381· 大小与 F 不同。只有当 G 与 H 合并之后,其合并块才是 F 的伙伴块。 8.5 无用单元收集 // c8-3.h 加标志域的广义表的头尾链表存储结构(由c5-5.h改)(见图815) enum ElemTag{ATOM,LIST}; // ATOM==0:原子,LIST==1:子表 typedef struct GLNode { int mark; // 加此域,其余同c5-5.h ElemTag tag; // 公共部分,用于区分原子结点和表结点 union // 原子结点和表结点的联合部分 { AtomType atom; // atom是原子结点的值域,AtomType由用户定义 struct { GLNode *hp,*tp; }ptr; // ptr是表结点的指针域,prt.hp和ptr.tp分别指向表头和表尾 }; }*GList,GLNode; // 广义表类型 // algo8-3.cpp 实现算法8.3的程序 #include"c1.h" typedef char AtomType; // 定义原子类型为字符型 #include"c8-3.h" #include"bo5-5.cpp" void MarkList(GList GL) // 算法8.3改 { // 遍历非空广义表GL(GL!=NULL且GL->mark不定),对表中所有未加标志的结点加标志 GList q,p=GL,t=NULL; // t指示p的母表 Status finished=FALSE; if(GL==NULL) return; while(!finished) { while(p->mark!=1) { p->mark=1; // MarkHead(p)的细化 q=p->ptr.hp; // q指向*p的表头 if(q&&q->mark!=1) if(q->tag==0) q->mark=1; // ATOM,表头为原子结点 else { // 继续遍历子表 p->ptr.hp=t; p->tag=ATOM; t=p; p=q; } } // 完成对表头的标志 图 815 加标志域的广义表的头尾链表存储结构 GList GLNode(原子) mark 0 atom GList GLNode GLNode GLNode(表) mark 1 ptr.hp ptr.tp · 382· 《数据结构》算法实现及解析(第二版) q=p->ptr.tp; // q指向*p的表尾 if(q&&q->mark!=1) { // 继续遍历表尾 p->ptr.tp=t; t=p; p=q; } else // BackTrack(finished)的细化 { while(t&&t->tag==1) // LIST,表结点,从表尾回溯 { q=t; t=q->ptr.tp; q->ptr.tp=p; p=q; } if(!t) finished=TRUE; // 结束 else { // 从表头回溯 q=t; t=q->ptr.hp; q->ptr.hp=p; p=q; p->tag=LIST; } // 继续遍历表尾 } } } void Traverse_GL(GList L,void(*v)(GList)) { // 利用递归算法遍历广义表L,由bo5-5.cpp改 if(L) // L不空 if(L->tag==ATOM) // L为单原子 v(L); else // L为广义表 { v(L); Traverse_GL(L->ptr.hp,v); Traverse_GL(L->ptr.tp,v); } } void visit(GList p) { if(p->tag==ATOM) printf("mark=%d %c\n",p->mark,p->atom); else printf("mark=%d list\n",p->mark); } void main() { char p[80]; 第 8 章 动态存储管理 · 383· SString t; GList l; printf("请输入广义表l(书写形式:空表:(),单原子:a,其它:(a,(b),c)):\n"); gets(p); StrAssign(t,p); CreateGList(l,t); // 在bo5-5.cpp中 MarkList(l); // 加标志 Traverse_GL(l,visit); } 程序运行结果: 请输入广义表l(书写形式:空表:(),单原子:a,其它:(a,(b),c)): (a,(b,c,d))(见图816) mark=1 list mark=1 a mark=1 list mark=1 list mark=1 b mark=1 list mark=1 c mark=1 list mark=1 d 图 816 创建加标志域的广义表(a,(b,c,d))的头尾链表存储结构 GList 0 0 d 0 1 NULL 0 1 NULL 0 0 b 0 1 0 0 c 0 10 0 a 0 1 · 384· 《数据结构》算法实现及解析(第二版) 第 9 章 查 找 在实际工作中,经常会遇到需要查找某个数据或某类数据的情况。如在学籍管理的信 息中查找某人的各科成绩;在高考信息中查找总分高于分数线的考生的姓名、住址和考号 等。一般要按关键字来查找,姓名、考号和总分都可以作为关键字。惟一地标识一个记录 的关键字称为主关键字,如考生的考号;标识若干个记录的关键字称为次关键字,如高考 分数。 一般来说,数据的值不止包括关键字,还包括其它信息。关键字只是查找的依据。为 了描述方便,有时仅讨论关键字。 9.1 静态查找表 静态查找表在查找过程中不改变表的状态——不插不删。它适合用于不变动或不常变 动的表的查找。如高考成绩查询表、本单位职工信息表等。 9.1.1  // c9-1.h 静态查找表的顺序存储结构(见图91) struct SSTable { ElemType *elem; // 数据元素存储空间基址,建表时按实际长度分配,0号单元留空 int length; // 表长度 }; 待查找的数据元素类型 ElemType 为结构体类型,其中必含有关键字 key 成员。为了 适应各种情况,ElemType 中的 key 的类型及结构体的其它成员不定,由调用基本操作的主 程序根据具体情况定义。 // c9-7.h 对两个数值型关键字的比较约定为如下的宏定义 #define EQ(a,b) ((a)==(b)) #define LT(a,b) ((a)<(b)) #define LQ(a,b) ((a)<=(b)) // bo9-1.cpp 静态查找表(顺序表和有序表)的基本操作(7个),包括算法9.1,9.2 void Creat_Seq(SSTable &ST,ElemType r[],int n) { // 操作结果:由含n个数据元素的数组r构造静态顺序查找表ST(见图92) int i; ST.elem=(ElemType*)calloc(n+1,sizeof(ElemType));// 动态生成n+1个数据元素空间(0号单元不用) 图 91 静态查找表的顺序存储结构 elem length SSTable ElemType 第 9 章 查 找 · 385· if(!ST.elem) exit(ERROR); for(i=1;i<=n;i++) ST.elem[i]=r[i-1]; // 将数组r的值依次赋给ST ST.length=n; } void Ascend(SSTable &ST) { // 重建静态查找表为按关键字非降序排序 int i,j,k; for(i=1;idata=R[i]; // 生成结点 if(i==low) T->lchild=NULL; // 左子树空 else SecondOptimal(T->lchild,R,sw,low,i-1); // 构造左子树 if(i==high) T->rchild=NULL; // 右子树空 else SecondOptimal(T->rchild,R,sw,i+1,high); // 构造右子树 return OK; } void FindSW(int sw[],SSTable ST) { // 按照有序表ST中各数据元素的Weight域求累计权值表sw int i; sw[0]=0; for(i=1;i<=ST.length;i++) sw[i]=sw[i-1]+ST.elem[i].weight; } typedef BiTree SOSTree; // 次优查找树采用二叉链表的存储结构 void CreateSOSTree(SOSTree &T,SSTable ST) { // 由有序表ST构造一棵次优查找树T。ST的数据元素含有权域weight。算法9.4 int sw[N+1]; // 累计权值 · 390· 《数据结构》算法实现及解析(第二版) if(ST.length==0) T=NULL; else { FindSW(sw,ST); // 按照有序表ST中各数据元素的weight域求累计权值表sw SecondOptimal(T,ST.elem,sw,1,ST.length); } } Status Search_SOSTree(SOSTree &T,KeyType key) { // 在次优查找树T中查找关键字等于key的元素。找到则返回OK;否则返回FALSE while(T) // T非空 if(T->data.key==key) return OK; else if(T->data.key>key) T=T->lchild; else T=T->rchild; return FALSE; // 顺序表中不存在待查元素 } void print(ElemType c) // Traverse()调用的函数 { printf("(%c %d) ",c.key,c.weight); } void main() { SSTable st; SOSTree t; Status i; KeyType s; // 以教科书例91的数据为例 ElemType r[N]={{'A',1},{'B',1},{'C',2},{'D',5},{'E',3},{'F',4},{'G',4},{'H',3},{'I',5}}; Creat_Ord(st,r,N); // 由全局数组产生非降序静态查找表st Traverse(st,print); CreateSOSTree(t,st); // 由有序表构造一棵次优查找树 printf("\n请输入待查找的字符: "); scanf("%c",&s); i=Search_SOSTree(t,s); if(i) printf("%c的权值是%d\n",s,t->data.weight); else printf("表中不存在此字符\n"); } 程序运行结果: (A 1) (B 1) (C 2) (D 5) (E 3) (F 4) (G 4) (H 3) (I 5) 请输入待查找的字符:G G的权值是4 9.1.4  第 9 章 查 找 · 391· 9.2 动态查找表 动态查找表在查找过程中可改变表的状态,即可插入或删除数据,它适合用在表的内 容要经常变化的情况下,如飞机航班的旅客信息表。 9.2.1    // func9-1.cpp 包括算法9.5(a)和func6-3.cpp,bo9-2.cpp和bo9-3.cpp调用 #include"func6-3.cpp" // 包括InitBiTree()、DestroyBiTree()、PreOrderTraverse()和InOrderTraverse()4函数 #define InitDSTable InitBiTree // 与初始化二叉树的操作同 #define DestroyDSTable DestroyBiTree // 与销毁二叉树的操作同 #define TraverseDSTable InOrderTraverse // 与中序遍历二叉树的操作同 BiTree SearchBST(BiTree T,KeyType key) { // 在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素, // 若查找成功,则返回指向该数据元素结点的指针;否则返回空指针。算法9.5(a) if(!T||EQ(key,T->data.key)) return T; // 查找结束 else if LT(key,T->data.key) // 在左子树中继续查找 return SearchBST(T->lchild,key); else return SearchBST(T->rchild,key); // 在右子树中继续查找 } // bo9-2.cpp 动态查找表(二叉排序树)的基本操作(8个),包括算法9.5(b),9.6~9.8 typedef ElemType TElemType; #include"c6-2.h" // 二叉树的存储结构 #include"func9-1.cpp" Status SearchBST(BiTree &T,KeyType key,BiTree f,BiTree &p) // 算法9.5(b) { // 在根指针T所指二叉排序树中递归地查找其关键字等于key的数据元素,若查找 // 成功,则指针p指向该数据元素结点,并返回TRUE;否则指针p指向查找路径上 // 访问的最后一个结点并返回FALSE,指针f指向T的双亲,其初始调用值为NULL if(!T) // 查找不成功 { p=f; return FALSE; } else if EQ(key,T->data.key) // 查找成功 { p=T; return TRUE; } else if LT(key,T->data.key) return SearchBST(T->lchild,key,T,p); // 在左子树中继续查找 else return SearchBST(T->rchild,key,T,p); // 在右子树中继续查找 } Status InsertBST(BiTree &T, ElemType e) { // 当二叉排序树T中不存在关键字等于e.key的元素时,插入e并返回TRUE;否则返回FALSE。算法9.6 · 392· 《数据结构》算法实现及解析(第二版) BiTree p,s; if(!SearchBST(T,e.key,NULL,p)) // 查找不成功 { s=(BiTree)malloc(sizeof(BiTNode)); s->data=e; s->lchild=s->rchild=NULL; if(!p) T=s; // 被插结点*s为新的根结点 else if LT(e.key,p->data.key) p->lchild=s; // 被插结点*s为左孩子 else p->rchild=s; // 被插结点*s为右孩子 return TRUE; } else return FALSE; // 树中已有关键字相同的结点,不再插入 } void Delete(BiTree &p) { // 从二叉排序树中删除结点p,并重接它的左或右子树。算法9.8 BiTree q,s; if(!p->rchild) // p的右子树空则只需重接它的左子树(待删结点是叶子也走此分支) { q=p; p=p->lchild; free(q); } else if(!p->lchild) // p的左子树空,只需重接它的右子树 { q=p; p=p->rchild; free(q); } else // p的左右子树均不空 { q=p; s=p->lchild; while(s->rchild) // 转左,然后向右到尽头(找待删结点的前驱) { q=s; s=s->rchild; } p->data=s->data; // s指向被删结点的“前驱”(将被删结点前驱的值取代被删结点的值) if(q!=p) // 情况1 q->rchild=s->lchild; // 重接*q的右子树 else // 情况2 q->lchild=s->lchild; // 重接*q的左子树 free(s); } } Status DeleteBST(BiTree &T,KeyType key) { // 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点, 第 9 章 查 找 · 393· // 并返回TRUE;否则返回FALSE。算法9.7 if(!T) // 不存在关键字等于key的数据元素 return FALSE; else { if EQ(key,T->data.key) // 找到关键字等于key的数据元素 Delete(T); else if LT(key,T->data.key) DeleteBST(T->lchild,key); else DeleteBST(T->rchild,key); return TRUE; } } // algo9-4.cpp 检验bo9-2.cpp的程序 #include"c1.h" #define N 10 // 数据元素个数 typedef int KeyType; // 设关键字域为整型 struct ElemType // 数据元素类型 { KeyType key; int others; }; #include"c9-7.h" #include"bo9-2.cpp" void print(ElemType c) { printf("(%d,%d) ",c.key,c.others); } void main() { BiTree dt,p; int i; KeyType j; ElemType r[N]={{45,1},{12,2},{53,3},{3,4},{37,5},{24,6},{100,7},{61,8},{90,9},{78,10}}; // 以教科书图9.7(a)为例,另加除关键字之外的其他信息 InitDSTable(dt); // 构造空表 for(i=0;ilchild; // lc指向p的左子树根结点 p->lchild=lc->rchild; // lc的右子树挂接为p的左子树 lc->rchild=p; p=lc; // p指向新的根结点 } void L_Rotate(BSTree &p) { // 对以*p为根的二叉排序树作左旋处理,处理之后p指向新的树 // 根结点,即旋转处理之前的右子树的根结点。算法9.10(见图99) BSTree rc; rc=p->rchild; // rc指向p的右子树根结点 p->rchild=rc->lchild; // rc的左子树挂接为p的右子树 rc->lchild=p; p=rc; // p指向新的根结点 } #define LH +1 // 左高 #define EH 0 // 等高 #define RH -1 // 右高 void LeftBalance(BSTree &T) { // 对以指针T所指结点为根的二叉树作左平衡旋转处理,本算法结束时, // 指针T指向新的根结点。算法9.12 BSTree lc,rd; lc=T->lchild; // lc指向*T的左子树根结点 switch(lc->bf) { // 检查*T的左子树的平衡度,并作相应平衡处理 case LH: // 新结点插入在*T的左孩子的左子树上,要作单右旋处理 T->bf=lc->bf=EH; R_Rotate(T); break; case RH: // 新结点插入在*T的左孩子的右子树上,要作双旋处理(见图910(a)) rd=lc->rchild; // rd指向*T的左孩子的右子树根 switch(rd->bf) { // 修改*T及其左孩子的平衡因子 case LH: T->bf=RH; lc->bf=EH; 图 97 平衡二叉树类型的存储结构 BSTNode lchild data bf rchild BSTNode BSTNode 图 98 调用 R_Rotate()图示 P Lc R p (b) 右旋后(a) 右旋前 P Lc R p lc 右旋 图 99 调用 L_Rotate()图示 P Rc L p (b) 左旋后(a) 左旋前 P Rc L p rc 左旋 · 396· 《数据结构》算法实现及解析(第二版) break; case EH: T->bf=lc->bf=EH; break; case RH: T->bf=EH; lc->bf=LH; } rd->bf=EH; L_Rotate(T->lchild); // 对*T的左子树作左旋平衡处理(见图910(b)) R_Rotate(T); // 对*T作右旋平衡处理(见图910(c)) } } void RightBalance(BSTree &T) { // 对以指针T所指结点为根的二叉树作右平衡旋转处理,本算法结束时, // 指针T指向新的根结点 BSTree rc,rd; rc=T->rchild; // rc指向*T的右子树根结点 switch(rc->bf) { // 检查*T的右子树的平衡度,并作相应平衡处理 case RH: // 新结点插入在*T的右孩子的右子树上,要作单左旋处理 T->bf=rc->bf=EH; L_Rotate(T); break; case LH: // 新结点插入在*T的右孩子的左子树上,要作双旋处理(见图911(a)) rd=rc->lchild; // rd指向*T的右孩子的左子树根 switch(rd->bf) { // 修改*T及其右孩子的平衡因子 case RH: T->bf=LH; rc->bf=EH; break; case EH: T->bf=rc->bf=EH; break; case LH: T->bf=EH; rc->bf=RH; } rd->bf=EH; R_Rotate(T->rchild); // 对*T的右子树作右旋平衡处理(见图911(b)) L_Rotate(T); // 对*T作左旋平衡处理(见图911(c)) } } 图 910 LR 型平衡旋转图示 (a) 旋转前 (b) 左子树左旋 (c) T 右旋 T TT RLR LRL L LRR T L R TT LR LRLLRR T L TT LR RLRLLRR 图 911 RL 型平衡旋转图示 (a) 旋转前 (b) 右子树右旋 (c) T 左旋 T TT L RL RLR RRLL T L R TT RL RLLRLR T TT R RL L RLLRLR 第 9 章 查 找 · 397· Status InsertAVL(BSTree &T,ElemType e,Status &taller) { // 若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个 // 数据元素为e的新结点,并返回1;否则返回0。若因插入而使二叉排序树 // 失去平衡,则作平衡旋转处理,布尔变量taller反映T长高与否。算法9.11 if(!T) { // 插入新结点,树“长高”,置taller为TRUE T=(BSTree)malloc(sizeof(BSTNode)); T->data=e; T->lchild=T->rchild=NULL; T->bf=EH; taller=TRUE; } else { if EQ(e.key,T->data.key) { // 树中已存在和e有相同关键字的结点则不再插入 taller=FALSE; return FALSE; } if LT(e.key,T->data.key) { // 应继续在*T的左子树中进行搜索 if(!InsertAVL(T->lchild,e,taller)) // 未插入 return FALSE; if(taller) // 已插入到*T的左子树中且左子树“长高” switch(T->bf) // 检查*T的平衡度 { case LH: // 原本左子树比右子树高,需要作左平衡处理 LeftBalance(T); taller=FALSE; break; case EH: // 原本左、右子树等高,现因左子树增高而使树增高 T->bf=LH; taller=TRUE; break; case RH: T->bf=EH; // 原本右子树比左子树高,现左、右子树等高 taller=FALSE; } } else { // 应继续在*T的右子树中进行搜索 if(!InsertAVL(T->rchild,e,taller)) // 未插入 return FALSE; if(taller) // 已插入到T的右子树且右子树“长高” switch(T->bf) // 检查T的平衡度 { case LH: T->bf=EH; // 原本左子树比右子树高,现左、右子树等高 taller=FALSE; break; case EH: // 原本左、右子树等高,现因右子树增高而使树增高 T->bf=RH; taller=TRUE; · 398· 《数据结构》算法实现及解析(第二版) break; case RH: // 原本右子树比左子树高,需要作右平衡处理 RightBalance(T); taller=FALSE; } } } return TRUE; } 在构造平衡二叉树时,每插入一个结点就检查是否仍为平衡二叉树。如果由于插入结 点导致二叉树失衡,则要通过旋转、改变根结点等措施保证仍为平衡的二叉排序树。插入 结点导致二叉树不平衡有 4 种情况: (1) 在左子树的左孩子分支上插入结点,导致不平衡,称 LL 型; (2) 在左子树的右孩子分支上插入结点,导致不平衡,称 LR 型; (3) 在右子树的右孩子分支上插入结点,导致不平衡,称 RR 型; (4) 在右子树的左孩子分支上插入结点,导致不平衡,称 RL 型。 对于 LL 型,做右旋,如图 98 所示,降低了左子树的左孩子分支的深度,重新构成 平衡二叉树。相应地,对于 RR 型,做左旋,如图 99 所示。对于 LR 型,先令左子树左 旋,再令整个树右旋,如图 910 所示,降低了 LR 的两个子树的深度,重新构成平衡二 叉树。对于 RL 型,先令右子树右旋,再令整个树左旋,如图 911 所示,降低了 RL 的 两个子树的深度,重新构成平衡二叉树。 // algo9-5.cpp 检验bo9-3.cpp的程序 #include"c1.h" #define N 5 // 数据元素个数 typedef char KeyType; // 设关键字域为字符型 struct ElemType // 数据元素类型 { KeyType key; int order; }; #include"c9-7.h" #include"c9-2.h" #include"bo9-3.cpp" void print(ElemType c) { printf("(%d,%d)",c.key,c.order); } void main() { BSTree dt,p; Status k; int i; KeyType j; ElemType r[N]={{13,1},{24,2},{37,3},{90,4},{53,5}}; // (以教科书图9.12为例) InitDSTable(dt); // 初始化空树 第 9 章 查 找 · 399· for(i=0;idata); else printf("表中不存在此值"); printf("\n"); DestroyDSTable(dt); } 程序运行结果(见图 912): (24,2)(13,1)(53,5)(37,3)(90,4)先序遍历平衡二叉树 (13,1)(24,2)(37,3)(53,5)(90,4) 请输入待查找的关键字: 37 (37,3) 9.2.2 B_  B+ // c9-3.h B_树的结点类型 struct Record // 记录类型(见图913) { KeyType key; // 关键字 Others others; // 其它部分(由主程定义) }; typedef struct BTNode { int keynum; // 结点中关键字个数,即结点的大小 BTNode *parent; // 指向双亲结点 struct Node // 结点向量类型(见图914) { KeyType key; // 关键字向量 BTNode *ptr; // 子树指针向量 Record *recptr; // 记录指针向量 }node[m+1]; // key,recptr的0号单元未用 }BTNode,*BTree; // B_树结点和B_树的类型(见图915) struct Result // B_树的查找结果类型(见图916) { BTNode *pt; // 指向找到的结点 int i; // 1..m,在结点中的关键字序号 int tag; // 1:查找成功,O:查找失败 } 图 913 记录类型 key others Record 图 912 生成的排序二叉树 5313 37 24 90 图 914 Node 结点向量类型 Node key ptr recptr Record BTNode    · 400· 《数据结构》算法实现及解析(第二版) // bo9-4.cpp 动态查找表(B_树)的基本操作,包括算法9.13,9.14 void InitDSTable(BTree &DT) { // 操作结果:构造一个空的动态查找表DT DT=NULL; } void DestroyDSTable(BTree &DT) { // 初始条件:动态查找表DT存在。操作结果:销毁动态查找表DT int i; if(DT) // 非空树 { for(i=0;i<=DT->keynum;i++) DestroyDSTable(DT->node[i].ptr); // 依次销毁第i棵子树 free(DT); // 释放根结点 DT=NULL; // 空指针赋0 } } int Search(BTree p, KeyType K) { // 在p->node[1..keynum].key中查找i,使得p->node[i].key≤ K<p->node[i+1].key int i=0,j; for(j=1;j<=p->keynum;j++) if(p->node[j].key<=K) i=j; return i; } Result SearchBTree(BTree T, KeyType K) { // 在m阶B_树T上查找关键字K,返回结果(pt,i,tag)。若查找成功,则特征值 // tag=1,指针pt所指结点中第i个关键字等于K;否则特征值tag=0,等于K的 // 关键字应插入在指针Pt所指结点中第i和第i+1个关键字之间。算法9.13 BTree p=T,q=NULL; // 初始化,p指向待查结点,q指向p的双亲 Status found=FALSE; int i=0; Result r; while(p&&!found) { i=Search(p,K); // p->node[i].key≤ Knode[i+1].key if(i>0&&p->node[i].key==K) // 找到待查关键字 found=TRUE; else { 图 915 m 阶 B_树结点及指针类型 BTNode node BTNode [0] [1] [m] BTree key ptr recptr keynum parent 图 916 B_树的查找结果类型 Result pt i tag BTNode          第 9 章 查 找 · 401· q=p; p=p->node[i].ptr; } } r.i=i; if(found) // 查找成功 { r.pt=p; r.tag=1; } else // 查找不成功,返回K的插入位置信息 { r.pt=q; r.tag=0; } return r; } void Insert(BTree &q,int i,Record *r,BTree ap) { // 将r->key、r和ap分别插入到q->key[i+1]、q->recptr[i+1]和q->ptr[i+1]中 int j; for(j=q->keynum;j>i;j--) // 空出q->node[i+1] q->node[j+1]=q->node[j]; q->node[i+1].key=r->key; q->node[i+1].ptr=ap; q->node[i+1].recptr=r; q->keynum++; } void split(BTree &q,BTree &ap) { // 将结点q分裂成两个结点,前一半保留,后一半移入新生结点ap int i,s=(m+1)/2; ap=(BTree)malloc(sizeof(BTNode)); // 生成新结点ap ap->node[0].ptr=q->node[s].ptr; // 后一半移入ap for(i=s+1;i<=m;i++) { ap->node[i-s]=q->node[i]; if(ap->node[i-s].ptr) ap->node[i-s].ptr->parent=ap; } ap->keynum=m-s; ap->parent=q->parent; q->keynum=s-1; // q的前一半保留,修改keynum } void NewRoot(BTree &T,Record *r,BTree ap) { // 生成含信息(T,r,ap)的新的根结点*T,原T和ap为子树指针 BTree p; p=(BTree)malloc(sizeof(BTNode)); p->node[0].ptr=T; T=p; if(T->node[0].ptr) T->node[0].ptr->parent=T; T->parent=NULL; · 402· 《数据结构》算法实现及解析(第二版) T->keynum=1; T->node[1].key=r->key; T->node[1].recptr=r; T->node[1].ptr=ap; if(T->node[1].ptr) T->node[1].ptr->parent=T; } void InsertBTree(BTree &T,Record *r,BTree q,int i) { // 在m阶B_树T上结点*q的key[i]与key[i+1]之间插入关键字K的指针r。若引起 // 结点过大,则沿双亲链进行必要的结点分裂调整,使T仍是m阶B_树。算法9.14改 BTree ap=NULL; Status finished=FALSE; int s; Record *rx; rx=r; while(q&&!finished) { Insert(q,i,rx,ap);//将r->key、r和ap分别插入到q->key[i+1]、q->recptr[i+1]和q->ptr[i+1]中 if(q->keynumnode[s].recptr; split(q,ap); // 将q->key[s+1..m],q->ptr[s..m]和q->recptr[s+1..m]移入新结点*ap q=q->parent; if(q) i=Search(q,rx->key); // 在双亲结点*q中查找rx->key的插入位置 } } if(!finished) // T是空树(参数q初值为NULL)或根结点已分裂为结点*q和*ap NewRoot(T,rx,ap); // 生成含信息(T,rx,ap)的新的根结点*T,原T和ap为子树指针 } void TraverseDSTable(BTree DT,void(*Visit)(BTNode,int)) { // 初始条件:动态查找表DT存在,Visit是对结点操作的应用函数 // 操作结果:按关键字的顺序对DT的每个结点调用函数Visit()一次且至多一次 int i; if(DT) // 非空树 { if(DT->node[0].ptr) // 有第0棵子树 TraverseDSTable(DT->node[0].ptr,Visit); for(i=1;i<=DT->keynum;i++) { Visit(*DT,i); if(DT->node[i].ptr) // 有第i棵子树 TraverseDSTable(DT->node[i].ptr,Visit); } } } // algo9-6.cpp 检验bo9-4.cpp的程序 第 9 章 查 找 · 403· #include"c1.h" #define m 3 // B_树的阶,暂设为3 #define N 16 // 数据元素个数 #define MAX 5 // 字符串最大长度+1 typedef int KeyType; // 设关键字域为整型 struct Others // 记录的其它部分 { char info[MAX]; }; #include"c9-3.h" #include"bo9-4.cpp" void print(BTNode c,int i) // TraverseDSTable()调用的函数 { printf("(%d,%s)",c.node[i].key,c.node[i].recptr->others.info); } void main() { Record r[N]={{24,"1"},{45,"2"},{53,"3"},{12,"4"},{37,"5"},{50,"6"},{61,"7"},{90,"8"}, {100,"9"},{70,"10"},{3,"11"},{30,"12"},{26,"13"},{85,"14"},{3,"15"}, {7,"16"}}; // (以教科书中图9.16为例) BTree t; Result s; int i; InitDSTable(t); for(i=0;ikind==BRANCH&&DT->first) // *DT是分支结点且有孩子 DestroyDSTable(DT->first); // 销毁孩子子树 if(DT->next) // 有兄弟 DestroyDSTable(DT->next); // 销毁兄弟子树 free(DT); // 释放根结点 DT=NULL; // 空指针赋0 } } Record *SearchDLTree(DLTree T,KeysType K) { // 在非空双链键树T中查找关键字等于K的记录,若存在, // 则返回指向该记录的指针;否则返回空指针。算法9.15,有改动 DLTree p; int i; 图 918 Record 记录类型 Record key others 图 919 双链树类型 DLTNode(分支结点) symbol next BRANCH first DLTree DLTNode DLTNode symbol next LEAF infoptr DLTNode(叶子结点) DLTNode Record DLTree 图 917 KeysType 关键字类型 KeysType ... num [0] [] MAX_KEY_LEN-1 ch 第 9 章 查 找 · 405· if(T) { p=T; // 初始化 i=0; while(p&&isymbol!=K.ch[i]) // 查找关键字的第i位 p=p->next; if(p&&ifirst; ++i; } // 查找结束 if(!p) // 查找不成功 return NULL; else // 查找成功 return p->infoptr; } else return NULL; // 树空 } void InsertDSTable(DLTree &DT,Record *r) { // 初始条件:双链键树DT存在,r为待插入的数据元素的指针 // 操作结果:若DT中不存在其关键字等于(*r).key.ch的数据元素,则按关键字顺序插r到DT中 DLTree p=NULL,q,ap; int i=0; KeysType K=r->key; if(!DT&&K.num) // 空树且关键字符串非空 { DT=ap=(DLTree)malloc(sizeof(DLTNode)); for(;ifirst=ap; ap->next=NULL; ap->symbol=K.ch[i]; ap->kind=BRANCH; p=ap; ap=(DLTree)malloc(sizeof(DLTNode)); } p->first=ap; // 插入叶子结点 ap->next=NULL; ap->symbol=Nil; ap->kind=LEAF; ap->infoptr=r; } else // 非空树 { p=DT; // 指向根结点 while(p&&isymbolnext; } if(p&&p->symbol==K.ch[i]) // 找到与K.ch[i]相符的结点 { q=p; p=p->first; // p指向将与K.ch[i+1]比较的结点 ++i; } else // 没找到,插入关键字 { ap=(DLTree)malloc(sizeof(DLTNode)); if(q->first==p) q->first=ap; // 在长子的位置插入 else // q->next==p q->next=ap; // 在兄弟的位置插入 ap->next=p; ap->symbol=K.ch[i]; ap->kind=BRANCH; p=ap; ap=(DLTree)malloc(sizeof(DLTNode)); i++; for(;ifirst=ap; ap->next=NULL; ap->symbol=K.ch[i]; ap->kind=BRANCH; p=ap; ap=(DLTree)malloc(sizeof(DLTNode)); } p->first=ap; // 插入叶子结点 ap->next=NULL; ap->symbol=Nil; ap->kind=LEAF; ap->infoptr=r; } } } } struct SElemType // 定义栈元素类型 { char ch; DLTree p; }; #include"c3-1.h" // 顺序栈 #include"bo3-1.cpp" // 顺序栈的基本操作 void TraverseDSTable(DLTree DT,void(*Vi)(Record)) { // 初始条件:双链键树DT存在,Vi是对结点操作的应用函数,ViR是对记录操作的应用函数 // 操作结果:按关键字的顺序输出关键字及其对应的记录 第 9 章 查 找 · 407· SqStack s; SElemType e; DLTree p; int i=0,n=8; if(DT) { InitStack(s); e.p=DT; e.ch=DT->symbol; Push(s,e); p=DT->first; while(p->kind==BRANCH) // 分支结点 { e.p=p; e.ch=p->symbol; Push(s,e); p=p->first; } e.p=p; e.ch=p->symbol; Push(s,e); Vi(*(p->infoptr)); i++; while(!StackEmpty(s)) { Pop(s,e); p=e.p; if(p->next) // 有兄弟结点 { p=p->next; while(p->kind==BRANCH) // 分支结点 { e.p=p; e.ch=p->symbol; Push(s,e); p=p->first; } e.p=p; e.ch=p->symbol; Push(s,e); Vi(*(p->infoptr)); i++; if(i%n==0) printf("\n"); // 输出n个元素后换行 } } } } // algo9-7.cpp 检验bo9-5.cpp的程序 #include"c1.h" · 408· 《数据结构》算法实现及解析(第二版) #define N 16 // 数据元素个数 struct Others // 记录的其它部分 { int ord; }; #define Nil ′′// 定义结束符为空格(与教科书不同) #include"c9-4.h" #include"bo9-5.cpp" void print(Record e) { int i; printf("("); for(i=0;ikind==BRANCH&&T->bh.ptr[i]) // 第i个结点不空 if(T->bh.ptr[i]->kind==BRANCH) // 是子树 DestroyDSTable(T->bh.ptr[i]); else // 是叶子 { free(T->bh.ptr[i]); T->bh.ptr[i]=NULL; } free(T); // 释放根结点 T=NULL; // 空指针赋0 } } int ord(char c) { c=toupper(c); if(c>=′A′&&c<=′Z′) return c-′A′+1; // 英文字母返回其在字母表中的序号 else return 0; // 其余字符返回0 } Record *SearchTrie(TrieTree T,KeysType K) { // 在键树T中查找关键字等于K的记录。算法9.16 TrieTree p; int i; for(p=T,i=0;p&&p->kind==BRANCH&&ibh.ptr[ord(K.ch[i])],++i); // 对K的每个字符逐个查找,*p为分支结点,ord()求字符在字母表中序号 if(p&&p->kind==LEAF&&p->lf.K.num==K.num&&EQ(p->lf.K.ch,K.ch)) // 查找成功 第 9 章 查 找 · 411· return p->lf.infoptr; else // 查找不成功 return NULL; } void InsertTrie(TrieTree &T,Record *r) { // 初始条件:Trie键树T存在,r为待插入的数据元素的指针 // 操作结果:若T中不存在其关键字等于(*r).key.ch的数据元素,则按关键字顺序插r到T中 TrieTree p,q,ap; int i=0,j; KeysType K1,K=r->key; if(!T) // 空树 { T=(TrieTree)malloc(sizeof(TrieNode)); T->kind=BRANCH; for(i=0;ibh.ptr[i]=NULL; p=T->bh.ptr[ord(K.ch[0])]=(TrieTree)malloc(sizeof(TrieNode)); p->kind=LEAF; p->lf.K=K; p->lf.infoptr=r; } else // 非空树 { for(p=T,i=0;p&&p->kind==BRANCH&&ibh.ptr[ord(K.ch[i])]; } i--; if(p&&p->kind==LEAF&&p->lf.K.num==K.num&&EQ(p->lf.K.ch,K.ch)) // T中存在该关键字 return; else // T中不存在该关键字,插入之 if(!p) // 分支空 { p=q->bh.ptr[ord(K.ch[i])]=(TrieTree)malloc(sizeof(TrieNode)); p->kind=LEAF; p->lf.K=K; p->lf.infoptr=r; } else if(p->kind==LEAF) // 有不完全相同的叶子 { K1=p->lf.K; do { ap=q->bh.ptr[ord(K.ch[i])]=(TrieTree)malloc(sizeof(TrieNode)); ap->kind=BRANCH; for(j=0;jbh.ptr[j]=NULL; q=ap; i++; }while(ord(K.ch[i])==ord(K1.ch[i])); · 412· 《数据结构》算法实现及解析(第二版) q->bh.ptr[ord(K1.ch[i])]=p; p=q->bh.ptr[ord(K.ch[i])]=(TrieTree)malloc(sizeof(TrieNode)); p->kind=LEAF; p->lf.K=K; p->lf.infoptr=r; } } } void TraverseDSTable(TrieTree T,void(*Vi)(Record*)) { // 初始条件:Trie键树T存在,Vi是对记录指针操作的应用函数 // 操作结果:按关键字的顺序输出关键字及其对应的记录 TrieTree p; int i; if(T) for(i=0;ibh.ptr[i]; if(p&&p->kind==LEAF) Vi(p->lf.infoptr); else if(p&&p->kind==BRANCH) TraverseDSTable(p,Vi); } } // algo9-8.cpp 检验bo9-6.cpp的程序 #include"c1.h" #define N 16 // 数据元素个数 #define LENGTH 27 // 结点的最大度+1(大写英文字母) struct Others // 记录的其它部分 { int ord; }; #define Nil ′′// 定义结束符为空格(与教科书不同) #include"c9-5.h" #include"c9-8.h" #include"bo9-6.cpp" void pr(Record *r) { printf("(%s,%d)",r->key.ch,r->others.ord); } void main() { TrieTree t; int i; char s[MAX_KEY_LEN+1]; KeysType k; Record *p; Record r[N]={{{"CAI"},1},{{"CAO"},2},{{"LI"},3},{{"LAN"},4},{{"CHA"},5},{{"CHANG"},6}, {{"WEN"},7},{{"CHAO"},8},{{"YUN"},9},{{"YANG"},10},{{"LONG"},11}, {{"WANG"},12},{{"ZHAO"},13},{{"LIU"},14},{{"WU"},15},{{"CHEN"},16}}; // 数据元素(以教科书式924为例) 第 9 章 查 找 · 413· InitDSTable(t); for(i=0;ikey!=NULL_KEY) // 该单元有数据 *p++=*(H.elem+i); H.count=0; H.sizeindex++; // 增大存储容量 m=hashsize[H.sizeindex]; p=(ElemType*)realloc(H.elem,m*sizeof(ElemType)); if(!p) exit(OVERFLOW); // 存储分配失败 H.elem=p; for(i=0;i=high+1;--j) L.r[j+1]=L.r[j]; // 记录后移 L.r[high+1]=L.r[0]; // 插入 } } void P2_InsertSort(SqList &L) { // 2_路插入排序 int i,j,first,final; RedType *d; d=(RedType*)malloc(L.length*sizeof(RedType)); // 生成L.length个记录的临时空间 d[0]=L.r[1]; // 设L的第1个记录为d中排好序的记录(在位置[0]) first=final=0; // first、final分别指示d中排好序的记录的第1个和最后1个记录的位置 for(i=2;i<=L.length;++i) // 依次将L的第2个~最后1个记录插入d中 if(L.r[i].keyd[final].key) { // 待插记录大于d中最大值,插到d[final]之后(不需移动d数组的元素) final=final+1; d[final]=L.r[i]; } else { // 待插记录大于d中最小值,小于d中最大值,插到d的中间(需要移动d数组的元素) j=final++; // 移动d的尾部元素以便按序插入记录 while(L.r[i].key typedef int InfoType; // 定义其它数据项的类型 #include"c9-7.h" #include"c10-1.h" void ShellInsert(SqList &L,int dk) { // 对顺序表L作一趟希尔插入排序。本算法是和一趟直接插入排序相比,作了以下修改: // 1.前后记录位置的增量是dk,而不是1; // 2.r[0]只是暂存单元,不是哨兵。当j<=0时,插入位置已找到。算法10.4 int i,j; for(i=dk+1;i<=L.length;++i) if LT(L.r[i].key,L.r[i-dk].key) { // 需将L.r[i]插入有序增量子表 L.r[0]=L.r[i]; // 暂存在L.r[0] for(j=i-dk;j>0&<(L.r[0].key,L.r[j].key);j-=dk) L.r[j+dk]=L.r[j]; // 记录后移,查找插入位置 L.r[j+dk]=L.r[0]; // 插入 } } void print(SqList L) { int i; for(i=1;i<=L.length;i++) printf("%d ",L.r[i].key); printf("\n"); } void print1(SqList L) { int i; for(i=1;i<=L.length;i++) printf("(%d,%d)",L.r[i].key,L.r[i].otherinfo); printf("\n"); } void ShellSort(SqList &L,int dlta[],int t) { // 按增量序列dlta[0..t-1]对顺序表L作希尔排序。算法10.5 int k; for(k=0;k1&&change;--i) { change=FALSE; for(j=0;ja[j+1]) { t=a[j]; a[j]=a[j+1]; a[j+1]=t; change=TRUE; } } } void print(int r[],int n) 第 10 章 内 部 排 序 · 427· { int i; for(i=0;i typedef int InfoType; // 定义其它数据项的类型 · 428· 《数据结构》算法实现及解析(第二版) #include"c10-1.h" int Partition(SqList &L,int low,int high) { // 交换顺序表L中子表L.r[low..high]的记录,使枢轴记录到位, // 并返回其所在位置,此时在它之前(后)的记录均不大(小)于它。算法10.6(a) RedType t; KeyType pivotkey; pivotkey=L.r[low].key; // 用子表的第一个记录作枢轴记录 while(low=pivotkey) --high; t=L.r[low]; // 将比枢轴记录小的记录交换到低端 L.r[low]=L.r[high]; L.r[high]=t; while(low typedef int InfoType; // 定义其它数据项的类型 #include"c10-1.h" 第 10 章 内 部 排 序 · 429· int Partition(SqList &L,int low,int high) { // 交换顺序表L中子表r[low..high]的记录,枢轴记录到位,并返回其 // 所在位置,此时在它之前(后)的记录均不大(小)于它。算法10.6(b) KeyType pivotkey; L.r[0]=L.r[low]; // 用子表的第一个记录作枢轴记录 pivotkey=L.r[low].key; // 枢轴记录关键字 while(low< high) { // 从表的两端交替地向中间扫描 while(low=pivotkey) --high; L.r[low]=L.r[high]; // 将比枢轴记录小的记录移到低端 while(low typedef int InfoType; // 定义其它数据项的类型 · 430· 《数据结构》算法实现及解析(第二版) #include"c10-1.h" int SelectMinKey(SqList L,int i) { // 返回在L.r[i..L.length]中key最小的记录的序号 KeyType min; int j,k; k=i; // 设第i个为最小 min=L.r[i].key; for(j=i+1;j<=L.length;j++) if(L.r[j].key typedef int InfoType; // 定义其它数据项的类型 #include"c9-7.h" #include"c10-1.h" typedef SqList HeapType; // 堆采用顺序表存储表示 void HeapAdjust(HeapType &H,int s,int m) // 算法10.10 { // 已知H.r[s..m]中记录的关键字除H.r[s].key之外均满足堆的定义,本函数 // 调整H.r[s]的关键字,使H.r[s..m]成为一个大顶堆(对其中记录的关键字而言) RedType rc; int j; rc=H.r[s]; for(j=2*s;j<=m;j*=2) { // 沿key较大的孩子结点向下筛选 if(j0;--i) // 把H.r[1..H.length]建成大顶堆 HeapAdjust(H,i,H.length); for(i=H.length;i>1;--i) { // 将堆顶记录和当前未经排序子序列H.r[1..i]中最后一个记录相互交换 t=H.r[1]; H.r[1]=H.r[i]; H.r[i]=t; HeapAdjust(H,1,i-1); // 将H.r[1..i-1]重新调整为大顶堆 } } void print(HeapType H) { int i; for(i=1;i<=H.length;i++) printf("(%d,%d)",H.r[i].key,H.r[i].otherinfo); printf("\n"); } #define N 8 void main() { RedType d[N]={{49,1},{38,2},{65,3},{97,4},{76,5},{13,6},{27,7},{49,8}}; HeapType h; int i; · 434· 《数据结构》算法实现及解析(第二版) for(i=0;i typedef int InfoType; // 定义其它数据项的类型 #include"c9-7.h" #include"c10-1.h" void Merge(RedType SR[],RedType TR[],int i,int m,int n) { // 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]。算法10.12 int j,k,l; for(j=m+1,k=i;i<=m&&j<=n;++k) // 将SR中记录由小到大地并入TR if LQ(SR[i].key,SR[j].key) TR[k]=SR[i++]; else TR[k]=SR[j++]; if(i<=m) for(l=0;l<=m-i;l++) TR[k+l]=SR[i+l]; // 将剩余的SR[i..m]复制到TR if(j<=n) for(l=0;l<=n-j;l++) TR[k+l]=SR[j+l]; // 将剩余的SR[j..n]复制到TR } void MSort(RedType SR[],RedType TR1[],int s, int t) { // 将SR[s..t]归并排序为TR1[s..t]。算法10.13 int m; RedType TR2[MAX_SIZE+1]; if(s==t) TR1[s]=SR[s]; else { 第 10 章 内 部 排 序 · 435· m=(s+t)/2; // 将SR[s..t]平分为SR[s..m]和SR[m+1..t] MSort(SR,TR2,s,m); // 递归地将SR[s..m]归并为有序的TR2[s..m] MSort(SR,TR2,m+1,t); // 递归地将SR[m+1..t]归并为有序的TR2[m+1..t] Merge(TR2,TR1,s,m,t); // 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] } } void MergeSort(SqList &L) { // 对顺序表L作归并排序。算法10.14 MSort(L.r,L.r,1,L.length); } void print(SqList L) { int i; for(i=1;i<=L.length;i++) printf("(%d,%d)",L.r[i].key,L.r[i].otherinfo); printf("\n"); } #define N 7 void main() { RedType d[N]={{49,1},{38,2},{65,3},{97,4},{76,5},{13,6},{27,7}}; SqList l; int i; for(i=0;i=0;j--) printf("%c",L.r[i].keys[j]); printf(" "); i=L.r[i].next; · 438· 《数据结构》算法实现及解析(第二版) } } void RadixSort(SLList &L) { // L是采用静态链表表示的顺序表。对L作基数排序,使得L成为按关键字自小到大的有序静态链表, // L.r[0]为头结点。算法10.17 int i; ArrType f,e; for(i=0;i0) { if(b[s].key>b[ls[t]].key) { i=s; s=ls[t]; // s指示新的胜者 ls[t]=i; } t=t/2; } ls[0]=s; } void CreateLoserTree(LoserTree ls) { // 已知b[0]到b[k-1]为完全二叉树ls的叶子结点,存有k个关键字,沿从叶子 // 到根的k条路径将ls调整成为败者树。算法11.3 int i; b[k].key=MIN_KEY; for(i=0;i=0;--i) // 依次从b[k-1],b[k-2],⋯, b[0]出发调整败者 Adjust(ls,i); } void K_Merge(LoserTree ls,External b) { // 利用败者树ls将编号从0到k-1的k个输入归并段中的记录归并到输出归并段。b[0]至b[k-1]为 // 败者树上的k个叶子结点,分别存放k个输入归并段中当前记录的关键字。算法11.1 int i,q; for(i=0;i0;t=t/2,p=ls[t]) if(wa[p].rnum=0;--i) { fread(&wa[i].rec,sizeof(RedType),1,fi); // 输入一个记录 wa[i].key=wa[i].rec.key; // 提取关键字 wa[i].rnum=1; // 其段号为“1” Select_MiniMax(ls,wa,i); // 调整败者 } } void get_run(LoserTree ls,WorkArea wa,int rc,int &rmax,FILE *fi,FILE *fo) { // 求得一个初始归并段,fi为输入文件指针,fo为输出文件指针。算法11.5 int q; KeyType minimax; while(wa[ls[0]].rnum==rc) // 选得的MINIMAX记录属当前段时 { q=ls[0]; // q指示MINIMAX记录在wa中的位置 minimax=wa[q].key; fwrite(&wa[q].rec,sizeof(RedType),1,fo); // 将刚选得的MINIMAX记录写入输出文件 fread(&wa[q].rec,sizeof(RedType),1,fi); // 从输入文件读入下一记录(改) if(feof(fi)) { // 输入文件结束,虚设记录(属“rmax+1”段) wa[q].rnum=rmax+1; wa[q].key=MAX_KEY; } else { // 输入文件非空时 wa[q].key=wa[q].rec.key; // 提取关键字 if(wa[q].keygr.key) i=3; else if(gr.code==′U′ &&fr.key==gr.key) i=4; else i=0; switch(i) { case 1: // 复制“旧”主文件中记录 fwrite(&fr,sizeof(RedType),1,h); if(!feof(f)) fread(&fr,sizeof(RedType),1,f); break; case 2: // 删除“旧”主文件中记录,即不复制 if(!feof(f)) fread(&fr,sizeof(RedType),1,f); if(!feof(g)) fread(&gr,sizeof(RcdType),1,g); break; case 3: // 插入 fn=P(gr); // 函数P把gr加工为h的结构 fwrite(&fn,sizeof(RedType),1,h); if(!feof(g)) fread(&gr,sizeof(RcdType),1,g); break; case 4: // 更改“旧”主文件中记录 Q(fr,gr); // 函数Q将fr和gr归并成一个h结构的记录 fwrite(&fr,sizeof(RedType),1,h); if(!feof(f)) fread(&fr,sizeof(RedType),1,f); if(!feof(g)) fread(&gr,sizeof(RcdType),1,g); break; default:exit(ERROR); // 其它均为出错情况 } } } void print(RedType t) { printf("%6d%4d\n",t.accounts,t.amount); } void printc(RcdType t) { printf("%6d%6d%8c\n",t.accounts,t.amount,t.code); } · 454· 《数据结构》算法实现及解析(第二版) void main() { RedType c,a[8]={{1,50},{5,78},{12,100},{14,95},{15,360},{18,200},{20,510},{INT_MAX,0}}; // 主文件数据 RcdType d,b[6]={{8,100, ′I′},{12,-25, ′U′},{14,38, ′U′},{18,-200, ′D′},{21,60, ′I′}, {INT_MAX,0, ′U′}}; // 已排序的事务文件数据 FILE *f,*g,*h; int j; f=fopen("old","wb"); // 以写的方式打开主文件old fwrite(a,sizeof(RedType),8,f); // 将数组a中的数据写入文件old fclose(f); // 关闭文件old,形成主文件 f=fopen("change","wb"); // 以写的方式打开事务文件change fwrite(b,sizeof(RcdType),6,f); // 将数组b中的数据写入文件change fclose(f); // 关闭文件change,形成已排序的事务文件 f=fopen("old","rb"); // 以读的方式打开主文件old printf("主文件内容:\n"); printf(" 帐号 余额\n"); do { j=fread(&c,sizeof(RedType),1,f); if(j==1) print(c); // 输出r的内容 }while(j==1); rewind(f); // 使f的指针重新返回文件的起始位置,以便重新读入内存 g=fopen("change","rb"); // 以读的方式打开已排序的事务文件change printf("已排序的事务文件内容:\n"); printf(" 帐号 存取数量 修改要求\n"); do { j=fread(&d,sizeof(RcdType),1,g); if(j==1) printc(d); // 输出r的内容 }while(j==1); rewind(g); // 使g的指针重新返回文件的起始位置,以便重新读入内存 h=fopen("new","wb"); // 以写的方式打开新主文件new MergeFile(f,g,h); // 生成新主文件 fclose(f); // 关闭文件old fclose(g); // 关闭文件change fclose(h); // 关闭文件new f=fopen("new","rb"); // 以读的方式打开新主文件new printf("新主文件内容:\n"); printf(" 帐号 余额\n"); do { j=fread(&c,sizeof(RedType),1,f); if(j==1) print(c); // 输出r的内容 }while(j==1); fclose(f); // 关闭文件new } 第 12 章 文 件 · 455· 程序运行结果(以教科书中图 12.4 为例): 主文件内容: 帐号 余额 1 50 5 78 12 100 14 95 15 360 18 200 20 510 32767 0 已排序的事务文件内容: 帐号 存取数量 修改要求 8 100 I 12 -25 U 14 38 U 18 -200 D 21 60 I 32767 0 U 新主文件内容: 帐号 余额 1 50 5 78 8 100 12 75 14 133 15 360 20 510 21 60 32767 0 · 456· 《数据结构》算法实现及解析(第二版) 附录 A 关于标准 C 程序 本书中的程序一般需经修改才能在标准 C 的环境下运行,主要有以下几个原因: (1) 教材的算法中采用了 C++语言的引用调用的参数传递方式。在形参表中,以&打 头的参数即为引用参数。 标准 C 不支持引用参数,对此需进行转换。下面以 bo1-1.cpp 和 bo1-1.c 中 DestroyTriplet()函数为例来说明这种转换。bo1-1.cpp 中含有引用参数的函数如下: Status DestroyTriplet(Triplet &T) { // 操作结果:三元组T被销毁 free(T); T=NULL; return OK; } 转换后在 bo1-1.c 中的标准 C 程序如下: Status DestroyTriplet(Triplet *T) /* 将&T改为*T */ { /* 操作结果:三元组T被销毁 */ free(*T); /* 将T改为*T */ *T=NULL; /* 将T改为*T */ return OK; } 对照以上 2 个函数可见:将 C++函数形参表中以&打头的参数改成以*打头的参数,再 在函数中该参数前加*即可。要注意的是,在这两个函数中,形参的类型是不同的。在 bo1-1.cpp 中,T 的类型是 Triplet;在 bo1-1.c 中,T 的类型是 Triplet 的指针。但为方便 起见,没有改写 C 程序中的注释。另外,在标准 C 程序中调用该函数,实参前应加&。如 main1-1.cpp 中调用 DestroyTriplet()的语句为 DestroyTriplet(T); 相应的标准 C 程序 main1-1.c 中调用 DestroyTriplet()的语句为(注意带下划线部分) DestroyTriplet(&T); 附录 A 关于标准 C 程序 · 457· 其中,在调用 DestroyTriplet()的两程序中,两实参 T 的类型是相同的。另外,在转换过程 中,遇到&*或*&可“抵消”,即将*&T 转换为 T。 (2) 标准 C 在指明所定义的结构体或枚举类型时,在类型前要加 strutc 或 enum,而 C++则不必加。如在 C++的 c2-1.h 中定义结构体 SqList 如下: struct SqList { ElemType *elem; // 存储空间基址 int length; // 当前长度 int listsize; // 当前分配的存储容量(以 sizeof(ElemType)为单位) }; C++在指明变量或形参的类型时,只需用 SqList 即可。如 bo2-1.cpp 中的一个函数如下: int ListLength(SqList L) { // 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 return L.length; } 如果在标准 C 程序中像 C++的 c2-1.h 那样定义变量 L 的类型,则在 bo2-1.c 中该函 数应如下(注意带下划线部分): int ListLength(struct SqList L) { /* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */ return L.length; } 说明变量 L 时要用 struct SqList。当用到某个结构体就要在其类型前加 struct,用到某 个枚举类型就要在其类型前加 enum 是很麻烦的。为了方便起见,可用 typedef 定义类型。 在标准 C 程序的 c2-1.h 中定义 SqList 如下(注意带下划线部分): typedef struct { ElemType *elem; /* 存储空间基址 */ int length; /* 当前长度 */ int listsize; /* 当前分配的存储容量(以sizeof(ElemType)为单位) */ }SqList; 这样,在 bo2-1.c 中相应的函数为 int ListLength(SqList L) { /* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */ return L.length; · 458· 《数据结构》算法实现及解析(第二版) } 这个函数与 bo2-1.cpp 很相似。可见,只要在定义结构体时使用 typedef,则在指明结 构体的类型时就不必加 struct。本书中的标准 C 都采用这种方法定义结构体。定义枚举类 型时使用 typedef 的方法与此类似。 (3) 标准 C 的共用体必须有变量名,而 C++可以省略。如在 c5-6.h(C++)中定义 GLNode1 如下: // c5-6.h 广义表的扩展线性链表存储结构 enum ElemTag{ATOM,LIST}; // ATOM==0:原子,LIST==1:子表 typedef struct GLNode1 { ElemTag tag; // 公共部分,用于区分原子结点和表结点 union // 原子结点和表结点的联合部分 { AtomType atom; // 原子结点的值域 GLNode1 *hp; // 表结点的表头指针 }; GLNode1 *tp; // 相当于线性链表的next,指向下一个元素结点 }*GList1,GLNode1; // 广义表类型GList1是一种扩展的线性链表 注意:其中的共用体没有变量名。在 bo5-6.cpp 中的函数 GListEmpty()如下: Status GListEmpty(GList1 L) { // 初始条件:广义表L存在。操作结果:判定广义表L是否为空 if(!L||L->tag==LIST&&!L->hp) return OK; else return ERROR; } 而标准 C 的 c5-6.h 如下(注意带下划线部分): /* c5-6.h 广义表的扩展线性链表存储结构 */ typedef enum{ATOM,LIST}ElemTag; /* ATOM==0:原子,LIST==1:子表 */ typedef struct GLNode1 { ElemTag tag; /* 公共部分,用于区分原子结点和表结点 */ union /* 原子结点和表结点的联合部分 */ { AtomType atom; /* 原子结点的值域 */ struct GLNode1 *hp; /* 表结点的表头指针 */ }a; struct GLNode1 *tp; /* 相当于线性链表的next,指向下一个元素结点 */ }*GList1,GLNode1; /* 广义表类型GList1是一种扩展的线性链表 */ 它内部的共用体必须有变量名。在 bo5-6.c 的函数 GListEmpty()中也变动如下(注意带下 附录 A 关于标准 C 程序 · 459· 划线部分): Status GListEmpty(GList1 L) { /* 初始条件:广义表L存在。操作结果:判定广义表L是否为空 */ if(!L||L->tag==LIST&&!L->a.hp) return OK; else return ERROR; } (4) C++可重载。即在一个程序中,可有几个同名的函数同时存在。只要它们的形参 个数或类型有所不同即可。标准 C 不可重载。在 C++转换成标准 C 时,必须将同名函数改 为不同名。如在 bo9-2.cpp 中有 1 个 SearchBST()函数(算法 9.5(b)),在 bo9-2.cpp 所包 含的文件 func9-1.cpp 中也有 1 个 SearchBST()函数(算法 9.5(a)),但这两个同名函数的 形参个数不同。C++根据函数的形参个数可分辨所调用的是哪个函数。而标准 C 没有这个 能力,所以把 bo9-2.c 中的 SearchBST()函数改为 SearchBST1()函数。 (5) C++允许在执行语句中变量使用之前定义变量。如 algo8-1.cpp 中的 PrintUser() 函数(注意带下划线部分): void PrintUser(Space p[]) { // 输出p数组所指的已分配空间 for(int i=0;isize,p[i]->tag); printf(" 块尾标志=%d\n",(FootLoc(p[i]))->tag); } } 这种形式在标准 C 语言中是不允许的。相应的 algo8-1.c 中的 PrintUser()函数为(注意 带下划线部分): void PrintUser(Space p[]) { /* 输出p数组所指的已分配空间 */ int i; for(i=0;isize,p[i]->tag); printf(" 块尾标志=%d\n",(FootLoc(p[i]))->tag); } } (6) C++允许在转换类型时采用函数的形式,如 algo8-2.cpp 中 AllocBuddy()函数的 · 460· 《数据结构》算法实现及解析(第二版) 一条语句如下(注意带下划线部分): pi=pa+int(pow(2,k-i)); 而在标准 C 程序 algo8-2.c 中必须改为以下形式(注意带下划线部分): pi=pa+(int)pow(2,k-i); (7) 在 C++中可用 new 申请空间,如 bo8-1.cpp 中主函数 main()的一条语句如下: p=new WORD[MAX+2]; // 申请大小为MAX*sizeof(WORD)个字节的空间 在标准 C 的 bo8-1.c 中要改为 p=(WORD*)malloc((MAX+2)*sizeof(WORD)); /* 申请大小为MAX*sizeof(WORD)个字节的空间 */ (8) 在 C++中可用 cout 和 cin 做输入输出语句,如 main1-1.cpp 中有 cout< TC VC 0 file(s) 0 bytes Directory of F:\BC CH1 CH2 CH3 CH4 CH5 CH6 CH7 CH8 CH9 CH10 CH11 CH12 0 file(s) 0 bytes Directory of F:\BC\CH1 文件名 扩展名 字节 章节 ALGO1-1 CPP 515 1.4.3 ALGO1-2 CPP 483 1.4.3 ALGO1-3 CPP 551 1.3 ALGO1-4 CPP 279 1.3 BO1-1 CPP 1,503 1.3 MAIN1-1 CPP 1,608 1.3 C1 H 660 1.3 C1-1 H 165 1.3 8 file(s) 5,764 bytes Directory of F:\BC\CH2 文件名 扩展名 字节 章节 ALGO2-1 CPP 1,292 2.2 ALGO2-2 CPP 1,576 2.2 ALGO2-3 CPP 1,775 2.2 ALGO2-4 CPP 1,704 2.2 ALGO2-5 CPP 2,107 2.3.1 ALGO2-6 CPP 6,154 2.3.1 ALGO2-7 CPP 900 2.3.1 ALGO2-8 CPP 1,825 2.3.1 ALGO2-9 CPP 1,399 2.3.1 ALGO2-10 CPP 835 2.3.2 ALGO2-11 CPP 2,402 2.3.3 ALGO2-12 CPP 1,326 2.3.1 ALGO2-13 CPP 1,706 2.3.1 BO2-1 CPP 4,114 2.2 BO2-2 CPP 4,203 2.3.1 BO2-31 CPP 4,014 2.3.1 BO2-32 CPP 4,249 2.3.1 BO2-4 CPP 4,243 2.3.2 BO2-5 CPP 4,835 2.3.3 BO2-6 CPP 5,997 2.3.3 BO2-7 CPP 4,523 2.4 BO2-8 CPP 3,171 2.3.1 BO2-9 CPP 1,026 2.3.1 FUNC2-1 CPP 1,695 2.3.1 FUNC2-2 CPP 615 2.3.1 FUNC2-3 CPP 482 2.2 MAIN2-1 CPP 3,259 2.2 MAIN2-2 CPP 2,021 2.3.1 MAIN2-31 CPP 1,958 2.3.1 MAIN2-32 CPP 2,367 2.3.1 MAIN2-4 CPP 1,278 2.3.2 MAIN2-5 CPP 1,519 2.3.3 MAIN2-6 CPP 2,961 2.3.3 MAIN2-7 CPP 988 2.4 MAIN2-8 CPP 2,003 2.3.1 C2-1 H 309 2.2 C2-2 H 150 2.3.1 C2-3 H 167 2.3.1 C2-4 H 135 2.3.3 C2-5 H 280 2.3.3 C2-6 H 233 2.4 TABLE TXT 108 2.3.1 41 file(s) 87,904 bytes Directory of F:\BC\CH3 文件名 扩展名 字节 章节 ALGO3-1 CPP 804 3.2.1 ALGO3-2 CPP 842 3.2.1 ALGO3-3 CPP 1,279 3.2.2 ALGO3-4 CPP 1,386 3.2.3 ALGO3-5 CPP 2,879 3.2.4 ALGO3-6 CPP 2,133 3.2.5 ALGO3-7 CPP 2,387 3.2.5 ALGO3-8 CPP 406 3.3 ALGO3-9 CPP 1,255 3.3 ALGO3-10 CPP 846 3.3 ALGO3-11 CPP 2,619 3.4.3 ALGO3-12 CPP 3,324 3.5 ALGO3-13 CPP 3,787 3.5 BO3-1 CPP 1,683 3.1.2 BO3-2 CPP 1,955 3.4.2 BO3-3 CPP 1,674 3.4.3 BO3-4 CPP 897 3.4.3 BO3-5 CPP 1,327 3.1.2 BO3-6 CPP 1,609 3.4.2 BO3-7 CPP 1,908 3.4.3 BO3-8 CPP 1,430 3.4.3 BO3-9 CPP 890 3.4.3 FUNC3-1 CPP 1,430 3.2.4 FUNC3-2 CPP 1,793 3.2.5 FUNC3-3 CPP 2,537 3.5 MAIN3-1 CPP 778 3.1.2 MAIN3-2 CPP 1,052 3.4.2 MAIN3-3 CPP 1,362 3.4.3 MAIN3-4 CPP 1,216 3.4.3 MAIN3-5 CPP 734 3.1.2 MAIN3-6 CPP 1,081 3.4.2 MAIN3-7 CPP 812 3.4.3 MAIN3-8 CPP 1,426 3.4.3 C3-1 H 314 3.1.2 C3-2 H 194 3.4.2 C3-3 H 271 3.4.3 C3-4 H 445 3.4.3 C3-5 H 380 3.4.3 A TXT 1,041 3.5 · 462· 《数据结构》算法实现及解析(第二版) B TXT 1,092 3.5 C TXT 940 3.5 D TXT 1,131 3.5 ED TXT 27 3.2.3 43 file(s) 57,376 bytes Directory of F:\BC\CH4 文件名 扩展名 字节 章节 ALGO4-1 CPP 2,079 4.3.2 ALGO4-2 CPP 5,971 4.4.1 ALGO4-3 CPP 6,212 4.4.2 ALGO4-4 CPP 3,250 4.4.2 BO4-1 CPP 4,307 4.2.1 BO4-2 CPP 4,815 4.2.2 BO4-3 CPP 6,583 4.2.3 MAIN4-1 CPP 1,918 4.2.1 MAIN4-2 CPP 1,565 4.2.2 MAIN4-3 CPP 2,101 4.2.3 C4-1 H 160 4.2.1 C4-2 H 139 4.2.2 C4-3 H 239 4.2.3 BOOKIDX TXT 196 4.4.2 BOOKINFO TXT 220 4.4.2 FILE TXT 28 4.4.1 NOIDX TXT 23 4.4.2 17 file(s) 39,806 bytes Directory of F:\BC\CH5 文件名 扩展名 字节 章节 ALGO5-1 CPP 1,435 5.3.2 ALGO5-2 CPP 959 5.2 BO5-1 CPP 2,097 5.2 BO5-2 CPP 5,245 5.3.2 BO5-3 CPP 6,131 5.3.2 BO5-4 CPP 10,679 5.3.2 BO5-5 CPP 4,144 5.7.3 BO5-6 CPP 4,350 5.7.3 FUNC5-1 CPP 1,205 5.7.3 MAIN5-1 CPP 1,245 5.2 MAIN5-2 CPP 1,000 5.3.2 MAIN5-3 CPP 1,098 5.3.2 MAIN5-4 CPP 1,327 5.3.2 MAIN5-5 CPP 1,449 5.7.3 MAIN5-6 CPP 1,478 5.7.3 C5-1 H 409 5.2 C5-2 H 310 5.3.2 C5-3 H 445 5.3.2 C5-4 H 381 5.3.2 C5-5 H 443 5.5 C5-6 H 436 5.5 21 file(s) 46,266 bytes Directory of F:\BC\CH6 文件名 扩展名 字节 章节 ALGO6-1 CPP 1,922 6.6.2 ALGO6-2 CPP 2,393 6.6.2 BO6-1 CPP 8,515 6.2.3 BO6-2 CPP 8,050 6.2.3 BO6-3 CPP 6,465 6.3.2 BO6-4 CPP 7,781 6.4.1 BO6-5 CPP 8,298 6.4.1 BO6-6 CPP 7,124 6.2.3 FUNC6-1 CPP 668 6.6.2 FUNC6-2 CPP 352 6.4.1 FUNC6-3 CPP 1,235 6.2.3 MAIN6-1 CPP 1,973 6.2.3 MAIN6-2 CPP 3,479 6.2.3 MAIN6-3 CPP 1,816 6.3.2 MAIN6-4 CPP 1,211 6.4.1 MAIN6-5 CPP 1,407 6.4.1 MAIN6-6 CPP 3,264 6.2.3 C6-1 H 236 6.2.3 C6-2 H 151 6.2.3 C6-3 H 277 6.3.2 C6-4 H 212 6.4.1 C6-5 H 149 6.4.1 C6-6 H 169 6.2.3 C6-7 H 242 6.6.2 24 file(s) 67,389 bytes Directory of F:\BC\CH7 文件名 扩展名 字节 章节 ALGO7-1 CPP 2,517 7.4.1 ALGO7-2 CPP 1,876 7.4.3 ALGO7-3 CPP 2,962 7.4.4 ALGO7-4 CPP 1,806 7.5.1 ALGO7-5 CPP 4,026 7.5.2 ALGO7-6 CPP 2,768 7.6.1 ALGO7-7 CPP 1,518 7.6.2 ALGO7-8 CPP 1,346 7.4.3 ALGO7-9 CPP 2,470 7.6.2 ALGO7-10 CPP 1,021 7.3.2 ALGO7-11 CPP 841 7.3.2 BO7-1 CPP 15,915 7.2.1 BO7-2 CPP 13,970 7.2.2 BO7-3 CPP 10,295 7.2.3 BO7-4 CPP 11,524 7.2.4 FUNC7-1 CPP 385 7.5.1 FUNC7-2 CPP 1,054 7.6.2 MAIN7-1 CPP 1,414 7.2.1 MAIN7-2 CPP 1,462 7.2.2 MAIN7-3 CPP 1,244 7.2.3 MAIN7-4 CPP 1,168 7.2.4 C7-1 H 610 7.2.1 C7-2 H 588 7.2.2 C7-21 H 949 7.2.2 C7-3 H 531 7.2.3 C7-31 H 1,059 7.2.3 C7-4 H 530 7.2.4 F7-1 TXT 101 7.3.2 F7-2 TXT 101 7.3.2 MAP TXT 641 7.6.2 30 file(s) 86,692 bytes Directory of F:\BC\CH8 文件名 扩展名 字节 章节 ALGO8-1 CPP 7,504 8.3.1 ALGO8-2 CPP 5,423 8.4.2 ALGO8-3 CPP 1,963 8.5 C8-1 H 533 8.3.1 C8-2 H 509 8.4.1 C8-3 H 503 8.5 6 file(s) 16,435 bytes Directory of F:\BC\CH9 文件名 扩展名 字节 章节 ALGO9-1 CPP 1,636 9.1.1 ALGO9-2 CPP 871 9.1.2 ALGO9-3 CPP 2,658 9.1.3 ALGO9-4 CPP 972 9.2.1 ALGO9-5 CPP 967 9.2.1 ALGO9-6 CPP 1,149 9.2.2 ALGO9-7 CPP 1,311 9.2.3 ALGO9-8 CPP 1,382 9.2.3 ALGO9-9 CPP 1,411 9.3.4 BO9-1 CPP 2,397 9.1.1 BO9-2 CPP 2,640 9.2.1 BO9-3 CPP 5,040 9.2.1 BO9-4 CPP 4,054 9.2.2 BO9-5 CPP 4,205 9.2.3 BO9-6 CPP 3,209 9.2.3 BO9-7 CPP 3,580 9.3.4 C9-1 H 168 9.1.1 C9-2 H 174 9.2.1 C9-3 H 644 9.2.2 C9-4 H 599 9.2.3 C9-5 H 718 9.2.3 C9-6 H 380 9.3.4 C9-7 H 141 9.1.1 C9-8 H 167 9.2.3 FUNC9-1 CPP 783 9.2.1 25 file(s) 41,256 bytes 附录 B 光盘文件目录 · 463· Directory of F:\BC\CH10 文件名 扩展名 字节 章节 ALGO10-1 CPP 818 10.2.1 ALGO10-2 CPP 2,887 10.2.2 ALGO10-3 CPP 1,608 10.2.3 ALGO10-4 CPP 743 10.3 ALGO10-5 CPP 1,142 10.3 ALGO10-6 CPP 1,137 10.3 ALGO10-7 CPP 1,225 10.4.1 ALGO10-8 CPP 1,627 10.4.2 ALGO10-9 CPP 1,580 10.4.3 ALG10-10 CPP 1,640 10.5 ALG10-11 CPP 4,621 10.6.2 BO10-1 CPP 2,143 10.2.1 BO10-2 CPP 644 10.3 C10-1 H 388 10.1 C10-2 H 449 10.2.2 C10-3 H 541 10.6.2 16 file(s) 23,193 bytes Directory of F:\BC\CH11 文件名 扩展名 字节 章节 ALGO11-1 CPP 3,123 11.3 ALGO11-2 CPP 5,205 11.4 ALGO11-3 CPP 1,597 11.4 ALGO11-4 CPP 74 11 BO11-1 CPP 2,172 11.3 5 file(s) 12,171 bytes Directory of F:\BC\CH12 文件名 扩展名 字节 章节 ALGO12-1 CPP 4,226 12.2 1 file(s) 4,226 bytes Directory of F:\TC CH1 CH2 CH3 CH4 CH5 CH6 CH7 CH8 CH9 CH10 CH11 CH12 0 file(s) 0 bytes Directory of F:\TC\CH1 ALGO1-1 C 526 ALGO1-2 C 504 ALGO1-3 C 583 ALGO1-4 C 282 BO1-1 C 1,549 MAIN1-1 C 1,604 C1 H 662 C1-1 H 174 8 file(s) 5,884 bytes Directory of F:\TC\CH2 ALGO2-1 C 1,398 ALGO2-2 C 1,631 ALGO2-3 C 1,862 ALGO2-4 C 1,790 ALGO2-5 C 2,220 ALGO2-6 C 6,772 ALGO2-7 C 941 ALGO2-8 C 1,889 ALGO2-9 C 1,437 ALGO2-10 C 860 ALGO2-11 C 2,524 ALGO2-12 C 1,365 ALGO2-13 C 1,759 BO2-1 C 4,347 BO2-2 C 4,385 BO2-31 C 4,185 BO2-32 C 4,423 BO2-4 C 4,476 BO2-5 C 5,005 BO2-6 C 6,319 BO2-7 C 4,708 BO2-8 C 3,305 BO2-9 C 1,064 FUNC2-1 C 1,764 FUNC2-2 C 640 FUNC2-3 C 491 MAIN2-1 C 3,332 MAIN2-2 C 2,057 MAIN2-31 C 1,989 MAIN2-32 C 2,407 MAIN2-4 C 1,299 MAIN2-5 C 1,551 MAIN2-6 C 3,077 MAIN2-7 C 1,000 MAIN2-8 C 2,040 C2-1 H 335 C2-2 H 179 C2-3 H 173 C2-4 H 145 C2-5 H 324 C2-6 H 249 TABLE TXT 69 42 file(s) 91,786 bytes Directory of F:\TC\CH3 ALGO3-1 C 850 ALGO3-2 C 881 ALGO3-3 C 1,319 ALGO3-4 C 1,435 ALGO3-5 C 3,033 ALGO3-6 C 2,244 ALGO3-7 C 2,419 ALGO3-8 C 416 ALGO3-9 C 1,309 ALGO3-10 C 877 ALGO3-11 C 2,741 ALGO3-12 C 3,459 ALGO3-13 C 3,944 BO3-1 C 1,811 BO3-2 C 2,092 BO3-3 C 1,788 BO3-4 C 971 BO3-5 C 1,374 BO3-6 C 1,704 BO3-7 C 2,057 BO3-8 C 1,567 BO3-9 C 954 FUNC3-1 C 1,483 FUNC3-2 C 1,821 FUNC3-3 C 2,633 MAIN3-1 C 787 MAIN3-2 C 1,064 MAIN3-3 C 1,370 MAIN3-4 C 1,225 MAIN3-5 C 748 MAIN3-6 C 1,091 MAIN3-7 C 821 MAIN3-8 C 1,435 C3-1 H 351 C3-2 H 220 C3-3 H 294 C3-4 H 474 C3-5 H 405 A TXT 1,041 B TXT 1,092 C TXT 940 D TXT 1,131 ED TXT 27 43 file(s) 59,698 bytes Directory of F:\TC\CH4 ALGO4-1 C 2,101 · 464· 《数据结构》算法实现及解析(第二版) ALGO4-2 C 6,076 ALGO4-3 C 6,522 ALGO4-4 C 3,407 BO4-1 C 4,418 BO4-2 C 5,112 BO4-3 C 6,884 MAIN4-1 C 1,920 MAIN4-2 C 1,584 MAIN4-3 C 2,127 C4-1 H 170 C4-2 H 156 C4-3 H 278 BOOKIDX TXT 200 BOOKINFO TXT 223 FILE TXT 28 NOIDX TXT 23 17 file(s) 41,229 bytes Directory of F:\TC\CH5 ALGO5-1 C 1,493 ALGO5-2 C 990 BO5-1 C 2,213 BO5-2 C 5,328 BO5-3 C 6,565 BO5-4 C 11,420 BO5-5 C 4,454 BO5-6 C 4,675 FUNC5-1 C 1,270 MAIN5-1 C 1,268 MAIN5-2 C 1,007 MAIN5-3 C 1,105 MAIN5-4 C 1,351 MAIN5-5 C 1,463 MAIN5-6 C 1,499 C5-1 H 440 C5-2 H 343 C5-3 H 483 C5-4 H 406 C5-5 H 480 C5-6 H 483 21 file(s) 48,736 bytes Directory of F:\TC\CH6 ALGO6-1 C 2,011 ALGO6-2 C 2,514 BO6-1 C 8,872 BO6-2 C 8,499 BO6-3 C 6,948 BO6-4 C 8,238 BO6-5 C 8,698 BO6-6 C 7,500 FUNC6-1 C 692 FUNC6-2 C 361 FUNC6-3 C 1,312 MAIN6-1 C 2,095 MAIN6-2 C 3,509 MAIN6-3 C 1,876 MAIN6-4 C 1,218 MAIN6-5 C 1,415 MAIN6-6 C 3,291 C6-1 H 255 C6-2 H 164 C6-3 H 289 C6-4 H 237 C6-5 H 159 C6-6 H 182 C6-7 H 251 24 file(s) 70,586 bytes Directory of F:\TC\CH7 ALGO7-1 C 2,880 ALGO7-2 C 1,913 ALGO7-3 C 3,075 ALGO7-4 C 1,889 ALGO7-5 C 4,165 ALGO7-6 C 2,858 ALGO7-7 C 1,554 ALGO7-8 C 1,434 ALGO7-9 C 2,532 ALGO7-10 C 1,000 ALGO7-11 C 867 BO7-1 C 16,862 BO7-2 C 14,746 BO7-3 C 10,728 BO7-4 C 11,968 FUNC7-1 C 388 FUNC7-2 C 1,082 MAIN7-1 C 1,413 MAIN7-2 C 1,484 MAIN7-3 C 1,256 MAIN7-4 C 1,182 C7-1 H 658 C7-2 H 659 C7-21 H 1,043 C7-3 H 594 C7-31 H 1,169 C7-4 H 592 F7-1 TXT 101 F7-2 TXT 101 MAP TXT 641 30 file(s) 90,834 bytes Directory of F:\TC\CH8 ALGO8-1 C 7,984 ALGO8-2 C 5,724 ALGO8-3 C 2,115 C8-1 H 589 C8-2 H 561 C8-3 H 543 6 file(s) 17,516 bytes Directory of F:\TC\CH9 ALGO9-1 C 1,702 ALGO9-2 C 905 ALGO9-3 C 2,780 ALGO9-4 C 1,000 ALGO9-5 C 1,004 ALGO9-6 C 1,162 ALGO9-7 C 1,333 ALGO9-8 C 1,410 ALGO9-9 C 1,455 BO9-1 C 2,517 BO9-2 C 2,796 BO9-3 C 5,302 BO9-4 C 4,255 BO9-5 C 4,416 BO9-6 C 3,415 BO9-7 C 3,777 FUNC9-1 C 804 C9-1 H 184 C9-2 H 190 C9-3 H 721 C9-4 H 675 C9-5 H 790 C9-6 H 405 C9-7 H 144 C9-8 H 170 25 file(s) 43,312 bytes Directory of F:\TC\CH10 ALGO10-1 C 827 ALGO10-2 C 3,047 ALGO10-3 C 1,685 ALGO10-4 C 747 ALGO10-5 C 1,199 ALGO10-6 C 1,201 ALGO10-7 C 1,267 ALGO10-8 C 1,674 ALGO10-9 C 1,664 ALG10-10 C 1,684 ALG10-11 C 4,847 BO10-1 C 2,323 BO10-2 C 666 附录 B 光盘文件目录 · 465· C10-1 H 429 C10-2 H 508 C10-3 H 588 16 file(s) 24,356 bytes Directory of F:\TC\CH11 ALGO11-1 C 3,231 ALGO11-2 C 5,416 ALGO11-3 C 1,656 ALGO11-4 C 76 BO11-1 C 2,247 5 file(s) 12,626 bytes Directory of F:\TC\CH12 ALGO12-1 C 4,398 1 file(s) 4,398 byte Directory of F:\Vc SHORTEST 0 file(s) 0 bytes Directory of F:\Vc\shortest DEBUG RES MAPVC TXT 840 SHORTEST CPP 2,091 SHORTE~1 CPP 11,452 STDAFX CPP 210 SHORTEST DSW 539 RESOURCE H 686 SHORTE~1 H 2,355 STDAFX H 1,054 SHORTEST H 1,346 SHORTEST RC 5,210 SHORTEST DSP 4,195 11 file(s) 29,978 bytes Directory of F:\Vc\shortest\Debug SHORTEST EXE 114,753 1 file(s) 114,753 bytes Directory of F:\Vc\shortest\res SHORTEST RC2 400 SHORTEST ICO 1,078 2 file(s) 1,478 bytes 注:光盘中文件的属性为“只读”。如要修改文件,可将文件拷贝到磁盘中,并将文件的“只读”属 性去掉。
还剩475页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

keymon

贡献于2016-03-09

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf