├── Chapter1 Abstract └── README.md ├── Chapter2 LinearList ├── README.md ├── 单链表、循环链表和双向链表的时间效率比较.png └── 顺序表和链表的比较.png ├── Chapter3 StackAndQueue ├── Chapter3Exe │ ├── AlgoDesignExe1.cpp │ ├── AlgoDesignExe10.cpp │ ├── AlgoDesignExe2.cpp │ ├── AlgoDesignExe3.cpp │ ├── AlgoDesignExe5.cpp │ ├── AlgoDesignExe6.cpp │ ├── AlgoDesignExe7.cpp │ ├── AlgoDesignExe8.cpp │ ├── AlgoDesignExe9.cpp │ ├── AlgoDesignQueue.h │ ├── AlgoDesignStack.cpp │ ├── AlgoDesignStack.h │ └── 双栈结构的表示.png ├── README.md ├── 括号匹配.png ├── 栈的操作.png └── 进制转换.png ├── Chapter4 String ├── Chapter4Exe │ ├── AlgoDesignExe1.cpp │ ├── AlgoDesignExe2.cpp │ ├── AlgoDesignExe3.cpp │ ├── AlgoDesignExe4.cpp │ ├── AlgoDesignExe5.cpp │ └── AlgoDesignExe6.cpp ├── README.md ├── next[j].png ├── n维数组.png └── 串值的链表存储方式.png ├── Chapter5 TreeAndBianryTree ├── Chapter5Exe │ ├── AlgoDesignExe1.cpp │ ├── AlgoDesignExe2.cpp │ ├── AlgoDesignExe3.cpp │ ├── AlgoDesignExe4.cpp │ ├── AlgoDesignExe5.cpp │ ├── AlgoDesignExe6.cpp │ ├── AlgoDesignExe7.cpp │ └── AlgoDesignExe8.cpp ├── README.md ├── 二叉树的五种基本形态.png ├── 二叉树的五种形态.png ├── 先序线索二叉树.png ├── 后序线索二叉树.png ├── 树的两种形态.png ├── 树的双亲表示法.png ├── 树结构和线性结构的比较.png ├── 树结构示例.png └── 遍历方法区别.png ├── Chapter6 Graph ├── README.md └── 图的存储结构分析.png ├── Chapter7 Search ├── LL型调整前状态.png ├── LL型调整后结果.png ├── LR型调整前状态.png ├── LR型调整后结果.png ├── README.md ├── RR型调整前状态.png ├── RR型调整后结果.png ├── 图10:LR型调整前-后对比示意图.png ├── 图11:LR型调整示例.png ├── 图12:RL型调整前状态.png ├── 图13:RL型调整示例.png ├── 图14:散列表查找流程图.png ├── 图1:平均查找长度定义.png ├── 图2:查找效率.png ├── 图3:查找方法比较.png ├── 图4:平衡调整的四种类型.png ├── 图5:平衡调整的四种类型_调整后.png ├── 图6:LL型调整前-后对比.png ├── 图7:LL型调整示例.png ├── 图8:RR型调整前-后对比示意图.png ├── 图9:RR型调整示例.png └── 数据结构.png ├── Chapter8 Sorting ├── README.md ├── 图01:排序方法的分类.png ├── 图02:主要学习内容.png └── 图03:排序方法比较.png └── README.md /Chapter1 Abstract/README.md: -------------------------------------------------------------------------------- 1 | # 第一章 数据结构绪论 2 | 3 | ## 1.1 数据结构的研究内容 4 | 5 | 计算机进行数值计算式,首先从具体问题抽象出数学模型,然后设计一个解此数学模型的算法,最后编写程序,进行测试、调试,直到解决问题。 6 | 7 | **数据结构**是一门研究**非数值计算**的程序设计中计算机的操作对象(表、树、图等)以及它们之间关系和操作的学科。 8 | 9 | ## 1.2 基本概念和术语 10 | 11 | ### 1.2.1 数据、数据元素、数据项和数据对象 12 | 13 | **数据**(**Data**):是客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。 14 | 15 | **数据元素**(**Data Element**):是数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。在有些情况下,数据元素也称为元素、记录、结点或顶点等。 16 | 17 | **数据项**(**Data Item**):是组成数据元素的、有独立含义的、不可分割的最小单位。 18 | 19 | **数据对象**(**Data Object**):是性质相同的数据元素的集合,是数据的一个子集。不论数据元素集合是无限集(如整数集),或是有限集(如字母字符集),还是由多个数据项组成的复合数据元素(如学生表)的集合,只要集合内元素的性质均相同,都可称之为一个数据对象。 20 | 21 | > 数据元素与数据对象: 22 | 23 | 数据元素——组成数据的基本单位,与数据的关系是:集合的个体。 24 | 25 | 数据对象——性质相同的数据元素的集合,与数据的关系是:集合的子集。 26 | 27 | ### 1.2.2 数据结构 28 | 29 | **数据结构**(**Data Structure**)是**相互之间存在一种或多种特定关系**的数据元素的集合。 30 | 31 | 换而言之,数据元素之间不是孤立存在的,它们之间存在某种关系,**数据元素相互之间的关系称为结构**(**structure**)。 32 | 33 | 数据结构是带结构的数据元素的集合。 34 | 35 | > 数据结构包括以下三个方面的内容: 36 | 37 | 1. 数据元素之间的逻辑关系,也称为**逻辑结构**。 38 | 39 | 2. 数据元素及其关系在计算机构中的表示(又称为映像),称为数据的**物理结构**或数据的**存储结构**。 40 | 41 | 3. 数据的**运算和实现**,即对数据元素可以施加的操作以及这些操作在相应的存储结构上的实现。 42 | 43 | > 数据结构的两个层次: 44 | 45 | - 逻辑结构 46 | 47 | - 描述描述数据元素之间的逻辑关系 48 | - 与数据的存储无关,独立于计算机 49 | - 是从具体问题抽象出来的数学模型 50 | 51 | - 物理结构(存储结构) 52 | 53 | - 数据元素及其关系在计算机存储器中的结构(存储方式) 54 | - 是数据结构在许算机中的表示 55 | 56 | - 逻辑结构与存储结构的关系 57 | 58 | - 存储结构是逻辑关系的映象与元素本身的映象 59 | - 逻辑结构是数据结构的抽象,存储结构是数据结构的实现 60 | - 两者综合起来建立了数据元素之间的结构关系。 61 | 62 | > 逻辑结构的种类 63 | 64 | 1. 划分方法一 65 | 66 | 1. 线性结构:有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前趋和一个直接后继。例如:线性表、栈、队列、串。 67 | 68 | 2. 非线性结构:一个结点可能有多个直接前趋和直接后继例如:树、图。 69 | 70 | 2. 划分方法二——四类基本逻辑结构 71 | 72 | 1. 集合结构:结构中的数据元素之间除了同属于一个集合的关系外,无任何其它关系。 73 | 74 | 2. 线性结构:结构中的数据元素之间存在着一对一的线性关系。 75 | 76 | 3. 树形结构:结构中的数据元素之间存在着一对多的层次关系。 77 | 78 | 4. 图状结构或网状结构:结构中的数据元素之间存在着多对多的任意关系。 79 | 80 | > 存储结构的种类 81 | 82 | 四种基本的存储结构:顺序存储结构、链式存储结构、索引存储结构、散列存储结构。 83 | 84 | - 顺序存储结构 85 | 86 | - 用一组**连续**的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的**存储位置**来表示。 87 | 88 | - C语言中用数组来实现顺序存储结构 89 | 90 | - 链式存储结构 91 | 92 | - 用一组**任意**的存储单元存储数据元素,数据元素之间的逻辑关系用**指针**来表示。 93 | 94 | - C语言中用指针来实现链式存储结构 95 | 96 | - 存储一个元素的同时,还存储了下一个元素的地址。 97 | 98 | - 索引存储结构 99 | 100 | - 在存储节点信息的同时,还建立附加的**索引表**。 101 | 102 | - 散列存储结构 103 | 104 | - 根据结点的关键字直接计算处该结点的存储地址。 105 | 106 | ### 1.2.3 数据类型和抽象数据类型 107 | 108 | 高级语言中的数据类型明显地或隐含地规定了在程序执行期间变量和表达的所有可能的**取值范围**,以及在这些数值范围上所允许进行的**操作**。 109 | 110 | > 数据类型(Data Type) 111 | 112 | 数据类型是一组性质相同的**值的集合**以及定义于这个值集合上的**一组操作**的总称。 113 | 114 | 数据类型 = 值的集合 + 值集合上的一组操作 115 | 116 | > 抽象数据类型(Abstract Data Type, ADT) 117 | 118 | **指一个数学模型以及定义在此数学模型上的一组操作。** 119 | 120 | - 由用户定义,从问题抽象出的**数据模型**(逻辑结构) 121 | 122 | - 还包括定义在数据模型上的一组**抽象运算**(相关操作) 123 | 124 | - 不考虑计算机内的具体存储结构与运算的具体实现算法 125 | 126 | > 抽象数据类型的形式定义 127 | 128 | 抽象数据类型可用(D, S, P)三元组表示: 129 | 130 | - D是数据对象 131 | 132 | - S是D上的关系集 133 | 134 | - P是对D的基本操作 135 | 136 | > 抽象数据类型(ADT)定义举例:Circle的定义 137 | 138 | 模板: 139 | 140 | ```C++ 141 | ADT抽象数据类型名{ 142 | Data 143 | 数据对象的定义 144 | 数据元素之间的逻辑关系定义 145 | Operation 146 | 操作1 147 | 初始条件 148 | 操作结果描述 149 | 操作2 150 | ...... 151 | 操作n 152 | ...... 153 | }ADT 抽象数据类型名 154 | ``` 155 | 156 | ```C++ 157 | ADT Circle{ 158 | 数据对象:D={r, x, y|r, x, y均为实数} 159 | 数据关系:R={|r是半径, 是圆心坐标} 160 | 基本操作: 161 | Circle(&C, r, x, y) 162 | 操作结果:构造一个圆 163 | double Area(C) 164 | 初始条件:圆已存在 165 | 操作结果:计算面积 166 | }ADT Circle 167 | ``` 168 | 169 | ## 1.3 抽象数据类型的表示与实现 170 | 171 | C语言实现抽象数据类型: 172 | 173 | - 用已有数据类型定义描述它的存储结构 174 | 175 | - 用函数定义描述它的操作 176 | 177 | > 抽象数据类型如何实现 178 | 179 | - 抽象数据类型可以通过固有的数据类型(如整型、实型、字符型)来表示和实现。 180 | 181 | - 即利用处理器中已存在的数据类型来说明新的结构,用已实现的操作来组合新的操作。 182 | 183 | ## 1.4 算法和算法分析 184 | 185 | > 算法的定义 186 | 187 | 算法:对特定问题**求解方法和步骤**的一种描述,它是指令的有限序列。其中每个指令表示一个或多个操作。 188 | 189 | > 算法的描述 190 | 191 | - 自然语言 192 | 193 | - 流程图 194 | 195 | - 伪代码、类语言 196 | 197 | - 程序代码 198 | 199 | > 算法与程序 200 | 201 | 程序 = 数据结构 + 算法 202 | 203 | - 数据结构通过算法实现操作 204 | 205 | - 算法根据数据结构设计程序 206 | 207 | > 算法特性 208 | 209 | - 有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。 210 | 211 | - 确定性:对于每种情况下所应执行的操作,在算法中都有确切的规定,不会产生二义性,使算法的执行者或阅读者都能明确其含义及如何执行。 212 | 213 | - 可行性:算法中的所有操作都可以通过已经实现的基本操作运算执行有限次来实现。 214 | 215 | - 输入:一个算法有零个或多个输入。当用函数描述算法时,输入往往是通过形参表示的,在它们被调用时,从主调函数获得输入值。 216 | 217 | - 输出。一个算法有一个或多个输出,它们是算法进行信息加工后得到的结果,无输出的算法没有任何意义。当用函数描述算法时,输出多用返回值或引用类型的形参表示。 218 | 219 | > 算法设计的要求 220 | 221 | - 正确性。在合理的数据输入下,能够在有限的运行时间内得到正确的结果。 222 | 223 | - 可读性。一个好的算法,首先应便于人们理解和相互交流,其次才是机器可执行性。可读性强的算法有助于人们对算法的理解,而难懂的算法易于隐藏错误,且难于调试和修改。 224 | 225 | - 健壮性。当输入的数据非法时,好的算法能适当地做出正确反应或进行相应处理,而不会产生一些莫名其妙的输出结果。 226 | 227 | - 高效性。高效性包括时间和空间两个方面。时间高效是指算法设计合理,执行效率高,可以用时间复杂度来度量;空间高效是指算法占用存储容量合理,可以用空间复杂度来度量。时间复杂度和空间复杂度是衡量算法的两个主要指标。 228 | 229 | > 算法效率 230 | 231 | - 时间效率:指的是算法所耗费的时间; 232 | 233 | - 空间效率:指的是算法执行过程中所耗费的存储空间。 234 | 235 | 时间效率和空间效率有时候是矛盾的。 236 | 237 | > 算法事件效率的度量 238 | 239 | - 算法时间效率可以用依据该算法编制的程序在计算机上执行所消耗的时间来度量。 240 | 241 | - 两种度量方法 242 | 243 | - 事后统计 244 | 245 | - 事前分析 246 | 247 | > 事前分析算法 248 | 249 | 一个算法的运行时间是指一个算法在计算机上运行所耗费的时间大致可以等于计算机执行一种简单的操作(如赋值、比较、移动等)所需的**时间**与算法中进行的简单操作**次数乘积**。 250 | 251 | 算法运行时间 = 一个简单操作所需的时间x简单操作次数 252 | 253 | > 算法时间复杂度的渐进表示法 254 | 255 | 若有某个辅助函数f(n),使得n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为**算法的渐进时间复杂度**(O是数量级的符号),简称**时间复杂度**。 256 | 257 | > 分析算法时间复杂度的基本方法 258 | 259 | 1. 找出**语句频度最大**的那条语句作为**基本语句** 260 | 261 | 2. 计算**基本语句**的频度得到问题规模n的某个函数f(n) 262 | 263 | 3. 取其数量级用符号“O”表示 264 | 265 | 有的情况下,算法中基本操作重复执行的次数还随问题的**输入数据集**不同而不同。 266 | 267 | > 算法时间复杂度 268 | 269 | - 最坏时间复杂度:指在最坏情况下,算法的时间复杂度。 270 | 271 | - 平均时间复杂度:指在所有可能输入实例在等概率出现的情况下,算法的期望运行时间。 272 | 273 | - 最好时间复杂度:指在最好情况下,算法的时间复杂度。 274 | 275 | 对于复杂的算法,可以将它分成几个容易估算的部分,然后利用大O加法法则和乘法法则,计算算法的时间复杂度。 276 | 277 | > 算法时间效率的比较 278 | 279 | - 当n取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊 280 | 281 | - 时间复杂度按数量级递增顺序为: 282 | 283 | - 常数阶->对数阶->线性阶->线性对数阶->平方阶->立方阶->...->K次方阶->指数阶 284 | 285 | - O(1)->O(logn)->O(n)->O(nlogn)->O(n^2)->O(n^3)->...->O(n^K)->O(2^n) 286 | 287 | > 渐进空间复杂度 288 | 289 | 空间复杂度:算法所需要的存储空间的度量。 290 | 291 | 算法要占据的空间: 292 | 293 | - 算法本身要占据的空间,输入/输出,指令,常数,变量等 294 | 295 | - 算法要使用的辅助空间 296 | 297 | ## 1.5 总结 298 | 299 | > 设计好算法的过程 300 | 301 | 抽象数据类型 = 数据的逻辑结构 + 抽象运算 302 | -------------------------------------------------------------------------------- /Chapter2 LinearList/README.md: -------------------------------------------------------------------------------- 1 | # 第二章 线性表 2 | 3 | ## 2.1 线性表的定义和特点 4 | 5 | **线性表是具有相同特性的数据元素的一个有限序列**。 6 | 7 | > 线性表(Linear List): 8 | 9 | 由n(n ≥ 0)个数据元素(结点)a1, a2, ... an组成的**有限序列**。 10 | 11 | - 其中数据元素的个数n定义为表的**长度** 12 | 13 | - 当n=0时称为**空表** 14 | 15 | - 将非空的线性表(n>0)记作:(a1, az, ... an) 16 | 17 | - 这里的数据元素ai(1≤i≤n)只是一个抽象的符号,其具体含义在不同的情况下可以不同。 18 | 19 | > 线性表的逻辑特征 20 | 21 | - 在非空的线性表,有且仅有一个开始结点a1,它没有直接前趋,而仅有一个直接后继a2; 22 | 23 | - 有且仅有一个终端结点an,它没有直接后继,而仅有一个直接前趋an-1; 24 | 25 | - 其余的内部结点ai(2≤i≤n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1。 26 | 27 | ## 2.2 案例引入 28 | 29 | > 顺序存储结构存在问题 30 | 31 | - 存储空询分配不灵活 32 | 33 | - 运算的空间复杂度高 34 | 35 | > 总结 36 | 37 | - 线性表中的数据元素的类型可以为**简单类型**,也可以为**复杂类型** 38 | 39 | - 许多实际应用问题所涉的基本操作有很大相似性,不应为每个具体应用单独编写一个程序 40 | 41 | - 从具体应用中抽象出共性的**逻辑结构和基本操作**(抽象数据类型),然后实现其**存储结构和基本操作** 42 | 43 | ## 2.3 线性表的定义 44 | 45 | > 基本操作 46 | 47 | - InitList(&L) 48 | 49 | - 操作结果:构造一个空的线性表L 50 | 51 | - DestoryList(&L) 52 | 53 | - 初始条件:线性表L已存在 54 | 55 | - 操作结果:销毁线性表L 56 | 57 | - ClearList(&L) 58 | 59 | - 初始条件:线性表L已存在 60 | 61 | - 操作结果:将线性表L重置为空表 62 | 63 | - ListEmpty(L) 64 | 65 | - 初始条件:线性表L已存在 66 | 67 | - 操作结果:若线性表L空表则返回TURE;否则返回FALSE。 68 | 69 | - ListLength(L) 70 | 71 | - 初始条件:线性表L已存在 72 | 73 | - 操作结果:返回线性表L中的数据元素个数。 74 | 75 | - GetElem(L, i, &e) 76 | 77 | - 初始条件:线性表L已存在,1 <= i <= ListLength(L) 78 | 79 | - 操作结果:用e返回线性表L中第个数据元素的值。 80 | 81 | - LocateElem(L, e, compare()) 82 | 83 | - 初始条件:线性表L已存在,compare()是数据元素判定函数 84 | 85 | - 操作结果:返回L中第1个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值为0。 86 | 87 | - PriorElem(L, cur_e, &pre_e) 88 | 89 | - 初始条件:线性表L已存在 90 | 91 | - 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败;pre_e无意义。 92 | 93 | - NextElem(L, cur_e, &next_e) 94 | 95 | - 初始条件:线性表L已存在 96 | 97 | - 操作结果:若cur_e是L的数据元素,且不是第最后个,则用next_e返回它的后继,否则操作失败,next_e无意义。 98 | 99 | - ListInsert(&L, i, e) 100 | 101 | - 初始条件:线性表L已存在,1 <= i <= ListLength(L)+1 102 | 103 | - 操作结果:在L的第i个位置**之前**插入新的数据元素e,L的长度加一。 104 | 105 | - ListDelete(&L, i, &e) 106 | 107 | - 初始条件:线性表L已存在,1 <= i <= ListLength(L) 108 | 109 | - 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一。 110 | 111 | - ListTraverse(&L, visited()) // 遍历 112 | 113 | - 初始条件:线性表L已存在 114 | 115 | - 操作结果:依次对线性表中每个元素调用visited()。 116 | 117 | ## 2.4 线性表的顺序表示和实现 118 | 119 | ### 2.4.1 线性表的顺序存储表示 120 | 121 | 线性表的顺序表示又称为**顺序存储结构**或**顺序映像**。 122 | 123 | 顺序存储定义:把**逻辑上相邻的数据元素**存储在**物理上相邻的存储单元**中的存储结构。 124 | 125 | 线形表顺序存储结构占用**一片续的存储空间**。知道某个元素的存储位置就可以计算其他元素的存储位置。 126 | 127 | > 数组静态分配 128 | 129 | ```C++ 130 | typedef struct{ 131 | ElemType data[MaxSize]; 132 | int length; 133 | } SqList; // 顺序表类型 134 | ``` 135 | 136 | 数组存放的是第一个元素的地址,因此也可以写成以下动态分配形式,用指针代替第一个元素地址。 137 | 138 | > 数组动态分配 139 | 140 | ```C++ 141 | typedef struct{ 142 | ElemType *data; 143 | int length; 144 | } SqList; // 顺序表类型 145 | ``` 146 | 147 | ```C++ 148 | SqList L; 149 | L.data = (ElemType*)malloc(sizeof(ElemType) * MaxSize); 150 | ``` 151 | 152 | 在头文件stdlib.h中: 153 | 154 | malloc(m)函数,开辟m字节长度的地址空间,并返回这段空间的首地址。 155 | 156 | free(p)函数,释放指针p所指变量的存储空间,即彻底删除一个变量。 157 | 158 | ### 2.4.2 顺序表基本操作的实现 159 | 160 | > 线性表L的初始化 161 | 162 | ```C++ 163 | Status InitList_Sq(SqList&L){// 构造一个空的顺序表L 164 | L.elem=new ElemType[MAXSIZE]; // 为顺序表分配空间 165 | if(!L.elem) exit(OVERFLOW); // 存储分配失败 166 | L.length = 0; // 空表长度为0 167 | return OK; 168 | ``` 169 | 170 | > 销毁线性表L 171 | 172 | ```C++ 173 | void DestoryList(SqList &L) 174 | { 175 | if(L.elem) delete L.elem; // 释放存储空间 176 | } 177 | ``` 178 | 179 | > 清空线性表L 180 | 181 | ```C++ 182 | void ClearList(SqList &L){ 183 | L.length = 0; // 将线性表的长度置为0 184 | } 185 | ``` 186 | 187 | > 求线性表的长度 188 | 189 | ```C++ 190 | int GetLength(SqList &L){ 191 | return L.length; 192 | } 193 | ``` 194 | 195 | > 判断线性表是否为空 196 | 197 | ```C++ 198 | int IsEmpty(SqList &L){ 199 | if(L.length == 0) return 0; 200 | return 1; 201 | } 202 | ``` 203 | 204 | > 顺序表的取值 205 | 206 | ```C++ 207 | // 取物理位置第i个元素 208 | int GetElem(SqList L, int i, ElemType &e){ 209 | if(i < 1 || i >= L.length) return ERROR; 210 | 211 | e = L.elem[i-1]; 212 | return OK; 213 | } 214 | ``` 215 | 216 | > 顺序表按值查找(顺序查找) 217 | 218 | - 在线性表L中查找与指定值e相同的数据元素的位置 219 | 220 | - 从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号,未找到,返回0。 221 | 222 | 平均查找长度ASL(Average Search Length): 223 | 224 | - 为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的**平均查找长度**。 225 | 226 | > 顺序表插入 227 | 228 | 插入不同位置的算法演示:插入位置在最后、插入位置在中间、插入位置在最前面 229 | 230 | 算法思想: 231 | 232 | 1. 判断插入位置i是否合理 233 | 234 | 2. 判断顺序表的存储空间是否已满,若已满返回ERROR 235 | 236 | 3. 将第n至第i位的元素一次向后移动一个位置,空出第i个位置 237 | 238 | 4. 将要插入的新元素e放入第i个位置 239 | 240 | 5. 表长加1,返回插入成功。 241 | 242 | ```C++ 243 | ListInsert_Sq(SqList& L, int i, ElemType e) 244 | { 245 | if(i < 1 || i > L.length+1) return ERROR; // i值不合法 246 | if(L.length == MAXSIZE) return ERROR; // 当前存储已满 247 | for(j = L.length-1; j >= i; j--){ 248 | L.elem[j+1] = L.elem[j]; 249 | } 250 | L.elem[i-1] = e; 251 | L.length++; 252 | return OK; 253 | } 254 | ``` 255 | 256 | 顺序表插入算法的平均时间复杂度为O(n)。 257 | 258 | > 顺序表的删除算法 259 | 260 | 算法思想: 261 | 262 | 1. 判断删除位置i是否合法 263 | 264 | 2. 将欲删除的元素保留在e中 265 | 266 | 3. 将第i+1至第n位的元素依次向前移动一个位置 267 | 268 | 4. 表长减1,删除成功返回OK 269 | 270 | ```C++ 271 | ListInsert_Sq(SqList& L, int i, ElemType &e) 272 | { 273 | if(i < 1 || i > L.length+1) return ERROR; // i值不合法 274 | e = L.elem[i]; 275 | for(j = i; j <= L.length-1; j++){ 276 | L.elem[j-1] = L.elem[j]; 277 | } 278 | L.length--; 279 | return OK; 280 | } 281 | ``` 282 | 283 | 顺序表删除算法的平均时间复杂度为O(n)。 284 | 285 | ### 2.4.3 顺序表小结 286 | 287 | 1. 利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致 288 | 289 | 2. 在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等 290 | 291 | - 这种存取元素的方法被称为随机存取法 292 | 293 | > 顺序表的操作算法分析 294 | 295 | - 时间复杂度 296 | 297 | - 查找、插入、删除算法的平均时间复杂度为O(n) 298 | 299 | - 空间复杂度 300 | 301 | - 顺序表操作算法的空间复杂度S(n) = O(1),没有占用辅助空间 302 | 303 | > 顺序表的优缺点 304 | 305 | 1. 优点 306 | 307 | - 存储密度大(结点本身所占存储量/结点结构所占存储量) 308 | 309 | - 可以随机存取表中任一元素 310 | 311 | 2. 缺点 312 | 313 | - 在插入、删除某一元素时,需要移动大量元素 314 | 315 | - 浪费存储空间 316 | 317 | - 属于静态存储形式,数据元素的个数不能自由扩充 318 | 319 | ## 2.5 线性表的链式表示和实现 320 | 321 | - 链式存储结构 322 | 323 | - 节点在存储器中的位置是任意的,即逻辑上相邻的数据在物理上不一定相邻 324 | 325 | - 线性表的链式表示又称为非顺序映像或链式映像。 326 | 327 | - 用一组**物理位置任意的存储单元**来存放线性表的数据元素。 328 | 329 | 单链表由头指针惟一确定,因此单链表可以用头指针的名字来命名。 330 | 331 | 各结点由两个域组成: 332 | 333 | - 数据域:存储元素数值数据 334 | 335 | - 指针域:存储直接后继结点的存储位置 336 | 337 | > 链式存储有关的术语 338 | 339 | 1. 结点:数据元素的存储映像。由数据域和指针域两部分组成 340 | 341 | 2. 链表:n个结点由**指针链**链接而成的一个链表 342 | 343 | 3. 链表类型 344 | 345 | 单链表:结点只有你一个指针域的链表; 346 | 347 | 双链表:结点有两个指针域的链表; 348 | 349 | 循环链表:首尾相接的链表; 350 | 351 | 4. 头指针、头节点和首元结点: 352 | 353 | 头指针:是指向链表中第一个结点的指针 354 | 355 | 首元结点:是指链表中存储第一个数据元素a1的结点 356 | 357 | 头结点:是在链表的首元结点之前附设的一个结点,一个附加结点。 358 | 359 | > 讨论1:如何表示空表 360 | 361 | - 若无头结点时,头指针为空时表示空表 362 | 363 | - 有头结点时,当头结点的指针域位空时表示空表 364 | 365 | > 讨论2:在链表中设置头结点有什么好处? 366 | 367 | 1. 便于首元结点的处理 368 | 369 | 首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无需进行特殊处理 370 | 371 | 2. 便于空表和非空表的统一处理 372 | 373 | 无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了 374 | 375 | > 讨论3:头结点的数据域内装的是什么? 376 | 377 | 头结点的数据域可以位空,也可以存放线性表长度等附加信息,但此结点不能计入链表长度值。 378 | 379 | > 链表的特点: 380 | 381 | 1. 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻 382 | 383 | 2. 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不相同 384 | 385 | 3. 链表是顺序存取的,顺序表是随机存取的 386 | 387 | ### 2.5.1 单链表的定义和表示 388 | 389 | > 带头结点的单链表 390 | 391 | 单链表是由**表头**唯一确定,因此单链表可以用头指针的名字来命名若头指针名是L,则把链表称为表L 392 | 393 | ```C++ 394 | typedef struct Lnode{ // 声明结点的类型和指向结点的指针类型 395 | ElemTyoe data; // 结点的数据域 396 | Lnode *next; // 结点的指针域 397 | } Lnode, *LinkList; // LinkList为指向结构体Lnode的指针类型 398 | ``` 399 | 400 | 定义链表L:LinkList L; 401 | 402 | 定义结点指针p:LNode *p; 403 | 404 | ### 2.5.2 单链表基本操作的实现 405 | 406 | > 算法2.6——单链表的初始化(带头结点的单链表) 407 | 408 | 单链表的初始化即构造一个空表。 409 | 410 | 算法步骤: 411 | 412 | 1. 生成新结点作为头结点,用头指针L指向头结点 413 | 414 | 2. 将头结点的指针域置空 415 | 416 | 算法描述: 417 | 418 | ```C++ 419 | Status InitList_L(LinkList &L){ 420 | L = new LNode; 421 | L->next = NULL; 422 | return OK; 423 | } 424 | ``` 425 | 426 | > 补充算法1:判断链表是否为空 427 | 428 | 空表:链表中无元素,称为空链表(头指针和头结点仍然在) 429 | 430 | 算法描述: 431 | 432 | ```C++ 433 | int ListEmpty(LinkList L){ // 若L为空表,则返回1,否则返回0 434 | if(L->next) 435 | return 1; 436 | else 437 | return 0; 438 | } 439 | ``` 440 | 441 | > 补充算法2:单链表的销毁:链表销毁后不存在 442 | 443 | 算法思路:从头指针开始,依次释放所有结点 444 | 445 | 算法描述: 446 | 447 | ```C++ 448 | Status DestoryList_L(LinkList &L){// 销毁单链表L 449 | Lnode *p; 450 | while(L){ // 直至L为空 451 | p = L; 452 | L = L->next; 453 | delete p; 454 | } 455 | return OK; 456 | } 457 | ``` 458 | 459 | > 补充算法3:清空链表 460 | 461 | 链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在) 462 | 463 | 算法思路:依次释放所有结点,并将头结点指针域设置为空 464 | 465 | 算法描述: 466 | 467 | ```C++ 468 | status ClearList(LinkList &L){ // 将L重置为空表 469 | Lnode *p, *ql 470 | p = L->next; 471 | while(p){ 472 | q = p->next; 473 | delete p; 474 | p = q; 475 | } 476 | L->next = NULL; // 头结点指针域为空 477 | return OK; 478 | } 479 | ``` 480 | 481 | > 补充算法3:求单链表的表长 482 | 483 | 算法思路:从首元结点开始,依次计数所有结点 484 | 485 | 算法描述: 486 | 487 | ```C++ 488 | int ListLength_L(LinkList L){ // 返回L中数据元素个数 489 | LinkList p; 490 | p = L->next; 491 | int i = 0; 492 | while(p){ // 遍历单链表,统计结点数 493 | i++; 494 | p = p->next; 495 | } 496 | return i; 497 | } 498 | ``` 499 | 500 | > 算法2.7——取址(取单链表中第i个元素的内容) 501 | 502 | 算法思路:从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,**链表不是随机存取结构**。 503 | 504 | 算法步骤: 505 | 506 | 1. 从第一个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值p = L->next; 507 | 508 | 2. j做计数器,累计当前扫描过的结点数,j初值为1; 509 | 510 | 3. 当p指向扫描到的下一个结点时,计数器j加1; 511 | 512 | 4. 当j==i时,p所指的结点就是要找的第i个结点。 513 | 514 | 算法描述: 515 | 516 | ```C++ 517 | Status GetElem_L(LinkList L, int i, ElemType &e){ 518 | // 获取线性表L中的某个数据元素的内容,通过变量e返回 519 | p = L->next; j = 1; 520 | while(p && j < i){ 521 | p = p->next; 522 | ++j; 523 | } 524 | if(!p || j > i) return ERROR; // 第i个元素不存在 525 | e = p->data; 526 | return OK; 527 | } 528 | ``` 529 | 530 | 查找: 531 | 532 | - 按值查找:根据指定数据获取该数据所在的位置(该数据的地址); 533 | 534 | - 按值查找:根据指定数据获取该数据所在位置序号(是第几个元素) 535 | 536 | > 算法2.8——按值查找:根据指定数据获取该数据所在的位置(地址) 537 | 538 | 算法步骤: 539 | 540 | 1. 从第一个结点起,依次和e相比较 541 | 542 | 2. 如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址; 543 | 544 | 3. 如果查遍整个链表都没有找到其值和e相等的元素,则返回0或NULL。 545 | 546 | 算法描述: 547 | 548 | ```C++ 549 | Lnode* LocateElem_L(LinkList L, ElemType e){ 550 | // 在线性表L中查找值e的数据元素 551 | // 找到,则返回L中值为e的数据元素的地址,查找失败返回NULL 552 | p = L->next; 553 | while(p && p->data != e){ 554 | p = p->next; 555 | } 556 | return p; 557 | } 558 | ``` 559 | 560 | > 算法2.8 变化——按值查找:根据指定数据获取该数据所在的位置序号 561 | 562 | 算法描述: 563 | 564 | ```C++ 565 | int LocateElem_L(LinkList L, ElemType e){ 566 | // 返回L中值为e的数据元素的位置序号,查找失败返回0 567 | p = L->next; j = 1; 568 | while(p && p->data != e){ 569 | p = p->next; 570 | j++; 571 | } 572 | if(p) return j; 573 | else return 0; 574 | } 575 | ``` 576 | 577 | > 算法2.9 插入——在第i个结点前插入值为e的新结点 578 | 579 | 算法步骤: 580 | 581 | 1. 首先找到ai-1的存储位置p 582 | 583 | 2. 生成一个数据域为e的新结点s。 584 | 585 | 3. 插入新结点: 586 | 587 | 1. 新结点的指针域指向结点ai 588 | 589 | 2. 结点ai-1的指针域指向新结点 590 | 591 | 算法描述: 592 | 593 | ```C++ 594 | Status ListInsert_L(LinkList &L, int i, ElemType e){ 595 | p = L; j = 0; 596 | // 寻找第i-1个结点,p指向i-1结点 597 | while(p && j < i - 1){ 598 | p = p->next; 599 | ++j; 600 | } 601 | // /i大于表长+1或者小于1,插入位置非法 602 | if(!p || j > i - 1) return ERROR; 603 | // 生成新结点s,将结点s的数据域置为e 604 | s = new LNode; 605 | s->data = e; 606 | // 将结点s插入L中 607 | s->next = p->next; 608 | p->next = s; 609 | } 610 | ``` 611 | 612 | > 算法2.10 删除——删除第i个结点 613 | 614 | 算法步骤: 615 | 616 | 1. 首先找到ai-1的存储位置p,保存要删除的ai的值 617 | 618 | 2. 令p->next指向ai+1 619 | 620 | 3. 释放结点ai的空间 621 | 622 | 算法描述: 623 | 624 | ```C++ 625 | // 将线性表L中第i个数据元素删除 626 | Status ListDelete_L(LinkList &L, int i, ElemType&e){ 627 | p = L; j = 0; 628 | while(p->next && j < i-1){ 629 | p = p->next; 630 | ++j; 631 | } 632 | // 寻找第i个结点,并令p指向其前驱 633 | if(!(p->next) || j > i-1) return ERROR;//删除位置不合理 634 | q = p->next; // 临时保存被删结点的地址以备释放 635 | p->next = q->next; // 改变删除结点前驱结点的指针域 636 | e = q->data; // 保存删除结点的数据域 637 | delete q; // 释放删除结点的空间 638 | return OK; 639 | } // ListDelete_L 640 | ``` 641 | 642 | > 算法2.11 建立单链表——头插法 643 | 644 | 头插法:元素插入在链表头部,也叫前插法 645 | 646 | 算法步骤: 647 | 648 | 1. 从一个空表开始,重复读入数据; 649 | 650 | 2. 生成新结点,将读入数据存放到新结点的数据域中 651 | 652 | 3. **从最后一个结点开始**,依次将各结点插入到链表的前端 653 | 654 | 算法描述: 655 | 656 | ```C++ 657 | void CreateList_H(LinkList &L, int n){ 658 | L = new LNode; 659 | L->next = NULL; // 先建立一个带头结点的单链表 660 | for(int i = n; i > 0; --i){ 661 | p = new LNode; // 生成新结点p 662 | cin >> p->data; // 输入元素值 663 | p->next = L->next; // 插入到表头 664 | L->next = p; 665 | } 666 | } 667 | ``` 668 | 669 | > 算法2.11 建立单链表——尾插法 670 | 671 | 尾查法:元素插入在链表尾部,也叫后插法 672 | 673 | 算法步骤: 674 | 675 | 1. 从一个空表开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。 676 | 677 | 2. 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。 678 | 679 | 算法描述: 680 | 681 | ```C++ 682 | // 正位序输入n个元素的值,建立带表头结点的单链表L 683 | void CreateList_R(LinkList& L, int n){ 684 | L = new LNode; 685 | L->next = NULL; 686 | r = L; // 尾指针r指向头结点 687 | for(int i = 0; i < n; ++i){ 688 | p = new LNode; // 生成新结点,输入元素值 689 | cin >> p->data; 690 | p->next = NULL; 691 | r->next = p; 692 | r = p; 693 | } 694 | } // CreateList_R 695 | ``` 696 | 697 | ### 2.5.3 单链表的查找、插入、删除算法时间效率分析 698 | 699 | 1. 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为O(n) 700 | 701 | 2. 插入和删除: 702 | 703 | 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)。 704 | 705 | 但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为O(n) 706 | 707 | 3. 头插法的时间复杂度是O(n) 708 | 709 | 4. 尾插法的时间复杂度是O(n) 710 | 711 | ### 2.5.4 循环链表 712 | 713 | 循环链表:是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)。 714 | 715 | 优点:**从表中任一结点出发均可找到表中其他结点**。 716 | 717 | 循环链表的空表表示:头指针的指针域指向自己。 718 | 719 | > 注意: 720 | 721 | 由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们**是否等于头指针**。 722 | 723 | > 头指针表示单循环链表 724 | 725 | 找a1的时间复杂度:O(1) 726 | 727 | 找an的时间复杂度:O(n) 728 | 729 | 但考虑到表的操作常常是在表的首尾位置上进行,因此更过的是考虑用**尾指针表示单循环链表** 730 | 731 | > 尾指针表示单循环链表 732 | 733 | 设尾指针为R: 734 | 735 | 找a1的时间复杂度:O(1),a1的存储位置是:R->next->next 736 | 737 | 找an的时间复杂度:O(1),an的存储位置是:R 738 | 739 | > 带尾指针循环链表的合并(将Tb合并在Ta之后) 740 | 741 | 操作步骤: 742 | 743 | 1. p存表头结点:p = Ta->next; 744 | 745 | 2. Tb表头连接到Ta表尾:Ta->next = Tb->next->next; 746 | 747 | 3. 释放Tb表头结点:delete Tb->next; 748 | 749 | 4. 修改指针:Tb->next = p; 750 | 751 | 算法描述: 752 | 753 | ```C++ 754 | LinkList Connect(LinkList Ta, LinkList Tb){ 755 | // 假设Ta、Tb都是非空的单循环链表 756 | p = Ta->next; // 1. p存表头结点 757 | Ta->next = Tb->next->next; // Tb表头连接到Ta表尾 758 | delete Tb->next; // 释放Tb表头结点 759 | Tb->next = p; // 修改指针 760 | return Tb; 761 | } 762 | ``` 763 | 764 | 时间复杂度为O(1) 765 | 766 | ### 2.5.5 双向链表 767 | 768 | 双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。 769 | 770 | 双向链表结构定义: 771 | 772 | ```C++ 773 | typedef struct DuLNode{ 774 | Elemtype data; 775 | struct DuLNode *prior, *next; 776 | }DuLNode, *DuLinkList; 777 | ``` 778 | 779 | 双向循环链表: 780 | 781 | 和单链的循环表类似,双向链表也可以有循环表 782 | 783 | - 让头结点的前驱指针指向链表的最后一个结点 784 | 785 | - 让最后一个结点的后继指针指向头结点。 786 | 787 | 在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为O(n)。 788 | 789 | > 算法2.13 双向链表的插入 790 | 791 | ```C++ 792 | void ListInsert_DuL(DuLinkList &L, int i, ElemType e){ 793 | // 在带头结点的双向循环链表L中第i个位置之前插入元素e 794 | if(!(p = GetElemP_DuL(L, i))) return ERROR; 795 | s = new DuLNode; 796 | s->data = e; 797 | s->prior = p->prior; 798 | p->prior->next = s; 799 | s->next = p; 800 | p->prior = s; 801 | return OK; 802 | } 803 | ``` 804 | 805 | > 算法2.14 双向链表的删除 806 | 807 | ```C++ 808 | void ListDelete_DuL(DuLinkList& L, int i, ElemType &e){ 809 | // 删除带头结点的双向循环链表L的第i个元素,并用e返回。 810 | if(!(p = GetElemP_DuL(L, i))) return ERROR; 811 | e = p->data; 812 | p->prior->next = p->next; 813 | p->next->prior = p->prior; 814 | delete p; 815 | return OK; 816 | } 817 | ``` 818 | 819 | > 单链表、循环链表和双向链表的时间效率比较 820 | 821 | ![单链表、循环链表和双向链表的时间效率比较](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter2%20LinearList/%E5%8D%95%E9%93%BE%E8%A1%A8%E3%80%81%E5%BE%AA%E7%8E%AF%E9%93%BE%E8%A1%A8%E5%92%8C%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8%E7%9A%84%E6%97%B6%E9%97%B4%E6%95%88%E7%8E%87%E6%AF%94%E8%BE%83.png "单链表、循环链表和双向链表的时间效率比较") 822 | 823 | ## 2.6 顺序表和链式表的比较 824 | 825 | > 链式存储结构的优点: 826 | 827 | - 结点空间可以动态申请和释放; 828 | 829 | - 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。 830 | 831 | > 链式存储结构的缺点 832 | 833 | - 存储密度小:每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。 834 | 835 | - 链式存储结构是非随机存取结构。对任一结点的操作都需要从头指针依指针链查找到该结点,这增加了算法的复杂度。 836 | 837 | > 顺序表和链式表的比较 838 | 839 | ![https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter2%20LinearList/%E9%A1%BA%E5%BA%8F%E8%A1%A8%E5%92%8C%E9%93%BE%E8%A1%A8%E7%9A%84%E6%AF%94%E8%BE%83.png]( "顺序表和链式表的比较") 840 | 841 | ## 2.7 线性表的应用 842 | 843 | 主要介绍线性表的合并和有序表的合并。 844 | 845 | > 线性表的合并 846 | 847 | - 问题描述: 848 | 849 | 假设利用两个线性表La和Lb分别表示两个集合A和B,现要求合一个新的集合A = A∪B 850 | 851 | - 算法步骤: 852 | 853 | 依次取出Lb中的每个元素,执行以下操作: 854 | 855 | 1. 在La中查找该元素 856 | 857 | 2. 如果找不到,则将其插入La的最后 858 | 859 | - 算法描述: 860 | 861 | ```C++ 862 | void union(List &La, List Lb){ 863 | La_len = ListLength(La); 864 | Lb_len = ListLength(Lb); 865 | for(int i = 1; i < Lb_len; i++){ 866 | GetElem(Lb, i, e); 867 | if(!LocateElem(La, e)){ 868 | ListInsert(&La, ++La_len, e); 869 | } 870 | } 871 | } 872 | ``` 873 | 874 | 上述算法时间复杂度为O(La_len*Lb_len)。 875 | 876 | > 有序表的合并 877 | 878 | - 问题描述: 879 | 880 | 已知线性表La和Lb的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列。 881 | 882 | - 算法步骤: 883 | 884 | 1. 创建一个空表Lc 885 | 886 | 2. 依次从La或Lb中“摘取”元素值较小的结点插入到Lc表的最后,直至其中一个表变空为止 887 | 888 | 3. 继续将La或Lb其中一个表的剩余结点插入在Lc表的最后 889 | 890 | > 算法2.16 有序表合并——用顺序表实现 891 | 892 | - 用顺序表实现 893 | 894 | ```C++ 895 | void MergeList_sq(SqList LA, SqList LB, SqList& LC){ 896 | // 指针pa和pb的初值分别指向两个表的第一个元素 897 | pa = LA.elem; 898 | pb = LB.elem; 899 | 900 | // 新表长度为待合并两表的长度和 901 | LC.length = LA.length + LB.length; 902 | // 为新表分配空间 903 | LC.elem = new ElemType[LC.length]; 904 | pc = LC.elem; 905 | 906 | // 找到LA和LB的最后一个元素 907 | pa_last = LA.elem + LA.length - 1; 908 | pb_last = LB.elem + LB.length - 1; 909 | 910 | // 两个表都非空 911 | while(pa <= pa_last && pb <= pb_last){ 912 | if(*pa <= *pb){ 913 | *pc++ = *pa++; 914 | }else{ 915 | *pc++ = *pb++; 916 | } 917 | } 918 | 919 | // LB已到达表尾,将LA中剩余元素加入LC 920 | while(pa <= pa_last) *pc++ = *pa++; 921 | 922 | // LA已到达表尾,将LB中剩余元素加入LC 923 | while(pb <= pb_last) *pc++ = *pb++; 924 | } 925 | ``` 926 | 927 | 时间复杂度为:O(ListLength(LA)+ListLength(LB)) 928 | 929 | 空间复杂度为:O(ListLength(LA)+ListLength(LB)) 930 | 931 | > 算法2.17 有序表合并——用链表实现 932 | 933 | ```C++ 934 | void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc){ 935 | pa = La->next; 936 | pb = Lb->next; 937 | // pc指针指向头结点 938 | pc = Lc = La; // 用La的头结点作为Lc的头结点 939 | while(pa && pb){ 940 | if(pa->data <= pb->data){ 941 | pc->next = pa; 942 | pc = pa; 943 | pa = pa->next; 944 | }else{ 945 | pc->next = pb; 946 | pc = pb; 947 | pb = pb->next; 948 | } 949 | } 950 | pc->next = pa ? pa : pb; // 插入剩余段 951 | delete Lb; // 删除Lb的头结点 952 | } 953 | ``` 954 | 955 | 时间复杂度为:O(ListLength(La)+ListLength(Lb)) 956 | 957 | 空间复杂度为:O(1) 958 | 959 | ## 2.8 案例分析与实现 960 | 961 | > 案例2.1:一元多项式的运算,实现两个多项式的加、减、乘运算 962 | 963 | 可用顺序表实现。 964 | 965 | > 案例2.2:稀疏多项式的运算 966 | 967 | 对于稀疏多项式,可考虑创建一个新的结构体,分别保存指数和系数。 968 | 969 | 利用顺序表实现时存在**存储空间分配不灵活、运算得空间复杂度高**等缺点。可考虑利用链式存储结构实现。 970 | 971 | 定义新的链表结构体: 972 | 973 | ```C++ 974 | typedef struct PNode{ 975 | float coeff; // 系数 976 | int expn; // 指数 977 | PNode *next; // 指针域 978 | }PNode, *Polynomial; 979 | ``` 980 | 981 | - 多项式相加 982 | 983 | - 多项式创建——算法步骤 984 | 985 | 1. 创建一个只有头结点的空链表; 986 | 987 | 2. 根据多项式的项的个数n,循环n次执行以下操作: 988 | 989 | - 生成一个新结点*s; 990 | 991 | - 输入多项式当前项的系数和指数赋给新结点*s的数据域; 992 | 993 | - 设置一前驱指针pre,用于指向待找到的第一个大于输入项指数的结点的前驱,pre初值指向头结点; 994 | 995 | - 指针q初始化,指向首元结点; 996 | 997 | - 循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输入项指数的结点*q; 998 | 999 | - 将输入项结点*s插入到结点q之前。 1000 | 1001 | - 多项式创建——算法描述(头插法) 1002 | 1003 | ```C++ 1004 | void CreatePolyn(Polynomial &P, int n){ 1005 | // 输入m项的系数和指数,建立表示多项式的有序链表P 1006 | P = new PNode; 1007 | P->next = NULL; // 先建立一个带头结点的单链表 1008 | for(int i = 1; i <= n; i++){ // 依次输入n个非零项 1009 | s = new PNode; // 生成新结点 1010 | cin >> s->coef >> s->expn; 1011 | pre = p; // pre用于保存q的前驱,初值为头结点 1012 | q = p->next; // q初始化,指向首元结点 1013 | while(q && q->expn < s->expn){ // 找到第一个大于输入项指数的项*q 1014 | pre = q; 1015 | q = p->next; 1016 | } 1017 | s->next = q; // 将输入项s插入到q和其前驱结点pre之间 1018 | pre->next = s; 1019 | } 1020 | } 1021 | ``` 1022 | 1023 | - 多项式相加——算法步骤 1024 | 1025 | 1. 指针p1和p2初始化,分别指向Pa和Pb的首元结点。 1026 | 1027 | 2. p3指向和多项式的当前结点,初值为Pa的头结点。 1028 | 1029 | 3. 当指针p1和p2均未到达相应表尾时,则循环比较p1和p2所指结点对应的指值(p1->expn与p2->expn),有下列三种情况: 1030 | 1031 | - 当p1->expn==p2->expn时,则将两个结点中的系数相加 1032 | 1033 | - 若和不为零,则修改p1所指结点的系数值,同时删除p2所指结点 1034 | 1035 | - 若和为零,则删除p1和p2所指结点 1036 | 1037 | - 当p1->expn < p2->expn时,则应摘取p1所指结点插入到“和多项式”链表中表; 1038 | 1039 | - 当p1->expn > p2->expn时,则应摘取p2所指结点插入到“和多项式”链表中表; 1040 | 1041 | 4. 将非空多项式的剩余段浦入到p3所指结点之后 1042 | 1043 | 5. 释放Pb的头结点。 1044 | 1045 | > 案例2.3 图书信息管理系统 1046 | -------------------------------------------------------------------------------- /Chapter2 LinearList/单链表、循环链表和双向链表的时间效率比较.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter2 LinearList/单链表、循环链表和双向链表的时间效率比较.png -------------------------------------------------------------------------------- /Chapter2 LinearList/顺序表和链表的比较.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter2 LinearList/顺序表和链表的比较.png -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe1.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe1.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe10.cpp: -------------------------------------------------------------------------------- 1 | struct LinkList{ 2 | int data; 3 | LinkList* next; 4 | }; 5 | 6 | int GetMax(LinkList* p){ 7 | if(!p->next){ 8 | return p->data; 9 | }else{ 10 | int max = GetMax(p->next); 11 | return p->data >= max ? p->data : max; 12 | } 13 | } 14 | 15 | int GetNum(LinkList* p){ 16 | if(!p->next){ 17 | return 1; 18 | }else{ 19 | return GetNum(p->next) + 1; 20 | } 21 | } 22 | 23 | int GetAvg(LinkList* p, int n){ 24 | if(!p->next){ 25 | return p->data; 26 | }else{ 27 | double ave = GetAvg(p->next, n-1); 28 | return (ave * (n-1) + p->data)/n; 29 | } 30 | } -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe2.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe2.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe3.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe3.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe5.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe5.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe6.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe6.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe7.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe7.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe8.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe8.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignExe9.cpp: -------------------------------------------------------------------------------- 1 | int Ack(int m, int n){ 2 | if(m == 0){ 3 | return n + 1; 4 | } else if(m != 0 && n == 0){ 5 | return Ack(m-1, 1); 6 | }else{ 7 | return Ack(m-1, Ack(m, n-1)); 8 | } 9 | } -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignQueue.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignQueue.h -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignStack.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignStack.cpp -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignStack.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/AlgoDesignStack.h -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/Chapter3Exe/双栈结构的表示.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/Chapter3Exe/双栈结构的表示.png -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/README.md: -------------------------------------------------------------------------------- 1 | # 第三章 栈和队列 2 | 3 | 栈和队列是两种重要的线性结构。从数据结构角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集,它们是操作受限的线性表,因此,可称为**限定性的数据结构**。但从数据类型角度看,它们是和线性表不相同的两类重要的抽象数据类型。 4 | 5 | ## 3.1 栈和队列的定义和特点 6 | 7 | ### 3.1.1 栈的定义和特点 8 | 9 | **栈**(**stack**)是限定仅在**表尾进行插入或删除**操作的线性表。因此,对栈来说,表尾端有其特殊含义,称为**栈顶**(**top**),相应地,表头端称为**栈底**(**bottom**)。不含元素的空表称为**空栈**。 10 | 11 | 栈的修改是按后进先出的原则进行的,因此,栈又称为**后进先出**(Last In First Out, LIFO)的线性表。 12 | 13 | > 栈与一般线性表的区别 14 | 15 | 栈与一般线性表的区别:**仅存在运算规则不同**。 16 | 17 | ||一般线性表|栈| 18 | |:---|:---:|:---:| 19 | |逻辑结构|一对一|一对一| 20 | |存储结构|顺序表、链表|顺序表、链表| 21 | |运算规则|随机存取|后进先出(LIFO)| 22 | 23 | ### 3.1.2 队列的定义和特点 24 | 25 | **队列**(**queue**)是一种**先进先出**(**First In First Out, FIFO**)的线性表。它只允许在表的一端进行插入,而在另一端删除元素。 26 | 27 | 在队列中,允许插入的一端称为**队尾**(**rear**),允许删除的一端则称为**队头**(**front**)。 28 | 29 | ## 3.2 案例引入 30 | 31 | ### 案例3.1 进制转换 32 | 33 | ![进制转换](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/%E8%BF%9B%E5%88%B6%E8%BD%AC%E6%8D%A2.png) 34 | 35 | ### 案例3.2 括号匹配的检验 36 | 37 | - 假设表达式中允许包含两种括号:圆括号和方括号 38 | 39 | - 其嵌套的顺序随意,即: 40 | 41 | 1. ([ ] ( ))或[ ( [ ] [ ] ) ]为正确格式; 42 | 43 | 2. [ ( ] )为错误格式; 44 | 45 | 3. ( [ () )或(()])为错误格式。 46 | 47 | ![括号匹配](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/%E6%8B%AC%E5%8F%B7%E5%8C%B9%E9%85%8D.png) 48 | 49 | ### 案例3.3 表达式求值 50 | 51 | - 表达式求值是程序设计语言编译中一个最基本的问题,它的实现需要运用栈。 52 | 53 | - 这里介绍的算法是由运算符优先级确定运算顺序的对表达式求值算法——算符优先算法。 54 | 55 | - 表达式的组成 56 | 57 | - 操作数(operand):常数、变量。 58 | 59 | - 运算符(operator):算术运算符、关系运算符和逻辑运算符。 60 | 61 | - 界限符(delimiter):左右括弧和表达式结束符。 62 | 63 | - 任何一个算术表达式都由**操作数**(常数、变量)、算术**运算符**(+、-、*、/)和界限符(括号、表达式结束符'#'、虚设的表达式起始符'#')组成。后两者统称为算符。 64 | 65 | - 为了实现表达式求值。需要设置两个栈: 66 | 67 | 一个是算符栈OPTR,用于寄存运算符。 68 | 69 | 另一个称为操作数栈OPND,用于寄存运算数和运算结果。 70 | 71 | - 求值的处理过程是自左至右扫描表达式的每一个字符 72 | 73 | - 当扫描到的是运算数,则将其压入栈OPND, 74 | 75 | - 当扫描到的是运算符时 76 | 77 | - 若这个运算符比OPTR栈顶运算符的优先级高,则入栈OPTR,继续向后处理 78 | 79 | - 若这个运算符比OPTR栈顶运算符优先级低,则从OPND栈中弹出两个运算数,从栈OPTR中弹出栈顶运算符进行运算,并将运算结果压入栈OPND。 80 | 81 | - 继续处理当前字符,直到遇到结束符为止。 82 | 83 | ### 案例3.4 舞伴问题 84 | 85 | 假设在舞会上,男士和女士各自排成一队。舞会开始时,依次从男队和女队的队买务出一人配成舞伴。如果两队初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲。现要求写一算法模拟上述舞伴配对问题。 86 | 87 | 显然,先入队的男士或女士先出队配成舞。因此该问题具有典型的先进先出特性,可以用队列作为算法的数据结构。 88 | 89 | - 首先构造两个队列 90 | 91 | - 依次将从男元素出队配成舞伴 92 | 93 | - 某队为空,则另外一队等待着则是下一舞曲第一个可获得舞伴的人。 94 | 95 | ## 3.3 栈的表示和操作的实现 96 | 97 | ### 3.3.1 栈的抽象数据类型的类型定义 98 | 99 | ```C++ 100 | ADT Stack{ 101 | 数据对象: 102 | D = {ai | ai ∈ ElemSet, i = 1,2,...,n, n≥0} 103 | 数据关系: 104 | R1 = { | ai-1, ai∈D, i = 1,2,...,n} 105 | 约定an端为栈顶,a1端为栈底 106 | 基本操作: 107 | 初始化、进栈、出栈、取栈顶元素等 108 | }ADT Stack 109 | ``` 110 | 111 | ![栈的基本操作](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/%E6%A0%88%E7%9A%84%E6%93%8D%E4%BD%9C.png) 112 | 113 | ### 3.3.2 顺序栈的表示和实现 114 | 115 | 存储方式:同一般线性表的顺序存储结构完全相同,**利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素**。栈底一般在低地址端。 116 | 117 | - 附设**top指针**,指示栈顶元素在顺序栈中的位置。 118 | 119 | - 另设**base指针**,指示栈底元素在顺序栈中的位置。 120 | 121 | - 用stacksize表示栈可使用的最大容量。 122 | 123 | 但是,为了方便操作,通常top指示真正的**栈顶元素之上**的下标地址。 124 | 125 | 空栈:base == top是栈空标志 126 | 127 | 栈满:top - base == stacksize 128 | 129 | - 栈满时的处理方法: 130 | 131 | 1. 报错,返回操作系统 132 | 133 | 2. 分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈 134 | 135 | 使用数组作为顺序栈存储方式的特点:简单、方便、但易产生溢出(数组大小固定) 136 | 137 | - 上溢(overflow):栈已经满,又要压入元素 138 | 139 | - 下溢(underflow):栈已经空,还要弹出元素 140 | 141 | 注:上溢是一种错误,使问题的处理无法进行;而下溢一般认为是一种结束条件,即问题处理结束。 142 | 143 | > 顺序栈的表示 144 | 145 | ```C++ 146 | #define MAXSIZE 100 147 | struct SqStack{ 148 | SElemType *base; // 栈底指针 149 | SElemType *top; // 栈顶指针 150 | int stacksize; // 栈可用最大容量 151 | }; 152 | ``` 153 | 154 | > 算法3.1 顺序栈的初始化 155 | 156 | ```C++ 157 | // 构造一个空栈 158 | State InitStack(SqStack &S){ 159 | S.base = new SElemType[MAXSIZE]; 160 | if(!S.base) exit(OVERFLOW); // 存储分配失败 161 | S.top = S.base; // 栈顶指针等于栈底指针 162 | S.stacksize = MAXSIZE; 163 | return OK; 164 | } 165 | ``` 166 | 167 | > 补充算法1 判断顺序栈是否为空 168 | 169 | ```C++ 170 | // 若栈为空,返回TRUE;否则返回FALSE 171 | State StackEmpty(SqStack &S){ 172 | if(S.top == S.base) 173 | return TRUE; 174 | else 175 | return FALSE; 176 | } 177 | ``` 178 | 179 | > 补充算法2 求顺序栈的长度 180 | 181 | ```C++ 182 | int StackLength(SqStack S) 183 | { 184 | return S.top - S.base; 185 | } 186 | ``` 187 | 188 | > 补充算法3 清空顺序栈 189 | 190 | ```C++ 191 | Status ClearStack(SqStack &S){ 192 | if(S.base) S.top = S.base; 193 | return OK; 194 | } 195 | ``` 196 | 197 | > 补充算法4 销毁顺序栈 198 | 199 | ```C++ 200 | Status DestoryStack(SqStack &S){ 201 | if(S.base){ 202 | delete S.base; 203 | S.stacksize = 0; 204 | S.base = S.top = NULL; 205 | } 206 | return OK; 207 | } 208 | ``` 209 | 210 | > 算法3.2 顺序栈的入栈 211 | 212 | - 步骤: 213 | 214 | 1. 判断是否栈满,若满则出错 215 | 216 | 2. 元素e压入栈顶 217 | 218 | 3. 栈顶指针加1 219 | 220 | ```C++ 221 | Status Push(SqStack &S, SElemType e){ 222 | if(S.top - S.base == S.stacksize) // 栈满 223 | return ERROR; 224 | *S.top = e; 225 | S.top++; 226 | // 等同于 227 | // *S.top++ = e; 228 | return OK; 229 | } 230 | ``` 231 | 232 | > 算法3.3 顺序栈的出栈 233 | 234 | - 步骤: 235 | 236 | 1. 判断是否栈空,若空则出错 237 | 238 | 2. 获取栈顶元素e 239 | 240 | 3. 栈顶指针减1 241 | 242 | ```C++ 243 | Status Pop(SqStack &S, SElemType &e){ 244 | if(S.top == S.base) return ERROR; 245 | S.top--; 246 | e = *S.top; 247 | // 等同于 248 | e = *--S.top; 249 | return OK; 250 | } 251 | ``` 252 | 253 | ### 3.3.3 链栈的表示和实现 254 | 255 | 链栈是运算受限的单链表,只能在链表头部进行操作。定义链栈的结构类型: 256 | 257 | ```C++ 258 | struct StackNode{ 259 | SElemType data; 260 | StackNode *next; 261 | }; 262 | typedef StackNode *LinkStack; 263 | ``` 264 | 265 | - 链表的头指针就是栈顶 266 | 267 | - 不需要头结点 268 | 269 | - 基本不存在栈满的情况 270 | 271 | - 空栈相当于,栈指针指向空 272 | 273 | - 插入和删除仅在栈顶处执行 274 | 275 | > 算法3.5 链栈的初始化 276 | 277 | ```C++ 278 | void InitStack(LinkStack &S){ 279 | // 构造一个空栈,栈顶指针置为空 280 | S = NULL; 281 | return OK; 282 | } 283 | ``` 284 | 285 | > 补充算法1 判断链栈是否位空 286 | 287 | ```C++ 288 | Status StackEmpty(LinkList S){ 289 | if(S == NULL) return TRUE; 290 | else return FALSE; 291 | } 292 | ``` 293 | 294 | > 算法3.6 链栈的入栈 295 | 296 | ```C++ 297 | Status Push(LinkStack &S, SElemType e){ 298 | p = new StackNode; // 生成新结点p 299 | p->data = e; // 将新结点数据域置为e 300 | p->next = S; // 将新结点插入栈顶 301 | S = p; // 修改栈顶指针 302 | return OK; 303 | } 304 | ``` 305 | 306 | > 算法3.7 链栈的出栈 307 | 308 | ```C++ 309 | Status Pop(LinkStack &S, SElemType &e){ 310 | if(S == NULL) return ERROR; 311 | e = S->data; 312 | p = S; 313 | S = S->next; 314 | delete p; 315 | return OK; 316 | } 317 | ``` 318 | 319 | > 算法3.8 取栈顶的元素 320 | 321 | ```C++ 322 | SElemType GetTop(LinkStack S){ 323 | if(S != NULL){ 324 | return S->data; 325 | } 326 | } 327 | ``` 328 | 329 | ## 3.4 栈与递归 330 | 331 | - 递归的定义 332 | 333 | - 若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的; 334 | 335 | - 若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。 336 | 337 | - 常用到递归方法的三种情况: 338 | 339 | - 递归定义的数学函数 340 | 341 | - 具有递归特性的数据结构 342 | 343 | - 二叉树 344 | 345 | - 广义表 346 | 347 | - 可递归求解的问题 348 | 349 | - 迷宫问题 350 | 351 | - 汉诺塔问题 352 | 353 | - 递归问题——用分治法求解 354 | 355 | - 分治法:对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解 356 | 357 | - 必备的三个条件 358 | 359 | 1. 能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是变化有规律的 360 | 361 | 2. 可以通过上述转换而使得问题简化 362 | 363 | 3. 必须有一个明确的递归出口,或称递归边界 364 | 365 | - 分治法求解递归问题算法的一般形式 366 | 367 | ```C++ 368 | void p(参数表){ 369 | // 基本项 370 | if(递归结束条件) 可直接求解步骤; // 递归边界 371 | // 归纳项 372 | else p(较小的参数); 373 | } 374 | ``` 375 | 376 | - 递归的优缺点 377 | 378 | - 优点:结构清晰,程序易读 379 | 380 | - 缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,恢复状态信息。时间开销大。 381 | 382 | - 实现递归转非递归 383 | 384 | - 方法1:尾递归、单向递归变为循环结构 385 | 386 | - 方法2:自用栈模拟系统的运行时栈 387 | 388 | ## 3.5 队列的表示和操作的实现 389 | 390 | - 队列描述 391 | 392 | - **队列**(**Queue**)是仅在表尾进行插入操作,在表头进行删除操作的线性表。 393 | 394 | - 表尾即an端,称为队尾;表头即a1端,称为队头; 395 | 396 | - 它是一种先进先出(FIFO)的线性表 397 | 398 | ### 3.5.1 队列的抽象数据类型定义 399 | 400 | ### 3.5.2 队列的顺序表示和实现 401 | 402 | 队列的物理存储可以用顺序存储结构,也可用链式存储结构。相应地队列的存储方式也分为两种,即**顺序队列**和**链式队列**。 403 | 404 | 队列的顺序表示——用一维数组`base[MAXQSIZE]` 405 | 406 | ```C++ 407 | #define MAXQSIZE 100 // 最大队列长度 408 | typedef struct{ 409 | QElemType *base; // 初始化的动态分配存储空间 410 | int front; // 头指针,即队头下标 411 | int rear; // 尾指针 412 | }SqQueue; 413 | ``` 414 | 415 | > 顺序队列存在的问题 416 | 417 | 顺序队列的真溢出问题:即front=0,rear=MAXQSIZE时,队列中存满,为真溢出; 418 | 419 | 而当入队、出队操作之后,随着front和rear移动,出现front!=0,rear=MAXQSIZE时,为假溢出。 420 | 421 | > 顺序队列假上溢解决办法 422 | 423 | - 解决假上溢的方法 424 | 425 | 1. 将队中元素依次向队头方向移动。缺点:浪费时间。每移动一次,队中元素都要移动。 426 | 427 | 2. 将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为maxqsize时,若向量的开始端空着,又可从头使用空着的空间。当front为maxqsize时,也是一样。 428 | 429 | - 引入循环队列——解决假上溢的问题 430 | 431 | `base[0]`接在`base[MAXQSIZE-1]`之后,若rear+1==M,则rear=0; 432 | 433 | 实现方法:利用模运算(mod, %) 434 | 435 | 插入元素:(尾指针后移一位) 436 | 437 | ```C++ 438 | Q.base[Q.rear] = x; 439 | Q.rear = (Q.rear+1) % MAXQSIZE; 440 | ``` 441 | 442 | 删除元素:(头指针后移一位) 443 | 444 | ```C++ 445 | x = Q.base[Q.front]; 446 | Q.front = (Q.front + 1) % MAXQSIZE; 447 | ``` 448 | 449 | 针对循环队列,判断队空、对满时,front都与rear相等,因此可解决的方案有: 450 | 451 | 1. 另外设一个标态以区别队空和队满 452 | 453 | 2. 另设一个变量,记录元素个数 454 | 455 | 3. 少用一个能素空间 456 | 457 | - 循环队列解决队满时判断方法——少用一个元素空间 458 | 459 | 队空时:front == rear 460 | 461 | 队满时:(rear+1)% MAXQSIZE == front; 462 | 463 | > 循环队列的类型定义 464 | 465 | ```C++ 466 | #define MAXQSIZE 100 // 最大队列长度 467 | typedef struct{ 468 | QElemType *base; // 初始化的动态分配存储空间 469 | int front; // 头指针,若队列不为空,指向队列头元素 470 | int rear; // 尾指针,若队列不为空,指向队尾元素的下一个位置 471 | }SqQueue; 472 | ``` 473 | 474 | > 循环队列的操作——队列的初始化(算法3.11) 475 | 476 | ```C++ 477 | Status InitQueue(SqQueue &Q){ 478 | Q.base = new QElemType[MAXQSIZE]; 479 | if(!Q.base) exit(OVERFLOW); // 存储分配失败 480 | Q.front = Q.rear = 0; 481 | return OK; 482 | } 483 | ``` 484 | 485 | > 循环队列的操作——求队列的长度(算法3.12) 486 | 487 | ```C++ 488 | int QueueLength(SqQueue Q){ 489 | return (Q.rear - Q.front + MAXQSIZE) % MAXQSIZE; 490 | } 491 | ``` 492 | 493 | > 循环队列的操作——循环队列入队(算法3.13) 494 | 495 | ```C++ 496 | Status EnQueue(SqQueue &Q, QElemType e){ 497 | if((Q.rear + 1) % MAXQSIZE == Q.front) return ERROR; // 队满 498 | Q.base[Q.rear] = e; // 新元素加入队尾 499 | Q.rear = (Q.rear + 1) % MAXQSIZE; // 队尾指针+1 500 | return OK; 501 | } 502 | ``` 503 | 504 | > 循环队列的操作——循环队列出队(算法3.14) 505 | 506 | ```C++ 507 | Status DeQueue(SqQueue &Q, QElemType &e){ 508 | if(Q.rear == Q.front) return ERROR; // 队空 509 | e = Q.base[Q.front]; // 保存队头元素 510 | Q.front = (Q.front + 1) % MAXQSIZE; // 队头指针+1 511 | return OK; 512 | } 513 | ``` 514 | 515 | > 循环队列的操作——取循环队列队头元素(算法3.15) 516 | 517 | ```C++ 518 | QElemType GetHead(SqQueue Q){ 519 | if(Q.rear != Q.front){ // 不为空 520 | return Q.base[Q.front]; // 返回队头指针元素的值,队头指针不变 521 | } 522 | } 523 | ``` 524 | 525 | ### 3.5.3 链队——队列的链式表示和实现 526 | 527 | 若用户无法估计所用队列的长度,则宜采用链队列。 528 | 529 | > 链式队列的类型定义 530 | 531 | ```C++ 532 | #define MAXQSIZE 100 // 最大队列长度 533 | struct Qnode{ 534 | QElemType data; 535 | Qnode *next; 536 | }; 537 | typedef Qnode *QueuePtr; 538 | 539 | struct LinkQueue{ 540 | QueuePtr front; // 队头指针 541 | QueuePtr rear; // 队尾指针 542 | }; 543 | ``` 544 | 545 | > 链队列的操作——链队列的初始化(算法3.16) 546 | 547 | ```C++ 548 | Status InitQueue(LinkQueue &Q){ 549 | Q.front = new QueuePtr(); 550 | Q.rear = new QueuePtr(); 551 | Q.front->next = NULL; 552 | return OK; 553 | } 554 | ``` 555 | 556 | > 链队列的操作——销毁链队列(补充) 557 | 558 | ```C++ 559 | Status DestoryQueue(LinkQueue &Q){ 560 | while(Q.front){ 561 | p = Q.front->next; 562 | delete (Q.front); 563 | Q.front = p; 564 | } 565 | return OK; 566 | } 567 | ``` 568 | 569 | > 链队列的操作——将元素e入队(算法3.17) 570 | 571 | ```C++ 572 | Status EnQueue(LinkQueue &Q, QElemType e){ 573 | p = new QueuePtr; 574 | if(!p) exit(OVERFLOW); // 存储分配失败 575 | p->data = e; 576 | p->next = NULL; 577 | Q.rear->next = p; 578 | Q.rear = p; 579 | return OK; 580 | } 581 | ``` 582 | 583 | > 链队列的操作——链队列的出队(算法3.18) 584 | 585 | ```C++ 586 | Status DeQueue(LinkQueue &Q, QElemType &e){ 587 | if(Q.front == Q.rear) return ERROR; // 队空 588 | p = Q.front->next; 589 | e = p->data; 590 | Q.front->next = p->next; 591 | if(Q.rear == p) Q.rear = Q.front; 592 | delete p; 593 | return OK; 594 | } 595 | ``` 596 | 597 | > 链队列的操作——求链队列的队头元素(算法3.19) 598 | 599 | ```C++ 600 | Status GetHead(LinkQueue &Q, QElemType &e){ 601 | if(Q.front == Q.rear) return ERROR; 602 | e = Q.front->next->data; 603 | return OK; 604 | } 605 | ``` 606 | 607 | ## 3.6 习题 608 | 609 | > 选择题 610 | 611 | 1. 设栈S和队列Q的初始状态为空,元素e1、e2、e3、e4、e5和e6依次进入栈s,一个元素出栈后即进入Q,若6个元素出队的序列是e2、e4、e3、e6、e5和e1,则栈S的容量至少应该是() 612 | 613 | A. 2 **B. 3** C. 4 D. 6 614 | 615 | 2. 若一个栈以向量`V[1...n]`存储,初始栈顶指针top设为n+1,则元素x进栈的正确操作是()。 616 | 617 | A. `top++; V[top]=x;` B. `V[top]=x; top++;` **C.** `top--; V[top]= x;` D. `V[top]=x; top--;` 618 | 619 | 答案:C;解释:初始栈顶指针top为n+1,说明元素从数组向量的高端地址进栈,又因为元素存储在向量空间`V[1...n]`中,所以进栈时top指针先下移变为n,之后将元素x存储在`V[n]`。 620 | 621 | > 算法设计题 622 | 623 | 1. 将编号为0和1的两个栈存放于一个数组空间`V[m]`中,栈底分别处于数组的两端。当第0号栈的栈顶指针`top[0]`等于-1时该栈为空;当第1号栈的栈顶指针`top[1]`等于m时,该栈为空。两个栈均从两端向中间增长(见图)。试编写双栈初始化,判断栈空、栈满、进栈和出栈等算法的函数。双栈数据结构的定义如下: 624 | 625 | ```C++ 626 | struct DblStack{ 627 | int top[2], bot[2]; // 栈顶和栈底指针 628 | SElemType* V; // 栈数组 629 | int m; // 栈最大可容纳元素个数 630 | }; 631 | ``` 632 | 633 | ![双栈结构的表示](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/%E5%8F%8C%E6%A0%88%E7%BB%93%E6%9E%84%E7%9A%84%E8%A1%A8%E7%A4%BA.png) 634 | 635 | [算法设计题1](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe1.cpp) 636 | 637 | 2. 回文是指正读反读均相同的字符序列,如“abba”和“abdba”均是回文,但“good”不是回文。试写一个算法判定给定的字符序列是否为回文。(提示:将一半字符入栈) 638 | 639 | [算法设计题2](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe2.cpp) 640 | 641 | 3. 设从键盘输入一整数的序列:a1, a2, a3,..., an,试编写算法实现:用栈结构存储输入的整数,当ai≠-1时,将ai进栈;当ai = -1时,输出栈顶整数并出栈。算法应对异常情况(入栈满等)给出相应的信息。 642 | 643 | [算法设计题3](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe3.cpp) 644 | 645 | 4. 从键盘上输入一个后缀表达式,试编写算法计算表达式的值。规定:逆波兰表达式的长度不超过一行,以"$"作为输入结束,操作数之间用空格分隔,操作符只可能有+、-、*、/四种运算。例如: 23434 + 2*$。 646 | 647 | 5. 假设以I和O分别表示入栈和出栈操作。栈的初态和终态均为空,入栈和出栈的操作序列可表示为仅由I和O组成的序列,称可以操作的序列为合法序列,否则称为非法序列。 648 | 649 | - 下面所示的序列中哪些是合法的? 650 | 651 | A. IOIIOIOO B. IOOIOIIO C. IIIOIOIO D. IIIOOIOO 652 | 653 | - 通过对上述的分析,写出一个算法,判定所给的操作序列是否合法。若合法,返回true,否则返回false(假定被判定的操作序列已存入一维数组中)。 654 | 655 | [算法设计题5](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe5.cpp) 656 | 657 | 6. 假设以带头结点的循环链表表示队列,并且只设一个指针指向队尾元素结点(注意:不设头指针),试编写相应的置空队列、判断队列是否为空、入队和出队等算法。 658 | 659 | [算法设计题6](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe6.cpp) 660 | 661 | 7. 假设以数组`Q[m]`存放循环队列中的元素,同时设置一个标志tag,以tag = 0和tag = 1来区别在队头指针(front)和队尾指针(rear)相等时,队列状态为“空”还是“满”。试编写与此结构相应的插入(enqueue)和删除(dequeue)算法。 662 | 663 | [算法设计题7](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe7.cpp) 664 | 665 | 8. 如果允许在循环队列的两端都可以进行插入和删除操作。要求: 666 | 667 | - 写出循环队列的类型定义; 668 | 669 | - 写出“从队尾删除”和“从队头插入”的算法。 670 | 671 | [算法设计题8](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe8.cpp) 672 | 673 | 9. 已知Ackermann函数定义如下:Ack(m, n) = 674 | 675 | - n + 1, 当m = 0时 676 | 677 | - Ack(m-1, 1), 当m≠0, n=0时 678 | 679 | - Ack(m-1, Ack(m, n-1)), 当m≠0, n≠0时 680 | 681 | 1. 写出计算Ack(m, n)的递归算法,并根据此算法给出Ack(2, 1)的计算过程。 682 | 683 | 2. 写出计算Ack(m,n)的非递归算法。 684 | 685 | [算法设计题9](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe9.cpp) 686 | 687 | 10. 已知f为单链表的表头指针,链表中存储的都是整型数据,试写出实现下列运算的递归算法: 688 | 689 | 1. 求链表中的最大整数; 690 | 691 | 2. 求链表的结点个数; 692 | 693 | 3. 求所有整数的平均值。 694 | 695 | [算法设计题10](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/Chapter3Exe/AlgoDesignExe10.cpp) 696 | -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/括号匹配.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/括号匹配.png -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/栈的操作.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/栈的操作.png -------------------------------------------------------------------------------- /Chapter3 StackAndQueue/进制转换.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter3 StackAndQueue/进制转换.png -------------------------------------------------------------------------------- /Chapter4 String/Chapter4Exe/AlgoDesignExe1.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/Chapter4Exe/AlgoDesignExe1.cpp -------------------------------------------------------------------------------- /Chapter4 String/Chapter4Exe/AlgoDesignExe2.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/Chapter4Exe/AlgoDesignExe2.cpp -------------------------------------------------------------------------------- /Chapter4 String/Chapter4Exe/AlgoDesignExe3.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/Chapter4Exe/AlgoDesignExe3.cpp -------------------------------------------------------------------------------- /Chapter4 String/Chapter4Exe/AlgoDesignExe4.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/Chapter4Exe/AlgoDesignExe4.cpp -------------------------------------------------------------------------------- /Chapter4 String/Chapter4Exe/AlgoDesignExe5.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/Chapter4Exe/AlgoDesignExe5.cpp -------------------------------------------------------------------------------- /Chapter4 String/Chapter4Exe/AlgoDesignExe6.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/Chapter4Exe/AlgoDesignExe6.cpp -------------------------------------------------------------------------------- /Chapter4 String/README.md: -------------------------------------------------------------------------------- 1 | # 第4章 串、数组和广义表 2 | 3 | 字符串一般简称为串。串是一种特殊的线性表,其特殊性体现在数据元素是一个字符,也就是说,**串是一种内容受限的线性表**。 4 | 5 | 本章后两部分讨论的多维数组和广义表可以看成是线性表的一种扩充,即线性表的数据元素自身又是一个数据结构。高级语言都支持数组,但在高级语言中,重点介绍数组的使用,而本章重点介绍数组的内部实现,并介绍对于一些特殊的二维数组如何实现压缩存储。最后介绍广义表的基本概念和存储结构。 6 | 7 | ## 4.1 串的定义 8 | 9 | **串**(string)(或字符串)是由零个或多个字符组成的有限序列,一般记为:s = "a1a2 … an"。 10 | 11 | 其中,s是串的名,用双引号括起来的字符序列是串的值;ai(1≤i≤n)可以是字母、数字或其他字符;串中字符的数目n称为串的长度。零个字符的串称为**空串**(null string),其长度为零。 12 | 13 | > 相关术语 14 | 15 | - **子串**:一个串中任意个连续的字符组成的子序列(含空串)称为该串的子串。 16 | 17 | - **主串**:包含子串的串相应地称为主串。 18 | 19 | - **字符位置**:通常称字符在序列中的序号为该字符在串中的位置。 20 | 21 | - **子串的位置**:子串的第一个字符在主串中的位置。 22 | 23 | - **空格串**:一个或多个空格组成的串,与空串不同。 24 | 25 | - **串相等**:当且仅当这两个串的值相等。也就是说,只有当两个串的长度相等,并且各个对应位置的字符都相等时才相等。 26 | 27 | - 所有的空串都是相等的。 28 | 29 | ## 4.2 案例的引入 30 | 31 | ## 4.3 串的类型定义、存储结构及其运算 32 | 33 | ### 4.3.1 串的抽象类型定义 34 | 35 | ```C++ 36 | ADT string{ 37 | 数据对象: 38 | 数据关系: 39 | 基本操作: 40 | 1. StrAssign(&T, chars); // 串赋值 41 | 2. StrCompare(S, T); // 串比较 42 | 3. StrLenght(S); // 求串长 43 | 4. Concat(&T, S1, S2); // 串连结 44 | 5. SubString(&Sub, S, pos, len); // 求子串 45 | 6. StrCopy(&T, S); // 串拷贝 46 | 7. StrEmpty(S); // 串判空 47 | 8. ClearString(&S); // 清空串 48 | 9. Index(S, T, pos); // 子串的位置 49 | 10. Replace(&S, T, V); // 串替换 50 | 11. StrInsert(&S, pos, T); // 子串插入 51 | 12. StrDelete(&S, pos, len); // 子串删除 52 | 13. DestoryString(&S); // 串销毁 53 | } 54 | ``` 55 | 56 | ### 4.3.2 串的存储结构 57 | 58 | 串中元素逻辑关系与线性表的相同,串可以采用与线性表相同的存储结构:**顺序存储和链式存储**。 59 | 60 | 1. 串的顺序存储结构 61 | 62 | 类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区,则可用定长数组如下描述: 63 | 64 | ```C++ 65 | #define MAXLEN 255 66 | struct SString{ 67 | char ch[MAXLEN+1]; 68 | int length; 69 | }; 70 | ``` 71 | 72 | 其中,MAXLEN表示串的最大长度,ch是存储字符串的一维数组,每个分量存储一个字符,length表示字符串的当前长度。其中,后面算法描述当中所用到的顺序存储的字符串都是从下标为1的数组分量开始存储的,下标为0的分量闲置不用。 73 | 74 | 2. 串的链式存储结构 75 | 76 | 顺序串的插入和删除操作不方便,需要移动大量的字符。因此,可采用单链表方式存储串。由于串结构的特殊性——结构中的每个数据元素是一个字符,则在用链表存储串值时,存在一个“结点大小”的问题,即每个结点可以存放一个字符,也可以存放多个字符。 77 | 78 | ![串值的链表存储方式](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/%E4%B8%B2%E5%80%BC%E7%9A%84%E9%93%BE%E8%A1%A8%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F.png) 79 | 80 | 如图(a)中所示,结点大小为4,图(b),结点大小为1。 81 | 82 | - 串的链式存储结构——块链结构 83 | 84 | ```C++ 85 | #define CHUNKSIZE 80 // 块的大小可自由定义 86 | struct Chunk{ 87 | char ch[CHUNKSIZE]; 88 | Chunk* next; 89 | }; 90 | 91 | struct LString{ 92 | Chunk *head, *tail; // 串的头指针和尾指针 93 | int curlen; // 串当前的长度 94 | }; // 字符串的块链结构 95 | ``` 96 | 97 | ### 4.3.3 串的模式匹配算法 98 | 99 | 子串的定位运算通常称为串的**模式匹配**或**串匹配**。即算法目的:确定主串中所含子串(模式串)第一次出现得位置。 100 | 101 | 串的模式匹配设有两个字符串S和T,设S为主串,也称正文串;设T为子串,也称为模式。在主串S中查找与模式T相匹配的子串,如果匹配成功,确定相匹配的子串中的第一个字符在主串S中出现的位置。 102 | 103 | 算法种类包括有:BF算法、KMP算法。 104 | 105 | 1. BF算法 106 | 107 | Brute-Force简称BF算法,亦称简单匹配算法,采用穷举法的思路:从S的每一个字符开始依次与T的字符进行匹配。 108 | 109 | > 算法设计思想 110 | 111 | Index(S, T, pos) 112 | 113 | 1. 将主串的第pos个字符和模式串的第一个字符比较, 114 | 115 | - 若相等,继续逐个比较后续字符; 116 | 117 | - 若不等,从主串的下一字符起,重新与模式串的第一个字符比较。 118 | 119 | 2. 直到主串的一个连续子串字符序列与模式串相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。 120 | 121 | 3. 否则,匹配失败,返回值0 122 | 123 | > 算法4.1——BF算法描述 124 | 125 | ```C++ 126 | int Index_BF(SString S, SString T){ 127 | int i = 1, j = 1; 128 | while(i <= S.lenght && j <= T.length){ 129 | if(S.ch[i] == T.ch[j]){ 130 | ++i; 131 | ++j; 132 | }else{ 133 | // 主串、子串指针回溯重新开始下一次匹配 134 | i = i - j + 2; 135 | j = 1; 136 | } 137 | } 138 | if(j >= T.lenght) return i - T.lenght; 139 | else return 0; 140 | } 141 | ``` 142 | 143 | 当m远小于n时,算法复杂度为O(n*m),平均复杂度为O(nm/2)。 144 | 145 | 2. KMP算法 146 | 147 | KMP算法,利用已经部分匹配的结果而加快模式串的滑动速度,且主串S的指针i**不必回溯**!可提速到O(n+m) 148 | 149 | 重点在于定义`next[j]`函数,表明当模式中第j个字符与主串中相应字符失配时,在模式中需要重新和主串中该字符进行比较的字符的位置。 150 | 151 | ![next_j]() 152 | 153 | > KMP算法 154 | 155 | ```C++ 156 | int Index_KMP(SString S, SString T, int pos){ 157 | int i = pos, j = 1; 158 | while(i < S.length && j < T.length){ 159 | if(j == 0 || S.ch[i] == T.ch[j]){ 160 | i++; 161 | j++; 162 | }else{ 163 | j = next[j]; // i不变,j后退 164 | } 165 | } 166 | if(j > T.length) return i - T.length; // 匹配成功 167 | else return 0; 168 | } 169 | 170 | void get_next(SString T, int& next[]){ 171 | int i = 1, next[1] = 0, j = 0; 172 | while(i < T.lenght){ 173 | if(j == 0 || T.ch[i] == T.ch[j]){ 174 | ++i; 175 | next[i] = j; 176 | }else{ 177 | j = next[j]; 178 | } 179 | } 180 | } 181 | ``` 182 | 183 | > next计算 184 | 185 | Next值就是字符串s的最长相同前缀和后缀子字符串的长度。 186 | 187 | 默认第一个和第二个字符的next值为0、1。那么从第三个开始依次执行如下操作: 188 | 189 | 找到**当前要求next字符的前一个**(称为这个字符),以它为标准,找到其next对应下标的字符(称为现在字符),**和这个字符做比较**。若相等,那么当前字符的next值就位此字符next**加1**;若不等,继续找现在字符next所指的下一个字符,还是和之前的字符比较,直到找到第一个位置为止,那么next为1。 190 | 191 | > nextVal计算 192 | 193 | NextVal值就是字符串s的的最长相同且满足后续字符不同的前缀和后缀子字符串的长度。 194 | 195 | 在求出next的值的基础上,求nextval的方法其实很简单。默认第一个nextval的值是0,第二个字符如果和第一个字符相等,那么它的nextval的值就为0,不等就为1。之后遵循如下方法: 196 | 197 | **找到当前要求nextval值的字符,看它的[next值]下标所指向的字符是否和它相等,相等那么nextval为当前所指下标的nextval值,不相等nextval的值就为本身字符的next值。** 198 | 199 | 对next进行修正,改进: 200 | 201 | ```C++ 202 | void get_nextval(SString T, int& nextval[]){ 203 | int i = 1, nextval[1] = 0, j = 0; 204 | while(i < T.lenght){ 205 | if(j == 0 || T.ch[i] == T.ch[j]){ 206 | ++i; 207 | ++j; 208 | if(T.ch[i] != T.ch[j]) nextval[i] = j; 209 | else nextval[i] = nextval[j]; 210 | }else{ 211 | j = nextval[j]; 212 | } 213 | } 214 | } 215 | ``` 216 | 217 | ## 4.4 数组 218 | 219 | ### 4.4.1 数组定义和特点 220 | 221 | 数组:按一定格式排列起来的具有相同类型的数据元素的集合。 222 | 223 | > 一维数组 224 | 225 | 一维数组:若线性表中的数据元素为非结构的简单元素,则称为一维数组。 226 | 227 | 一维数组的逻辑结构:线性结构。定长的线性表。 228 | 229 | 声明格式:`数据类型 变量名称[长度]`; 230 | 231 | > 二维数组 232 | 233 | 二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。 234 | 235 | 二维数组的逻辑结构: 236 | 237 | - 非线性结构:每一个数据元素既在一个行表中,又在一个列表中。 238 | 239 | - 线性结构定长的线性表:该线性表的每个数据元素也是一个定长的线性表。 240 | 241 | 声明格式:`数据类型 变量名称[行数][列数]`; 242 | 243 | > 多维数组 244 | 245 | 三维数组:若二维数组中的元素又是一个一维数组,则称作三维数组。 246 | 247 | n维数组:若n-1维数组中的元素又是一个一维数组结构,则称作n维数组。 248 | 249 | 结论:线性表结构是数组结构的一个特例,而数组结构又是线性表结构的扩展。 250 | 251 | 数组特点:**结构固定**,定义后,维数和维界不再改变。 252 | 253 | 数组基本操作:除了结构的初始化和销毁之外,只有**取元素**和**修改元素值**的操作。 254 | 255 | ### 4.4.2 数组的抽象数据类型定义 256 | 257 | 以二维数组为例,二维数组的抽象数据类型的数据对象和数据关系的定义: 258 | 259 | n=2(维数为2,二维数组) 260 | 261 | b1:第1维长度(行数) 262 | 263 | b2:第2维长度(列数) 264 | 265 | aj1j2:第1维下标为j1,第2维下标为j2 266 | 267 | ### 4.4.3 数组的顺序存储 268 | 269 | 在数组存储过程中,数组可以是多维的,但存储数据元素的内存单元地址是一维的,因此,在存储数组结构之前,需要解决将多维关系映射到一维关系的问题。 270 | 271 | 针对二维数组,可以以行序为主序(C、PASCAL、JAVA、Basic);也可以是列序为主序(FORTRAN)。 272 | 273 | ![n维数组元素地址](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/n%E7%BB%B4%E6%95%B0%E7%BB%84.png) 274 | 275 | ### 4.4.4 特殊矩阵的压缩存储 276 | 277 | 矩阵的常规存储:将矩阵描述为一个二维数组。 278 | 279 | 矩阵的常规存储的特点:可以对其元素进行随机存取;矩阵运算非常简单;存储的密度为1。 280 | 281 | 但是对于不适宜常规存储的矩阵:值相同的元素很多且呈某种规律分布;零元素多。特殊矩阵主要包括**对称矩阵、三角矩阵和对角矩阵**等,以及稀疏矩阵:矩阵中非零元素的个数较少(一般小于5%) 282 | 283 | 因此可以采用矩阵压缩存储:为多个相同的非零元素只分配一个存储空间,对零元素不分配空间。 284 | 285 | 1. 对称矩阵: 286 | 287 | 特点:在nxn的矩阵a中,满足aij = aji 288 | 289 | 存储方法:只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。 290 | 291 | 对称矩阵可以以行序为主序将元素存放在一个一维数组`sa[n(n+1)/2]`中,其中aij存放在(i*(i-1)/2)+(j-1)位置。 292 | 293 | 2. 三角矩阵 294 | 295 | 特点:特点对角线以下(或者以上)的数据元素(不包括对角线全部为常数c。 296 | 297 | 存储方法:重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素空间。 298 | 299 | 3. 对角矩阵 300 | 301 | 特点:在nxn的方阵中,所有非零元素都集中在以主对角线为中心的带状区域中,区域外的值全为0,则称为对角矩阵。常见的有三对角矩阵、五对角矩阵、七对角矩阵等。 302 | 303 | 存储方法:可按某个原则(或以行为主,或以对角线的顺序)将其压缩存储到一维数组上。 304 | 305 | 4. 稀疏矩阵存储 306 | 307 | 稀疏矩阵:在mxn的矩阵中,有超过95%的元素位零,非零元素仅不到5%。 308 | 309 | - 三元组法: 310 | 311 | 三元组顺序表,又称为**有序的双下标法** 312 | 313 | 压缩存储原则:可通过三元组法来表示稀疏矩阵,三元组:(i, j, aij)。即存各非零元素的值、行列位置和矩阵的行列数。 314 | 315 | 其中,为更为可靠描述,通常会加上总体的描述信息:总行数、总列数、非零元素总个数。 316 | 317 | 优点:非零元在表中按行序有序存储,因此**便于进行依行顺序处理的矩阵运算**。 318 | 319 | 缺点:**不能随机存取**。若按行号存取某一行中的非零元,则需从头开始进行查找。 320 | 321 | - 十字链表法 322 | 323 | 稀疏矩阵的链式存储结构: 324 | 325 | 优点:它能够**灵活地插入**因运算而产生的新的非零元素,**删除**因运算而产生的新的零元素,实现矩阵的各种运算。 326 | 327 | 在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了(row, col, value)以外,还要有两个域: 328 | 329 | - right:用于链接同一行中的下一个非零元素; 330 | 331 | - down:用以链接同一列中的下一个非零元素。 332 | 333 | 同时,为了方便元素查找,可为每一行、每一列添加头指针结点。 334 | 335 | ## 4.5 广义表 336 | 337 | ### 4.5.1 广义表的定义 338 | 339 | 广义表是线性表的推广,也称为列表。广泛地用于人工智能等领域的表处理语言LISP语言,把广义表作为基本的数据结构,就连程序也表示为一系列的广义表。 340 | 341 | 广义表(又称列表Lists)是n≥0个元素a0, a1, a2, ..., an-1的有限序列,其中每一个ai可以是单个元素,也可以是广义表,分别称为广义表LS的原子和子表。 342 | 343 | 广义表通常记作:LS=(a1, a2, ..., an),其中:LS为表名,n为表的长度,每一个元素ai为表的元素。 344 | 345 | 习惯上,一般用大写字母表示广义表,小写字母表示原子。 346 | 347 | 表头:若LS非空,则第一个元素a1就是表头,记作head(LS)=a1。其中,表头可以是原子,也可以是子表。 348 | 349 | 表尾:除表头外的其他元素组成的表。记作tail(LS) = (a2,...,an)。其中表尾不是一个元素,而实一个子表。 350 | 351 | ```C++ 352 | A = () // 空表,长度为零 353 | B = (()) // 长度为1,表头、表尾均为() 354 | C = (a, (b, c)) // 长度为2,由原子a和子表(b,c)构成,表头为a;表尾为((b, c)) 355 | D = (x, y, z) // 长度为3,每一项都是原子,表头为x,表尾为(y, z) 356 | E = (C, D) // 长度为2,每一项都是子表,表头为C,表尾为(D) 357 | F = (a, F) // 长度为2,第一项为原子,第二项为本身。表头为a;表尾为(F) 358 | ``` 359 | 360 | > 广义表的性质 361 | 362 | 1. 广义表中的数据元素有相对次序;一个直接前驱和一个直接后继 363 | 364 | 2. **广义表的长度**:定义为最外层所包含元素的个数; 365 | 366 | 3. **广义表的深度**:定义为该广义表展开后所含括号的重数;其中,原子的深度为0,空表的深度为1。 367 | 368 | 4. 广义表可以为其他广义表共享; 369 | 370 | 5. 广义表可以是一个递归的表,即广义表也可以是其本身的一个子表。 371 | 372 | 6. 广义表是一个多层次的结构,可以用图形象地表示。 373 | 374 | > 广义表与线性表的区别 375 | 376 | **广义表可以看成是线性表的推广,线性表是广义表的特例**。 377 | 378 | 广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构。 379 | 380 | **当二维数组的每行(或每列)作为子表处理时,二维数组即为一个广义表**。 381 | 382 | 另外,树和有向图也可以用广义表来表示。由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。 383 | 384 | > 广义表的运算 385 | 386 | 1. 取表头GetHead(LS): 取出的表头为非空广义表的第一个元素,它可以是一个单原子,也可以是一个子表。 387 | 388 | 2. 取表尾GetTail(LS): 取出的表尾为除去表头之外,由其余元素构成的表。即**表尾一定是一个广义表**。 389 | 390 | ### 4.5.2 广义表的存储 391 | 392 | 由于广义表中的数据元素可以有不同的结构(或是原子,或是列表),因此难以用顺序存储结构表示,通常采用链式存储结构。常用的链式存储结构有两种,**头尾链表的存储结构**和**扩展线性链表的存储结构**。 393 | 394 | ## 4.6 案例分析与实现 395 | 396 | ## 4.7 习题 397 | 398 | > 算法设计题 399 | 400 | 1. 写一个算法统计在输入字符串中各个不同字符出现的频度并将结果存入文件(字符串中的合法字符为A~Z这26个字母和0~9这10个数字)。 401 | 402 | [算法设计题1](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/Chapter4Exe/AlgoDesignExe1.cpp) 403 | 404 | 2. 写一个递归算法来实现字符串逆序存储,要求不另设串存储空间。 405 | 406 | [算法设计题2](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/Chapter4Exe/AlgoDesignExe2.cpp) 407 | 408 | 3. 编写算法,实现下面函数的功能。函数`void insert(char*s, char* t, int pos)`将字符串t插入到字符串s中,插入位置为pos。假设分配给字符串s的空间足够让字符串t插入。(说明:不得使用任何库函数) 409 | 410 | [算法设计题3](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/Chapter4Exe/AlgoDesignExe3.cpp) 411 | 412 | 4. 已知字符串S1中存放一段英文,写出算法format(s1, s2, s3, n),将其按给定的长度n格式化成两端对齐的字符串S2,其多余的字符送S3。 413 | 414 | [算法设计题4](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/Chapter4Exe/AlgoDesignExe4.cpp) 415 | 416 | 5. 设二维数组a[1...m, l...n]含有m x n个整数。 417 | 418 | 1. 写一个算法判断a中所有元素是否互不相同?输出相关信息(yes/no); 419 | 420 | 2. 试分析算法的时间复杂度。 421 | 422 | [算法设计题5](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/Chapter4Exe/AlgoDesignExe5.cpp) 423 | 424 | 6. 设任意n个整数存放于数组A[1..n]中,试编写算法,将所有正数排在所有负数前面(要求:算法时间复杂度为O(n))。 425 | 426 | [算法设计题6](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/Chapter4Exe/AlgoDesignExe6.cpp) 427 | -------------------------------------------------------------------------------- /Chapter4 String/next[j].png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/next[j].png -------------------------------------------------------------------------------- /Chapter4 String/n维数组.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/n维数组.png -------------------------------------------------------------------------------- /Chapter4 String/串值的链表存储方式.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter4 String/串值的链表存储方式.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe1.cpp: -------------------------------------------------------------------------------- 1 | int LeafNodeCount(BiTree T) 2 | { 3 | if(T == NULL) 4 | { 5 | return 0; 6 | } 7 | else if(T->lchild == NULL && T->rchild == NULL) 8 | { 9 | return 1; 10 | } 11 | else 12 | { 13 | return LeafNodeCount(T->lchild) + LeafNodeCount(T->rchild); 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe2.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe2.cpp -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe3.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe3.cpp -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe4.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe4.cpp -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe5.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe5.cpp -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe6.cpp: -------------------------------------------------------------------------------- 1 | int Level(BiTree bt) 2 | { 3 | int num = 0; 4 | if(bt) 5 | { 6 | queue q; 7 | q.push(bt); 8 | while(!q.empty()) 9 | { 10 | p = q.front(); 11 | q.pop(); 12 | if((p->lchild && !p->rchild) || (!p->lchild && p->rchild)) 13 | { 14 | num++; 15 | } 16 | if(p->lchild) q.push(p->lchild); 17 | if(p->rchild) q.push(p->rchild); 18 | } 19 | } 20 | return num; 21 | } -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe7.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe7.cpp -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/Chapter5Exe/AlgoDesignExe8.cpp: -------------------------------------------------------------------------------- 1 | void AllPath(BiTNode *p, TElemType *data, int len) 2 | { 3 | if (p) 4 | { 5 | if (p->lchild == NULL && p->rchild == NULL) 6 | { 7 | visit(p); 8 | for (int i = len - 1; i >= 0; i--) 9 | printf("%d ", data[i]); 10 | 11 | printf("\n"); 12 | } 13 | 14 | else 15 | { 16 | data[len++] = p->data; 17 | AllPath(p->lchild, data, len); 18 | AllPath(p->rchild, data, len); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/README.md: -------------------------------------------------------------------------------- 1 | # 第5章 树和二叉树 2 | 3 | ## 5.1 树与二叉树的定义 4 | 5 | ### 5.1.1 树的定义 6 | 7 | 树(非线性结构)是以分支关系定义的层次结构:结点之间有分支、具有层次关系。 8 | 9 | > 树的定义 10 | 11 | 树(Tree)是n(n≥0)个结点的**有限集**。 12 | 13 | - 若n=0,称为空树; 14 | 15 | - 若n>0,则它满足如下两个条件: 16 | 17 | 1. **有且仅有一个**特定的称为根(Root)的结点; 18 | 19 | 2. **其余结点**可分m(m≥0)个互不相交的有限集T1,T2,T3...Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。 20 | 21 | 树的结构定义是一个递归的定义,即在树的定义中又用到树的定义,它道出了树的固有特性。树还可有其他的表示形式:嵌套集合表示 22 | (即是一些集合的集体,对于其中任何两个集合,或者不相交,或者一个包含另一个);广义表的形式表示,根作为由子树森林组成的表的名字写在表的左边;凹入表示法(类似书的编目)。 23 | 24 | ### 5.1.2 树的基本术语 25 | 26 | ![树结构示例](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E6%A0%91%E7%BB%93%E6%9E%84%E7%A4%BA%E4%BE%8B.png) 27 | 28 | 1. **根节点**:非空树中无前驱结点的结点; 29 | 30 | 2. **结点**:树中的一个独立单元。包含一个数据元素及若于指向其子树的分支,如图中的A 、B 、C 、D等。 31 | 32 | 3. **结点的度**:结点拥有的子树数称为结点的度。例如,A的度为3,C的度为1,F的度为0。 33 | 34 | 4. **树的度**:树的度是树内各结点度的最大值。图中所示的树的度为3。 35 | 36 | 5. **叶子**:度为0的结点称为叶子或终端结点。结点K、L、F、G、M、I、J都是树的叶子。 37 | 38 | 6. **非终端结点**:度不为0的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。 39 | 40 | 7. **双亲和孩子**:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B的双亲为A,B的孩子有E和F。 41 | 42 | 8. **兄弟**:同一个双亲的孩子之间互称兄弟。例如,H、I和J互为兄弟。 43 | 44 | 9. **祖先**:从根到该结点所经分支上的所有结点。例如,M的祖先为A、D和H。 45 | 46 | 10. **子孙**:以某结点为根的子树中的任一结点都称为该结点的子孙。如B的子孙为E、K、L和F。 47 | 48 | 11. **层次**:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加1。 49 | 50 | 12. **堂兄弟**:双亲在同一层的结点互为堂兄弟。例如,结点G与E、F、H、I、J互为堂兄弟。 51 | 52 | 13. **树的深度**:树中结点的最大层次称为**树的深度或高度**。图中所示的树的深度为4。 53 | 54 | 14. **有序树和无序树**:如果将树中结点的各子树从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。 55 | 56 | 15. **森林**:是m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以用森林和树相互递归的定义来描述树。 57 | 58 | > 树结构和线性结构的比较 59 | 60 | ![树结构和线性结构的比较](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E6%A0%91%E7%BB%93%E6%9E%84%E5%92%8C%E7%BA%BF%E6%80%A7%E7%BB%93%E6%9E%84%E7%9A%84%E6%AF%94%E8%BE%83.png) 61 | 62 | ### 5.1.3 二叉树的定义 63 | 64 | 引入二叉树的原因: 65 | 66 | - 二又树的结构最简单,规律性最强; 67 | 68 | - 可以证明,所有树都能转为唯一对应的二叉树,不失一般性。 69 | 70 | 二叉树是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及**两棵互不相交**的分别称作这个根的左子树和右子树的二又树组成。 71 | 72 | 1. 每个结点最多有俩孩子,二叉树中不存在度大于2的结点。 73 | 74 | 2. 子树有左右之分,其次序不能颠倒。 75 | 76 | 3. 二叉树可以是空集合,根可以有空的左子树或空的右子树。 77 | 78 | 二叉树的递归定义表明二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。由于这两棵子树也是二叉树,则由二叉树的定义,它们也可以是空树。 79 | 80 | 需要注意的是:**二叉树不是树的特殊情况,与树是两个概念**。 81 | 82 | 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要区分,说明它是左子树,还是右子树。 83 | 84 | 树中当结点只有一个孩子时,就无须区分它是左还是右的次序。因此二者是不同的。这是二叉树与树的最主要的差别。 85 | 86 | 因此,考虑到二叉树的特点,思考:具有三个结点的二叉树可能有几种不同形态?普通树有几种不同形态? 87 | 88 | 二叉树有五种形态;树有两种形态: 89 | 90 | ![二叉树](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E4%BA%94%E7%A7%8D%E5%BD%A2%E6%80%81.png) 91 | 92 | ![树](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E6%A0%91%E7%9A%84%E4%B8%A4%E7%A7%8D%E5%BD%A2%E6%80%81.png) 93 | 94 | > 二叉树的5种基本形态 95 | 96 | 二叉树的5种基本形态包括有:空二叉树、根和空的左右子树、根和左子树、根和右子树、根和左右子树。 97 | 98 | ![二叉树的5种基本形态](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E4%BA%94%E7%A7%8D%E5%9F%BA%E6%9C%AC%E5%BD%A2%E6%80%81.png) 99 | 100 | ## 5.2 案例引入 101 | 102 | > 案例5.1:数据压缩的问题 103 | 104 | 将数据文件转换成由0、1组成的二进制串,称之为编码。 105 | 106 | 等长编码方案、不等长编码方案1、不等长编码方案2 107 | 108 | > 案例5.2:利用二叉树求解表达式的值 109 | 110 | 一般情况下,一个表达式由一个运算符和两个操作数构成,两个操作数之间有次序之分,并且操作数本身也可以是表达式,这个结构类似于二叉树,因此可以利用二叉树来表示表达式。 111 | 112 | ## 5.3 树和二叉树的抽象数据类型定义 113 | 114 | 根据树的结构定义,加上树的一组基本操作就构成了树的抽象数据类型定义。 115 | 116 | ADT BinaryTree{ 117 | // 数据对象D: 118 | // 数据关系R: 119 | // 基本操作P: 120 | }ADT BinaryTree; 121 | 122 | ## 5.4 二叉树的性质和存储结构 123 | 124 | ### 5.4.1 二叉树的性质 125 | 126 | 性质1:在二叉树的第i层上至多有2^(i-1)个结点(i≥1) 127 | 128 | 性质2:深度为k的二叉树一共至多有2^k-1 个结点(k≥1) 129 | 130 | 性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2+1。即叶子结点的数目等于度为2的结点数为n2,再加1。 131 | 132 | > 两种特殊形式的二叉树 133 | 134 | 1. 满二叉树 135 | 136 | 满二叉树:深度为k且含有2^k-1个结点的二叉树。 137 | 138 | 特点: 139 | 140 | - 每一层上的结点数都是最大结点数(即每层都满) 141 | 142 | - 叶子节点全部在最底层。 143 | 144 | 对满二叉树结点位置进行编号,按照从根结点开始,自**上而下,自左而右**进行编号,可以发现每一结点位置都有元素。 145 | 146 | 满二叉树在同样深度的二叉树中**结点个数最多**。 147 | 148 | 满二叉树在同样深度的二叉树中**叶子结点个数最多**。 149 | 150 | 2. 完全二叉树 151 | 152 | 完全二叉树:深度为k的,有n个结点的二叉树,当且仅当其**每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时**,称之为完全二叉树。 153 | 154 | 同理,在满二叉树中,从最后一个结点开始,**连续去掉任意**个结点,即是一棵完全二叉树。 155 | 156 | 完全二叉树的特点是: 157 | 158 | - 叶子结点只可能在层次最大的两层上出现; 159 | 160 | - 对任一结点,若其右分支下的子孙(即右子树)的最大层次为i,则其左分支下的子孙(左子树)的最大层次必为i或i+1。 161 | 162 | 性质4:具有n个结点的完全二叉树的深度为⌊log2n⌋ + 1。 163 | 164 | 性质5:如果对一棵有n个结点的完全二叉树(其深度为⌊log2n⌋+ 1)的结点按层序编号(从第1层到第⌊log2n⌋+1层,每层从左到右),则对任一结点i(1≤i≤n),有: 165 | 166 | 1. 如果i=1,则结点1是二叉树的根,无双亲;如果i>1,则其双亲是结点⌊i/2⌋。 167 | 168 | 2. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。 169 | 170 | 3. 如果2i+1>n,则结点无右孩子;否则其右孩子是结点2i+1。 171 | 172 | 性质5表明了完全二叉树中**双亲结点编号与孩子结点编号**之间的关系。 173 | 174 | ### 5.4.2 二叉树的存储结构 175 | 176 | 二叉树的存储结构主要有顺序存储结构和链式存储结构,其中链式存储结构又分为二叉链表和三叉链表。 177 | 178 | 二叉树的顺序存储实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素。 179 | 180 | ```C++ 181 | // 二叉树顺序存储表示 182 | #define MAXSIZE 100 183 | Typedef TElemType SqBiTree[MAXSIZE]; 184 | SqBiTree bt; 185 | ``` 186 | 187 | 二叉树的顺序存储缺点:存储密度低,适合存储满二叉树和完全二叉树。 188 | 189 | > 二叉树的链式存储结构 190 | 191 | ```C++ 192 | typedef struct BiNode{ 193 | TElemType data; 194 | BiNode *lchild, *rchild; 195 | }BiNode, *BiTree; 196 | ``` 197 | 198 | 在n个结点的二叉表中,有n+1个空指针域。 199 | 200 | 三叉链表即在二叉链表的基础上增加一个指向父节点的指针。 201 | 202 | ```C++ 203 | typedef struct TriNode{ 204 | TElemType data; 205 | TriNode *lchild, *parent, *rchild; 206 | }TriNode, *TriTree; 207 | ``` 208 | 209 | ## 5.5 遍历二叉树和线索二叉树 210 | 211 | ### 5.5.1 遍历二叉树 212 | 213 | - 遍历定义:顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。 214 | 215 | - 遍历目的:得到树中所有结点的一个线性排列。 216 | 217 | - 遍历用途:它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。 218 | 219 | - 遍历方法:规定在先左后右的情况下,主要遍历方法有三种:先序遍历、中序遍历、后续遍历。 220 | 221 | 1. 三种遍历方法描述如下: 222 | 223 | ![遍历方法描述](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E9%81%8D%E5%8E%86%E6%96%B9%E6%B3%95%E5%8C%BA%E5%88%AB.png) 224 | 225 | 1. 先序遍历 226 | 227 | 访问步骤,若二叉树为空,则空操作;否则: 228 | 229 | 1. 访问根节点; 230 | 231 | 2. 先续遍历左子树; 232 | 233 | 3. 先续遍历右子树。 234 | 235 | 2. 中序遍历 236 | 237 | 访问步骤,若二叉树为空,则空操作;否则: 238 | 239 | 1. 中序遍历左子树; 240 | 241 | 2. 访问根结点; 242 | 243 | 3. 中序遍历右子树。 244 | 245 | 3. 后续遍历 246 | 247 | 访问步骤,若二叉树为空,则空操作;否则: 248 | 249 | 1. 后续遍历左子树; 250 | 251 | 2. 后续遍历右子树; 252 | 253 | 3. 访问根结点。 254 | 255 | 2. 根据遍历顺序确定二叉树 256 | 257 | - 若二叉树中各结点的值均不同,则二叉树结点的先序序列、中序序列、后序列都是唯一的。 258 | 259 | - 由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以确定唯一一棵二叉树 260 | 261 | 例题:已知二叉树的先序和中序序列,构造出相应的二叉树:**可以先由先序序列确定根,由中序确定左右子树**。 262 | 263 | 已知中序序列和后序序列,**由后序遍历可知,根结点必在后续序列尾部**。 264 | 265 | 3. 遍历的算法实现——先序遍历 266 | 267 | 二叉树先续遍历算法(递归) 268 | 269 | ```C++ 270 | status PreOrderTraverse(BiTree T){ 271 | if(T == NULL) return OK; // 空二叉树 272 | else{ 273 | visit(T);// 访问根结点 274 | PreOrderTraverse(T->lchild); // 递归遍历左子树 275 | PreOrderTraverse(T->rchild); // 递归遍历右子树 276 | } 277 | } 278 | ``` 279 | 280 | 4. 遍历算法实现——中序遍历 281 | 282 | 二叉树中序遍历算法(递归) 283 | 284 | ```C++ 285 | status InOrderTraverse(BiTree T){ 286 | if(T == NULL) return OK; // 空二叉树 287 | else{ 288 | InOrderTraverse(T->lchild); // 递归遍历左子树 289 | visit(T);// 访问根结点 290 | InOrderTraverse(T->rchild); // 递归遍历右子树 291 | } 292 | } 293 | ``` 294 | 5. 遍历算法实现——后序遍历 295 | 296 | 二叉树后序遍历算法(递归) 297 | 298 | ```C++ 299 | status PostOrderTraverse(BiTree T){ 300 | if(T == NULL) return OK; // 空二叉树 301 | else{ 302 | PostOrderTraverse(T->lchild); // 递归遍历左子树 303 | PostOrderTraverse(T->rchild); // 递归遍历右子树 304 | visit(T);// 访问根结点 305 | } 306 | } 307 | ``` 308 | 309 | 6. 遍历算法分析 310 | 311 | - 如果去掉输出语句,从递归的角度看,三种算法是完全相同的,或说这三种算法的访问路径是相同的,只是访问结点的时机不同。 312 | 313 | - 时间效率:O(n);每个结点只访问一次 314 | 315 | - 空间效率:O(n);栈占用的最大辅助空间 316 | 317 | 7. 遍历二叉树的非递归算法 318 | 319 | 中序遍历非递归算法:二叉树中序遍历的非递归算法的关键在于:**在中序遍历过某节点的整个左子树后,如何找到该接点的根以及右子树**。 320 | 321 | 基本思想: 322 | 323 | - 建立一个空栈S,指针p指向根结点 324 | 325 | - 申请一个结点空间q,用来存放栈顶弹出的元素。 326 | 327 | - 当p非空或者栈S非空时,循环执行以下操作: 328 | 329 | - 如果p非空,则将p进栈,p指向该结点的左孩子; 330 | 331 | - 如果p为空,则弹出栈顶元素并访问,将p指向该结点的右孩子。 332 | 333 | 中序遍历非递归算法实现: 334 | 335 | ```C++ 336 | Status InOrderTraverse(BiTree T){ 337 | BiTree p; InitStack(S); p = T; 338 | while(p || !StackEmpty(S)){ 339 | if(p){ // 不为空 340 | Push(S, p); 341 | p = p->lchild; 342 | } 343 | else{ 344 | Pop(S, q); 345 | print("%c", q->data); 346 | p = q->rchild; 347 | } 348 | } 349 | return OK; 350 | } 351 | ``` 352 | 353 | 无论是递归还是非递归遍历二叉树,因为每个结点被访问一次,则不论按哪一种次序进行遍历,对含n个结点的二叉树,其时间复杂度均为O(n)。所需辅助空间为遍历过程中栈的最大容量,即树的深度,最坏情况下为n,则空间复杂度也为O(n)。 354 | 355 | 8. 二叉树的层次遍历 356 | 357 | 对于一颗二叉树,从根结点开始,按**从上到下、从左到右**的顺序访问每一个结点。每一个结点仅仅访问一次。 358 | 359 | 算法思路: 360 | 361 | - 将根结点入队; 362 | 363 | - 队不为空时循环:从队列中出列一个结点*p,访问它: 364 | 365 | - 若它右左孩子结点,将左孩子结点入队; 366 | 367 | - 若它有右孩子结点,将右孩子结点入队。 368 | 369 | 二叉树层次遍历算法实现: 370 | 371 | ```C++ 372 | typedef struct{ 373 | BTNode data[MaxSize]; // 存放队中元素 374 | int front, rear; // 队头和队尾指针 375 | }SqQueue; // 顺序循环队列类型 376 | 377 | void LevelOrder(BTNode *b){ 378 | BTNode* p; 379 | SqQueue* qu; 380 | InitQueue(sqQueue); // 初始化队列 381 | enQueue(qu, b); // 根结点指针入队 382 | while(!QueueEmpty(qu)){ 383 | // 队不为空,则循环 384 | deQueue(qu, p); // 出队结点p 385 | cout << p->data; 386 | if(p->lchild != NULL){ 387 | // 有左孩子时将其入队 388 | enQueue(qu, p->lchild); 389 | } 390 | if(p->rchild != NULL){ 391 | // 有右孩子时将其入队 392 | enQueue(qi, p->rchild); 393 | } 394 | } 395 | } 396 | ``` 397 | 398 | 9. 二叉树遍历算法的应用——二叉树的建立(算法5.3) 399 | 400 | 按照先续遍历序列建立二叉树的二叉链表 401 | 402 | 算法步骤: 403 | 404 | - 扫描字符序列,读入字符,建立二叉树的存储结构; 405 | 406 | - 如果ch是一个"#"字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作:在建立二叉树的过程中按照二叉树先序方式建立: 407 | 408 | - 申请一个结点空间T; 409 | 410 | - 将ch赋给T->data; 411 | 412 | - 递归创建T的左子树 413 | 414 | - 递归创建T的右子树 415 | 416 | 算法实现: 417 | 418 | ```C++ 419 | void CreateBiTree(BiTree& T){ 420 | // 按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T 421 | cin >> ch; 422 | if(ch == "#") T = NULL; // 递归结束,建空树 423 | else{ 424 | // 递归创建二叉树 425 | T = new BiTree; // 生成根结点 426 | T->data = ch; // 根结点数据域置为ch 427 | CreateBiTree(T->lchild);// 递归创建左子树 428 | CreateBiTree(T->rchild);// 递归创建右子树 429 | } 430 | } 431 | ``` 432 | 433 | 10. 二叉树遍历算法的应用——复制二叉树(算法5.4) 434 | 435 | 算法步骤: 436 | 437 | 如果是空树,递归结束,否则执行以下操作: 438 | 439 | - 申请一个新结点空间, 复制根结点; 440 | 441 | - 递归复制左子树; 442 | 443 | - 递归复制右子树。 444 | 445 | 算法实现: 446 | 447 | ```C++ 448 | void Copy(BiTree T, BiTree& NewT){ 449 | // 复制一棵和T完全相同的二叉树 450 | if(T == NULL){ 451 | // 如果是空树,递归结束 452 | newT = NULL; 453 | return; 454 | } 455 | else{ 456 | newT = new BoTree; 457 | newT->data = T->data;// 复制根结点 458 | Copy(T->lchild, newT->lchild);// 递归复制左子树 459 | Copy(T->rchild, newT->rchild);// 递归复制左子树 460 | } 461 | } 462 | ``` 463 | 464 | 11. 二叉树遍历算法的应用——计算二叉树的深度(算法5.5) 465 | 466 | 如果是空树,递归结束,深度为0,否则执行以下操作: 467 | 468 | - 递归计算左子树的深度记为m; 469 | 470 | - 递归计算右子树的深度记为n; 471 | 472 | - 如果m大于n,二叉树的深度为m+1,否则为n+1。 473 | 474 | 算法实现: 475 | 476 | ```C++ 477 | int Depth(BiTree T){ 478 | // 计算二叉树T的深度 479 | if(T == NULL) return 0; 480 | else{ 481 | m = Depth(T->lchild); // 递归计算左子树的深度记为m 482 | n = Depth(T->rchild); // 递归计算右子树的深度记为n 483 | if(m > n) return(m+1); // 二叉树深度为m与n的较大者加1 484 | else{ 485 | return (n+1); 486 | } 487 | } 488 | } 489 | ``` 490 | 491 | 计算二叉树的深度是在后序遍历二叉树的基础上进行的运算。 492 | 493 | 12. 二叉树遍历算法的应用——统计二叉树结点的个数(算法5.6) 494 | 495 | 如果是空树,则结点个数为0;否则,结点个数为左子树的结点个数加上右子树的结点个数再加上1。 496 | 497 | 算法实现: 498 | 499 | ```C++ 500 | int NodeCount(BiTree T){ 501 | if(T == NULL) return 0; 502 | else{ 503 | return NodeCount(T->lchild) + NodeCount(T->rchild) + 1; 504 | } 505 | } 506 | ``` 507 | 508 | 13. 二叉树遍历算法的应用——统计二叉树叶子结点的个数(补充算法) 509 | 510 | 如果是空树,则结点个数为0;否则,结点个数为左子树的叶子结点个数加上右子树的叶子结点个数再加上1。 511 | 512 | 算法实现: 513 | 514 | ```C++ 515 | int LeafCount(BiTree T){ 516 | if(T == NULL) return 0; 517 | if(T->lchild == NULL && T->rchild == NULL) 518 | { 519 | // 如果是叶子节点返回1 520 | return 1; 521 | } 522 | else{ 523 | return LeafCount(T->lchild) + LeafCount(T->rchild); 524 | } 525 | } 526 | ``` 527 | 528 | ### 5.5.2 线索二叉树 529 | 530 | 1. 线索二叉树的基本概念 531 | 532 | 遍历二叉树是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序序列、中序序列或后序序列。 533 | 534 | 但是,当以二叉链表作为存储结构时,只能找到结点的左、右孩子信息,而不能直接得到结点**在任一序列中的前驱和后继信息**,这种信息只有在遍历的动态过程中才能得到,为此**引入线索二叉树来保存这些在动态过程中得到的(任一序列中的)有关前驱和后继的信息**。 535 | 536 | 由于有n个结点的二叉链表中必定存在n+1个空链域,因此可以充分利用这些空链域来存放结点的前驱和后继信息。可做如下规定:若结点有左子树,则其lchild域指示其左孩子,**否则令lchild域指示其前驱**;若结点有右子树,则其rchild域指示其右孩子,**否则令rchild域指示其后继**。同时,为了避免混淆,尚需改变结点结构,增加两个标志域:LTag和RTag,其中标志位为0表示存储的为左/右孩子,为1表示存储的为前驱/后继。 537 | 538 | 这种改变指向的指针称为“线索”,加上了线索的二叉树称为线索二叉树(Threaded Binary Tree),对二叉树按某种遍历次序使其变为线索二叉树的过程叫**线索化**。 539 | 540 | ```C++ 541 | // 二叉树的二叉线索存储表示 542 | typedef struct BiThrNode 543 | { 544 | TElemType data; 545 | BiThrNode *lchild, *rchild; 546 | int LTag, RTag; 547 | }BiThrNode, *BiThrTree; 548 | ``` 549 | 550 | 以下为先序线索二叉树、中序线索二叉树、后序线索二叉树示意图。 551 | 552 | ![先序线索二叉树](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E5%85%88%E5%BA%8F%E7%BA%BF%E7%B4%A2%E4%BA%8C%E5%8F%89%E6%A0%91.png) 553 | 554 | ![中序线索二叉树]() 555 | 556 | ![后序线索二叉树](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E5%90%8E%E5%BA%8F%E7%BA%BF%E7%B4%A2%E4%BA%8C%E5%8F%89%E6%A0%91.png) 557 | 558 | 为了方便起见,仿照线性表的存储结构,在二叉树的线索链表上也添加一个头结点,并令其**lchild域的指针指向二叉树的根结点**,其**rchild域的指针指向遍历遍历时访问的最后一个结点**;同时,**令二叉树遍历序列中第一个结点的lchild域指针和最后一个结点rchild域的指针均指向头结点**。这好比为二叉树建立了一个双向线索链表,既可从第一个结点起顺后继进行遍历,也可从最后一个结点起顺前驱进行遍历。 559 | 560 | 即其中LTag = 0,lchild指向根结点;RTag = 1,rchild指向遍历序列中最后一个结点。 561 | 562 | 2. 构造线索二叉树 563 | 564 | TODO 565 | 566 | ## 5.6 树和森林 567 | 568 | 树(Tree)是n(n≥0)个结点的有限集。若n=0,称为空树。 569 | 570 | 若n>0,当有且仅有一个特定的称为根(root)的结点;其余结点可分为m(m≥0)个互不相交的有限集T1,T2,T3,...,Tm 571 | 572 | 森林是m(m≥0)个互不相交的树的集合。 573 | 574 | ### 5.6.1 树的存储结构 575 | 576 | 1. 双亲表示法 577 | 578 | 这种表示方法中,以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置。 579 | 580 | ![树的双亲表示法](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/%E6%A0%91%E7%9A%84%E5%8F%8C%E4%BA%B2%E8%A1%A8%E7%A4%BA%E6%B3%95.png) 581 | 582 | 双亲表示法特点:找双亲结点容易,找孩子结点难。 583 | 584 | 类型描述: 585 | 586 | ```C++ 587 | struct PTNode{ 588 | TElemType data; 589 | int parent; // parent结点位置 590 | }; 591 | 592 | #define MAX_TREE_SIZE 100 593 | struct PTree{ 594 | PTNode nodes[MAX_TREE_SIZE]; 595 | int r, n; // 根结点的位置和节点个数 596 | }; 597 | ``` 598 | 599 | 2. 孩子表示法 600 | 601 | 把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。 602 | 603 | 特点:找孩子容易,找双亲难。 604 | 605 | 类型描述: 606 | 607 | ```C++ 608 | // 孩子结点结构 609 | struct CTNode{ 610 | int child; 611 | CTNode* next; 612 | }; 613 | typedef CTNode *ChildPtr; 614 | // 双亲结点结构 615 | struct CTBox{ 616 | TElemType data; 617 | ChildPtr firstChild; // 孩子链表头指针 618 | } 619 | // 树结构 620 | struct CTree{ 621 | CTBox nodes[MAX_TREE_SIZE]; 622 | int n, r; 623 | }; 624 | ``` 625 | 626 | 可以把双亲表示法和孩子表示法结合起来,即将双亲表示和孩子链表合在一起,称为带双亲的孩子链表。 627 | 628 | 3. 孩子兄弟表示法(二叉树表示法、二叉链表表示法) 629 | 630 | 又称二叉树表示法,或**二叉链表表示法**,即以二叉链表做树的存储结构。链表中结点的两个链域分别指向该结点的**第一个孩子结点**和**下一个兄弟结点**,分别命名为firstchild域和nextsibling域。 631 | 632 | 类型描述: 633 | 634 | ```C++ 635 | struct CSNode{ 636 | ElemType data; 637 | CSNode *firstChild, *nextsibling; 638 | }; 639 | typedef CSNode *CSTree; 640 | ``` 641 | 642 | 这种存储结构的优点是它和二叉树的二叉链表表示完全一样,便于将一般的树结构转换为二叉树进行处理,利用二叉树的算法来实现对树的操作。因此孩子兄弟表示法是应用较为普遍的一种树的存储表示方法。 643 | 644 | ### 5.6.2 森林与二叉树的转换 645 | 646 | - 将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操T。 647 | 648 | - 由于树和二叉树都可以用二叉链表作存储结构,则**以二叉链表作媒介**可以导出树与二叉树之间的一个对应关系。 649 | 650 | 给定一棵树,可以找到唯一的以可二叉树与之对应。这个一一对应的关系说明**森林或树与二叉树可以相互转换**。 651 | 652 | 1. 将树转换为二叉树 653 | 654 | 1. 加线:在兄弟之间加一连线; 655 | 656 | 2. 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系; 657 | 658 | 3. 旋转:以树的根结点为轴心,将整树顺时针转45° 659 | 660 | 树变二叉树:**兄弟相连留长子** 661 | 662 | 2. 将二叉树转换为树 663 | 664 | 1. 加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子......沿分支找到的所有右孩子,都p的双亲用线连起来; 665 | 666 | 2. 抹线:抹掉原二叉树中双亲与右孩子之间的连线; 667 | 668 | 3. 调整:将结点按层次排列,形成树结构 669 | 670 | 二叉树变树:**左孩右右连双亲,去掉原来右孩线** 671 | 672 | 3. 森林转换称二叉树 673 | 674 | 1. 将各棵树分别转换成二叉树 675 | 676 | 2. 将每棵树的根结点用线相连 677 | 678 | 3. 以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构 679 | 680 | 森林变二叉树:**树变二叉根相连** 681 | 682 | 4. 二叉树转换成森林 683 | 684 | 1. 抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树; 685 | 686 | 2. 还原:将孤立的二叉树还原成树 687 | 688 | 二叉树变森林:**去掉全部右孩线,孤立二叉再还原**。 689 | 690 | ### 5.6.3 树和森林的遍历 691 | 692 | 1. 树的遍历(三种方式) 693 | 694 | 1. 先根(次序)遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。 695 | 696 | 2. 后根(次序)遍历:若树不空,则先依次后根遍历各棵子树,然后再访问根结点。 697 | 698 | 3. 层次遍历:若树不空,则自上而下自左至右访问树中每个结点。 699 | 700 | 2. 森林的遍历 701 | 702 | 将森林看作由三部分构成:1、森林中第一棵树的根结点;2.森林中第一棵树的子树森林;3.森林中其它树构成的森林。 703 | 704 | 1. 先序遍历 705 | 706 | 若森林不为空,则 707 | 708 | 1. **访问森林中第一棵树的根结点**; 709 | 710 | 2. 先序遍历森林中第一棵树的子树森林; 711 | 712 | 3. 先序遍历森林中(除第一棵树之外)其余树构成的森林。 713 | 714 | 2. 中序遍历 715 | 716 | 若森林不为空,则 717 | 718 | 1. 中序遍历森林中第一棵树的子树森林; 719 | 720 | 2. 访问森林中第一棵树的根结点; 721 | 722 | 3. 中序遍历森林中(除第一棵树之外)其余树构成的森林。 723 | 724 | ## 5.7 哈夫曼树及其应用 725 | 726 | ### 5.7.1 哈夫曼树的基本概念 727 | 728 | 哈夫曼(Huffman)树又称**最优树,是一类带权路径长度最短的树**,哈夫曼树的定义,涉及路径、路径长度、权等概念。 729 | 730 | - **路径**:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。 731 | 732 | - **路径长度**:路径上的**分支数目**称作路径长度。 733 | 734 | - **树的路径长度**:从树根到每一结点的路径长度之和。 735 | 736 | 在结点数目相同的二叉树中,**完全二叉树的路径长度最短的二叉树**。但路径长度最短的二叉树不一定就是完全二叉树。 737 | 738 | - **权**:将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。在数据结构中,实体有结点(元素)和边(关系)两大类,所以对应有**结点权和边权**。结点权或边权具体代表什么意义,由具体情况决定。如果在一棵树中的结点上带有权值,则对应的就有带权树等概念。 739 | 740 | - **结点的带权路径长度**:从该结点到树根之间的**路径长度与结点上权的乘积**。 741 | 742 | - **树的带权路径长度**:树中所有**叶子结点**的带权路径长度之和,通常记作WPL。 743 | 744 | - **哈夫曼树**:假设有m个权值{w1, W2,…,Wm},可以构造一棵含n个叶子结点的二叉树,每个叶子结点的权为W;则其中**带权路径长度WPL最小的二叉树称做最优二叉树或哈夫曼树**。 745 | 746 | 带权路径最短的树,比较的前提是所有树的度相同,即树的各结点度的最大值,即结点拥有的子树数。 747 | 748 | **满二叉树不一定是曼哈夫树**,哈夫曼树中权越大的叶子离根越近,具有相同带权结点的哈夫曼树不唯一。 749 | 750 | ### 5.7.2 哈夫曼树的构造算法 751 | 752 | 1. 哈夫曼树的构造过程 753 | 754 | 在构造哈夫曼树时,**首先选择权小的**,这样保证权大的离根较近,这样一来,在计算树的带权路径长度时,自然会得到最小带权路径长度,这种生成算法是一种典型的**贪心法**。 755 | 756 | 1. 根据给定的n个权值{W1, W2, ..., Wn},构造n棵只有根结点的二叉树,这n棵二叉树构成一个森林F。**构造森林全是根**。 757 | 758 | 2. 在森林F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。**选用两小造新树**。 759 | 760 | 3. 在森林F中删除这两棵树,同时将新得到的二叉树加入F中。**删除两小添新人**。 761 | 762 | 4. 重复(2)和(3),直到F只含一棵树为止。这棵树便是哈夫曼树。**重复2、3剩单根**。 763 | 764 | 特点: 765 | 766 | 包含n棵树的森林要经过n-1次合并才能形成哈去曼树,共产生n-1新结点,且这n-1个新结点都是具有两个孩子的分支结点。。 767 | 768 | - **包含n个叶子结点的哈夫曼树中共有2n-1个结点**。 769 | 770 | - **哈夫曼树的结点的度数为0或2,没有度为1的结点**。 771 | 772 | 2. 哈夫曼算法的实现 773 | 774 | 哈夫曼树是一种二叉树,由于哈夫曼树中没有度为1的结点,则一棵有n个叶子结点的哈夫曼树共有2n-1个结点,可以存储在一个大小为2n-1的一维数组中。树中每个结点还要包含其**双亲信息**和**孩子结点的信息**。 775 | 776 | ```C++ 777 | typedef struct{ 778 | int weight; 779 | int parent, lch, rch; 780 | }HTNode, *HuffmanTree; 781 | ``` 782 | 783 | 哈夫曼树中共有2n-1个结点不使用0下标,数组大小为2n。将叶子结点集中存储在前面部分1~n个位置,而后面的n-1个位置存储其余非叶子结点。 784 | 785 | > 构造哈夫曼树——算法5.10 786 | 787 | 构造哈夫曼树算法的实现可以分成两大部分。 788 | 789 | 1. 初始化:首先动态申请2n个单元;然后循环2n-1次,从1号单元开始,依次将1至2n-1所有单元中的双亲、左孩子、右孩子的下标都初始化为0;最后再循环n次,输入前n个单元中叶子结点的权值。 790 | 791 | 2. 创建树:循环n-1次,通过n-1次的选择、删除与合并来创建哈夫曼树。 792 | 793 | - 选择是从当前森林中选择双亲为0且权值最小的两个树根结点s1和s2; 794 | 795 | - 删除是指将结点s1和s2的双亲改为非0; 796 | 797 | - 合并就是将s1和s2的权值和作为一个新结点的权值依次存入到数组的第n+1之后的单元中,同时记录这个新结点左孩子的下标为s1,右孩子的下标为s2。 798 | 799 | 算法实现: 800 | 801 | ```C++ 802 | void CreateHuffmanTree(HuffmanTree HT, int n) 803 | { 804 | if(n <= 1) return; 805 | m = 2 * n - 1; 806 | HT = new HTNode[m+1]; // 0号单元未用, 807 | for(int i = 1; i <= m; i++) 808 | { 809 | // 将1~m号单元中的双亲、左孩子,右孩子的下标都初始化为0 810 | HT[i].lch = 0; 811 | HT[i].rch = 0; 812 | HT[i].parent = 0; 813 | } 814 | for(int i = 1; i <= n; i++) 815 | { 816 | // 输入前n个元素的weight值 817 | cin >> HT[i].weight; 818 | } 819 | /*-初始化工作结束,下面开始创建哈夫曼树-*/ 820 | for(int i = n+1; i <= m; i++) 821 | { 822 | Select(HT, i-1, s1, s2); 823 | // 在HT[k](1≤k≤i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2 824 | HT[s1].parent = i; 825 | HT[s2].parent = i; 826 | // 得到新结点i,从森林中删除s1和s2,将s1和s2的双亲域由0改为i 827 | HT[i].lch = s1; 828 | HT[i].rch = s2; 829 | HT[i].weight = HT[s1].weight + HT[s2].weight; 830 | } 831 | } 832 | ``` 833 | 834 | ### 5.7.3 哈夫曼编码 835 | 836 | 在进行数据压缩时,为了使压缩后的数据文件尽可能短,可采用不定长编码。其基本思想是:为出现次数较多的字符编以较短的编码。为确保对数据文件进行有效的压缩和对压缩文件进行正确的解码,可以利用哈夫曼树来设计二进制编码。 837 | 838 | 关键:要设计长度不等的编码,则必须使**任一字符的编码都不是另一个字符的编码的前缀**。上述编码方式又称为前缀编码。 839 | 840 | 前缀编码:如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码。 841 | 842 | 哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。 843 | 844 | 哈夫曼编码的性质: 845 | 846 | 1. **哈夫曼编码是前缀编码**。 847 | 848 | 2. **哈夫曼编码是最优前缀编码**。 849 | 850 | 哈夫曼编码实现方法: 851 | 852 | 1. 统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)。 853 | 854 | 2. 利用哈夫曼树的特点:权越大的叶子离根越近;**将每个字符的概率值作为权值,构造哈夫曼树**。则概率越大的结点,路径越短。 855 | 856 | 3. 在哈夫曼树的每个分支上标上0或1: 857 | 858 | - 结点的左分支标0,右分支标1 859 | 860 | - 把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。 861 | 862 | 两个问题: 863 | 864 | 1. 为什么哈夫曼编码能够保证是前缀编码?。 865 | 866 | 因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀 867 | 868 | 2. 为什么哈夫曼编码能够保证字符编码总长最短? 869 | 870 | 因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。 871 | 872 | > 哈夫曼编码的算法实现 873 | 874 | 在构造哈夫曼树之后,求哈夫曼编码的主要思想是:**依次以叶子为出发点,向上回溯至根结点为止**。回溯时走左分支则生成代码0,走右分支则生成代码1。 875 | 876 | 算法步骤: 877 | 878 | 1. 分配存储n个字符编码的编码表空间HC,长度为n+1;分配临时存储每个字符编码的动态数组空间cd,cd[n-1]置为'\0'。 879 | 880 | 2. 逐个求解n个字符的编码,循环n次,执行以下操作: 881 | 882 | - 设置变量start用于记录编码在cd中存放的位置,start初始时指向最后,即编码结束符位置n-1; 883 | 884 | - 设置变量c用于记录从叶子结点向上回溯至根结点所经过的结点下标,c初始时为当前待编码字符的下标i,f用于记录i的双亲结点的下标; 885 | 886 | - 从叶子结点向上回溯至根结点,求得字符i的编码,当f没有到达根结点时,循环执行以下操作: 887 | 888 | - 回溯一次start向前指一个位置,即--start; 889 | 890 | - 若结点c是f的左孩子,则生成代码0,否则生成代码1,生成的代码0或1保存在cd[start]中; 891 | 892 | - 继续向上回溯,改变c和f的值。 893 | 894 | - 根据数组cd的字符串长度为第i个字符编码分配空间HC[i],然后将数组cd中的编码复制到HC[i]中。 895 | 896 | 3. 释放临时空间cd。 897 | 898 | 算法描述: 899 | 900 | ```C++ 901 | void CreateHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n) 902 | { 903 | // 从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中 904 | HC = new char*[n+1]; // 分配存储n个字符编码的编码表空间 905 | cd = new char[n]; // 分配临时存放每个字符编码的动态数组空间 906 | cd[n-1] = '\0'; // 编码结束符 907 | for(int i = 1; i <= n; i++) // 逐个字符求哈夫曼编码 908 | { 909 | start = n-1; // start开始时指向最后,即编码结束符位置 910 | c = i; 911 | f = HT[i].parent; // f指向结点c的双亲结点 912 | while(f != 0) // 从叶子节点开始向上回溯,知道根结点 913 | { 914 | --start; // 回溯一次start位置向前指一个位置 915 | if(HT[f].lch == c) // 结点c是f的左孩子,则生成代码0 916 | { 917 | cd[start] = '0'; 918 | } 919 | else // 结点c是f的右孩子,则生成代码1 920 | { 921 | cd[start] = '1'; 922 | } 923 | c = f; 924 | f = HT[f].parent; // 继续向上回溯 925 | } // 求出第i个字符的编码 926 | HC[i] = new char[n-start]; // 为第i个字符编码分配空间 927 | strcpy(HC[i], &cd[start]); 928 | } 929 | delete cd; 930 | } 931 | ``` 932 | 933 | > 文件的编码和译码 934 | 935 | 1. 编码 936 | 937 | 有了字符集的哈夫曼编码表之后,对数据文件的编码过程是:依次读入文件中的字符c,在哈夫曼编码表HC中找到此字符,将字符c转换为编码表中存放的编码串。 938 | 939 | 2. 译码 940 | 941 | 对编码后的文件进行译码的过程必须借助于哈夫曼树。具体过程是:依次读入文件的二进制码,从哈夫曼树的根结点(即HT[m])出发,若当前读入0,则走向左孩子,否则走向右孩子。一旦到达某一叶子HT[i]时便译出相应的字符编码HC[i]。然后重新从根出发继续译码,直至文件结束。 942 | 943 | ## 5.8 案例分析与实现 944 | 945 | ## 5.9 习题 946 | 947 | 1. 利用二叉链表存储树, 则根结点的右指针()。 948 | 949 | A. 指向最左孩子 B. 指向最右孩子 C. 为空 D. 非空 950 | 951 | 解答:C,利用二叉链表存储树时,右指针指向兄弟结点,因为根节点没有兄弟结点,故根结点的右指针为空 952 | 953 | 2. 一棵非空的二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树一定满足()。 954 | 955 | A. 所有的结点均无左孩子 B. 所有的结点均无右孩子 C. 只有一个叶子结点 D. 是任意一棵二叉树 956 | 957 | 解答:因为先序遍历结果是“中左右”, 序遍历结果是“左右中”,当没有左子树时,就是“中右”和“右中”;当没有右子树时,就是“中左”和“左中”。则所有的结点均无左孩子或所有的结点均无右孩子均可,所以A、B不能选,又所有的结点均无左孩子与所有的结点均无右孩子时,均只有一个叶子结点,故选C。 958 | 959 | 3. 在一棵度为4的树T中,若有20个度为4的结点,10个度为3的结点,1个度为2的结点,10个度为1的结点,则树T的叶结点个数是( ) 960 | 961 | A.41 B.82 C.113 D.122 962 | 963 | 解答:考查树结点数的特性。设树中度为i(i=0,1,2,3,4)的结点数分别为Ni,树中结点总数为N,则树中各结点的度之和等于N-1,即N=1+N1+2N2+3N3+4N4=N0+N1+N2+N3+N4。根据题设中的数据,即可得到N0=82,即树T的叶结点的个数是82。 964 | 965 | 算法设计题: 966 | 967 | 1. 统计二叉树的叶结点个数。 968 | 969 | [算法设计题1](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe1.cpp) 970 | 971 | 2. 判别两棵树是否相等。 972 | 973 | [算法设计题2](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe2.cpp) 974 | 975 | 3. 交换二叉树每个结点的左孩子和右孩子。 976 | 977 | [算法设计题3](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe3.cpp) 978 | 979 | 4. 设计二叉树的双序遍历算法(双序遍历是指对于二叉树的每一个结点来说,先访问这个结点,再按双序遍历它的左子树,然后再一次访问这个结点,接下来按双序遍历它的右子树)。 980 | 981 | [算法设计题4](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe4.cpp) 982 | 983 | 5. 计算二叉树最大的宽度(二叉树的最大宽度是指二叉树所有层中结点个数的最大值)。 984 | 985 | [算法设计题5](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe5.cpp) 986 | 987 | 6. 用按层次顺序遍历二叉树的方法,统计树中度为1的结点数目。 988 | 989 | [算法设计题6](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe6.cpp) 990 | 991 | 7. 求任意二叉树中第一条最长的路径长度,并输出此路径上各结点的值。 992 | 993 | [算法设计题7](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe7.cpp) 994 | 995 | 8. 输出二叉树中从每个叶子结点到根结点的路径。 996 | 997 | [算法设计题8](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/Chapter5Exe/AlgoDesignExe8.cpp) 998 | -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/二叉树的五种基本形态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/二叉树的五种基本形态.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/二叉树的五种形态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/二叉树的五种形态.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/先序线索二叉树.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/先序线索二叉树.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/后序线索二叉树.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/后序线索二叉树.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/树的两种形态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/树的两种形态.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/树的双亲表示法.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/树的双亲表示法.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/树结构和线性结构的比较.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/树结构和线性结构的比较.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/树结构示例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/树结构示例.png -------------------------------------------------------------------------------- /Chapter5 TreeAndBianryTree/遍历方法区别.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter5 TreeAndBianryTree/遍历方法区别.png -------------------------------------------------------------------------------- /Chapter6 Graph/README.md: -------------------------------------------------------------------------------- 1 | # 第6章 图 2 | 3 | 图是一种比线性表和树更为复杂的数据结构。在线性表中,数据元素之间仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继;在树形结构中,数据元素之间有着明显的层次关系,并且每一层中的数据元素可能和下一层中的多个元素(即其孩子结点)相关,但只能和上一层中一个元素(即其双亲结点)相关;而在图结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。在数据结构中,应用图论的知识讨论如何在计算机上实现图的操作,因此主要学习图的存储结构,以及若于图的操作的实现。 4 | 5 | ## 6.1 图的定义和基本术语 6 | 7 | ### 6.1.1 图的定义 8 | 9 | 图(Graph)G由两个集合V和E组成,记为G=(V,E): 10 | 11 | - V(vertex)是顶点的**有穷非空集合**; 12 | 13 | - E(Edge)是V中顶点偶对的**有穷集合**,这些顶点偶对称为**边**。 14 | 15 | V(G)和E(G)通常分别表示图G的顶点集合和边集合,E(G)可以为空集。若E(G)为空,则图G只有顶点而没有边。 16 | 17 | 对于图G,若边集E(G)为有向边的集合,则称该图为有向图;若边集E(G)为无向边的集合,则称该图为无向图。 18 | 19 | 对于图G,若图中任意两个点之间都有一条边相连,则称为**完全图**。对于n个顶点,则无向完全图有n(n-1)/2条边,有向完全图有n(n-1)条边。 20 | 21 | ### 6.1.2 图的基本术语 22 | 23 | 用n表示图中顶点数目,用e表示边的数目,下面介绍图结构中的一些基本术语。 24 | 25 | 1. **无向完全图**和**有向完全图**:对于无向图,若具有n(n- 1)/2条边,则称为无向完全图。对于有向图,若具有n(n-1)条弧,则称为有向完全图。 26 | 27 | 2. **稀疏图**和**稠密图**:有很少条边或弧(如e < nlogn)的图称为稀疏图,反之称为稠密图。 28 | 29 | 3. **权**和**网**:在实际应用中,每条边可以标上具有某种含义的数值,该数值称为该边上的权。这些权可以表示从一个顶点到另一个顶点的距离或耗费。**这种带权的图通常称为网**。 30 | 31 | 4. **邻接**:描述图中两个顶点之间的关系,有边/弧相连的两个顶点,称为两顶点邻接。 32 | 33 | 5. **邻接点**:对于无向图G,如果图的边(v, v')∈E,则称顶点v和v'互为邻接点,即v和v'相邻接。边(v, v')**依附于**顶点v和v',或者说边(v, v')与顶点v和v'**相关联**。在有向图中,存在``,则称**v邻接到v',v'邻接于v**。 34 | 35 | 6. **度**、**入度**和**出度**:顶点的度是指和v**相关联的边的数目**,记为TD(v)。对于有向图,顶点v的度分为入度和出度。入度是以顶点v为头的弧的数目,记为ID(v);出度是以顶点v为尾的弧的数目,记为OD(v),顶点v的度为TD(v) = ID(v) + OD(v)。 36 | 37 | 7. **路径**和**路径长度**:接续的边构成的顶点序列。在无向图G中,从顶点v到顶点v'的路径是一个顶点序列。如果G是有向图,则路径也是有向的。路径长度是一条路径上经过的边或弧的数目或权值之和。 38 | 39 | 8. **回路**或**环**:第一个顶点和最后一个顶点相同的路径称为回路或环。 40 | 41 | 9. **简单路径**、**简单回路**或**简单环**:序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。 42 | 43 | 10. **连通**、**连通图**和**连通分量**:在无向图G中,如果从顶点v到顶点v'有路径,则称v和v'是连通的。如果对于图中任意两个顶点Vi、Vj,Vi和Vj都是连通的,则称G是连通图。所谓连通分量,指的是无向图中的极大连通子图。其中极大连通子图意思是:**该子图是连通子图,将G的任何不在该子图中的顶点加入,子图不再连通**。 44 | 45 | 11. **强连通图**和**强连通分量**:在有向图G中,如果对于每一对Vi, Vj,从Vi到Vj和从Vj到Vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称作有向图的强连通分量。 46 | 47 | 12. 极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,该子图不再连通。 48 | 49 | 13. **连通图的生成树**:包含无向图G所有顶点的极小连通图;一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边,这样的连通子图称为连通图的生成树。 50 | 51 | 14. **有向树**和**生成森林**:有一个顶点的入度为0,其余顶点的入度均为1的有向图称为有向树。一个有向图的生成森林是由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。 52 | 53 | ## 6.2 案例引入 54 | 55 | ## 6.3 图的类型定义 56 | 57 | 图是一种数据结构,加上一组基本操作,就构成了抽象数据类型。抽象数据类型图的定义如下: 58 | 59 | ```C++ 60 | ADTGraph{ 61 | 数据对象: V是具有相同特性的数据元素的集合,称为顶点集。 62 | 数据关系: 63 | R = {VR} 64 | VR = {|v, w属于V,且P(v, w) 表示从v到w的弧,谓词P(v, w)定义了弧的意义或信息} 65 | 基本操作: 66 | 图的创建、增删改查等。其中重要的包括有构造图、深度优先搜索、广度优先搜索。 67 | ``` 68 | 69 | ## 6.4 图的存储结构 70 | 71 | 由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序存储结构,但**可以借助二维数组来表示元素之间的关系,即邻接矩阵表示法(数组表示法)**。另一方面,由于图的任意两个顶点间都可能存在关系,因此,用链式存储表示图是很自然的事,图的链式存储有多种,有**邻接表**、**十字链表**和**邻接多重表**,应根据实际需要的不同选择不同的存储结构。 72 | 73 | ### 6.4.1 邻接矩阵 74 | 75 | 1. 邻接矩阵表示法 76 | 77 | 建立一个**顶点表**(记录哥各个顶点信息)和一个**邻接矩阵**(表示各个顶点之间关系)。 78 | 79 | **邻接矩阵**(**Adjacency Matrix**)是表示顶点之间相邻关系的矩阵。设G(V, E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵: 80 | 81 | ```C++ 82 | A[i][j] = 1; 若∈E或者(i, j)∈E 83 | A[i][j] = 0; 否则 84 | ``` 85 | 86 | - 分析1:无向图的邻接矩阵是对阵的; 87 | - 分析2:顶点i的度=第i行(列)中的1的个数; 88 | - 分析3:完全图的邻接矩阵中,对焦元素为0,其余为1。 89 | 90 | 有向图的邻接矩阵需考虑边的方向。 91 | 92 | - 分析1:有向图的邻接矩阵可能是不对称的; 93 | - 分析2:顶点的出度=第i行元素之和,顶点的入度=第i列元素之和,顶点的度=第i行元素之和+第i列元素之和。 94 | 95 | 网(即有权图)的邻接矩阵表示法: 96 | 97 | ```C++ 98 | A[i][j] = Wij; 若或(vi, vj)∈VR 99 | A[i][j] = ∞; 无边(弧) 100 | 其中,Wij表示边上的权值;∞表示计算机允许的、大于所有边上权值的数。 101 | ``` 102 | 103 | 用邻接矩阵表示法表示图,除了一个用于存储邻接矩阵的二维数组外,还需要用一个一维数组来存储顶点信息。其形式说明如下: 104 | 105 | ```C++ 106 | //-----图的邻接矩阵存储表示----- 107 | #define MaxInt 32767 108 | #define MVNum 100 // 最大顶点数 109 | typedef char VerTexType; // 设顶点的数据类型为字符型 110 | typedef int ArcType; // 假设边的权值类型为整型 111 | typedef struct 112 | { 113 | VerTexType vexs[MVNum]; // 顶点表 114 | ArcType arcs[MVNum][MVNum]; // 邻接矩阵 115 | int vexnum, arcnum; // 图的当前点数和边数 116 | }AMGraph; 117 | ``` 118 | 119 | 2. 采用邻接矩阵表示法创建无向网 120 | 121 | 已知一个图的点和边,使用邻接矩阵表示法来创建此图的方法比较简单,下面以一个无向网为例来说明创建图的算法。 122 | 123 | 算法步骤: 124 | 125 | 1. 输入总顶点数和总边数。 126 | 2. 依次输入点的信息存入顶点表中。 127 | 3. 初始化邻接矩阵,使每个权值初始化为极大值。 128 | 4. 构造邻接矩阵。依次输入每条边依附的顶点和其权值,确定两个顶点在图中的位置之后,使相应边赋予相应的权值,同时使其对称边赋予相同的权值。 129 | 130 | 算法实现: 131 | 132 | ```C++ 133 | Status CreateUDN(AMGraph &G) 134 | { 135 | // 采用邻接矩阵表示法,创建无向网G 136 | cin >> G.vexnum >> G.arcnum; // 输入总顶点数,总边数 137 | for(int i = 0; i < G.vexnum; i++) 138 | { 139 | // 依次输入顶点信息 140 | cin >> G.vexs[i]; 141 | } 142 | // 初始化邻接矩阵,边的权值均置为极大值MaxInt 143 | for(int i = 0; i < G.vexnum; i++) 144 | { 145 | for(int j = 0; i < G.vexnum; j++) 146 | { 147 | G.arcs[i][j] = MaxInt; 148 | } 149 | } 150 | // 构造邻接矩阵 151 | for(int k = 0; k < G.arcnum; k++) 152 | { 153 | // 输人一条边依附的顶点及权值 154 | cin >> v1 >> v2 >> w; 155 | // 确定v1和v2在G中的位置,即顶点数组的下标 156 | i = LocateVex(G, v1); 157 | j = LocateVex(G, v2); 158 | // 边的权值为w 159 | G.arcs[i][j] = w; 160 | G.arcs[j][i] = G.arcs[i][j]; 161 | } 162 | return OK; 163 | } 164 | 165 | int LocatVex(AMGraph G, VerTexType u) 166 | { 167 | // 图G中查找顶点u,存在则返回顶点表中的下标;否则返回-1 168 | for(int i = 0; i < G.vexnum; i++) 169 | { 170 | if(u == G.vexs[i]) 171 | { 172 | return i; 173 | } 174 | } 175 | return -1; 176 | } 177 | ``` 178 | 179 | 该算法的时间复杂度是0(n^2)。 180 | 181 | 若要建立无向图,只需对上述算法做两处小的改动:**一是初始化邻接矩阵时,将边的权值均初始化为0;二是构造邻接矩阵时,将权值w改为常量值1即可**。同样,将该算法稍做修改即可建立一个有向网或有向图,即邻接矩阵为非对称矩阵。 182 | 183 | 3. 邻接矩阵表示法的优缺点 184 | 185 | > 优点 186 | 187 | 1. 便于判断两个顶点之间是否有边,即根据A[i][j] = 0或1来判断。 188 | 189 | 2. 方便找任意顶点的所有邻接点; 190 | 191 | 3. 便于计算各个顶点的度。对于无向图,邻接矩阵第i行元素之和就是顶点i的度;对于有向图,第i行元素之和就是顶点i的出度,第i列元素之和就是顶点i的入度。 192 | 193 | > 缺点 194 | 195 | 1. 不便于增加和删除顶点。 196 | 197 | 2. 不便于统计边的数目,需要扫描邻接矩阵所有元素才能统计完毕,时间复杂度为O(n^2) 198 | 199 | 3. 空间复杂度高。如果是有向图,n个顶点需要n^2个单元存储边。 200 | 201 | 如果是无向图,因其邻接矩阵是对称的,所以对规模较大的邻接矩阵可以采用压缩存储的方法,仅存储下三角(或上三角)的元素,这样需要n(n-1)/2个单元即可。 202 | 203 | ### 6.4.2 邻接表 204 | 205 | 1. 邻接表表示法 206 | 207 | **邻接表**(**Adjacency List**)是图的一种链式存储结构。在邻接表中,对图中每个顶点vi建立一个单链表,把与vi相邻接的顶点放在这个链表中。邻接表中每个单链表的第一个结点存放有关顶点的信息,把这一结点看成链表的表头,其余结点存放有关边的信息,这样邻接表便由两部分组成:**表头结点表**和**边表**。 208 | 209 | 1. **表头结点表**:由所有表头结点以顺序结构的形式存储,以便可以随机访问任一顶点的边链表。表头结点包括数据域(data)和链域(firstarc)两部分。其中,数据域用于存储顶点vi的名称或其他有关信息;链域用于指向链表中第一个结点(即与顶点vi邻接的第一个邻接点)。 210 | 211 | 2. **边表**:由表示图中顶点间关系的2n个边链表组成。边链表中边结点包括**邻接点域(adjvex)、数据域(info)和链域(nextarc)三部分**。其中,邻接点域指示与顶点vi邻接的点在图中的位置;数据域存储和边相关的信息,如权值等;链域指示与顶点vi邻接的下一条边的结点。 212 | 213 | 特点: 214 | 215 | - 邻接表不唯一; 216 | - 若**无向图**中有n个顶点、e条边,则其邻接表需n个头结点和2e个表结点。适合存储稀疏图; 217 | - **无向图**中顶点vi的度为第i个单链表中的结点数。 218 | 219 | 有向图的邻接表特点: 220 | 221 | - 顶点vi的出度为第i个单链表中的结点个数 222 | - 顶点vi的入读为整个单链表中邻接点域值是i-1的结点个数。 223 | 224 | 反之,逆邻接表记录的是入度,所以找入度易,找出度难。 225 | 226 | **当邻接表的存储结构形成后,图便惟一确定**。 227 | 228 | 根据上述讨论,要定义一个邻接表,需要先定义其存放顶点的头结点和表示边的边结点。图的邻接表存储结构说明如下: 229 | 230 | ```C++ 231 | #define MVNum 100 // 最大顶点数 232 | typedef struct ArcNode // 边结点 233 | { 234 | int adjvex; // 该边所指向的顶点的位置 235 | ArcNode* nextarc; // 指向下一条边的指针 236 | OtherInfo info; // 和边相关的信息 237 | }ArcNode; 238 | typedef struct VNode // 顶点信息 239 | { 240 | VerTexType data; 241 | ArcNode *firstarc; // 指向第一条依附于该顶点的边的指针 242 | }VNode, AdjList[MVNum]; // AdjList表示邻接表类型 243 | typedef struct ALGraph 244 | { 245 | AdjList vertices; // 邻接表数组 246 | int vexnum, arcnum; // 图的当前定点数和边数 247 | }; 248 | ``` 249 | 250 | 2. 采用邻接表表示法创建无向图 251 | 252 | 算法思想: 253 | 254 | 1. 输入总顶点数和总边数。 255 | 2. 依次输入点的信息存入顶点表中,使每个表头结点的指针域初始化为NULL。 256 | 3. 创建邻接表。依次输入每条边依附的两个顶点,确定这两个顶点的序号i和j之后,将此边结点分别插入vi和vj对应的两个边链表的头部。 257 | 258 | > 算法实现 259 | 260 | ```C++ 261 | Status CreateUDG(ALGraph &G) 262 | {// 采用邻接表表示法,创建无向图G 263 | cin >> G.vexnum >> G.arcnum; // 输入总顶点数,总边数 264 | for(int i = 0; i < G.vexnum; i++) 265 | {// 输入各点,构造表头结点表 266 | cin >> G.vertices[i].data; // 输入顶点值 267 | G.vertices[i].firstarc = NULL; // 初始化各表头结点的指针域为NULL 268 | } 269 | for(k = 0; k < G.arcnum; k++) 270 | {// 输入各边,构造邻接表 271 | cin >> v1 >> v2; 272 | // 确定v1和v2在G中的位置,即顶点在G.vertices中的序号 273 | i = LocateVex(G, v1); 274 | j = LocateVex(G, v2); 275 | p1 = new ArcNode; // 生成一个新的边结点 276 | p1->adjvex = j; // 邻接点序号为j 277 | // 将新结点p1插入顶点vi的边表头部 278 | p1->nextarc = G.vertices[i].firstarc; 279 | G.vertices[i].firstarc = p1; 280 | p2 = new ArcNode; // 生成另一个对称的新的边结点 281 | p2->adjvex = i; 282 | // 将新结点p2插入顶点vj的边表头部 283 | p2->nextarc = G.vertices[j].firstarc; 284 | G.vertices[j].firstarc = p2; 285 | } 286 | return OK; 287 | } 288 | ``` 289 | 290 | 该算法的时间复杂度是O(n + e)。 291 | 292 | 建立有向图的邻接表与此类似,只是更加简单,每读入一个顶点对序号,仅需生成一个邻接点序号为j的边表结点,并将其插入到vi;的边链表头部即可。若要创建网的邻接表,可以将边的权值存储在info域中。 293 | 294 | 3. 邻接表表示法的优缺点 295 | 296 | 优点: 297 | 298 | 1. 便于增加和删除顶点。 299 | 2. 便于统计边的数目,按顶点表顺序扫描所有边表可得到边的数目,时间复杂度为O(n + e)。 300 | 3. 空间效率高。对于一个具有n个顶点e条边的图G,若G是无向图,则在其邻接表表示中有n个顶点表结点和2e个边表结点;若G是有向图,则在它的邻接表表示或逆邻接表表示中均有n个顶点表结点和e个边表结点。因此,邻接表或逆邻接表表示的空间复杂度为O(n + e),适合表示稀疏图。对于稠密图,考虑到邻接表中要附加链域,因此常采取邻接矩阵表示法。 301 | 302 | 缺点: 303 | 304 | 1. 不便于判断顶点之间是否有边,要判定vi和vj之间是否有边,就需扫描第i个边表,最坏情况下要耗费O(n)时间。 305 | 2. 不便于计算有向图各个顶点的度。对于无向图,在邻接表表示中顶点vi的度是第i个边表中的结点个数。在有向图的邻接表中,第i个边表上的结点个数是顶点vi的出度,但求vi的入度较困难,需遍历各顶点的边表。若有向图采用逆邻接表表示,则与邻接表表示相反,求顶点的入度容易,而求顶点的出度较难。 306 | 307 | 4. 邻接矩阵与邻接表示法的关系 308 | 309 | 1. **联系**:邻接表中每个链表对应邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。 310 | 311 | 2. **区别**: 312 | 313 | - 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。 314 | 315 | - 邻接矩阵的空间复杂度为O(n^2),而领接表的空间复杂度为O(n+e); 316 | 317 | 3. **用途**:邻接矩阵多用于稠密图;而邻接表多用于稀疏图。 318 | 319 | ### 6.4.3 十字链表 320 | 321 | **十字链表**(**Orthogonal List**)是有向图的另一种链式存储结构。我们也可以把它看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表。 322 | 323 | **有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点**。 324 | 325 | 在弧结点中有5个域:其中尾域(tailvex)和头域(headvex)分别指示弧尾和弧头这两个顶点在图中的位置,链域hlink指向弧头相同的下一条弧,而链域tlink指向弧尾相同的下一条弧,info域指向该弧的相关信息。 326 | 327 | 头结点即顶点结点,它由3个域组成:其中data域存储和顶点相关的信息,如顶点的名称等;firstin和firstout为两个链域,分别指向以该顶点为弧头或弧尾的第一个弧结点。 328 | 329 | ### 6.4.4 邻接多重表 330 | 331 | **邻接多重表**(**Adjacency Multilist**)是无向图的另一种链式存储结构。 332 | 333 | 邻接多重表的结构和十字链表类似。在邻接多重表中,每一条边用一个结点表示。其中,mark为标志域,可用以标记该条边是否被搜索过;ivex和jvex为该边依附的两个顶点在图中的位置;ilink指向下一条依附于顶点ivex的边;jlink指向下一条依附于顶点jvex的边,info为指向和边相关的各种信息的指针域。 334 | 335 | 每一个顶点也用一个结点表示,其中,data域存储和该顶点相关的信息,firstedge域指示第一条依附于该顶点的边。 336 | 337 | ## 6.5 图的遍历 338 | 339 | 和树的遍历类似,图的遍历也是从图中某一顶点出发,按照某种方法对图中所有顶点访问且仅访问一次。图的遍历算法是求解图的连通性问题、拓扑排序和关键路径等算法的基础。 340 | 341 | 为避免重复访问,可设置辅助数组visited[n],用来标记每个被访问过的顶点。 342 | 343 | - 初始状态visited[i]为0; 344 | - 顶点i被访问,改visited[i]为1,防止被多次访问 345 | 346 | 根据搜索路径的方向,通常有两条遍历图的路径:深度优先搜索和广度优先搜索。它们对无向图和有向图都适用。 347 | 348 | ### 6.5.1 深度优先搜索 349 | 350 | 1. 深度优先搜索遍历的过程 351 | 352 | **深度优先搜索**(**DepthFirst Search, DFS**)遍历类似于树的先序遍历,是树的先序遍历的推广。对于一个连通图,深度优先搜索遍历的过程如下: 353 | 354 | 1. 从图中某个顶点v出发,访问v。 355 | 356 | 2. 找出刚访问过的顶点的第一个未被访问的邻接点,访问该顶点。以该顶点为新顶点,重复此步骤,直至刚访问过的顶点没有未被访问的邻接点为止。 357 | 358 | 3. 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,访问该顶点。 359 | 360 | 4. 重复步骤(2)和(3),直至图中所有顶点都被访问过,搜索结束。 361 | 362 | 2. 深度优先搜索遍历的算法实现 363 | 364 | 显然,深度优先搜索遍历连通图是一个递归的过程。为了在遍历过程中便于区分顶点是否已被访问,需附设访问标志数组visited[n],其初值为"false,一旦某个顶点被访问,则其相应的分量置为"true"。 365 | 366 | 算法6.3 深度优先搜索遍历连通图 367 | 368 | 算法步骤: 369 | 370 | 1. 从图中某个顶点v出发,访问v,并置visited[v]的值为true。 371 | 372 | 2. 依次检查v的所有邻接点w,如果visited[w]的值为false,再从w出发进行递归遍历,直到图中所有顶点都被访问过。 373 | 374 | ```C++ 375 | bool visited[MVNum]; 376 | void DFS(Graph G, int v) 377 | { 378 | cout << v; 379 | visited[v] = true; 380 | for(w = FitstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w)) 381 | if(!visited[w]) 382 | DFS(G, w); 383 | } 384 | ``` 385 | 386 | 算法6.4 深度优先搜索遍历非连通图 387 | 388 | ```C++ 389 | void DFSTracerse(Graph G) 390 | { 391 | for(v = 0; v < G.vexnum; ++v) 392 | { 393 | visited[v] = false; 394 | } 395 | for(v = 0; v < G.vexnum; ++v) 396 | { 397 | if(!visited[v]) 398 | DFS(G, v); 399 | } 400 | } 401 | ``` 402 | 403 | 算法6.5 采用邻接矩阵表示图的深度优先搜索遍历 404 | 405 | ```C++ 406 | void DFs(AMGraph G, int v) 407 | { 408 | cout << v; 409 | visited[v] = true; 410 | for(int w = 0; w < G.vexnum; w++) 411 | { 412 | if((G.arcs[v][w] != 0) && (!visited[w])) 413 | { 414 | DFS(G, w); 415 | } 416 | } 417 | } 418 | ``` 419 | 420 | 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在的行,时间复杂度为O(n^2)。 421 | 422 | 算法6.6 采用邻接表表示图的深度优先搜索遍历 423 | 424 | ```C++ 425 | void DFS_AL(ALGraph G, int v) 426 | {// 图G为邻接表类型,从第v个顶点出发深度优先搜索遍历图G 427 | cout << v; 428 | visited[v] = true; 429 | p = G.vertices[v].firstarc; 430 | while(p != NULL) 431 | { 432 | w = p->adjvex; // 表示w是v的邻接点 433 | if(!visited[w]) 434 | DFS(G,w); 435 | p = p->nextarc; 436 | } 437 | } 438 | ``` 439 | 440 | 用邻接表来表示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问个头结点的时间,时间复杂度为O(n+e)。 441 | 442 | 因此,**稠密图适于在邻接矩阵上进行深度遍历**,**稀疏图适于在邻接表上进行深度遍历**。 443 | 444 | ### 6.5.2 广度优先搜索 445 | 446 | 1. 广度优先搜索遍历的过程 447 | 448 | **广度优先搜索**(**Breadth First Search, BFS**)遍历类似于树的按层次遍历的过程。广度优先搜索遍历的过程如下: 449 | 450 | 1. 从图中某个顶点v出发,访问v。 451 | 452 | 2. 依次访问v的各个未曾访问过的邻接点。 453 | 454 | 3. 分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤(3),直至图中所有已被访问的顶点的邻接点都被访问到。 455 | 456 | 2. 广度优先搜索遍历的算法实现 457 | 458 | 算法6.7 广度优先搜索遍历连通图 459 | 460 | 算法步骤: 461 | 462 | 1. 从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队。 463 | 464 | 2. 只要队列不空,则重复下述操作: 465 | 466 | - 队头顶点u出队; 467 | 468 | - 依次检查u的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队。 469 | 470 | 算法描述: 471 | 472 | ```C++ 473 | void BFS(Graph G, int v) 474 | { 475 | cout << v; 476 | visited[v] = true; 477 | InitQueue(Q); 478 | EnQueue(Q, v); //v入队 479 | while(!QueueEmpty(Q)) 480 | { 481 | DeQueue(Q, u); // 队首元素出队并置为u 482 | for(w = FirstAdjVex(G, u); w >= 0; NextAdjVex(G, u, w)) 483 | { 484 | if(!visited[w]) 485 | { 486 | cout << w; 487 | visited[w] = true; 488 | EnQueue(Q, w); 489 | } 490 | } 491 | } 492 | } 493 | ``` 494 | 495 | 遍历图的过程实质上是通过边找邻接点的过程,因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,即当用邻接矩阵存储时,时间复杂度为O(n^2);用邻接表存储时,时间复杂度为O(n+e)。 496 | 497 | ### 6.5.3 DFS与BFS算法效率比较 498 | 499 | - 空间复杂度相同,都是O(n)(借用了堆栈或队列); 500 | 501 | - 时间复杂度只与存储结构、(邻接矩阵或邻接表)有关,而与搜索路径无关。 502 | 503 | ## 6.6 图的应用 504 | 505 | ### 6.6.1 最小生成树 506 | 507 | 生成树的共同特点: 508 | 509 | - 顶点个数与图的顶点个数相同; 510 | 511 | - 是图的极小连通子图,去掉一条边则非连通; 512 | 513 | - 一个有n个顶点的连通图的生成树有n-1条边; 514 | 515 | - **在生成树中再加一条边必然形成回路**; 516 | 517 | - 生成树中的任意两个顶点间的路径是惟一的。 518 | 519 | 在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树称为该连通网的**最小代价生成树**(**Minimum Cost Spanning Tree**),简称为**最小生成树**。 520 | 521 | 构造最小生成树有多种算法,其中多数算法利用了最小生成树的下列一种简称为MST的性质:假设N=(V, E)是一个连通网,U是顶点集V的一个非空子集。若(u, v)是一条具有最小权值(代价)的边,其中u∈U,v∈V-U,则必存在一棵包含边(u, v)的最小生成树。 522 | 523 | MST性质解释: 524 | 525 | 在生成树的构造过程中,图中n个顶点分属两个集合: 526 | 527 | - 已落在生成树上的顶点集:U 528 | 529 | - 尚未落在生成树上的顶点集:V-U 530 | 531 | 接下来则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边。 532 | 533 | 普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法是两个利用MST性质构造最小生成树的算法。下面先介绍普里姆算法。 534 | 535 | 1. 普里姆算法 536 | 537 | 普里姆算法的构造过程: 538 | 539 | 假设N=(V,E)是连通网,TE是N上最小生成树中边的集合。 540 | 541 | 1. U = {u0}(u0∈V), TE = {}。 542 | 543 | 2. 在所有u∈U, v∈V- U的边(u,v)∈E中找一条权值最小的边(u0,v0)并入集合TE,同时v0并入U。 544 | 545 | 3. 重复步骤2,直至U = V为止。 546 | 547 | 此时TE中必有n-1条边,则T= (V, TE)为N的最小生成树。 548 | 549 | 普里姆算法的算法实现: 550 | 551 | 假设一个无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,要求输出T的各条边。为实现这个算法需附设一个辅助数组closedge,以记录从U到V-U具有最小权值的边。对每个顶点vi∈V- U,在辅助数组中存在一个相应分量closedge[i-1], 它包括两个域:lowcost和adjvex,其中Iowcost存储最小边上的权值,adjvex存储最小边在U中的那个顶点。显然,closedge[i-1].lowcost = Min{ cost(u, vi)|u∈U},其中cost(u,v)表示赋于边(u,v)的权。 552 | 553 | ```C++ 554 | // 辅助数组的定义,用来记录从顶点集U到V-U的权值最小的边 555 | 556 | struct 557 | { 558 | VerTexType adjvex; // 最小边在U中的那个顶点 559 | ArcType lowcost; // 最小边上的权值 560 | }closedge[MVNum]; 561 | ``` 562 | 563 | 算法6.8 普里姆算法 564 | 565 | 1. 首先将初始顶点u加入U中,对其余的每一个顶点Vj,将closedge[j]均初始化为到u的边息。 566 | 567 | 2. 循环n - 1次,做如下处理: 568 | 569 | - 从各组边closedge中选出最小边closedge[k],输出此边; 570 | 571 | - 将k加入U中; 572 | 573 | - 更新剩余的每组最小边信息closedge[j],对于V-U中的边,新增加了一条从k到j的边,如果新边的权值比closedge[i].lowcost小,则将closedge[j].lowcost 更新为新边的权值。 574 | 575 | 算法描述: 576 | 577 | ```C++ 578 | void MiniSpanTree_Prim(AMGraph G, VerTexType u) 579 | { 580 | // 无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,输出T的各条边 581 | k = LocateVex(G, u); // k为顶点u的下标 582 | for(j = 0; j < G.vexnum; ++j) 583 | { 584 | if(j != k) closedge[j] = {u, G.arcs[k][j]}; 585 | closedge[k].lowcost = 0; // 初始,U={u} 586 | for(int i = 1; i < G.vexnum; ++i) 587 | { 588 | // 求出T的下一个结点:第k个顶点,closedge[k]中存有当前最小边 589 | u0 = closedge[k].adjvex; // u0为最小边的一个顶点 590 | v0 = G.vexs[k]; // v0为最小边的另一个顶点 591 | cout << u0 << v0; 592 | closedge[k].lowcost = 0; 593 | for(j = 0; j < G.vexnum; ++j) 594 | { 595 | if(G.arcs[k][j] < closedge[j].lowcost) 596 | { 597 | closedge[j] = {G.vexs[k], G.arcs[k][j]}; 598 | } 599 | } 600 | } 601 | } 602 | } 603 | ``` 604 | 605 | 2. 克鲁斯卡尔算法 606 | 607 | 算法步骤: 608 | 609 | 1. 将数组Edge中的元素按权值从小到大排序。 610 | 611 | 2. 依次查看数组Edge中的边,循环执行以下操作: 612 | 613 | - 依次从排好序的数组Edge中选出一条边(U1,U2); 614 | 615 | - 在Vexset中分别查找V1和V2所在的连通分量vs1和vs2,进行判断: 616 | 617 | - 如果vs1和vs2不等,表明所选的两个顶点分属不同的连通分量,输出此边,并合并vs1和vs2两个连通分量; 618 | 619 | - 如果vs1和vs2相等,表明所选的两个顶点属于同一个连通分量,舍去此边而选择下一条权值最小的边。 620 | 621 | 算法描述: 622 | 623 | ```C++ 624 | void MiniSpanTree_Kruskal(AMGraph G) 625 | { 626 | Sort(Edge); // 将数组Edge中的元素按权值从小到大排序 627 | for(i = 0; i < G.arcnum; i++) 628 | { 629 | v1 = LocateVex(G, Edge[i].Head); // v1为边的始点Head的下标 630 | v2 = LocateVex(G, Edge[i].Tail); // v2为边的始点Head的下标 631 | vs1 = Vexset[v1]; // 获取边Edge[i]的始点所在的连通分量vs1 632 | vs2 = Vexset[v2]; // 获取边Edge[i]的始点所在的连通分量vs2 633 | if(vs1 != vs2) 634 | { 635 | cout << Edge[i].Head << Edge[i].Tail; 636 | for(j = 0; j < G.vexnum; ++j) 637 | { 638 | if(Vexset[j] == vs2) 639 | Vexset[j] = vsl; 640 | } 641 | } 642 | } 643 | } 644 | ``` 645 | 646 | ### 6.6.2 最短路径 647 | 648 | 问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。 649 | 650 | 在带权有向网中,习惯上称路径上的第一个顶点为源点(Source),最后一个顶点为终点(Destination)。 651 | 652 | 针对单源最短路径一用Dijkstra(迪杰断特拉)算法;针对所有顶点间的最短路径一用Floyd(弗洛伊德)算法。 653 | 654 | 1. Dijistra算法 655 | 656 | 1. 初始化:先找出从源点v0到各终点vk的直达路径(v0,vk),即通过一条弧到达的路径; 657 | 658 | 2. 选择:从这些路径中找出一条长度最短的路径(v0,u); 659 | 660 | 3. 更新:然后对其余各条路径进行适当调整: 661 | 662 | 若在图中存在弧(u,vk),且(v0,u)+(u,vk)<(v0,vk),则以路径(v0,u,vk)代替(v0,vk)。 663 | 664 | 在调整后的各条路径中,再找长度最短的路径,依此类推。 665 | 666 | **Dijistra算法:按照长度递增次序产生最短路径。** 667 | 668 | 1. 把V分成两组: 669 | 670 | (1) S:已求出最短路径的顶点的集合; 671 | 672 | (2)T=V - S:尚未确定最短路径的顶点解和。 673 | 674 | 2. 将T中顶点按最短路径递增的次序加入到S中 675 | 676 | 保证:a. 从源点v0到S中各顶点的最短路径长度都不大于从v0到T中任何顶点的最短路径长度。 677 | 678 | b. 每个顶点对应一个距离值:S中顶点:从v0到此顶点的最短路径;T中顶点:从v0到此顶点的只包括S中顶点作中间顶点的最短路径长度。 679 | 680 | 2. Floyd算法 681 | 682 | 1. 初始时设置一个n阶方阵,令其对角线元素为0,若存在弧,则对应元素为全职,否则为正无穷。 683 | 684 | 2. 逐步试着在原直接路径中增加中间顶点,若加入中间顶点后路径变短,则修改之;否则,维持原值。所有顶点试探完毕,算法结束。 685 | 686 | 687 | ### 6.6.3 拓扑排序 688 | 689 | 拓扑排序针对有向无环图。**有向无环图**:无环的有向图,简称DAG图(Directed Acycline Graph)。 690 | 691 | 有向无环图常用来描述一个工程或系统的进行过程。(通常把计划、施工、生产、程序流程等当成是一个工程)一个工程可以分为若干个子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成。 692 | 693 | **AOV网**(拓扑排序):用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以**顶点表示活动**,**弧表示活动之间的优先制约关系**,称这种有向图为**顶点表示活动的网**,简称AOV网(Activity On Vertex network)。 694 | 695 | **AOE网**(关键路径):用一个有向图表示一个工程的各子工程及其相互制约的关系,**以弧表示活动**,**以顶点表示活动的开始或结束事件**,称这种有向图为**边表示活动的网**,简称为AOE网(Activity On Edge)。 696 | 697 | 1. AOV网的特点 698 | 699 | - 若从i到j有一条有向路径,则i是j的前驱,j是i的后继; 700 | 701 | - 若是网中有向边,则i是j的直接前驱,j是i的直接后继; 702 | 703 | - AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。 704 | 705 | 2. 拓扑排序 706 | 707 | 在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为**拓扑有序序列**,相应的拓扑有序排序的算法称为**拓扑排序**。 708 | 709 | 拓扑排序方法: 710 | 711 | - 在有向图中选一个没有前驱的顶点且输出之; 712 | 713 | - 从图中删除该顶点和所有以它为尾的弧; 714 | 715 | - 重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。 716 | 717 | 检测AOV网中是否存在环:对有向图构造其顶点得拓扑有序序列,若网中所有顶点都在它得拓扑有序序列中,则该AOV网必定不存在环。 718 | 719 | 3. 关键路径 720 | 721 | 把工程计划表示为**边表示活动的网络**,即**AOE网**,用**顶点表示事件**,**弧表示活动**,**弧的权表示活动持续时间**。 722 | 723 | 针对AOE网,主要关心两方面问题:完成整项工程至少需要多少时间;哪些活动是影响工程进度的关键。 724 | 725 | 确定关键路径的4个描述量:ve(vj)表示事件vj的最早发生时间;vl(vj)表示事件vj的最晚发生时间;e(i)表示活动ai的最早开始时间;l(i)表示活动ai的最迟开始时间,则l(i)-e(i)表示完成活动ai的时间余量。 726 | 727 | **关键活动**:关键路径上的活动,即l(i)==e(i) 728 | 729 | 730 | 731 | -------------------------------------------------------------------------------- /Chapter6 Graph/图的存储结构分析.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter6 Graph/图的存储结构分析.png -------------------------------------------------------------------------------- /Chapter7 Search/LL型调整前状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/LL型调整前状态.png -------------------------------------------------------------------------------- /Chapter7 Search/LL型调整后结果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/LL型调整后结果.png -------------------------------------------------------------------------------- /Chapter7 Search/LR型调整前状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/LR型调整前状态.png -------------------------------------------------------------------------------- /Chapter7 Search/LR型调整后结果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/LR型调整后结果.png -------------------------------------------------------------------------------- /Chapter7 Search/README.md: -------------------------------------------------------------------------------- 1 | # 第7章 查找 2 | 3 | ## 7.1 查找的基本概念 4 | 5 | **查找表**是由同一类型的数据元素(或记录)构成的集合。由于“集合”中的数据元素之间存在着**松散的关系**,因此查找表是一种应用灵便的结构。 6 | 7 | **查找**:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或(记录) 8 | 9 | - **关键字**:用来标识一个数据元素(或记录)的某个数据项的值 10 | 11 | - **主关键字**:可唯一地标识一个记录的关键字是主关键字; 12 | 13 | - **次关键字**:反之,用以识别若干记录的关键字是次关键字。 14 | 15 | 1. 查找的目的: 16 | 17 | - 查询某个“特定的"数据元素是否在查找表中; 18 | 19 | - 检索某个“特定的“数据元素的各种属性; 20 | 21 | - 在查找表中插入一个数据元素; 22 | 23 | - 删除查找表中的某个数据元素。 24 | 25 | 2. 查找的分类 26 | 27 | - **静态查找表**:仅作“查询”(检索)操作的查找表 28 | 29 | - **动态查找表**:作“插入”和“删除”操作的查找表 30 | 31 | 3. 如何评价查找算法 32 | 33 | - 查找算法的评价指标“:关键字的平均比较次数,也称为**平均查找长度**,(ASL, Average Search Length)。 34 | 35 | ![图1:平均查找长度定义](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%B9%B3%E5%9D%87%E6%9F%A5%E6%89%BE%E9%95%BF%E5%BA%A6%E5%AE%9A%E4%B9%89.png) 36 | 37 | 查找的方法取决于查找表的结构,即表中数据元素是依何种关系组织在一起的。 38 | 39 | 由于对查找表来说,在集合中查询或检索一个“特定的”数据元素时,若无规律可循,只能对集合中的元素一一加以辨认直至找到为止。而这样的“查询”或“检索”是任何计算机应用系统中使用频度都很高的操作,因此设法提高查找表的查找效率,是本章讨论问题的出发点。 40 | 41 | 为提高查找效率,一个办法就是在构造查找表时,在集合中的数据元素之间人为地加上某种确定的约束关系。 42 | 43 | ## 7.2 线性表的查找 44 | 45 | ### 7.2.1 顺序查找(线性查找) 46 | 47 | 应用范围:顺序表或线性链表表示的**静态查找表**;表内元素之间无序 48 | 49 | ```C++ 50 | int Search_Seq(SSTable ST, KeyTable key) 51 | { 52 | // 若成功返回其位置信息,否则返回0 53 | for(i = ST.length; i >= 1; i--) 54 | { 55 | if(ST.R[i].key == key) return i; 56 | } 57 | return 0; 58 | } 59 | ``` 60 | 61 | 改进:把待查关键字key存入表头(“哨兵”、”监视哨”),从后往前逐个比较,可免去查找过程中每一步都要检测是否查找完毕,加快速度。 62 | 63 | ```C++ 64 | int Search_Seq(SSTable ST, KeyTable key) 65 | { 66 | ST.R[0].key = key; 67 | for(i = ST.length; ST.R[i].key != key; i--); 68 | return i; 69 | } 70 | ``` 71 | 72 | 当ST.length较大时,此改进能使进行一次查找所需的平均时间几乎减少一半。 73 | 74 | - 时间复杂度:O(n),查找成功时的平均查找长度为:`ASL(n) = (1+2+...+n)/n = (n+1)/2`。 75 | 76 | - 空间复杂度:一个辅助空间,O(1)。 77 | 78 | 讨论: 79 | 80 | 1. 记录的查找概率不相等时如何提高查找效率? 81 | 82 | 查找表存储记录原则按查找概率高低存储: 83 | 84 | 1. 查找概率越高,比较次数越少; 85 | 86 | 2. 查找概率越低,比较次数较多。 87 | 88 | 2. 记录的查找概率无法测定时如何提高查找效率? 89 | 90 | 方法——按查找概率**动态调整**记录顺序: 91 | 1. 在每个记录中设一个访问频度域; 92 | 93 | 2. 始终保持记录按非递增有序的次序排列; 94 | 95 | 3. 每次查找后均将刚查到的记录直接移至表头。 96 | 97 | 优点:算法简单,逻辑次序无要求,且不同存储结构均适用。 98 | 99 | 缺点:ASL太长,时间效率太低 100 | 101 | ### 7.2.2 折半查找(二分或对分查找) 102 | 103 | **折半查找**:每次将待查记录所在区间缩小一半。 104 | 105 | - 折半查找算法:(非递归算法) 106 | 107 | - 设表长为n,low、high和mid分别指向待查元素所在区间的上界、下界和中点,key为给定的要查找的值; 108 | 109 | - 初始时,令low=1,high=n,mid=floar((low+high)/2) 110 | 111 | - 让k与mid指向的记录比较 112 | 113 | - 若key==R[mid].key,查找成功; 114 | - 若keyR[mid].key,low=mid+1; 116 | 117 | - 重复上述操作,直至low>high时,查找失败 118 | 119 | ```C++ 120 | int Search_Bin(SSTable ST, KeyTable key) 121 | { 122 | low = 1; high = ST.length; 123 | while(low <= high) 124 | { 125 | mid = (low + high) / 2; 126 | if(ST.R[mid].key == key) return mid; 127 | else if(key < ST.R[mid].key) 128 | high = mid-1; 129 | else 130 | low = mid + 1; 131 | } 132 | return 0; 133 | } 134 | ``` 135 | 136 | 递归版: 137 | 138 | ```C++ 139 | int Search_Bin(SSTable ST, KeyTable key, int low, int high) 140 | { 141 | if(low > high) return 0; 142 | mid = (low + high) / 2; 143 | if(key == ST.elem[mid].key) return mid; 144 | else if(key < ST.elem[mid].key) 145 | Search_Bin(ST,key, mid-1, high); 146 | else 147 | Search_Bin(ST,key, low, mid+1); 148 | } 149 | ``` 150 | 151 | 假定每个元素的查找概率相等,查找成功时的平均查找长度(ASL),设表长n=2^h-1,则h=log2(n+1),查找概率相等为:1/n,`ASL≈log2(n+1)-1 (n>50)`。 152 | 153 | 优点:效率比顺序查找高 154 | 155 | 缺点:只适用于**有序表**,且限于**顺序存储结构**(对线性链表无效)。 156 | 157 | ### 7.2.3 分块查找(索引顺序查找) 158 | 159 | 分块查找条件基础: 160 | 161 | 1. 将表分成几块,且表有序或者分块有序。若i < j,则第j块中所有记录的关键字均大于第i块中的最大关键字。 162 | 163 | 2. 建立”索引表“(每个结点含有最大关键字域和指向本块第一个结点的指针,且按关键字有序)。 164 | 165 | 查找过程:先确定待查记录所在块(顺序或折半查找),再在块内查找(顺序查找)。 166 | 167 | 查找效率:ASL = LB + LW;即对索引表查找的ASL加上对块内查找的ASL 168 | 169 | ![图2:分块查找效率](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E6%9F%A5%E6%89%BE%E6%95%88%E7%8E%87.png) 170 | 171 | 优点:插入和删除较容易,无需进行大量移动 172 | 173 | 缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算 174 | 175 | 适用情况:如果线性表既要**快速查找**又经常**动态变化**,则可采用分块查找。 176 | 177 | ![图3:查找方法比较](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E6%9F%A5%E6%89%BE%E6%96%B9%E6%B3%95%E6%AF%94%E8%BE%83.png) 178 | 179 | ## 7.3 树表的查找 180 | 181 | ### 7.3.1 二叉排序树 182 | 183 | **二叉排序树**(Binary Sort Tree)又称为二叉搜索树、二叉查找树。 184 | 185 | 1. 二叉排序树定义: 186 | 187 | 二叉排序树或是空树,或是满足如下性质的二叉树: 188 | 189 | 1. 若其左子树非空,则左子树上所有结点的值均小于根结点的值; 190 | 191 | 2. 若其右子树非空,则右子树上所有结点的值均大于等于根结点的值; 192 | 193 | 3. 其左右子树本身又各是一棵二叉排序树 194 | 195 | 2. 二叉排序树的性质: 196 | 197 | 中序遍历非空的二叉排序树所得到的数据元素序列是一个按关键字排列的**递增有序**序列。 198 | 199 | 3. 二叉排序树的操作——查找 200 | 201 | - 若查找的关键字等于根节点,成功。 202 | 203 | - 否则: 204 | 205 | - 若小于根节点,查其左子树 206 | 207 | - 若大于根节点,查找右子树 208 | 209 | - 在左右子树上的操作类似 210 | 211 | ```C++ 212 | // 二叉排序树的存储结构 213 | struct ElemType 214 | { 215 | KeyType key; // 关键字项 216 | InfoType otherInfo; // 其他数据项 217 | }; 218 | 219 | struct BSTNode 220 | { 221 | ElemType data; // 数据域 222 | struct BSTNode *lchild, *rchild; // 左右孩子指针 223 | }; 224 | typedef BSTNode* BSTree; 225 | 226 | // 递归查找 227 | BSTree SearchBST(BSTree T, KeyType key) 228 | { 229 | if(!T || key == T->data.key) return T; 230 | else if(key < T->data.key) 231 | return SearchBST(T->lchild, key); 232 | else 233 | return SearchBST(T->rchild, key); 234 | } 235 | ``` 236 | 237 | 4. 二叉排序树的查找分析 238 | 239 | 二叉排序树上查找某关键字等于给定值的结点过程,其实就是走了一条从根到该结点的路径。 240 | 241 | 比较的关键字次数 = 此结点所在层次数;最多的比较次数 = 树的深度 242 | 243 | 对于含有n个结点的二叉排序树的平均查找长度和树的形状有关。 244 | 245 | - 最好情况:与折半查找中的判定树相同,O(log2n); 246 | 247 | - 最坏情况:退化为单支树(类似于线性列表),树深度为n,ASL = (n+1)/2,O(n); 248 | 249 | 为了提高形态不均衡的二叉排序树的查找效率,应当进行“平衡化”处理(即**平衡二叉树**),尽量使二叉树的形状均衡! 250 | 251 | 5. 二叉排序树的操作——插入 252 | 253 | - 若二叉排序树为空,则插入结点作为根结点插入到空树中 254 | 255 | - 否则,继续在其左、右子树上查找 256 | 257 | - 树中已有,不再插入 258 | 259 | - 树中没有 260 | 261 | - 查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子 262 | 263 | **插入的元素一定是叶子节点**。 264 | 265 | 6. 二叉排序树的操作——生成 266 | 267 | 从空树出发,经过一系列的查找、插入操作之后,可生成一颗二叉排序树。 268 | 269 | 一个无序序列可通过构造二叉排序树而变成一个有序序列。构造树的过程就是对无序序列进行排序的过程。 270 | 271 | 插入的结点均为叶子结点,故无需移动其他结点。相当于在有序序列上插入记录而无需移动其他记录。但是,**关键字的输入顺序不同,建立的二叉排序树也不同**。 272 | 273 | 7. 二叉排序树的操作——删除 274 | 275 | 从二叉排序树中删除一个结点,不能把以该结点为根的子树者都删除,只能删掉该结点,并且**还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变**。 276 | 277 | 由于中序遍历二叉排序树可以得到一个递增有序的序列。那么,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点。 278 | 279 | - 将因删除结点而断开的二叉链表重新链接起来 280 | 281 | - 防止重新链接后树的高度增加 282 | 283 | 1. 如果删除的结点是叶子结点:直接删除该结点。 284 | 285 | 2. 被删除的结点只有左子树或者只有右子树,用其左子树或者右子树替换它(结点替换)。 286 | 287 | 3. 本删除的结点既有左子树,也有右子树:可以用其中序的前驱结点替换,然后删除该前驱结点(前驱结点是左子树中最大的结点);也可以用中序的后继结点,然后删除该后继结点(后继是右子树中最小的结点)。 288 | 289 | ### 7.3.2 平衡二叉树 290 | 291 | 1. 平衡二叉树的定义 292 | 293 | **平衡二叉树**(balanced binary tree),又称为AVL树(Adelson-Velskii and Landis)。 294 | 295 | 一棵平衡二叉树或者是空树,或者是具有以下性质的**二叉排序树**: 296 | 297 | - 左子树与右子树的高度之差的绝对值小于等于1; 298 | 299 | - 左子树和右子树也是**平衡二叉排序树**。 300 | 301 | 左子树与右子树的高度差又称为结点的平衡因子(BF),`平衡因子=结点左子树的高度-结点右子树的高度`。根据平衡二叉树的定义,平衡二又树上所有结点的平衡因子只能是-1、0,或1。 302 | 303 | 对于一棵有n个结点的AVL树,其高度保持在O(log2n)数量级,ASL也保持在O(log2n)量级。 304 | 305 | 2. 失衡二叉排序树的分析与调整 306 | 307 | 当我们在一个平衡二叉排序树上插入一个结点时,有可能导致**失衡**,即出现平衡因子绝对值大于1的结点。 308 | 309 | 如果在一个AVL树中插入一个新结点后造成失衡,则必须**重新调整树的结构**,使之回复平衡。 310 | 311 | 平衡调整的四种类型:LL型、LR型、RL型和RR型 312 | 313 | ![图4:平衡调整的四种类型示意图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%B9%B3%E8%A1%A1%E8%B0%83%E6%95%B4%E7%9A%84%E5%9B%9B%E7%A7%8D%E7%B1%BB%E5%9E%8B.png) 314 | 315 | ![图5:平衡调整的四种类型调整后示意图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%B9%B3%E8%A1%A1%E8%B0%83%E6%95%B4%E7%9A%84%E5%9B%9B%E7%A7%8D%E7%B1%BB%E5%9E%8B_%E8%B0%83%E6%95%B4%E5%90%8E.png) 316 | 317 | 平衡调整的原则:1)降低高度;2)保持二叉排序树性质 318 | 319 | 1. LL型调整 320 | 321 | ![图6:LL型调整前-后对比示意图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/LL%E5%9E%8B%E8%B0%83%E6%95%B4%E5%89%8D-%E5%90%8E%E5%AF%B9%E6%AF%94.png) 322 | 323 | - B结点带左子树一起上升 324 | 325 | - A结点成为B的右孩子 326 | 327 | - 原来B结点的右子树作为A的左子树 328 | 329 | ![图7:LL型调整示例](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE7%EF%BC%9ALL%E5%9E%8B%E8%B0%83%E6%95%B4%E7%A4%BA%E4%BE%8B.png) 330 | 331 | 2. RR型调整 332 | 333 | ![图8:RR型调整前-后对比示意图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE8%EF%BC%9ARR%E5%9E%8B%E8%B0%83%E6%95%B4%E5%89%8D-%E5%90%8E%E5%AF%B9%E6%AF%94%E7%A4%BA%E6%84%8F%E5%9B%BE.png) 334 | 335 | - B结点带右节点一起上升 336 | 337 | - A结点成为B结点的左孩子 338 | 339 | - 原来B结点的左子树作为A的右子树 340 | 341 | ![图9:RR型调整示例](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE9%EF%BC%9ARR%E5%9E%8B%E8%B0%83%E6%95%B4%E7%A4%BA%E4%BE%8B.png) 342 | 343 | 3. LR型调整 344 | 345 | ![图10:LR型调整前-后对比示意图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE10%EF%BC%9ALR%E5%9E%8B%E8%B0%83%E6%95%B4%E5%89%8D-%E5%90%8E%E5%AF%B9%E6%AF%94%E7%A4%BA%E6%84%8F%E5%9B%BE.png) 346 | 347 | - C结点穿过A、B结点上升 348 | 349 | - B结点成为C的左孩子,A结点成为C的右孩子 350 | 351 | - 原来C结点的左子树作为B的右子树,原来C结点的右子树作为A的左子树 352 | 353 | ![图11:LR型调整示例](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE11%EF%BC%9ALR%E5%9E%8B%E8%B0%83%E6%95%B4%E7%A4%BA%E4%BE%8B.png) 354 | 355 | 4. RL型调整 356 | 357 | ![图12:RL型调整前示意图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE12%EF%BC%9ARL%E5%9E%8B%E8%B0%83%E6%95%B4%E5%89%8D%E7%8A%B6%E6%80%81.png) 358 | 359 | ![图13:RL型调整示例](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE13%EF%BC%9ARL%E5%9E%8B%E8%B0%83%E6%95%B4%E7%A4%BA%E4%BE%8B.png) 360 | 361 | ## 7.4 哈希表的查找(散列表的查找) 362 | 363 | ### 7.4.1 散列表的基本概念 364 | 365 | 基本思想:**记录的存储位置与关键字之间的存在对应关系**,对应关系常成为hash函数。 366 | 367 | **优点:查找效率高;缺点:空间效率低!** 368 | 369 | **散列方法(杂凑法)**:选取某个函数,依该函数按关键字**计算元素的存储位置**,并按此存放。查找时,**由同一个函数对给定值k计算地址**,将k与地址单元中元素关键码进行对比,确定查找是否成功。 370 | 371 | **散列函数(杂凑函数)**:散列方法中适用的**转换函数**。 372 | 373 | **冲突**:不同的关键码映射到同一个散列地址,则称为冲突。 374 | 375 | ### 7.4.2 散列函数的构造方法 376 | 377 | 在散列查找方法中,冲突是不可避免的,只能尽可能避免。使用散列表要解决的两个主要为题包括: 378 | 379 | 1. 构造好的散列函数 380 | 381 | - 所选函数尽可能简单,以便提高转换速度; 382 | 383 | - 所选函数对关键码计算出的地址,应在散列地址集中致均匀分布,以减少空间浪费。 384 | 385 | 2. 制定一个好的解决冲突的方案 386 | 387 | - 查找时,如果从散列函数计算出的地址中查不到关键码,则应当依据解决冲突的规则,有规律地查询其它相关单元。 388 | 389 | 构造散列函数考虑的因素:执行速度、关键字的长度、散列表的大小、关键字的分布情况、查找频率。 390 | 391 | 根据元素集合的特性构造,**要求一**:n个数据原仅占用n个地址,虽然散列查找是以空间换时间,但仍希望散列的**地址空间尽量小**。**要求二**:无论用什么方法存储,目的都是尽量**均匀**地存放元素,以避免冲突。 392 | 393 | 常见构造方法包括有:直接定址法、数字分析法、平方取中法、折叠法、**除留余数法**、随机数法。 394 | 395 | **直接定址法**:以关键码key的某个线性函数值为散列地址,不会产生冲突。但是要占用连续地址空间,空间效率低。 396 | 397 | **除留余数法**:`Hash(key) = key mod p(其中p是一个整数)`,常见p值取小于表长的质数。 398 | 399 | ### 7.4.3 处理冲突的方法 400 | 401 | 1. 开放地址法(开地址法) 402 | 403 | 当有冲突时就去寻找**下一个**空的散列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素存入。常见方法包括:**线性探测法**、**二次探测法**、**伪随机探测法**。`Hi = (Hash(key) + di) mod m, m是散列表长度, di是增量序列` 404 | 405 | 线性探测法:`di为1, 2, ..., m-1线性序列`,一旦冲突,就找下一个地址,直到找到空地址存入。 406 | 407 | 二次探测法:`di为1^2, -1^2, 2^2, -2^2, ..., q^2二次序列`。 408 | 409 | 伪随机探测法:`di为伪随机数序列`。 410 | 411 | 2. 链地址法(拉链法) 412 | 413 | 将相同散列地址的记录链成一个单链表,**m个散列地址就设m个单链表**,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构。 414 | 415 | 链地址法建立散列表步骤: 416 | 417 | 1. 取数据元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表;否则执行步骤2解决冲突。 418 | 419 | 2. 根据选择的冲突处理方法,计算关键字key的下一个存储地址。若该地址对应的链表为不为空,则利用链表的前插法或后插法将该元素插入此链表。 420 | 421 | 连地址法的优点:非同义词**不会冲突**,无聚集现象;链表上结点空间动态申请,更适合于表长不确定的情况; 422 | 423 | 3. 再散列法(双散列函数法) 424 | 425 | 4. 建立一个公共溢出区 426 | 427 | ### 7.4.4 散列表的查找及性能分析 428 | 429 | ![图14:散列表查找流程图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/%E5%9B%BE14%EF%BC%9A%E6%95%A3%E5%88%97%E8%A1%A8%E6%9F%A5%E6%89%BE%E6%B5%81%E7%A8%8B%E5%9B%BE.png, '散列表查找流程图') 430 | 431 | 散列表的查找效率,如果使用平均查找长度ASL来衡量,则ASL取决于:散列函数、处理冲突的方法、散列表的**装填因子**α(`α=表中填入的记录数/哈希表的长度`)。 432 | 433 | 其中,α越大,表中记录的数据越多,说明表装填的越满,发生冲突的可能性越大,查找时比较次数就越多。 434 | 435 | ASL与装填因子α有关,既不是严格的O(1),也不是O(n)。 436 | 437 | `ASL≈1+α/2(拉链法);ASL≈1/2*(1+(1/(1-α)))(线性探测法);ASL≈-(1/α)*ln(1-α)(随机探测法)`。 -------------------------------------------------------------------------------- /Chapter7 Search/RR型调整前状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/RR型调整前状态.png -------------------------------------------------------------------------------- /Chapter7 Search/RR型调整后结果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/RR型调整后结果.png -------------------------------------------------------------------------------- /Chapter7 Search/图10:LR型调整前-后对比示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图10:LR型调整前-后对比示意图.png -------------------------------------------------------------------------------- /Chapter7 Search/图11:LR型调整示例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图11:LR型调整示例.png -------------------------------------------------------------------------------- /Chapter7 Search/图12:RL型调整前状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图12:RL型调整前状态.png -------------------------------------------------------------------------------- /Chapter7 Search/图13:RL型调整示例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图13:RL型调整示例.png -------------------------------------------------------------------------------- /Chapter7 Search/图14:散列表查找流程图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图14:散列表查找流程图.png -------------------------------------------------------------------------------- /Chapter7 Search/图1:平均查找长度定义.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图1:平均查找长度定义.png -------------------------------------------------------------------------------- /Chapter7 Search/图2:查找效率.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图2:查找效率.png -------------------------------------------------------------------------------- /Chapter7 Search/图3:查找方法比较.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图3:查找方法比较.png -------------------------------------------------------------------------------- /Chapter7 Search/图4:平衡调整的四种类型.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图4:平衡调整的四种类型.png -------------------------------------------------------------------------------- /Chapter7 Search/图5:平衡调整的四种类型_调整后.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图5:平衡调整的四种类型_调整后.png -------------------------------------------------------------------------------- /Chapter7 Search/图6:LL型调整前-后对比.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图6:LL型调整前-后对比.png -------------------------------------------------------------------------------- /Chapter7 Search/图7:LL型调整示例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图7:LL型调整示例.png -------------------------------------------------------------------------------- /Chapter7 Search/图8:RR型调整前-后对比示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图8:RR型调整前-后对比示意图.png -------------------------------------------------------------------------------- /Chapter7 Search/图9:RR型调整示例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/图9:RR型调整示例.png -------------------------------------------------------------------------------- /Chapter7 Search/数据结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter7 Search/数据结构.png -------------------------------------------------------------------------------- /Chapter8 Sorting/README.md: -------------------------------------------------------------------------------- 1 | # 第8章 排序 2 | 3 | ## 8.1 基本概念和排序方法概述 4 | 5 | 排序:将一组杂乱无章的数据按一定规律顺次排列起来,即将无序序列**排成一个有序序列**的运算。如果参加排序的数据结点包含多个数据域,那么排序往往是针对其中某个域而言。 6 | 7 | ![图01:排序方法的分类]([http://](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter8%20Sorting/%E5%9B%BE01%EF%BC%9A%E6%8E%92%E5%BA%8F%E6%96%B9%E6%B3%95%E7%9A%84%E5%88%86%E7%B1%BB.png), '排序方法分类') 8 | 9 | 按照存储介质可分为: 10 | 11 | - **内部排序**:数据量不大、数据在内存,无序内外存交换数据。 12 | 13 | - **外部排序**:数据量较大、数据在外存(如:文件排序)。 14 | 15 | 外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外村,显然外部排序要复杂得多。 16 | 17 | 按比较器个数可分为: 18 | 19 | - **串行排序**:单处理机(同一时刻比较一对元素) 20 | 21 | - **并行排序**:多处理机(同一时刻比较多对元素) 22 | 23 | 按主要操作可分为: 24 | 25 | - **比较排序**:用比较的方法,例如:插入排序、交换排序、选择排序、归并排序等。 26 | 27 | - **基数排序**:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置。 28 | 29 | 按辅助空间可分为: 30 | 31 | - **原地排序**:辅助空间用量为O(1)的排序方法。(所占的辅助存储空间与参加排序的数据量大小无关) 32 | 33 | - **非原地排序**:辅助空间用量超过O(1)的排序方法。 34 | 35 | 按稳定性可分为:(排序稳定性只对结构类型数据排序有意义) 36 | 37 | - **稳定排序**:能够使任何数值相等的元素,排序以后相对次序不变。 38 | 39 | - **非稳定排序**:不是稳定排序的方法。 40 | 41 | 按自然性可分为: 42 | 43 | - **自然排序**:输入数据越有序,排序的速度越快的排序方法。 44 | 45 | - **非自然排序**:不是自然排序的方法。 46 | 47 | 本章节主要讨论的是内部、串行、比较排序。 48 | 49 | 主要内容如图所示: 50 | 51 | ![图02:主要学习内容](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter8%20Sorting/%E5%9B%BE02%EF%BC%9A%E4%B8%BB%E8%A6%81%E5%AD%A6%E4%B9%A0%E5%86%85%E5%AE%B9.png,'主要学习内容') 52 | 53 | ## 8.2 插入排序 54 | 55 | 基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,值到对象全部插入为止。**即边插入边排序,保证子序列中随时都是排好序的**。 56 | 57 | 基本操作:有序插入 58 | 59 | - 在有序序列中插入一个元素,保持序列有序,有序长度不断增加。 60 | 61 | - 起初,a[0]是长度为1的子序列。然后,逐一将a[1]至a[n-1]插入到有序子序列中。 62 | 63 | 插入排序的种类:**直接插入排序**(顺序法定位插入位置)、**二分插入排序**(二分法定位插入位置)、**希尔排序**(缩小增量多变插入排序)。 64 | 65 | 1. 直接插入排序 66 | 67 | 采用**顺序查找法**找到插入位置。 68 | 69 | 1. 复制插入元素; 70 | 71 | 2. 记录后移,查找插入位置; 72 | 73 | 3. 插入到正确位置。 74 | 75 | ```C++ 76 | void InsertSort(SqList &L) 77 | { 78 | int i, j; 79 | for(int i = 2; i <= L.length; ++i) 80 | { 81 | if(L.r[i].key < L.r[i-1].key) 82 | { 83 | L.r[0] = L.r[i]; // 复制为哨兵 84 | for(int j = i -1; L.r[0].key < L.r[j].key; --j) 85 | { 86 | L.r[j+1] = L.r[j]; // 记录后移 87 | } 88 | L.r[j+1] = L.r[0]; // 插入到正确位置 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | 实现排序的基本操作有两个:“比较”和“移动”。 95 | 96 | 性能分析:当原始数据越接近有序,排序速度越快;最快情况下(输入数据是逆序的,O(n^2));平均情况下,耗时差不多是最坏情况下的一半,O(n^2)。 97 | 98 | 2. 折半插入排序 99 | 100 | 查找插入位置时采用折半查找法。 101 | 102 | ```C++ 103 | void BInsertSort(SqList &L) 104 | { 105 | for(int i = 2; i <= L.length; i++) 106 | { 107 | L.r[0] = L.r[i]; // 当前插入元素存到“哨兵”位置 108 | low = 1; high = i-1; 109 | // 采用二分法查找插入位置 110 | while(low <= high) 111 | { 112 | mid = (low + high) / 2; 113 | if(L.r[0].key < L.r[mid].key)) high = mid - 1; 114 | else low = mid + 1; 115 | } // 循环结束,high+1为插入位置 116 | 117 | // 移动元素 118 | for(int j = i - 1; j >= high+1; --j) 119 | { 120 | L.r[j+1] = L.r[j]; 121 | } 122 | L.r[high+1] = L.r[0]; // 插入到正确位置 123 | } 124 | } 125 | ``` 126 | 127 | 算法效率分析: 128 | 129 | - 折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快; 130 | 131 | - 它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第i个对象时,需要经过log2i+1次关键码比较,才能确定它应插入的位置; 132 | 133 | - 当n较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差; 134 | 135 | - 在对象的初始排序列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行关键码比较次数要少; 136 | 137 | - 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列 138 | 139 | - 减少了比较次数,但没有减少移动次数; 140 | 141 | - 平均性能优于直接插入排序。 142 | 143 | 时间复杂度为:O(n^2);空间复杂度为:O(1);也是一种稳定的排序方法。 144 | 145 | 3. 希尔排序 146 | 147 | 先将整个待排记录序列分割成**若干子序列**,分别进行**直接插入排序**,待整个序列中的记录“**基本有序**”时,再对全体记录进行一次直接插入排序。 148 | 149 | 希尔排序算法:逐步缩小增量;多遍插入排序。 150 | 151 | 希尔排序特点: 152 | 153 | - 依次移动,移动位置较大,跳跃式地接近排序后的最终位置; 154 | 155 | - 最后一次只需要少量移动; 156 | 157 | - 增量序列必须是递减的,最后一个必须是1; 158 | 159 | - 增量序列应该是互质的。、 160 | 161 | ```C++ 162 | void ShellSort(SqList &L, int dlta[], int t) 163 | { 164 | // 按增量序列dlta[0,...,t-1]对顺序表L作希尔排序 165 | for(k = 0; k < t; k++) 166 | { 167 | ShellInsert(L, dlta[k]); // 一趟增量为dlta[k]的插入排序 168 | } 169 | } 170 | 171 | void ShellInsert(SqList &L, int dk) 172 | { 173 | // 对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子 174 | for(int i = dk + 1; i <= L.length; i++) 175 | { 176 | if(r[i].key < r[i-dk].key) 177 | { 178 | r[0] = r[i]; 179 | for(j = i -dk; j > 0 && (r[0].key < r[j].key); j = j - dk) 180 | { 181 | r[j+dk] = r[j]; 182 | } 183 | r[j+dk] = r[0]; 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | 性能分析:希尔排序算法效率与**增量序列的取值**有关。 190 | 191 | 时间复杂度是n和d的函数:O(n^1.25)~O(1.6n^1.25);空间复杂度为O(1);是一种**不稳定**的排序算法;**不宜在链式存储结构上实现**。 192 | 193 | ## 8.3 交换排序 194 | 195 | 排序过程中,两两比较,如果发生逆序则交换,知道所有记录都排好序为止。常见的交换排序方法有**冒泡排序**、**快速排序**。 196 | 197 | ### 8.3.1 冒泡排序 198 | 199 | 冒泡排序:基于简单交换思想,每趟不断将数据两两比较,并按“前小后大”规则交换。 200 | 201 | ```C++ 202 | void bubble_sort(SqList &L) 203 | { 204 | int m, i, j; 205 | RedType x; 206 | for(int m = 1; m <= n-1; m++) 207 | { 208 | for(int j = 1; j <= n-m; j++) 209 | { 210 | // 如果发生逆序 211 | if(L.r[j].key > L.r[j+1].key) 212 | { 213 | x = L.r[j]; 214 | L.r[j] = L.r[j+1]; 215 | L.r[j+1] = x; 216 | } 217 | } 218 | } 219 | } 220 | ``` 221 | 222 | 优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;当**一旦某一趟比较时不出现记录交换,说明已经排好序了,就可以结束本算法**。 223 | 224 | 改进的冒泡排序算法 225 | 226 | ```C++ 227 | void bubble_sort(SqList &L) 228 | { 229 | int m, i, j, flag = 1; 230 | RedType x; 231 | for(int m = 1; m <= n-1 && flag == 1; m++) 232 | { 233 | flag = 0; 234 | for(int j = 1; j <= m; j++) 235 | { 236 | // 如果发生逆序 237 | if(L.r[j].key > L.r[j+1].key) 238 | { 239 | flag = 1; // 发生交换,flag置为1,若本趟没发生变换,flag保持为0; 240 | x = L.r[j]; 241 | L.r[j] = L.r[j+1]; 242 | L.r[j+1] = x; 243 | } 244 | } 245 | } 246 | } 247 | ``` 248 | 249 | 时间复杂度:最好情况下(正序)比较次数为n-1,移动次数为0;最坏情况下(逆序)比较次数为(1/2 * (n^2-n)),移动次数为(3/2 * (n^2-n))。 250 | 251 | 冒泡排序**最好**时间复杂度为O(n);**最坏**时间复杂度为O(n^2);**平均**时间复杂度为O(n^2);辅助空间为O(1);冒泡排序是**稳定**的。 252 | 253 | ### 8.3.2 快速排序 254 | 255 | 快速排序: 256 | 257 | - 从需要排序的数据当中,任取一个元素为**中心**; 258 | 259 | - 依次比较其他元素,所有比它小的元素一律前放,比它大的元素一律后放,形成**左右两个子表**; 260 | 261 | - 对各子表重新选择中心元素并**依此规则调整**,直到每个子表的元素只剩一个。 262 | 263 | **基本思想**:通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序。 264 | 265 | **具体实现**:选定一个中间数作为参考,所有元素与之比较,小的调到其左边,大的调到其右边。每一趟的子表的形成是采用从两头向中间交替式逼近法;由于每趟中对各子表的操作都相似,可采用**递归算法**。 266 | 267 | ```C++ 268 | void QSort(SqList &L, int low, int high) // 对顺序表L快速排序 269 | { 270 | if(low < high) 271 | { 272 | pivotloc = Partition(L, low, high); 273 | // 将L.r[low,...,high]一分为二,pivotloc为枢轴元素排好序的位置 274 | QSort(L, low, pivotloc-1); // 对低子表递归排序 275 | QSort(L, pivotloc+1, high); // 对高子表递归排序 276 | } 277 | } 278 | 279 | int Partition(SqList &L, int low, int high) 280 | { 281 | L.r[0] = L.r[low]; 282 | pivotkey = L.r[low].key; 283 | 284 | // low与high重合时,循环结束 285 | while(low < high) 286 | { 287 | while(low < high && L.r[high].key >= pivotkey) --high; 288 | // 当high位置上的值比当前值小,循环结束,high位置上的元素搬至low位置上的元素 289 | L.r[low] = L.r[high]; 290 | 291 | while(low < high && L.r[low].key <= pivotkey) ++low; 292 | // 当low位置上的值比当前值大,循环结束,low位置上的元素被搬至high位置上 293 | L.r[high] = L.r[low]; 294 | } 295 | L.r[low] = L.r[0]; 296 | return low; 297 | } 298 | ``` 299 | 300 | 性能分析:时间复杂度O(nlogn);空间复杂度平均为O(logn),最坏情况下栈空间可达O(n)。 301 | 302 | 快速排序是一种**不稳定**的排序方法。且**快速排序不适用于对原本有序或基本有序的记录序列进行排序**。 303 | 304 | - **划分元素的选取**是影响时间性能的关键 305 | 306 | - 输入数据次序越乱,所选划分元素值的随机性越好,排序速度越快,快速排序**不是自然排方法**。 307 | 308 | - 改变划分元素的选取方法,至多只能改变算法平均情况的下的时间性能,无法改变最坏情况下的时间性能。即最坏情况下,快速排序的时间复杂性总是O(n^2) 309 | 310 | ## 8.4 选择排序 311 | 312 | ### 8.4.1 简单选择排序 313 | 314 | **基本思想**:在待排序的数据中选出最大(小)的元素放在其最终的位置。 315 | 316 | 1. 首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换; 317 | 318 | 2. 再通过n-2次比较,从剩余的n-1个记录中找出关键字次少的记录,将它与第二个记录交换; 319 | 320 | 3. 重复上述操作,共进行n-1趟排序后,排序结束。 321 | 322 | ```C++ 323 | void SelectSort(SqList &K) 324 | { 325 | for(int i = 1; i < L.length; i++) 326 | { 327 | k = i; 328 | for(int j = i + 1; j <= L.length; j++) 329 | { 330 | if(L.r[j].key < L.r[k].key) 331 | k = j; // 记录最小值位置 332 | } 333 | if(k != i) 334 | { 335 | x = L.r[i]; 336 | L.r[i] = L.r[k]; 337 | L.r[k] = x; 338 | } 339 | } 340 | } 341 | ``` 342 | 343 | 时间复杂度: 344 | 345 | - 记录移动次数: 346 | 347 | - 最好情况:0; 348 | 349 | - 最坏情况:3*(n-1) 350 | 351 | - 比较次数:无论待排序列处于什么状态,选择排序所需进行的“比较“次数都相同。 352 | 353 | 简单选择排序是**不稳定排序**。 354 | 355 | ### 8.4.2 堆排序 356 | 357 | 1. 堆的定义 358 | 359 | 堆:若n个元素的序列{a1, a2, ..., an}满足`ai ≤ a2i && ai ≤ a2i+1`或者`ai ≥ a2i && ai ≥ a2i+1`,则分别称该序列为**小根堆**和**大根堆**。 360 | 361 | 从堆的定义可以看出,堆实质是满足如下性质的**完全二叉树**:**二叉树中任一非叶子节点均小于(大于)它的孩子结点**。 362 | 363 | **堆排序**:若在输出**堆顶**的最小值(最大值)后,使得剩余n-1个元素的序列重新又建成一个堆,则得到n个元素的次小值(次大值),如此反复,便得到一个有序序列,这个过程称为堆排序。 364 | 365 | 实现堆排序重点需解决两个问题: 366 | 367 | 1. 如何由一个无序序列建成一个堆? 368 | 369 | 2. 如何在堆输出堆顶元素后,调整剩余元素为一个新的堆。 370 | 371 | 针对第二个问题,以小根堆为例: 372 | 373 | 1. 输出堆顶元素之后,以堆中**最后一个元素替代之**; 374 | 375 | 2. 然后将根结点值与左、右子树的根结点值进行比较,并与其中**小者**进行**交换**; 376 | 377 | 3. 重复上述操作,直至该结点成为叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“**筛选**"。 378 | 379 | ```C++ 380 | void HeapAdjust(elem R[], int s, int m) 381 | { 382 | // 已知R[s,...,m]中记录的关键字出R[s]之外均满足堆的定义,本函数调整R[s]的关键字,是R[s,...,m]成为一个大根堆 383 | rc = R[s]; 384 | for(int j = 2 * s; j <= m; j *= 2) // 沿key较大的孩子结点向下筛选 385 | { 386 | if(j < m && R[j] < R[j+1]) 387 | ++j; // j为key较大的记录的小标 388 | if(rc >= R[j]) break; 389 | 390 | R[s] = R[j]; 391 | s = j; 392 | } 393 | R[s] rc; 394 | } 395 | ``` 396 | 397 | 2. 堆的建立: 398 | 399 | 1. 单节点的二叉树是堆; 400 | 401 | 2. 在完全二叉树中所有以叶子节点(序号i>n/2)为根的子树是堆。这样只需要依次将以序号为n/2,n/2-1,...,1的结点为根的子树均调整为堆即可。即对应由n个元素组成的无序序列,“筛选”只需从第n/2个元素开始。 402 | 403 | ```C++ 404 | for(int i = n/2; i >= 1; i--) 405 | { 406 | HeapAdjust(R, i, n); 407 | } 408 | ``` 409 | 410 | 3. 堆排序: 411 | 412 | 实质上,堆排序就是利用完全二叉树中父结点与孩子结点之间的内在关系来排序的。 413 | 414 | ```C++ 415 | void HeapSort(elem R[]) // 对R[1]到R[n]进行堆排序 416 | { 417 | int i; 418 | for(int i = n / 2; i >= 1; i--) 419 | { 420 | HeapAdjust(R, i, n); // 建立初始堆 421 | } 422 | for(int i = n; i > 1; i--) 423 | { 424 | Swap(R[1], R[i]); // 根与最后一个元素交换 425 | HeapAdjust(R, 1, i - 1); // 对R[1]到R[i-1]重新建堆 426 | } 427 | } 428 | ``` 429 | 430 | 性能分析: 431 | 432 | - 初始堆化所需时间不超过O(n) 433 | 434 | - 排序阶段(不含初始堆化) 435 | 436 | - 一次重新堆化所需时间不超过O(logn) 437 | 438 | - n-1次循环所需时间不超过O(nlogn) 439 | 440 | - 堆排序的时间主要耗费在建初始堆和调整建新堆时进行的反复筛选上。堆排序在最坏情况下,其时间复杂度也为O(nlogn),这是堆排序的最大优点。无论待排序列中的记录是正序还是逆序排列,都不会使堆排序处于“最好”或“最坏”的状态。 441 | 442 | - 另外,堆排序仅需一个记录大小供交换用的辅助存储空间。 443 | 444 | - 然而堆排序是一种**不稳定**的排序方法,它不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的。 445 | 446 | ## 8.5 归并排序 447 | 448 | **归并排序**:将两个或两个以上的有序子序列“归并”为一个有序序列。在内部排序中,通常采用的是**2-路归并排序**。 449 | 450 | 归并排序的关键问题在于:如何将两个有序序列合成一个有序序列? 451 | 452 | 时间效率:O(nlogn);空间效率:O(n);归并排序是**稳定**的。 453 | 454 | ## 8.6 基数排序 455 | 456 | 基数排序也叫**桶排序**或**箱排序**:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后在按序号将非空的连接。 457 | 458 | 时间效率:O(k*(n+m)),k为关键字个数,m为关键字取值范围;空间效率:O(n);基数排序是**稳定**的。 459 | 460 | ## 8.7 外部排序(略) 461 | 462 | ## 8.8 排序方法比较 463 | 464 | ![图03:排序方法比较](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter8%20Sorting/%E5%9B%BE03%EF%BC%9A%E6%8E%92%E5%BA%8F%E6%96%B9%E6%B3%95%E6%AF%94%E8%BE%83.png,'排序方法比较') 465 | 466 | 1. 时间性能 467 | 468 | 1. 按平均的时间性能来分,有三类排序方法: 469 | 470 | - 时间复杂度为O(nlogn)的方法有:快速排序、堆排序和归并排序,其中以快速排序为最好; 471 | 472 | - 时间复杂度为O(n^2)的有:直接插入排序、冒泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此; 473 | 474 | - 时间复杂度为O(n)的排序方法只有:基数排序。 475 | 476 | 2. 当待排记录序列按关键字顺序**有序**时,直接插入排序和冒泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间性能退化为O(n^2),因此是应该尽量避免的情况。 477 | 478 | 3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。 479 | 480 | 2. 空间性能 481 | 482 | 指的是排序过程中所需的辅助空间大小。 483 | 484 | 1. 所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1) 485 | 486 | 2. 快速排序为O(logn),为栈所需的辅助空间 487 | 488 | 3. 归并排序所需辅助空间最多,其空间复杂度为O(n) 489 | 490 | 4. 链式基数排序需附设队列首尾指针,则空间复杂度为O(rd) 491 | 492 | 3. 排序的稳定性 493 | 494 | - 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。 495 | 496 | - 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。 497 | 498 | - 对于不稳定的排序方法,只要能举出一个实例说明即可。 499 | 500 | - **快速排序和堆排序是不稳定的排序方法**。 501 | 502 | 4. 关于“排序方法的时间复杂度的下限” 503 | 504 | - 本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字"进行排序的排序方法,可以证明,这类排序法可能达到的**最快的时间复杂度为O(nlogn)**。(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)。 505 | 506 | - 可以用一棵**判定树**来描述这类基于“比较关键字”进行排序的排序方法。 507 | 508 | -------------------------------------------------------------------------------- /Chapter8 Sorting/图01:排序方法的分类.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter8 Sorting/图01:排序方法的分类.png -------------------------------------------------------------------------------- /Chapter8 Sorting/图02:主要学习内容.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter8 Sorting/图02:主要学习内容.png -------------------------------------------------------------------------------- /Chapter8 Sorting/图03:排序方法比较.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vuean/DataStructure-Algorithmics/c5d1175c65d9ee6eae8ea9695ec49d38c89c8361/Chapter8 Sorting/图03:排序方法比较.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataStructure-Algorithmics 2 | 3 | 数据结构与算法基础(青岛大学-王卓) 4 | 5 | [第一章 绪论](https://github.com/Vuean/DataStructure-Algorithmics/tree/main/Chapter1%20Abstract) 6 | 7 | [第二章 线性表](https://github.com/Vuean/DataStructure-Algorithmics/tree/main/Chapter2%20LinearList) 8 | 9 | [第三章 栈和队列](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter3%20StackAndQueue/README.md) 10 | 11 | [第四章 串、数组和广义表](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter4%20String/README.md) 12 | 13 | [第五章 树和二叉树](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter5%20TreeAndBianryTree/README.md) 14 | 15 | [第六章 图](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter6%20Graph/README.md) 16 | 17 | [第七章 查找](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter7%20Search/README.md) 18 | 19 | [第八章 排序](https://github.com/Vuean/DataStructure-Algorithmics/blob/main/Chapter8%20Sorting/README.md) --------------------------------------------------------------------------------