├── C语言笔记(翁恺版本).md ├── C语言笔记(翁恺版本).pdf ├── README.md ├── 数据结构第一周笔记——基本概念(慕课浙大版本--XiaoYu).md ├── 数据结构第七周笔记——图(中)(慕课浙大版本--XiaoYu).md ├── 数据结构第三周笔记——树(上)(慕课浙大版本--XiaoYu).md ├── 数据结构第九周笔记——排序(上)(慕课浙大版本--XiaoYu).md ├── 数据结构第二周笔记——线性结构(慕课浙大版本--XiaoYu).md ├── 数据结构第五周笔记——树(下)(慕课浙大版本--XiaoYu).md ├── 数据结构第八周笔记——图(下)(慕课浙大版本--XiaoYu).md ├── 数据结构第六周笔记——图(上)(慕课浙大版本--XiaoYu).md ├── 数据结构第十一周笔记—— 散列查找 (慕课浙大版本--XiaoYu).md ├── 数据结构第十二周笔记—— 综合习题选讲 (慕课浙大版本--XiaoYu).md ├── 数据结构第十周笔记——排序(下)(慕课浙大版本--XiaoYu).md └── 数据结构第四周笔记——树(中)(慕课浙大版本--XiaoYu).md /C语言笔记(翁恺版本).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2002XiaoYu/C-and-DataStructure-Notes/5a21dc682788625118c37622eb52f85d04c4830c/C语言笔记(翁恺版本).pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C-and-DataStructure-Notes 2 | 这是来自慕课浙大的翁恺C语言笔记(4w字)跟刘姥姥的数据结构笔记(10w字) 3 | 以上所有内容均来自小余学习过程中自己写的原创笔记,如果对你有所帮助请帮我点一个star 4 | 5 | --- 6 | 7 | ### C语言作为大学生理工科必学科目,一个好的课程是不可避免的,我在这里推荐中国慕课APP中的浙江大学翁恺老师的C语言,由浅入深,让人对编程提起兴趣。学习完不可避免会产生遗忘,这里有份详细的笔记会适合你进行复习的,期末考试复习必备(会比重新观看视频快很多哦)

学习完如果你仍有兴趣,可以继续学习进阶的数据结构,笔记同样为您准备好了,总字数10w将浙大陈越的课程中所有思路都记录了下来 8 | -------------------------------------------------------------------------------- /数据结构第一周笔记——基本概念(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第一周笔记——基本概念(慕课浙大版本--XiaoYu) 2 | 3 | ## 数据结构定义 4 | 5 | 1. 没有官方的定义,我选取慕课给出的3个定义中最通俗易懂的记录下来 6 | 7 | 2. 数据结构(data structure)是计算机中存储,组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法--中文维基百科 8 | 9 | #### 三个例子 10 | 11 | 1. 例1:如何在书架上摆放图书 12 | 2. 二分查找:二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用[顺序存储结构](https://baike.baidu.com/item/顺序存储结构/1347176),而且表中元素按关键字有序排列 13 | 1. 首先,假设表中元素是按升序排列,将表中间位置记录的[关键字](https://baike.baidu.com/item/关键字)与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置[记录](https://baike.baidu.com/item/记录/1837758)将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的[记录](https://baike.baidu.com/item/记录/1837758),使查找成功,或直到子表不存在为止,此时查找不成功。 14 | 15 | 3. 例2:写程序实现一个函数PrintN,使得传入一个正整数为N的参数后,能顺序打印从1到N的全部正整数 16 | 17 | ```c 18 | //循环实现 19 | void PrintN ( int N ) 20 | { 21 | int i; 22 | for( i=1 ;i<=N ;i++){ 23 | printf("%d\n",i); 24 | } 25 | return; 26 | } 27 | 28 | //递归实现 弊端:递归的程序对空间的占用有的时候是很恐怖的 29 | void PrintN ( int N ) 30 | { 31 | if( N ){ 32 | printN( N - 1 ); 33 | printf("%d\n",N); 34 | } 35 | return; 36 | } 37 | 38 | //解决问题方法的效率,也跟空间的利用效率有关 39 | ``` 40 | 41 | 4. 例3:写程序计算给定多项式在给定点x处的值 42 | 43 | ![image-20220626145149531](https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/925/image-20220626145149531.png) 44 | 45 | ```c 46 | //直接翻译的结构 47 | double f( int n, double a[], double x) 48 | { 49 | int i; 50 | double p = a[0]; 51 | for ( i = 0 ; i <= n ; i++ ){ 52 | p += (a[i] * pow(x,i)); 53 | } 54 | return p; 55 | } 56 | //秦久邵的方法 57 | double f( int n, double a[], double x) 58 | { 59 | int i; 60 | double p = a[n] 61 | for( i = n ; i > 0 ; i-- ){ 62 | p = a[i-1] + x * p; 63 | } 64 | return p; 65 | } 66 | ``` 67 | 68 | 秦久邵的方法公式图 69 | 70 | ![image-20220626145709436](https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/925/image-20220626145709436.png) 71 | 72 | 73 | 74 | clock():捕捉从程序开始运行到clock()被调用时所耗费的时间。这个时间单位是clock tick,即"时钟打点" 75 | 76 | 常数CLK_TCK:机器时钟每秒所走的时钟打点数 77 | 78 | ```c 79 | //这套流程的模板 80 | #include 81 | #include 82 | 83 | clock_t start,stop;//clock_t是clock()函数返回的变量类型 84 | 85 | double duration;//记录被测函数的运行时间 86 | 87 | int main() 88 | {//不在测试范围内的准备工作写在clock()调用之前 89 | start = clock();//开始计时 90 | MyFunction();//把被测函数加在这里 91 | stop = clock();//停止计时 92 | duration = ((double)(stop - start))/CLK_TCK;//计算时间 93 | //其他不在测试范围的处理写在后面,例如输出duration的值 94 | return 0; 95 | } 96 | ``` 97 | 98 | ![image-20220626151549866](https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/925/image-20220626151549866.png) 99 | 100 | 尝试计算这个图中的式子跑了多久 101 | 102 | ```c 103 | #include 104 | #include 105 | #include 106 | clock_t start,stop; 107 | double duration; 108 | #define ,MAXN 10 //多项式最大项数,即多项式阶数+1 109 | double f1(int n , double a[] , double x); 110 | double f2(int n , double a[] , double x); 111 | 112 | int main() 113 | { 114 | int i; 115 | double a[MAXN];//存储多项式的系数 116 | for (i = 0; i < MAXN; i++) a[i] = (double)i; 117 | 118 | //不在测试范围内的准备工作写在clock()调用之前 119 | start = clock();//开始计时 120 | f1(MAXN-1 , a , 1.1);//把被测函数加在这里 121 | stop = clock();//停止计时 122 | duration = ((double)(stop - start))/CLK_TCK;//计算时间 123 | //其他不在测试范围的处理写在后面,例如输出duration的值 124 | printf("ticks1 = %f\n",(double)(stop-start)); 125 | printf("duration1 = %6.2e\n",duration); 126 | 127 | start = clock();//开始计时 128 | f2(MAXN-1 , a , 1.1);//把被测函数加在这里 129 | stop = clock();//停止计时 130 | duration = ((double)(stop - start))/CLK_TCK;//计算时间 131 | //其他不在测试范围的处理写在后面,例如输出duration的值 132 | printf("ticks1 = %f\n",(double)(stop-start)); 133 | printf("duration2 = %6.2e\n",duration); 134 | 135 | 136 | return 0; 137 | } 138 | 139 | //跑出来结果都是0,因为运行太快了,clock函数捕捉不到它的区别 140 | //解决方案:让被测函数重复运行充分多次,使得测出的总的时钟打点间隔充分长,最后计算被测函数平均每次运行的时间即可 141 | ``` 142 | 143 | 以下是解决方案修改后的函数,只截取修改的部分 144 | 145 | ```c 146 | #define ,MAXK 1e7 //被测函数最大重复调用次数 147 | double f1(int n , double a[] , double x); 148 | double f2(int n , double a[] , double x); 149 | 150 | int main() 151 | { 152 | int i; 153 | double a[MAXN];//存储多项式的系数 154 | for (i = 0; i < MAXN; i++)//重复调用函数以获得充分多的时钟打点数 155 | f1(MAXN-1,a,1.1); 156 | stop = clock(); 157 | start = clock();//开始计时 158 | duration = ((double)(stop - start))/CLK_TCK/MAXK;//计算函数单词运行的时间 159 | //其他不在测试范围的处理写在后面,例如输出duration的值 160 | printf("ticks1 = %f\n",(double)(stop-start)); 161 | printf("duration1 = %6.2e\n",duration); 162 | 163 | //以下第二个f2保持不变进行对比 164 | 165 | start = clock();//开始计时 166 | f2(MAXN-1 , a , 1.1);//把被测函数加在这里 167 | stop = clock();//停止计时 168 | duration = ((double)(stop - start))/CLK_TCK;//计算时间 169 | //其他不在测试范围的处理写在后面,例如输出duration的值 170 | printf("ticks1 = %f\n",(double)(stop-start)); 171 | printf("duration2 = %6.2e\n",duration); 172 | 173 | 174 | return 0; 175 | } 176 | ``` 177 | 178 | **解决问题方法的效率,跟算法的巧妙程度有关** 179 | 180 | 181 | 182 | ### 什么是数据结构 183 | 184 | 1. **数据对象**在计算机中的组织方式 185 | 1. 逻辑结构(一对多的逻辑结构有个名字叫做"树")=>树形结构 线性结构(一对一) 图的结构(多对多) 186 | 2. 物理存储结构 187 | 3. 抽象数据类型(Abstract Data Type) 188 | 1. 数据类型 189 | 1. 数据对象集:就是我们说的"是什么东西" 190 | 2. 数据集合相关联的操作集 191 | 2. 抽象:描述数据类型的方法不依赖于具体实现 192 | 1. 与存放数据的机器无关 193 | 2. 与数据存储的物理结构无关 194 | 3. 与实现操作的算法和编程语言都无关只描述 195 | 4. 只描述数据对象集和相关操作集"是什么",并不涉及"如何做到"的问题 196 | 2. 数据对象必定与一系列加在其上的操作相关联 197 | 3. 完成这些操作所用的方法就是算法 198 | 199 | **例4:"矩阵"的抽象数据类型定义** 200 | 201 | 1. 类型名称:矩阵(Matrix) 202 | 203 | image-20220626155802588 204 | 205 | Multiply:乘的意思 206 | 207 | a是矩阵元素的值:那要用二维数组去存他还是一维数组又或者是十字链表呢?答案是不用关心,我们需要的只是一个矩阵 208 | 209 | Matrix Add(...):先按行加?先按列加?使用什么语言? 答案是统统不管,这就是"抽象" 210 | 211 | ## 什么是算法 212 | 213 | ## 定义 214 | 215 | 1. 算法(**Algorithm**) 216 | 217 | 1. 一个有限指令集 218 | 2. 接受一些输入(有些情况不需要输入) 219 | 3. 一定至少会产生一个输出(否则就没有意义了) 220 | 4. 一定在有限步骤之后终止的,他不像是操作系统只要不关机就可以一直跑在上面 221 | 5. 描述算法的时候不能有无限循环的概念的 222 | 6. 每一条指令必须 223 | 1. 有充分明确的目标,不可以有歧义 224 | 2. 计算机能处理的范围之内(目标不可以太远大) 225 | 3. 描述手段应该"抽象",不依赖于任何一种计算机语言以及具体的实现手段 226 | 227 | ##### 例1:选择排序算法的伪码描述 228 | 229 | ```c 230 | void SelectionSort ( int List[], int N) 231 | { 232 | //将N个整数List[0]...List[N-1]进行非递减排序 233 | for(i = 0; i < N; i++){ 234 | MinPosition = ScanForMin(List, i, N-1); 235 | //ist[i]到List[N-1]中找最小元,并将其位置赋给MinPosition; 236 | Swap(List[i],List[MinPosition]); 237 | //排序部分的最小元换到有序部分的最后位置; 238 | } 239 | } 240 | //这不是C语言,虽然他带有C语言的一些特征,但他for循环里面的内容是用自然语言来描述的.上面伪码描述特点:抽象 241 | 242 | 抽象---- 243 | List到底是数组还是链表(虽然看上去很像数组)? 其实不管是数组还是链表都不会报错 244 | Swap用函数还是用宏去实现(虽然他看上去很像一个函数)? 但其实用宏写也可以,在我们使用算法的时候是不关心的 245 | ``` 246 | 247 | ### 什么是好的算法? 248 | 249 | 1. 空间复杂度S(n)——根据算法写成的程序在执行时**占用存储单元的长度**。这个长度往往与输入数据的规模有关。空间复杂度过高的算法可能导致使用的内存超限,造成程序非正常中断。 250 | 2. 时间复杂度T(n)——根据算法写成的程序在执行时**耗费时间的长度**。这个长度往往也跟输入数据的规模有关。时间复杂度过高的低效算法可能导致我们在有生之年都等不到运行结果。 251 | 252 | 这个n是我们要处理的内容,这个程序所用的时间与空间都跟这个n是有直接关系的 253 | 254 | ```c 255 | //递归实现 弊端:递归的程序对空间的占用有的时候是很恐怖的 256 | void PrintN ( int N ) 257 | { 258 | if( N ){ 259 | //假设N=10w,第一步就是10w-1,调用这个函数之前,你的系统需要把当前的这个函数所有的现有的状态都存到系统内存的某一个地方 260 | //原本是存一下使用后就可以删掉了,使用递归之后在你执行10w-99999之前要把前面所有的运算先执行一遍而不是直接10w-99999,一次性存这么多内容,内存会爆掉的 261 | //S(N)=C(常数)*N =>线性增长 262 | printN( N - 1 ); 263 | printf("%d\n",N); 264 | } 265 | return; 266 | } 267 | ``` 268 | 269 | 借用上面例3的案例 270 | 271 | ```c 272 | //计算机算加减比算乘除快很多 273 | //直接翻译的结构 274 | double f( int n, double a[], double x) 275 | { 276 | int i; 277 | double p = a[0]; 278 | for ( i = 0 ; i <= n ; i++ ){ 279 | p += (a[i] * pow(x,i)); 280 | } 281 | return p; 282 | }//这里一共运行了(1+2+...+n)=(n²+n)/2次乘法 时间复杂度:T(n) = C1n² +C2n 283 | //秦久邵的方法 284 | double f( int n, double a[], double x) 285 | { 286 | int i; 287 | double p = a[n] 288 | for( i = n ; i > 0 ; i-- ){ 289 | p = a[i-1] + x * p; 290 | } 291 | return p; 292 | }//这里一共就运行了n次乘法 时间复杂度:T(n) = C *n 293 | ``` 294 | 295 | image-20220626165535815 296 | 297 | ### 复杂度的渐进表示法 298 | 299 | “上界(upper bound)是一个与偏序集有关的特殊元素,指的是偏序集中大于或等于它的子集中一切元素的元素。若数集S为实数集R的子集有上界,则显然它有无穷多个上界,而其中最小的一个上界常常具有重要的作用,称它为数集S的上确界。” 300 | 301 | 太大的上界跟太小的下界对我们分析算法的时候是没有太大的帮助的,尽可能跟它的真实情况贴得越近越好 302 | 303 | image-20220626170030324 304 | 305 | 当我们在取大O的时候,我们一般取的是最小的那个上界。当我们在取Ω的时候通常是写的我们能力范围内找到的最大的那个下界 306 | 307 | image-20220626170944925 308 | 309 | 310 | 311 | image-20220626171628558 312 | 313 | ### 复杂度分析窍门 314 | 315 | image-20220626172743421 316 | 317 | 比如说,如果我们有两段算法,我们知道它们复杂度的上界是什么。如果把两段算法拼在一起的时候,总时间就是两段的和。 318 | 319 | 那么它们的上界,就是两个上界中间,比较大的那个 320 | 321 | 当我们把两段算法嵌套起来的时候,两个复杂度要相乘的时候,它的上界就是它们上界的乘积 322 | 323 | 通过这个我们可以知道如果T(n)是关于n的一个k阶多项式的话,真正起作用的只有最大的那一项 ,其他项都是可以忽略不计的 324 | 325 | 一个for循环的时间复杂度等于循环次数乘以循环体代码的复杂度,这是一个相乘的关系 326 | 327 | if-else :结构的复杂度取决于if的条件判断复杂度和两个分支部分的复杂度,总体复杂度取决三者中最大 328 | 329 | ## 1.3应用实例:最大子列和问题 330 | 331 | image-20220626172839932 332 | 333 | ```c 334 | //这是一个从Ai到Aj连续的一段子列的和 335 | //对N个整数来说,有很多这样的连续的子列,我们要求的是所有连续子列和里面最大的那个,如果这个和是负数的话,我们最后就返回0作为结束 336 | //方法1:暴力破解;方法2: 337 | 338 | //以下是暴力破解算法1: 339 | int MaxSubseqSum1( int A[], int N) 340 | { 341 | int ThisSum,MaxSum = 0; 342 | int i,j,k; 343 | for( i = 0 ; i < N ;i++ ){ 344 | //i是子列左端位置 345 | for( j = i ; j < N; j++ ){ 346 | //j是子列右端位置 347 | ThisSum = 0;//This是从A[i]到A[j]的子列和 348 | for( k = i; k <= j ;k++){ 349 | ThisSum += A[k]; //A[i]一直加到A[j] 350 | } 351 | if(ThisSum > MaxSum);//如果刚得到的这个子列和更大 352 | MaxSum = ThisSum;//则更新结果 353 | 354 | }//j循环结束 355 | }//i循环结束 356 | return MaxSum; 357 | } 358 | //复杂度:T(N) = O(N³),因为三层嵌套的for循环 359 | 360 | 361 | //算法2:上面中的k循环其实是没有必要的,属于多余的。我只需要在前面一个j的基础上加一个元素就好了 362 | int MaxSubseqSum1( int A[], int N) 363 | { 364 | int ThisSum,MaxSum = 0; 365 | int i,j,k; 366 | for( i = 0 ; i < N ;i++ ){ 367 | //i是子列左端位置 368 | ThisSum = 0;//This是从A[i]到A[j]的子列和 369 | for( j = i ; j < N; j++ ){ 370 | //j是子列右端位置 371 | ThisSum += A[j];//对于相同的i,不同的j,只要在j-1次循环的基础上累加1项即可 372 | if(ThisSum > MaxSum);//如果刚得到的这个子列和更大 373 | MaxSum = ThisSum;//则更新结果 374 | 375 | }//j循环结束 376 | }//i循环结束 377 | return MaxSum; 378 | } 379 | //复杂度是:T(N) = O(N²),因为两层嵌套的for循环 380 | 381 | //算法3:分而治之:把一个比较大的复杂问题切分成小块,然后分头解决,最后再把结果合并起来,这就是分而治之 382 | //第一步:先"分",也就是说把数组从中间一分为二(二分法),然后递归地去解决左右两边的问题 383 | //递归地去解决左边的问题,我们会得到左边的一个最大子列和,同理得到右边的最大子列和 384 | //特殊情况:跨越边界的最大子列和 385 | //第二步:后"合"找到两个最大子列和和这个跨越边界的最大子列和后,最后的结果一定是这三个数中间最大的那一个 386 | 387 | #include 388 | 389 | int Max3( int A, int B, int C ) 390 | { /* 返回3个整数中的最大值 */ 391 | return A > B ? A > C ? A : C : B > C ? B : C; 392 | } 393 | 394 | int DivideAndConquer( int List[], int left, int right ) 395 | { /* 分治法求List[left]到List[right]的最大子列和 */ 396 | int MaxLeftSum, MaxRightSum; /* 存放左右子问题的解 */ 397 | int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界线的结果*/ 398 | 399 | int LeftBorderSum, RightBorderSum; 400 | int center, i; 401 | 402 | if( left == right ) { /* 递归的终止条件,子列只有1个数字 */ 403 | if( List[left] > 0 ) return List[left]; 404 | else return 0; 405 | } 406 | 407 | /* 下面是"分"的过程 */ 408 | center = ( left + right ) / 2; /* 找到中分点 */ 409 | /* 递归求得两边子列的最大和 */ 410 | MaxLeftSum = DivideAndConquer( List, left, center ); 411 | MaxRightSum = DivideAndConquer( List, center+1, right ); 412 | 413 | /* 下面求跨分界线的最大子列和 */ 414 | MaxLeftBorderSum = 0; LeftBorderSum = 0; 415 | for( i=center; i>=left; i-- ) { /* 从中线向左扫描 */ 416 | LeftBorderSum += List[i]; 417 | if( LeftBorderSum > MaxLeftBorderSum ) 418 | MaxLeftBorderSum = LeftBorderSum; 419 | } /* 左边扫描结束 */ 420 | 421 | MaxRightBorderSum = 0; RightBorderSum = 0; 422 | for( i=center+1; i<=right; i++ ) { /* 从中线向右扫描 */ 423 | RightBorderSum += List[i]; 424 | if( RightBorderSum > MaxRightBorderSum ) 425 | MaxRightBorderSum = RightBorderSum; 426 | } /* 右边扫描结束 */ 427 | 428 | /* 下面返回"治"的结果 */ 429 | return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum ); 430 | } 431 | 432 | int MaxSubseqSum3( int List[], int N ) 433 | { /* 保持与前2种算法相同的函数接口 */ 434 | return DivideAndConquer( List, 0, N-1 ); 435 | } 436 | int main() { 437 | int k; 438 | scanf("%d", &k); 439 | int a[k] = {0}; 440 | for (int i = 0 ; i < k; i++) 441 | scanf("%d", &a[i]); 442 | printf("%d\n", MaxSubseqSum3(a, k)); 443 | return 0; 444 | } 445 | ``` 446 | 447 | image-20220626180316692 448 | 449 | image-20220626180901926 450 | 451 | image-20220626181133741 452 | 453 | 当两个复杂度加在一起的时候,我们得到的是比较大的那项,所以我们取O弃N 454 | 455 | N/2的k次方=1其实就是2的k次方=N 456 | 457 | 每展开一层就多一个cN 458 | 459 | 不是说4变成了1,而是在求[4,-3,5,-2]这个区间的最大子列和时,要选取[4,-3,5]才能达到最大值 460 | 461 | 一般地,区间[L,mid],[mid,R]合并后的最大子列和,等于下面这几种情况的最大值: 462 | 1、区间[L,mid]的最大子列和 463 | 2、区间[mid,R]的最大子列和 464 | 3、区间[L,mid]所有元素的和,加上区间[mid,R]的最大前缀和 465 | 4、区间[mid,R]所有元素的和,加上区间[L,mid]的最大后缀和 466 | 467 | ```c 468 | //算法3---分治法 469 | //以下内容来自教案给出的答案 470 | int Max3( int A, int B, int C ) 471 | { /* 返回3个整数中的最大值 */ 472 | return A > B ? A > C ? A : C : B > C ? B : C; 473 | } 474 | 475 | int DivideAndConquer( int List[], int left, int right ) 476 | { /* 分治法求List[left]到List[right]的最大子列和 */ 477 | int MaxLeftSum, MaxRightSum; /* 存放左右子问题的解 */ 478 | int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界线的结果*/ 479 | 480 | int LeftBorderSum, RightBorderSum; 481 | int center, i; 482 | 483 | if( left == right ) { /* 递归的终止条件,子列只有1个数字 */ 484 | if( List[left] > 0 ) return List[left]; 485 | else return 0; 486 | } 487 | 488 | /* 下面是"分"的过程 */ 489 | center = ( left + right ) / 2; /* 找到中分点 */ 490 | /* 递归求得两边子列的最大和 */ 491 | MaxLeftSum = DivideAndConquer( List, left, center ); 492 | MaxRightSum = DivideAndConquer( List, center+1, right ); 493 | 494 | /* 下面求跨分界线的最大子列和 */ 495 | MaxLeftBorderSum = 0; LeftBorderSum = 0; 496 | for( i=center; i>=left; i-- ) { /* 从中线向左扫描 */ 497 | LeftBorderSum += List[i]; 498 | if( LeftBorderSum > MaxLeftBorderSum ) 499 | MaxLeftBorderSum = LeftBorderSum; 500 | } /* 左边扫描结束 */ 501 | 502 | MaxRightBorderSum = 0; RightBorderSum = 0; 503 | for( i=center+1; i<=right; i++ ) { /* 从中线向右扫描 */ 504 | RightBorderSum += List[i]; 505 | if( RightBorderSum > MaxRightBorderSum ) 506 | MaxRightBorderSum = RightBorderSum; 507 | } /* 右边扫描结束 */ 508 | 509 | /* 下面返回"治"的结果 */ 510 | return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum ); 511 | } 512 | 513 | int MaxSubseqSum3( int List[], int N ) 514 | { /* 保持与前2种算法相同的函数接口 */ 515 | return DivideAndConquer( List, 0, N-1 ); 516 | } 517 | ``` 518 | 519 | 520 | 521 | image-20220626195900466 522 | 523 | ```c 524 | //算法4:在线处理 525 | //在线处理指的是一组数据。例如[-1,3,-2,4,-6,1,6,-1],我算到第4位数停了下来,返回的数据对于前4位数来说是正确的 526 | //每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都可以正确给出当前的解 527 | int MaxSubseqSum4( int A[],int N ) 528 | { 529 | int ThisSum,MaxSum; 530 | int i; 531 | ThisSum = MaxSum = 0; 532 | for( i = 0; i < N; i++ ){ 533 | ThisSum += A[i];//向右累加 534 | if( ThisSum > MaxSum) 535 | MaxSum = ThisSum;//发现更大则更新当前结果 536 | else if(ThisSum < 0)//如果当前子列和为负 537 | ThisSum = 0;//则不可能使后面的部分和增大,抛弃之 538 | } 539 | return MaxSum; 540 | } 541 | //复杂度:T(N) = O(N) 是线性的 542 | //作为运行效率最高的算法,也是有所副作用的,在这里的副作用是:正确性不是特别明显 543 | 544 | 545 | ``` 546 | 547 | -------------------------------------------------------------------------------- /数据结构第七周笔记——图(中)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第七周笔记——图(中)(慕课浙大版本--XiaoYu) 2 | 3 | ## 树之习题选讲-Tree Traversals Again 4 | 5 | ### 树习题-TTA.1 题意理解 6 | 7 | 非递归中序遍历的过程 8 | 9 | 1. Push的顺序为先序遍历(pre) 10 | 2. Pop的顺序给出中序遍历(in) 11 | 12 | image-20220728170236134 13 | 14 | 15 | 16 | 17 | 18 | ### 树习题-TTA.2 核心算法 19 | 20 | image-20220728191451717 21 | 22 | ![image-20220728194407506](https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/925/image-20220728194407506.png) 23 | 24 | 25 | 26 | ```c 27 | 上图分别是先序、中序、后序遍历通过规律我们可以看到他们之间的位置分配 28 | //伪代码 29 | void solve(int preL,int inL,int n) 30 | { 31 | if(n == 0) return;//n等于0的时候什么都不做(n真的会右等于0的时候吗?为什么写他?)调用完了之后右边没有元素,此时n等于0,进行判断正常结束进程 32 | if(n == 1){post[postL] == pre[preL];return;}//只有一个结点的时候,pre、in、post都应该等于同一个数字 33 | root = pre[preL]; 34 | post[postL+n-1] = root; 35 | for(i = 0; i < n; i++) 36 | if(in[inL+i] == root) break; //判断in这个位置上的元素是不是等于根结点。结合上图看就是要找根结点在图中的哪里,找到了就跳出循环 37 | //在跳出循环之后我们就同时知道了左子树包含了多少元素,然后递归的去解决左边跟右边的问题 38 | L = i;R = n - L -1//得出左右两边的元素个数 39 | solve(preL+1,inL,postL,L);//左边的 40 | solve(preL+L+1,inL+L+1,postL+L,R)//右边的。第一个是蓝色的第一个位置,第二个也是蓝色一个的位置,第三个是蓝色的第一个位置(和前两个不同的是这个时候1在蓝色的后面,所以不用加上1了,第四个参数是右边子问题的总个数) 41 | 42 | } 43 | ``` 44 | 45 | 46 | 47 | ## 树之习题选讲-Complete Binary Search Tree(完全 二叉 搜索 树) 48 | 49 | ### 树习题-CBST.1 数据结构的选择 50 | 51 | #### 题意理解 52 | 53 | image-20220730184530957 54 | 55 | ``` 56 | 题目要求: 57 | 输入一系列整数,要填入一个完全二叉树里面,同时这颗树还需要满足二叉搜索树的性质。也就是说左边的结点的键值都比根结点要小,右边的全部都比他要大,左右子树也都满足二叉搜索树递归的定义 58 | ``` 59 | 60 | 经过修改后的图片如下: 61 | 62 | image-20220730185418653 63 | 64 | **树的表示法:链表 vs 数组** 65 | 66 | ``` 67 | 在这道题目中我们需要的操作: 68 | 1.填写数字(某种遍历),意思就是说对这棵树里面的每一个结点访问一次填写一个数字 69 | 2.层序遍历 70 | ``` 71 | 72 | **为什么在很多情况下我们宁愿用链表去表示一颗树而不用数组呢?** 73 | 74 | ``` 75 | 用数组是可以的,好处是不涉及任何指针的操作。指针的操作通常是比较危险又比较耗时的 76 | 缺点:用数组去存,遇到左右不完全相等的树的时候,需要把中间那些不存在的结点的空间保留下来,要是树极端一点就非常耗费空间,在链表就没有这个问题 77 | ``` 78 | 79 | 在这个问题中我们需要解决的问题: 80 | 81 | 1. **完全二叉树,不浪费空间**(从根结点到最后一个叶子结点一层层走下来没有一个是空的,没有一个元素是浪费的) 82 | 2. 层序遍历 == 直接顺序输出(按照下标顺序输出就结束了,用数组的话)所以我们决定用数组解决这个问题 83 | 84 | ### 树习题-CBST.2 核心算法 85 | 86 | 如果我们知道左子树一共包含了多少个结点,那就知道根结点R上放的一定是从小到大排第几位的那个数字 87 | 88 | 如何知道左子树有多少个结点呢? 89 | 90 | 1. 给定n个数之后,完全二叉树的结构是固定的,可以非常准确的算出左边一共多少个结点 91 | 2. 先给我们要输入的序列从小到大排一个序列。根据完全二叉树一共有多少个结点,通过导出的公式是可以精确计算他的左子树一共有多少个结点 92 | 3. image-20220730200103573典型先序遍历的应用 93 | 94 | #### 核心算法 95 | 96 | image-20220730215836941 97 | 98 | TRoot是树T根结点所在的位置,那个元素的下标存在TRoot里面 99 | 100 | ```c 101 | void solve(int ALeft,int ARight,int TRoot) 102 | { 103 | //初始调用为solve(0,N-1,0)->A的起始点就是A的第0个元素,A的终止点就是A的最后一个元素(N-1是他的下标),结果树T是完全二叉树,他的根结点一定存在T[0]这个位置,所以刚开始传入的是0 104 | //整个函数要完成的任务就是从A传进去的这一段里面选出一个正确的数字,填到我们结果的这颗树,TRoot的这颗树上 105 | n = ARight - ALeft + 1;//这一段里面元素总个数n(最右边下标减去最左边下标加上1) 106 | if(n == 0) return; 107 | L = GetLeftLenght(n);//计算出n个结点的树(完全二叉树)其左子树有多少个结点 108 | T[TRoot] = A[ALeft + L]; 109 | //左孩子的下标是多少?正常情况下根结点是TRoot的话那他的左孩子下标应该是TRoot×2,但是那是在堆里面(堆的第0个元素是存放哨兵的地方,不是用来存真实值的),在本题中是从0开始算下标的 110 | LeftRoot = TRoot * 2 + 1;//(例子:左孩子第一个元素0*2+1为1,第二个3...) 111 | RightTRoot = LeftTRoot + 1;//(例子:右孩子第一个元素1+1为2,第二个4...) 112 | //准备递归左右边 113 | solve(ALeft,ALeft + L - 1,LeftTRoot);//左边,ALeft + L - 1是取掉根结点元素的前一个值 114 | solve(ALeft + L + 1,ARight,RightTRoot);//右边 115 | 116 | } 117 | ``` 118 | 119 | #### 排序(目前凑合用的,后面详细讲解使用) 120 | 121 | 库函数:qsort 122 | 123 | ```c 124 | #include//调用qsort 125 | int main() 126 | { 127 | .... 128 | qsort(A,N,sizeof(int),compare);//根据返回的两个数是正数还是负数还是0来决定两个元素谁排在前面或者后面或者不做交换 129 | //第一个参数:待排序序列的首元素的位置(就是把读进来的数存在一个叫A的数组里面)也就是说如果我们要将数组排序的话,这个就是数组首元素的地址 130 | //第二个参数:代排序列的总长度(一共要排N个元素) 131 | //第三个参数:要排元素的大小 132 | //第四个参数:不是变量,是一个函数的名字(可以不叫compare,这个随意),他的作用是比较两个元素的大小。因为qsort就是比较一对元素的大小来决定哪个元素应该在前面的,哪个元素应该在后面的 133 | .... 134 | } 135 | 136 | /*compare标准接口 137 | int compare(const void*a,const void*b)传入的是两个带比较元素的指针。需要返回的是三种整数之一(负数正数或者0,这里浙大的课程中字幕第一次出错负数显示为复数,但其实是负数,需要注意) 138 | { 139 | return *(int*)a - *(int*)b;//后序也可以非常复杂 140 | } 141 | */ 142 | ``` 143 | 144 | ### 树习题-CBST.3 计算左子树的规模 145 | 146 | h是层数 147 | 148 | image-20220730232211145 149 | 150 | 真正计算H的时候X的出来的可能不是整数,但不要紧,X是多少都没有关系,求H的话可以忽略掉X然后的出来的答案向下取整即可。得到的H就是完美二叉树层数(H),然后就可以反算出X了 151 | 152 | 完美二叉树的左子树:image-20220730232601997 153 | 154 | image-20220730232638342 155 | 156 | 如果X的个数蔓延到右子树那边去的话(就是最后一层的x跑到右边去,那上面左子树的式子就不能加上X) 157 | 158 | 所以我们需要知道最下面一层x的最大值和最小值可以取多少 159 | 160 | 1. 在这个式子中,最小值X要取到0(为什么不是1而是0呢?L是左子树哦)image-20220730233019880 161 | 162 | 1. 因为在上述式子中H至少为2啦,H为1是根结点的位置层数,左子树至少从第二层开始,最少只有他自己一个 163 | 164 | 2. 要使 ![img](https://img-ph-mirror.nosdn.127.net/gD61yyzBdJCC3bj5y02Eeg==/6631369031884693815.png) 得到正确结果,![img](https://img-ph-mirror.nosdn.127.net/lEiAQJQKvY14Qo2qj0E1Aw==/1072982611321422975.png)能取的最大值是 165 | 166 | 1. image-20220730233616555 167 | 168 | 3. 完整步骤: 169 | 170 | 1. image-20220730233846536 171 | 172 | 173 | 174 | ## 树之习题选讲-Huffman Codes(哈夫曼) 175 | 176 | ### 树习题-HC.1 题意理解 177 | 178 | Huffman编码不唯一 179 | 180 | **注意:最优编码不一定通过Huffman算法得到,但是Huffman算法一定能得到最优编码** 181 | 182 | image-20220731183340358 183 | 184 | image-20220731184926731 185 | 186 | #### Huffman Codes 的特点 187 | 188 | 1. 最优编码——总长度(WPL)最小 189 | 2. 无歧义解码——前缀码:数据仅存于叶子结点 190 | 191 | ``` 192 | 前缀码对应到一颗二叉树里面意味着每一个字符都要放在这棵树的叶子结点上 193 | 如果有任何一个字符放在了中间的内部结点上,那就意味着一定存在另外一个字符(他的编码会经过这个字符的编码),也就是说这个字符就会成为另外一个字符的前缀编码,这样解码的时候就会有歧义 194 | ``` 195 | 196 | 3. Huffman树是没有度为1的结点——满足1、2则必然有3 197 | 198 | ``` 199 | 什么是度为1的结点: 200 | “度是一个计算机的单位,度为1的节点就说明该处的子节点个数为1,度为2就说明个数为2,而度为0的结点叫叶子结点,由二叉树的性质可以知道,二叉树中叶子结点总是比度为2的结点多一个。” 201 | 202 | 这个在程序中不需要判断的,因为满足第一跟第二的话,一定没有度为1的结点 203 | 为什么? 204 | 反证法:如果在这棵树里面存在一个度为1的结点,那么这个结点,假设他又这样一颗子树的话,他肯定不是叶子结点。那么在这个结点上就一定不会放真正的字符,那么他的这个一棵子树下面,一定是有叶子结点的。那些叶子结点对应着真正的字符的。如果我们把这个结点给他去掉的话,并不影响其他的编码仍然保持为前缀码这个特点,因为所有的字符还都在叶子结点上。 205 | 那么他原来子树上所有的叶子结点,所有的编码都会缩短一个点位,于是我们就得到一套更短的编码。但我们说他本身已经是最优的编码了,那已经本身是属于不在可以优化的了,已经没办法得到更短的编码了,所有反证得出一定没有度为1的结点 206 | ``` 207 | 208 | **注意:满足2、3可不一定有1!** 209 | 210 | image-20220731195452124 211 | 212 | image-20220731195626707 213 | 214 | 上图中两边都符合2、3,但是右边才是最优编码。由此得出符合2、3的不一定是最优编码 215 | 216 | ### 树习题-HC.2 计算最优编码长度 217 | 218 | 1. 计算最优编码长度 219 | 220 | 1. ```C 221 | //声明H 222 | MinHeap H = CreateHeap(N);//创建一个空的、容量为N的最小堆(调用CreateHeap创建一个容量为N的最 小堆) 223 | H = ReadData(N);//将f[]读入H->Data[]中,为后面建立最小堆做好准备 224 | HuffmanTree T = Huffman(H);//建立Huffman树,提示:在调用哈夫曼算法的时候必须要先有一个最小堆(建立最小堆是在哈夫曼算法里面完成的) 225 | //哈夫曼算法里面先建立一个最小堆,每次从里面弹出两个最小的结点,然后生成一颗新的树再把它压回去 226 | 计算最优编码长度还是需要我们自己去写一个函数的 227 | int CodeLen = WPL(T,0);//CodeLen返回的就应该是这颗哈夫曼树所对应的最优编码总长度 228 | //T是传入的树根,0是开始的地方 229 | //深度是传进去的每一棵子树的根结点他当前的深度是多少,此代码是从整棵树的根结点开始的(当前是0,从0开始) 230 | 231 | //进行的是一个递归的先序遍历过程 232 | int WPL(HuffmanTree T,int Depth) 233 | { 234 | if(!T->Left && !T->Right ) 235 | return (Depth*T0->Weight);//当前深度乘以他的权重 236 | else//否则T一定有2个孩子 237 | return (WPL(T->Left,Depth+1) + WPL(T->Right,Depth+1));//递归的解决左边的问题和右边的问题,返回两边的和,递归返回的深度是要加1的 238 | } 239 | ``` 240 | 241 | 242 | ### 树习题-HC.3 检查编码 243 | 244 | image-20220801133232845 245 | 246 | ``` 247 | 2与3冲突、100与1001,如果100的最后一个0是叶子结点,那他就不应该还能继续延伸下去 248 | 1与4冲突、1011与101,1011最后一个1是叶子结点,101的最后一个1在内部结点上(因为"1011"相对于"101后面还有一个1,就是说后面还有一个右子树)就冲突了 249 | ``` 250 | 251 | image-20220801132836279 252 | 253 | ## 7.1 最短路径问题 254 | 255 | ### 7.1.1 概述 256 | 257 | ```c 258 | a点到b点之间路径可达最短的一条线路就是最短路径 259 | 260 | 或者说是从a点到b点最省钱的路线,这也是一种最短路的问题 ->这种情况下什么是长什么是短? 261 | //区别就在边上的权重(我们赋予了他们什么意义),比如我们要找最便宜的路径,那么两点之间的那个边的权重就应该定义成票的价格 262 | //我们要找的就是票的价格的和是最小的那条路 263 | 假如我们要找的是a站点到b站点最快的那个路径,这时候什么叫快? 264 | //是 经停的站最少的快 还是说 途经时间最短的快。在这里我们选择前者 265 | //这时候边的权重就定义为1,经停站的个数就是每条边加起来的了 266 | ``` 267 | 268 | #### 最短路径问题的抽象 269 | 270 | 在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径 271 | 272 | 1. 这条路径就是两点之间的最短路径(ShortestPath) 273 | 2. 第一个顶点为源点(Source) 274 | 3. 最后一个顶点为终点(Destination) 275 | 276 | #### 问题分类 277 | 278 | 1. 单源最短路问题: 279 | 1. 从某固定源点出发,求其到所有其他顶点的最短路径 280 | 2. (有向)无权图 281 | 3. (无向)有权图 282 | 2. 多源最短路径问题: 283 | 1. 求任意两顶点间的最短路径 284 | 285 | ### 7.1.2 无权图的单源最短路 286 | 287 | 1. 按照递增(非递减)的顺序找出到各个顶点的最短路 288 | 2. image-20220801185718394 289 | 3. 这是一圈一圈向外扫描的(BFS广度优先搜索),同时也就解决了James Bond从孤岛跳上岸,最少需要多少步 290 | 291 | image-20220801190113983 292 | 293 | 上图中if中判断的条件里的是BFS的,在这里是不需要的,这里需要的是: 294 | 295 | ```c 296 | dist[W] = S到W的最短距离//无穷大、无穷小、-1都可以。因为这些数可以一看到就知道还未被访问过,下面采用-1的方式 297 | dist[S] = 0//初始值是0,表示他到他自己的距离 298 | path[W] = S到W的路上经过的某顶点//记录路径,对每一个顶点W,把源点到w的路上一定要经过的某一个顶点存在这个path里面 299 | ``` 300 | 301 | image-20220801190047751 302 | 303 | 单源最短函数算法 304 | 305 | ```c 306 | void Unweighted ( Vertex S )//unweighted是无权的意思 307 | { 308 | Enqueue(S,Q); 309 | while(!IsEmpty(Q)){ 310 | V = Dequeue(Q);//每次弹出来一个顶点,就意味着这个顶点到s的最短路径已经被找到了 311 | for ( V 的每一个邻接点 W ) 312 | if( dist[W] == -1 ){//正常的数都是正数,-1是属于不会被访问过的 313 | dist[W] == dist[V]+1;//这个时候W的最短距离就找到了 314 | path[W] = V//谁是S到W路上必经的顶点呢?那就是他的前一个顶点(V是顶点的编号)。记录下来这个的作用是:顺着path这个数组一个一个往前推,直到推到源点得到一个反向的路径(然后将其压到堆栈里面(堆栈起反序作用,后序先出),从而得到正确的路径) 315 | Enqueue(W,Q); 316 | } 317 | } 318 | } 319 | ``` 320 | 321 | 如果有|V|个顶点和|E|条边的图用邻接表存储,则算法的时间复杂度是多少? 322 | 323 | ``` 324 | 是 T = O(|V|+|E|) 325 | ``` 326 | 327 | ### 7.2.2-s 无权图的单源最短路示例 328 | 329 | image-20220801223448836 330 | 331 | ```c 332 | void Unweighted ( Vertex S )//unweighted是无权的意思 333 | { 334 | Enqueue(S,Q); 335 | while(!IsEmpty(Q)){//这是判断他是不是为空再决定要不要进行下去 336 | V = Dequeue(Q); 337 | for ( V 的每一个邻接点 W ) 338 | if( dist[W] == -1 ){//正常的数都是正数,-1是属于不会被访问过的 339 | dist[W] == dist[V]+1; 340 | path[W] = V 341 | Enqueue(W,Q); 342 | } 343 | } 344 | } 345 | 346 | //dist是点到源点的最短距离 347 | //path是指当前点在那条路径上 348 | //所以所有的信息就都可以从表格中得到 349 | ``` 350 | 351 | ### 7.1.3 有权图的单源最短路 352 | 353 | image-20220801223914092由图得知从红色的位置到绿色的最短路径是哪条? 354 | 355 | 是这条啦:image-20220801224456016 356 | 357 | 有权图跟无权图最短路的区别: 358 | 359 | ``` 360 | 有权图的最短路不一定是经过顶点数最少的那条路,上图就很好的证实了这点 361 | 上图的最短路上的全重合时1加8等于9.但有权图的最短路是1+4+1=6(权重更低) 362 | ``` 363 | 364 | 如果路径上的权重还有负数的话,是不是最短路又会发生改变,比如这样:image-20220801225311696 365 | 366 | 这个图我如果不停的循环,每圈赚5块,那无限转圈不就反而倒赚正无穷(美好的愿望hh)image-20220801225512626 367 | 368 | 这种情况叫做有一个**负值圈**叫做(negative-cost cycle) 369 | 370 | 1. 图里面要是有这样一种负值圈的话,基本上所有的算法都会挂掉,所有后面不考虑这种情况 371 | 2. 算法相通之处:按照递增(非递减)的顺序找出到各个顶点的最短路 372 | 373 | #### Dijkstra算法 374 | 375 | 跟BFS相似的地方在于他都是把顶点一个个往那个集合里面收 376 | 377 | 1. 令S = {源点s + 已经确定了最短路径的顶点vi} 378 | 379 | 2. 定义一个距离的数组叫做dist:对于任何一个没有被收入的顶点v,我们把dist v定义为 什么呢?定义为源点到这个v的最短路径长度,但是这不是最终的最短路径,但是该路径仅经过s中的顶点。即路径{s->(vi属于S)->v}的最小长度(多半不是我们真正想要的最小长度,但随着一个个顶点不断被加到这个集合里面,这个dist v会慢慢的变小变小,直到最后被完善成那个最短路径),当他成为最短路径后,这个v就被收到了这个集合里面 380 | 381 | 3. 算法能够这样执行很重要的前提:路径是按照递增(非递减)的顺序生成的,则 382 | 383 | 1. 真正的最短路必须只经过S中的顶点(为什么?) 384 | 385 | 2. 采用反证法: 386 | 387 | ``` 388 | 假如我们下一个要把v收进去的时候,从s到v的路径上还存在另外一个点w。这个w是在这个s以外的,那想要从s到v必然要先到达w再到v,这个时候就会有矛盾,s到w的距离显然小于s到v的距离,而v是下一个马上就要被收进去的顶点,我们的路径是按照递增顺序生成的,这就是意味着凡是距离比v要小的那些顶点都应该在他之前就已经被收进去了,w到s的距离显然比v到s的距离小,所有w肯定应该已经在s这个集合里面了,不可能在外面。所以就得出上述的结论 389 | ``` 390 | 391 | 3. 每次就从未收录的顶点中选一个dist最小的收录(贪心算法) 392 | 393 | 4. 增加一个v进去S,可能影响另外一个w的dist值:得到的两个重要的事实在下面的图中选项B里 394 | 395 | 5. image-20220801234849458 396 | 397 | 6. ``` 398 | 1不仅在w的路径上,而且从v到w必定存在一条直接的边(意思就是收w一定是v的邻接点。况且v被收录进去能影响的也就他一圈的邻接点了,所以收录进去的时候看看这个值周围一圈邻接点看谁有没有比他更小的) 399 | 为什么不可能是从s先到v,然后v再经过另外一个顶点再到w呢?这种情况是不可能的,因为路径是按照递增顺序生成的,如果v和w之间还有另外一个顶点的话,那么这个顶点到源点的距离一定比v到源点的距离要大,但是我们假设的是w的dist值应该是从s到w,那条这条路径的长度仅仅经过这个集合里面的顶点。如果另外还有一个节点在v后面的话,那这个顶点不可能在s里面,因为v是新增进去的,v应该是里面集合最长的 400 | ``` 401 | 402 | 1. 更小的dist值可能为:dist[w] = min{dist[w],dist[v] + 的权重} 403 | 404 | #### Dijkstra算法框架 405 | 406 | ```c 407 | void Dijkstra(Vertex s) 408 | { 409 | while(1){ 410 | V = 未收录顶点中dist最小者; 411 | 412 | collected[V] = true;//收到集合里面 413 | for( V的每个邻接点 W ) 414 | if( collected[W] === false ){ 415 | if( dist[V]+E < dist[W] ){//this不能随便初始化,因为这个不等式在的原因,我们当描述一个顶点跟s没有直接的路可以通的时候,一定要把它定义成正无穷,这样当我们发现一个更短路径的时候,才可以把这个往更小的地方去更新(假如我们随便把它定义成-1的话,那这个不等式永远都不会成立了) 416 | dist[W] = dist[V] + E;//是下标 417 | path[W] = V; 418 | } 419 | } 420 | 421 | } 422 | }//Dijkstra算法不能解决有负边的情况,因为这个算法的思路是按照距离从小到大的顺序去收集每一个顶点,如果有一条边是负的,那么对于某一个w来说就有可能说就有了一个dist v 然后他减掉了一个正值,我们就可以得到一个比v还要短的w。然后w之前是排在v的后面的,所以整个算法会乱掉 423 | 424 | 假如有边的话?要怎么做 425 | 426 | Dijkstra算法的时间复杂度:不科学,没有欸。因为在上面的代码中只是一个伪代码演示 427 | 在 dist[W] = dist[V] + E; 中说的是如果我有一个更短的距离,我要把这个值更新为这个最短的距离。这个更新不一定是一个简单的赋值 428 | 方法1:直接扫描所有未收录顶点 -O(|V|)//这个的时间复杂度就是OV 429 | //使用方法1这种粗暴的方式的话那后面的赋值语句真的就只是一行了。得出来的整体复杂度就是 T = O(|V|²+|E|),扫描一遍是V个,一共扫描V遍且扫描的时候涉及边的每个邻接点,也就是每条边又被访问了一遍 430 | //对于稠密图效果好,稠密图是指那些有很多边的(边的条数跟顶点的个数是OV平方数量级的) 431 | 方法2:将dist存在最小堆中 -O(log|V|) 432 | //每次只要把堆的根节点弹出来,然后调整这个最小堆,每次获得最小距离的这一步操作是一个logv的时间复杂做的算法,比前面OV快多了 433 | //更新dist[W]的值 -O(log|V|)变成稍微复杂的事情,不仅要把值更新,还得把它插回那个最小堆里面去 434 | //总体复杂度:T = O(|V|log|V| + |E|log|V|),这个堆稀疏图效果好,如果是稠密图的话可以将复杂度写成elogv 435 | 稀疏图:e跟V是同一个数量级的,不是V平方数量级的,复杂度就是vlogv 436 | ``` 437 | 438 | image-20220802000630555 439 | 440 | 441 | 442 | ### 7.1.3-s 有权图的单源最短路示例 443 | 444 | ```c 445 | void Dijkstra( Vertex s ) 446 | { 447 | while(1){ 448 | V = 未收录顶点中dist最小者; 449 | if( 这样的v不存在 ) 450 | break; 451 | collected[V] = true; 452 | for( V的每个邻接点W ) 453 | if( collected[W] == false ) 454 | if( dist[V] + E < dist[W] ){//小于正无穷就执行,dist[W]还未经过的点是默认为正无穷 455 | dist[W] = dist[V] + E; 456 | path[W] = V; 457 | } 458 | } 459 | } 460 | 461 | 下图中的dist表示源点到目标点之间的权重,path表示当前点的上一个点的下标 462 | ``` 463 | 464 | 刚开始:image-20220802135358543正式进入Dijkstra算法:image-20220802135529500进行邻接点排除前:image-20220802140507807 465 | 466 | 排除完后:image-20220802140800630 467 | 468 | ### 7.1.4 多源最短路算法 469 | 470 | 方法1:直接将单源最短路算法调用|V|遍 471 | 472 | 1. image-20220802162534649对稀疏图效果好 473 | 474 | 方法2:Floyd算法 475 | 476 | 1. image-20220802162637428对稠密图效果好 477 | 478 | #### Floyd算法 479 | 480 | 1. 只经过一部分顶点,只经过编号小于等于k的那些顶点 481 | 2. image-20220802163030611最初的D负一次方是定义为连接矩阵 482 | 3. image-20220802163007951 483 | 4. 如果i和j之间没有直接的边,D[i] [j]应该定义为:正无穷(不是0也不是负无穷) 484 | 5. image-20220802163903479 485 | 1. 这个说人话就是加入k如果导致i到j的最短路径发生了变化(其实就是这个k的加入导致更短的啦),那么i到k和k到j必定是两段最短的路径 486 | 487 | **多源最短路算法** 488 | 489 | ```c 490 | void Floyd() 491 | { 492 | for( i = 0; i < N ; i++ ) 493 | for( j = 0; j < N ; j++ ){ 494 | D[i][j] = G[i][j]; 495 | //path[i][j] = -1;这个二维数组是用来记录路径的,方便求最短路径,初始化为-1表示ij之间现在是没有路径的 496 | } 497 | for( k = 0; k < N ; k++ ) 498 | for( i = 0; i < N ; i++ ) 499 | for( j = 0; j < N ; j++ ) 500 | if( D[i][k] + D[k][j] < D[i][j] ){ 501 | D[i][j] = D[i][k] + D[k][j]; 502 | //如果我们不仅要ij之间的最短距离,还要求输出他们两个之间的那个最短路径 503 | //path[i][j] = k;当路径被更新之后,就意味着i到j就会经过k,我们就把k记在passi和j这个元素里 504 | //这样记录的优势:很容易的把i到j之间的最短路径也打印出来,采用递归打印(i到k的路径,然后把k打出来,最后在递归的打印k到j的那段路径)出来 505 | //i到j的最短路径:i到k的最短路径 + k + k到j的最短路径,三段组成 506 | } 507 | } 508 | 509 | //三重算法可以简单得到ford算法的时间 T = O(|V|³) 510 | ``` 511 | 512 | 513 | 514 | ## 小白专场:哈利 波特的考试-C语言实现 515 | 516 | ### 小白-HP.1 题意理解 517 | 518 | image-20220802215252987 519 | 520 | ``` 521 | 样例第一列表示动物的个数(也就是顶点的个数) 第二列表示边的个数 522 | D是最短距离的矩阵,记录从顶点i到顶点j之间的最短距离 523 | 524 | Harry究竟应该带哪只动物去的问题的时候,首先检查每一行里面最大的那个数字,最大的那个数字代表着第一个动物变成最大数字的那个动物是最麻烦的,无穷符号指当前的那个动物 525 | 526 | 要带什么动物去会使我们最难变的动物变成最好变的呢?答案是动物4号,它最难变的咒语是70 527 | ``` 528 | 529 | ### 小白-HP.2 程序框架搭建 530 | 531 | ```c 532 | int main() 533 | { 534 | 读入图; 535 | 分析图; 536 | return 0; 537 | } 538 | ``` 539 | 540 | 代码实际演示 541 | 542 | ```C 543 | int main() 544 | { 545 | //有两种图,一种邻接表的,一种邻接矩阵的。这里选择了邻接矩阵的方法来表示图 546 | Graph G = BuildGraph(); 547 | FindAnimal( G ); 548 | return 0; 549 | } 550 | 551 | //FindMaxDist(i)是最大距离,对第i个动物去求他所有最短距离里面的最大值,要求的是所有这些最大值里面的最小值 552 | //FindMin是最小距离 553 | ``` 554 | 555 | image-20220802224222941 556 | 557 | ### 小白-HP.3 选择动物 558 | 559 | ```c 560 | void FindAnimal(MGraph Graph) 561 | { 562 | WeightType D[MaxVertexNum][MaxVertexNum],MaxDist,MinDist; 563 | Vertex Animal, i; 564 | 565 | Floyd(Graph , D); 566 | 567 | //FindMin:从每个动物i的最短距离的最大值中,找到最小值MinDist,以及对应的动物编号Animal 568 | MinDist = INFINITY; 569 | for( i = 0;iMv;i++){ 570 | MaxDist = FindMaxDist(D,i,Graph->Nv);//找到第i个动物它的最大的那个距离赋给MaxDist这个变量 571 | 572 | //还有一个特殊情况需要考虑:带一只动物去根本不可能,当图不连通的时候,图有不止一个连通集。该从哪里意识到图不连通了呢? 573 | if(MaxDist == INFINITY){//返回距离无穷大的话说明有i无法变出的动物 574 | printf("0\n"); 575 | return; 576 | } 577 | 578 | 579 | if( MinDist > MaxDist ){//找到最长距离更小的动物 580 | MinDist = MaxDist; Animal = i + 1;//更新距离,记录编号。这里是i+1是因为动物编号从1开始,但是图的循环从0开始,所有需要加一 581 | } 582 | } 583 | printf("%d %d\n",Animal,MinDist); 584 | } 585 | ``` 586 | 587 | 上述代码中涉及到封装起来的函数 588 | 589 | ```c 590 | WeightType FindMaxDist(WeightType D[][MaxVertexNum],Vertex i,int N) 591 | { 592 | WeightType MaxDist; 593 | Vertex j; 594 | 595 | MaxDist = O;//初始化为一个很小的数,比如0 596 | for(j = 0;j < N;j++ )//找出i到其他动物j的最长距离 597 | if( i != j && D[i][j]>MaxDist )//邻接矩阵所有的两个顶点之间的距离全部初始化为正无穷,所以对角元的值永远是正无穷。所以必须要把对角元排除掉跳过去,不排除的话这个无穷大必然会符合这个判断条件从而让MaxDist永远的返回正无穷 598 | MaxDist=[i][j]; 599 | return MaxDist; 600 | } 601 | ``` 602 | 603 | 604 | 605 | ### 小白-HP.4 模块的引用与裁剪 606 | 607 | image-20220803001249910 608 | 609 | ```c 610 | #define MaxVertexNum 100//最大顶点数设置为100 611 | #define INFINITY 65535//无穷大设为双字节无符号整数的最大值65535 612 | typedef int Vertex//用顶点下标表示顶点,为整型 613 | typedef int WeightType;//边的权值设为整型 614 | 615 | //边的定义 616 | typedef struct ENode *PtrToENode; 617 | struct ENode{ 618 | Vertex V1,V2;//有向边 619 | WeightType Weight;//权重 620 | }; 621 | typedef PtrToENode Edge; 622 | 623 | //图结点的定义 624 | typedef struct GNode *PtrToENode; 625 | struct GNode{ 626 | int Nv;//顶点数 627 | int Ne;//边数 628 | WeightType G[MaxVertexNum][MaxVertexNum];//邻接矩阵 629 | DataType Data[MaxVertexNum];//存顶点的数据 630 | //注意:很多情况下,顶点无数据,此时Data[]可以不出现 631 | }; 632 | typedef PtrToENode MGraph;//以邻接矩阵存储的图类型 633 | ``` 634 | 635 | #### CreateGraph原始代码 636 | 637 | ```c 638 | MGraph CreateGraph(int VertexNum) 639 | {/*初始化一个有VertexNum个顶点但没有边的图*/ 640 | Vertex V,W; 641 | MGraph Graph; 642 | 643 | Graph=(MGraph)malloc(sizeof(struct GNode));/*建立图*/ 644 | Graph->Nv VertexNum; 645 | Graph->Ne = 0; 646 | /*初始化邻接矩阵*/ 647 | /*注意:这里默认顶点编号从0开始,到(Graph->Nv-1)*/ 648 | for (V=0; VNv; V++) 649 | for (W=0; WNv; W++) 650 | Graph->G[V][W] = INFINITY; 651 | 652 | return Graph; 653 | } 654 | 655 | void InsertEdge(MGraph Graph,Edge E) 656 | { 657 | /*插入边*/ 658 | Graph->G[E->V1][E->V2] = E->Weight; 659 | /*若是无向图,还要插入边 因为是无向图,所以我们需要把读进来的这个权重同时赋予矩阵里面两个元素*/ 660 | Graph->G[E->V2][E->V1] = E->Weight; 661 | ``` 662 | 663 | 664 | 665 | ```c 666 | MGraph BuildGraph() 667 | { 668 | MGraph Graph; 669 | Edge E; 670 | Vertex V; 671 | int Nv,i; 672 | 673 | scanf("%d",&Nv);//读入顶点个数 674 | Graph = CreateGraph(Nv);//初始化有Nv个顶点但没有边的图 675 | 676 | scanf("%d",&(Graph->Ne));//读入边数 677 | if(Graph->Ne != 0 ){//如果有边 678 | E = (Edge)malloc(sizeof(struct ENode));//建立边结点 679 | //读入边,格式为"起点 终点 权重",插入邻接矩阵 680 | for(i=0;iNe;i++){ 681 | scanf("%d %d %d",&E->V1,&E->V2&E->Weight);//读进来的时候,编号是从1开始的 682 | //注意:;如果权重不是整型,Weight的读入格式要改 683 | E->V1--; E->V2--;//读进来的编号都-1,那就变成起始编号从0开始的了,原本1变成0,2变成1,依次变化 684 | InsertEdge(Graph,E);//插入边的时候,图默认顶点的编号是从0开始 685 | } 686 | } 687 | //如果顶点有数据的话,读入数据。没有的话这个for循环可以删掉 688 | for(V=0;VNv;V++) 689 | scanf(" %c",$(Graph->Data[V])); 690 | 691 | return Graph; 692 | } 693 | ``` 694 | 695 | 问:图的顶点从0开始编号,而本题目中动物从1开始编号。读输入时该如何处理使得图的相关模块可以顺利应用? 696 | 697 | 答案:E->V1--; E->V2--; 698 | 699 | 700 | 701 | #### 标准的Floyd算法 702 | 703 | ```C 704 | bool Floyd(MGraph Graph ,WeightType D[][MaxVertexNum],Vertex path[][MaxVertexNum])//返回布尔值(bool) 705 | { 706 | Vertex i,j,k; 707 | //初始化 708 | for(i = 0;iNv;i++) 709 | for(j = 0;jNv;j++){ 710 | D[i][j] = Graph->G[i][j]; 711 | path[i][j] = -1; 712 | } 713 | 714 | for(k = 0;k Nv;k++) 715 | for(i=0;iNv;i++) 716 | for(j=0;jNv;j++) 717 | if(D[i][k] + D[k][j] < D[i][j]){ 718 | D[i][j] = D[i][k] + D[k][j]; 719 | if( i == j && D[i][j]<0)//若发现负值圈,从i出发,走了一圈又回到i,这个最短距离是一个负的值,也对应了一个负值圈 720 | return false;//不能正确解决,返回错误标记 721 | path[i][j] = k; 722 | } 723 | return true;//算法执行完毕,返回正确标记 724 | } 725 | ``` 726 | 727 | 在本题中进行的改动部分: 728 | 729 | image-20220803005320198 -------------------------------------------------------------------------------- /数据结构第三周笔记——树(上)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第三周笔记——树(上)(慕课浙大版本--XiaoYu) 2 | 3 | ## 3.1 树与树的表示 4 | 5 | #### 什么是树 6 | 7 | 1. 人类社会家谱,社会组织结构,图书信息管理都是树的一种体现(层次型的组织结构) 8 | 2. 分层次组织在管理上具有更高的效率 9 | 1. 数据管理的基本操作之一:查找 10 | 11 | ### 3.1.1 引子(顺序查找) 12 | 13 | #### 查找(Searching) 14 | 15 | ``` 16 | 查找:根据某个给定关键字K,从集合R中找出关键字与K相同的记录 17 | 静态查找:集合中记录是固定的 18 | 没有插入和删除操作,只有查找(例如查字典) 19 | 动态查找:集合中记录是动态变化的 20 | 除查找,还可能发生插入和删除 21 | ``` 22 | 23 | ##### 静态查找 24 | 25 | 方法1:顺序查找 26 | 27 | ```C 28 | int SequentialSearch (List Tbl,ElementType K) 29 | { 30 | // 在Element[1]~Element[n]中查找关键字为K的数据元素 31 | int i; 32 | Tbl->ElementP[0] = K;//建立哨兵 33 | for(i = Tbl->Length;Tbl->Element[i] != K;i--);//倒过来做的,从下标大的地方开始往前循环 34 | return i;//查找成功返回所在单元下标;不成功不返回0 35 | } 36 | 37 | typedef struct LNode{ 38 | ElementType Element[MAXSIZE];//决定了放的元素有多少个 39 | int Length; 40 | }; 41 | 42 | 顺序查找的一种实现(无"哨兵") 43 | int SequentialSearch (List Tbl ,ElementType K) 44 | { 45 | //在Element[1]~Element[n]中查找关键字为K的数据元素 46 | int i; 47 | 48 | for(i=Tbl->Length; i>0 && Tbl->Element[i] != K;i--); 49 | return i;//查找成功返回所在单元下标,不成功返回0 50 | } 51 | 在顺序查找中,如果把下列程序中的循环条件"i>0"去掉,会发生什么后果? 52 | 答案:要查找的元素不存在时发生数组越界(i指向小于0的位置) 53 | 54 | "哨兵"的作用:可以在数组的最后或者边界上面设一个值,不用每次去判断它的下标是不是达到我的边界。根据for循环,当碰到我们放置的那个值的时候,循环就该退出来了。这样我们在写判断的时候就可以少写一个判断的分支 55 | 我的理解是这个哨兵通常放置在下标0的地方,然后循环从大往小循环,循环到1如果都没有我们需要的值的时候,放在下标0的地方的哨兵是等于我们需要的值来退出循环,代替了for条件中的不能小于0,防止for循环陷入死循环或者说越界跑到下标负数的地方去了。然后返回值如果是0的话我们就心知肚明没有找到我们想要的值了 56 | 57 | 这种顺序查找算法的时间复杂度为O(n),平均数是n/2 58 | ``` 59 | 60 | ### 3.1.2 引子(二分查找例子) 61 | 62 | #### 方法3:二分查找(Binary Search) 63 | 64 | **使用前提**:有序存放 65 | 66 | image-20220630133518543 67 | 68 | **例子**:假设有13个数据元素,按关键字由小到大顺序存放,二分查找关键字为444的数据元素过程如下: 69 | 70 | image-20220630133710273 71 | 72 | image-20220630133856929 73 | 74 | image-20220630134122043 75 | 76 | 77 | 78 | ``` 79 | 两个边界分别是left跟right 80 | 具体过程数据就不展示了,用图片展示出来了,减少我的工作量 81 | 在13个元素的二分查找中,找第10个元素比找第8个元素快?对 82 | ``` 83 | 84 | **例子2**:仍然以上面13个数据元素构成的有序线性表为例,二分查找关键字为43的数据元素如下: 85 | 86 | image-20220630134238735 87 | 88 | image-20220630134317471 89 | 90 | image-20220630134845190 91 | 92 | ### 3.1.3 引子(二分查找实现) 93 | 94 | #### 二分查找算法 95 | 96 | ```c 97 | int BinarySearch(List Tbl,ElementType K)//List Tbl是结构的指针,包含了数组Element和它的大小Length 98 | { 99 | //在表Tbl中查找关键字为K的数据元素 100 | int left,right,mid,NoFound = -1; 101 | 102 | left = 1;//初始左边界 103 | right = Tbl->Length//初始右边界 104 | while(left <= right) 105 | { 106 | mid = (left + right)/2 //计算中间元素坐标 107 | if(K < Tbl->Element[mid]) right = mid - 1;//调整右边界 108 | else if(K > Tbl->Element[mid]) left = mid + 1;//调整左边界 109 | else return mid;//查找成功,返回数据元素下标 110 | } 111 | return NotFound;//查找不成功,返回-1 112 | } 113 | 所以我们就return NotFound 114 | 115 | 在二分查找的程序实现中,如果left和right的更新不是取mid+1和mid-1而是都取mid,程序也是正确的?当然是错误的啦 116 | 117 | 118 | //List定义: List是stl实现的双向链表,与向量(vectors)相比, 它允许快速的插入和删除,但是随机访问却比较慢。使用时需要添加头文件 #include 119 | ``` 120 | 121 | **二分查找算法具有对数的时间复杂度O(logN)** 122 | 123 | #### 11个元素的二分查找判定树 124 | 125 | image-20220630141214141 126 | 127 | **二分查找的启示**: 128 | 129 | 1. 判定树上每个结点需要的查找次数刚好为该结点所在的层数 130 | 2. 查找成功时的查找次数不会超过判定树的深度 131 | 3. n个结点的判定树的深度是[log2n]+1 (这里的2是下标) 132 | 4. ASL(平均成功查找次数) = (4 * 4 + 4 * 3 + 2 * 2 +1)/11 = 3 133 | 134 | ### 3.1.4 树的定义和术语 135 | 136 | 当在树里面插入结点 删除结点的时候比在数组里面方便得多 137 | 138 | #### 树的定义 139 | 140 | **树(Tree)**:n(n >= 0)个结点构成的有限集合。 141 | 142 | 当n = 0时,称为空树; 143 | 144 | 对于任一棵非空树(n > 0),它具备以下性质: 145 | 146 | 1. 树中有一个称为"根(Root)"的特殊结点,用r表示; 147 | 2. 其余结点可分为m(m>0)个互不相交的有限集T1,T2,....,Tm,其中每个集合本身又是一颗树,称为原来树的"子树(SubTree)" 148 | 3. image-20220630212408181 149 | 150 | #### 树与非树 151 | 152 | 不是树的例子 153 | 154 | 1. image-20220630212516643 155 | 2. 子树是不相交的; 156 | 3. 除了根结点外,每个结点有且只有一个父结点; 157 | 4. 一颗N个结点的树有N-1条边 158 | 5. **树是保证结点联通的最小的一种连接方式(边最少)** 159 | 6. 有一个m棵树的集合(也叫森林)共有k条边,问这m颗树共有多少个结点?k+m 160 | 161 | #### 树的一些基本术语 162 | 163 | 1. 结点的度(Degree):结点的子树个数 164 | 2. 树的度:树的所有结点中最大的度数 165 | 3. 叶结点(Leaf):度为0的结点 166 | 4. 父结点(Parent):有子树的结点是其子树的根节点的父节点 167 | 5. 子结点(Child):若A结点是B结点的父节点,则称B结点是A结点的子结点;子节点也称孩子结点 168 | 6. 兄弟结点(Sibling):具有同一父节点的各结点彼此是兄弟结点 169 | 7. image-20220630221534749 170 | 8. 祖先结点(Ancestor):沿着树根到某一结点路径上的所有结点都是这个结点的祖先结点 171 | 9. 子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙 172 | 10. 结点的层次(Lever):规定根节点在1层,其他任一结点的层数是其父节点的层数加1 173 | 11. 树的深度(Depth):树的所有结点中的最大层次是这颗树的深度 174 | 175 | ### 3.1.1 树的表示 176 | 177 | 数组实现:把这些结点按顺序存储在数组里面 178 | 179 | image-20220630231222581 180 | 181 | 链表表示: 182 | 183 | image-20220630231244287 184 | 185 | 这样树的结构:每个结点的结构的样子是不一样的。有的结点有3个指针,有的结点有1个指针,有的没有指针。这样整个这个结构的形式都不一样,会给后面的程序实现带来困难(因为访问之前没办法确认会带来多少个儿子) 186 | 187 | ```c 188 | //另外的构想:能不能为了保持结构的统一,我将每个指针都跟结点中最多的对齐,其他结点不需要的指针就当空指针用。这样的好处就是所有树结点结构是统一的,程序处理起来方便。但同样会带来问题: 189 | 190 | //如果这个树有n个结点,那意味着每个结点有3个指针域,整个树会有3n个指针域,而实际上我们的边只有n-1条,这样就会有2n+1个指针域是空的,造成空间上的浪费 191 | ``` 192 | 193 | 194 | 195 | #### 儿子-兄弟表示法 196 | 197 | image-20220630235030353 198 | 199 | 这样可以将整颗树的结点把它串起来 200 | 201 | 实现效果如下: 202 | 203 | image-20220630235200617 204 | 205 | ``` 206 | 这种方法的优点: 207 | 1.树种的每个结点结构都是统一的,都是两个指针域,同时它的空间浪费也不大 208 | n个结点2n个指针域 其中n-1条边。 209 | 意味着n-1个域是非空的,真正空的域是n+1 210 | 211 | 问题:在用“儿子-兄弟”法表示的树中,如果从根结点开始访问其“次子”的“次子”,所经过的结点数与下面哪种情况一样?(注意:比较的是结点数,而不是路径) 212 | 答案:从根结点开始访问其“长子”的“长子”的“长子”的“长子” 213 | ``` 214 | 215 | #### 二叉树的图 216 | 217 | image-20220630235703723 218 | 219 | **二叉树特点**: 220 | 221 | 1. 链表实现方法:旋转45度 222 | 2. 每个结点都有两个指针,一个指向左边一个指向右边,每个结点最多是两个儿子 223 | 3. 二叉树就是度为2的一种树 224 | 4. image-20220630235936075 225 | 226 | ## 二叉树及存储结构 227 | 228 | ### 3.2.1 二叉树的定义及性质 229 | 230 | #### 二叉树的定义 231 | 232 | ``` 233 | 二叉树T:一个有穷的结点集合 234 | 235 | ​ 这个集合可以为空 236 | 若不为空,则它是由根结点和称为其左子树TL和右子树TR的两个不相交的二叉树组成(L和R是下标) 237 | ``` 238 | 239 | **二叉树具体五种基本形态** 240 | 241 | image-20220701000338791 242 | 243 | **二叉树的子树有左右之分** 244 | 245 | image-20220701000422530 246 | 247 | #### 特殊的二叉树 248 | 249 | ##### 斜二叉树(Skewed Binary Tree) 250 | 251 | 1. 只有左边或者只有右边,相当于一个链表 252 | 2. image-20220701000612336 253 | 254 | ##### 完美二叉树(Perfect Binary Tree) 255 | 256 | **或者叫做满二叉树(Full Binary Tree)** 257 | 258 | image-20220701000716448 259 | 260 | 特点: 一个深度为k(>=-1)且有2^(k+1) - 1个结点的二叉树称为完美二叉树。 满二叉树 261 | 262 | #### 完全二叉树(Complete Binary Tree) 263 | 264 | ``` 265 | 有n个结点的二叉树,对树种结点按从上到下,从左到右的顺序进行编号,编号为i(1 <= i <= n)结点与满二叉树中编号为i结点在二叉树中位置相同 266 | 跟上方的区别就是除了最底下一层可以右边缺一点,上面跟满二叉树是一样的。最底下一层左边顺序不能乱 267 | ``` 268 | 269 | #### 二叉树的几个重要性质 270 | 271 | image-20220701001640388 272 | 273 | 叶结点的总数等于有两个儿子的结点的总数加1 274 | 275 | ``` 276 | 有一颗二叉树,其两个儿子的结点个数为15个,一个儿子的结点个数为32个,问该二叉树的叶结点个数是多少? 277 | 16 278 | ``` 279 | 280 | #### 二叉树的抽象数据类型定义 281 | 282 | ```c 283 | 类型名称:二叉树 284 | 数据对象集:一个有穷的结点集合。 285 | 若不为空,则由根结点和其左、右二叉子树组成。 286 | 287 | 操作集: BT属于BinTree,Item属于ElementType 288 | 重要操作有: 289 | 1.Boolean IsEmpty(BinTree BT):判别BT是否为空; 290 | 2.void Traversal(BinTree BT):遍历,按某顺序访问每个结点;(重要) 291 | 3.BinTree CreateBinTree():创建一个二叉树 292 | ``` 293 | 294 | ##### 常用的遍历方法 295 | 296 | ```c 297 | 1.void PreOrderTraversal(BinTree BT)://先序--根、左子树、右子树 298 | 2.void InOrderTraversal(BinTree BT)://中序--左子树、根、右子树 299 | 3.void PostOrderTraversal(BinTree BT)://后序--左子树、右子树、根 300 | 4.void LevelOrderTraversal(BinTree BT)://层次遍历(层序遍历,从上到下、从左到右 301 | ``` 302 | 303 | ### 3.2.2 二叉树的存储结构 304 | 305 | #### 1.顺序存储结构 306 | 307 | ```c 308 | 完全二叉树:按从上至下,从左到右顺序存储n个结点的完全二叉树的结点父子关系; 309 | //这个树最适合数组方式解决 310 | ``` 311 | 312 | image-20220701003345507 313 | 314 | 1. 非根结点(序号 i > 1)的父节点的序号是[i/2] 315 | 1. 这句话的意思就是说假设我们目前知道M结点时6,如果想知道它的父节点就是6/2=3,Q结点就是7/2=3.5,把小数去掉,父节点一样是3 316 | 2. 结点(序号为i)的左孩子结点的序号是2i,(若2i <= n,否则没有左孩子) 317 | 1. 举例:意思就是B左孩子是C,O的左孩子是M 318 | 3. 结点(序号为i)的右孩子结点的序号是2i+1,(若2i+1 <= n,否则没有右孩子) 319 | 1. 举例:类似上方的意思B右孩子是S... 320 | 321 | **一般二叉树**也可以采用这种结构,缺点:但会造成空间浪费....(将缺少的结点补上一个空结点) 322 | 323 | image-20220701004348299 324 | 325 | image-20220701004413217 326 | 327 | ``` 328 | 问题: 329 | 330 | 如果参照完全二叉树的表示方法用数组存储下面这棵二叉树,那么结点e所对应的数组下标是多少(树根下标为1)? 331 | 答案:6 332 | ``` 333 | 334 | image-20220701004541010 335 | 336 | 2.**链表存储** 337 | 338 | image-20220701004912537 339 | 340 | ```c 341 | typedef struct TreeNode *BinTree; 342 | typedef BinTree Position; 343 | struct TreeNode{ 344 | ElementType Data; 345 | BinTree Left; 346 | BinTree Right; 347 | } 348 | ``` 349 | 350 | image-20220701005059244 351 | 352 | ## 二叉树的遍历 353 | 354 | ### 3.3.1 先序中序后序遍历 355 | 356 | #### 二叉树的遍历 357 | 358 | ##### (1)先序遍历 359 | 360 | ```c 361 | 遍历过程为: 362 | 1.访问根节点 363 | 2.先序遍历其左子树 364 | 3.先序遍历其右子树 365 | 366 | 对应的递归程序: 367 | void PreOrderTraversal(BinTree BT)//BT是树 368 | { 369 | if( BT ){//看树是不是空的,空的就退出去,不空就访问根节点 370 | printf("d",BT->Data); 371 | PreOrderTraversal(BT->Left);//指向左子树的那个根节点的地址进行递归 372 | PreOrderTraversal(BT->Right);//指向右子树的那个根节点的地址进行递归 373 | } 374 | } 375 | ``` 376 | 377 | image-20220701101511679 378 | 379 | ``` 380 | 先是从根开始 A(B D F E)(C G H I) 381 | 先序遍历=>A B D F E C G H I 382 | ``` 383 | 384 | ##### (2)中序遍历 385 | 386 | ```c 387 | 遍历过程为: 388 | 1.中序遍历其左子树 389 | 2.访问根结点; 390 | 3.中序遍历其右子树 391 | 对应的递归程序: 392 | void PreOrderTraversal(BinTree BT)//BT是树 393 | { 394 | if( BT ){//看树是不是空的,空的就退出去,不空就访问根节点 395 | PreOrderTraversal(BT->Left);//指向左子树的那个根节点的地址进行递归 396 | printf("d",BT->Data); 397 | PreOrderTraversal(BT->Right);//指向右子树的那个根节点的地址进行递归 398 | } 399 | } 400 | ``` 401 | 402 | image-20220701102142532 403 | 404 | ``` 405 | 中序遍历 => D B E F A G H C I 406 | (D B E F) A ( G H C I) 407 | 先是左边再右边,这里我原本的疑问是为什么是先E在F而不是先F再E 408 | 原因是E-F是一个树,然后因为这个是左子树,所以从左边开始,E在F的左边 409 | ``` 410 | 411 | image-20220701103417703 412 | 413 | ##### (3)后序遍历 414 | 415 | ```c 416 | 遍历过程为: 417 | 1.后序遍历其左子树 418 | 2.中序遍历其右子树 419 | 3.访问根结点 420 | 对应的递归程序: 421 | void PreOrderTraversal(BinTree BT)//BT是树 422 | { 423 | if( BT ){//看树是不是空的,空的就退出去,不空就访问根节点 424 | PreOrderTraversal(BT->Left);//指向左子树的那个根节点的地址进行递归 425 | PreOrderTraversal(BT->Right);//指向右子树的那个根节点的地址进行递归 426 | printf("d",BT->Data); 427 | } 428 | } 429 | ``` 430 | 431 | image-20220701103619016 432 | 433 | ``` 434 | 注意点:后序遍历,那当然得从下面开始咯,所以会发现B跟E是E优先。H跟C比是H优先 435 | (D E F B)(H G I C) A 436 | 后序遍历 => D E F B H G I C A 437 | ``` 438 | 439 | ##### 总结 440 | 441 | ``` 442 | 先序、中序和后序遍历过程:遍历过程中经过结点的路线一样,只是访问各结点的时机不同 443 | 图中先从入口到出口的曲线上用三种符号分别标记除先序,中序和后序的访问各结点时刻 444 | ``` 445 | 446 | image-20220701112206952 447 | 448 | 449 | 450 | ### 3.3.2 中序非递归遍历 451 | 452 | #### 二叉树的非递归遍历 453 | 454 | ##### 中序遍历非递归遍历算法 455 | 456 | **非递归算法实现的基本思路:**使用堆栈 457 | 458 | 这里建议看视频的演示,非常形象 459 | 460 | ```c 461 | 1.遇到一个结点,就把它压栈,并去遍历它的左子树; 462 | 2.当左子树遍历结束后,从栈顶弹出这个结点并访问它; 463 | 3.然后按其右指针再去中序遍历该结点的右子树 464 | 465 | 以下是完整代码实现演示 466 | void InOrderTraversal(BinTree BT) 467 | { 468 | BinTree T = BT;//把BT赋给临时变量T 469 | Stack S = CreateStack(MaxSize);//创建并初始化堆栈 470 | while(T || !IsEmpty(S) ){//树不空且堆栈不空 471 | while(T){//判断堆栈空不空 472 | //一直向左并将沿途结点压入堆栈 473 | Push(S,T);//把T压入堆栈S中 474 | T = T->Left;//然后把T往左挪,就是边挪边把结点压到堆栈里面去,压到底T为NULL就退出来 475 | } 476 | if(!IsEmpty(S)){ 477 | T = Pop(S);//结点弹出堆栈 478 | printf("%5d",T->Data);//(访问)打印结点 479 | T = T->Right;//转向右子树 480 | } 481 | } 482 | } 483 | 484 | ppoopoppoo错误 485 | 486 | PPOPOOPPOO正确 487 | 第二次碰到同一个的时候就print出来 488 | ``` 489 | 490 | image-20220701114302678 491 | 492 | ##### 先序遍历的非递归遍历算法? 493 | 494 | 把上方那个算法中if中的printf("%5d",T->Data)移到while(T)中Push的后面 495 | 496 | ##### 后序遍历非递归遍历算法? 497 | 498 | ### 3.3.3 层序遍历 499 | 500 | **二叉树遍历的核心问题**:二维结构的线性化(变成一维的) 501 | 502 | ``` 503 | 1.从结点访问其左、右儿子结点 504 | 2.访问左儿子后,右儿子结点怎么办? 505 | 1.需要一个存储结构保存暂时不访问的结点 506 | 2.存储结构:堆栈、队列 507 | ``` 508 | 509 | #### 队列实现 510 | 511 | ``` 512 | 遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子入队 513 | ``` 514 | 515 | image-20220701122430994 516 | 517 | image-20220701122450231 518 | 519 | image-20220701122514520 520 | 521 | image-20220701122530744 522 | 523 | image-20220701122549970 524 | 525 | image-20220701122603151 526 | 527 | 最后结果就是:层序遍历 => A B C D F G I E H 528 | 529 | ```c 530 | //代码实现 531 | void LevelOrderTraversal(BinTree BT) 532 | { 533 | Queue Q; BinTree T; 534 | if(!BT) return;//若是空树直接返回 535 | Q = CreateQueue(MaxSize);//创建并初始化队列Q 536 | AddQ(Q,BT);//把BT这个根节点放到Q这个队列里 537 | while(!IsEmptyQ(Q)){ 538 | T = DeleteQ(Q);//抛出元素赋给T就是一个指针 539 | printf("%d\n",T->Data);//访问去除队列的结点 540 | if(T->Left) Add(Q,T->Left);//左右儿子放到队列里去 541 | if(T->Right) AddQ(Q,T->Right); 542 | } 543 | } 544 | ``` 545 | 546 | 547 | 548 | ### 3.3.4 遍历应用例子 549 | 550 | #### 【**例**】遍历二叉树的应用:输出二叉树中的叶子结点 551 | 552 | 在二叉树的遍历算法中增加检测结点的"左右子树是否都为空" 553 | 554 | ```c 555 | void PreOrderPrintLeaves(BinTree BT) 556 | { 557 | if(BT){ 558 | if(!BT-Left && !BT->Right) 559 | printf("%d",BT->Data); 560 | PreOrderPrintLeaves(BT -> Left); 561 | PreOrderPrintLeaves(BT -> Right); 562 | } 563 | } 564 | ``` 565 | 566 | #### 【例】求二叉树的高度 567 | 568 | image-20220701124015460 569 | 570 | ```c 571 | void PreOrderGetHeight(BinTree BT) 572 | { 573 | int HL,HR,MaxH 574 | if(BT){ 575 | HL = PostOrderGetHeight(BT->Left);//求左子树的深度,这两个都是递归哦 576 | HR = PostOrderGetHeight(BT->Right);//求右子树的深度 577 | MaxH = (HL > HR) ? HL :HR;//取左右子树较大的深度 578 | return (MaxH + 1);//返回树的深度 579 | } 580 | else return 0;//空树深度为0 581 | } 582 | ``` 583 | 584 | #### 【例】二元运算表达式树及其遍历 585 | 586 | image-20220701124745933 587 | 588 | **中缀表达式是不准的,会收到运算符优先度的影响**,其它都是准的 589 | 590 | 解决中缀表达式不准的问题:左子树开始前加个左括号,结束后加个右括号,通过加括号的形式解决优先度问题 591 | 592 | #### 【例】由两种遍历序列确定二叉树 593 | 594 | 已知三种遍历中的任意两种遍历序列,能否唯一确定一颗二叉树呢? 595 | 596 | 答案:必须要有中序遍历才行 597 | 598 | 没有中序的困扰: 599 | 600 | ​ 1.先序遍历序列:A B 601 | 602 | ​ 2.后序遍历序列:B A 603 | 604 | 会发现符合条件的树不止一个 605 | 606 | 607 | 608 | **先序第一个是根**,**后序最后一个是根** 609 | 610 | image-20220701125607995 611 | 612 | 问:已知有颗5个结点的二叉树,其前序遍历序列是a????,中序遍历序列是a????,可以断定: 613 | 614 | image-20220701125653916 615 | 616 | 答案: 该树根结点是a,且没有左子树 617 | 618 | ## 小白专场 619 | 620 | ### 题意理解及二叉树表示 621 | 622 | image-20220701221930149 623 | 624 | image-20220701224453225 625 | 626 | 不是同构的(反面教材): 627 | 628 | image-20220701224543157 629 | 630 | image-20220701224727874 631 | 632 | image-20220701224754107 633 | 634 | #### 求解思路 635 | 636 | 1. **二叉树表示** 637 | 638 | 1. image-20220701224950324(链表的表示方法啊) 639 | 640 | 2. image-20220701225028557(一般的用数组表示二叉树做法) 641 | 642 | 3. 结构数组表示二叉树:静态链表(物理上存储是数组,思想是一种链表的思想 ) 643 | 644 | 1. image-20220701225755398 645 | 646 | A B C D是代表节点本身的信息(用来标识节点的),并且Left根Right不是指向左儿子右儿子的,而是指向他们的下标的,指向空节点用-1表示 647 | 648 | ```c 649 | 静态链表数组实现代码 650 | #define MaxTree 10 651 | #define ElementType char 652 | #define Tree int 653 | #define Null -1 654 | struct TreeNode 655 | { 656 | ElementType Element; 657 | Tree Left;//这里Left跟Right不是指针 658 | Tree Right; 659 | }T1[MaxTree],T2[MaxTree];//NULL在C语言这个stdlb.h中定义是0 660 | //静态链表优势:有链表的灵活性,但他的存储又是在数组上面 661 | ``` 662 | 663 | 664 | 665 | 2. **建二叉树** 666 | 667 | 3. **同构判别** 668 | 669 | ### 程序框架、建树及同构判别 670 | 671 | ```c 672 | int main() 673 | { 674 | 建二叉树1 675 | 建二叉树2 676 | 判别是否同构并输出 677 | 678 | return 0; 679 | } 680 | 681 | 需要设计的函数: 682 | 1.读数据建二叉树 683 | 2.二叉树同构判别 684 | 685 | 怎么判别二叉树是不是同构的代码实现 686 | int main() 687 | { 688 | Tree R1,R2; 689 | 690 | R1 = BuildTree(T1);//建第一棵树R1 691 | R2 = BuildTree(T2);//建第二课树R2 692 | if(Isomorphic(R1,R2)) printf("Yes\n");//判断二叉树是不是同构的 693 | else printf("No\n"); 694 | 695 | return 0; 696 | } 697 | ``` 698 | 699 | image-20220701234728261 700 | 701 | %c:转化为整数 702 | 703 | Root:树根 704 | 705 | 根节点:哪个节点不存在其他节点指向他,那就是根节点 706 | 707 | Root = ??? =>T[i]中没有任何结点的left(cl)和right(cr)指向它。只有一个 708 | 709 | image-20220701235112746 710 | 711 | ``` 712 | 最后if(!check[i]) break;中返回的值是0的就是根结点,因为有指向的都被转化为1了 713 | 714 | T[i].Left = cl - '0';的原因是我们是以字符的形式读取进来的,字符2减去字符0就是整数2 715 | ``` 716 | 717 | #### 如何判别两二叉树同构 718 | 719 | image-20220701235555687 720 | 721 | image-20220701235650631 722 | 723 | -------------------------------------------------------------------------------- /数据结构第九周笔记——排序(上)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第九周笔记——排序(上)(慕课浙大版本--XiaoYu) 2 | 3 | ## 9.1 简单排序(冒泡、插入) 4 | 5 | ### 9.1.1 概述 6 | 7 | ```c 8 | void X_Sort(ElementType A[],int N)//sort就是排序的意思,X是排序算法的名称 9 | //统一默认输入的参数有两个(一个是待排的元素放在一个数组里,数据类型为ElementType任意类型。另外一个是正整数N,表示的是我们要排的元素到底有多少个,默认讨论整数(从小到达)的排序) 10 | 1.N是正整数 11 | 2.只讨论基于比较的排序(>=< 有定义) 12 | 3.只讨论内部排序(假设我们内存空间足够大,所有数据可以一次性导入内存空间里,然后所有的排序是在内存里面一次性完成的) 13 | //外部排序(假设我有的内存空间有2GB,但是要求我们对10TB的数据进行排序,这个时候内部排序就不行了) 14 | 4.稳定性:任意两个相等的数据,排序先后的相对位置不发生改变 15 | 5.没有一种排序是任何情况下都表现最好 16 | ``` 17 | 18 | ### 9.1.2 冒泡排序 19 | 20 | ```c 21 | void Bubble_Sort(ElementType A[],int N) 22 | { 23 | for(i = 0; i < P;P--){ 24 | flag = 0;//表示还没有执行果任何一次交换 25 | for(i = 0;i < P;i++ ){//一趟冒泡 26 | if(A[i] > A[i+1] ){ 27 | Swap(A[i],A[i+1]); 28 | flag = 1;//要交换的时候标识变为1 29 | } 30 | } 31 | if( flag == 0 ) break;//全程无交换 32 | } 33 | } 34 | 35 | 最好情况:顺序T = O(N) 全程无交换 36 | 最坏情况:逆序T = O(N²) 37 | ``` 38 | 39 | 冒泡排序优点: 40 | 41 | 1. 所有的待排元素是放在一个单向链表里的(冒泡排序可以对数组,对单项链表都可以实现,其他排序不好实现) 42 | 2. 算法稳定 43 | 44 | ### 9.1.3 插入排序 45 | 46 | ```c 47 | void Insertion_Sort( ElementType A[],int N ) 48 | { 49 | for( P = 1;P < N; P++ ){ 50 | Tmp = A[P];//摸下一张牌,Tmp为临时存放的位置 51 | for( i = P; i > 0 && A[i - 1] > Tmp;i--)//旧牌大 52 | A[i] = A[i - 1];//移除空位 53 | A[i] = Tmp;//新牌落位 54 | } 55 | } 56 | //最好情况:顺序T = O(N) 57 | //最坏情况:逆序T = O(N²) 58 | ``` 59 | 60 | 插入排序好处: 61 | 62 | 1. 程序短,简单 63 | 2. 比冒泡排序好在:冒泡排序是两两交换,两两元素互换的时候他要涉及到第三步。而插入排序则是每个元素向后错,最后他一次性放到他那个空位里面去(不是插入排序最主要的) 64 | 3. 稳定 65 | 4. 66 | 67 | **给定初始序列{34, 8, 64, 51, 32, 21},冒泡排序和插入排序分别需要多少次元素交换才能完成?** 68 | 69 | 冒泡9次,插入9次 70 | 71 | **对一组包含10个元素的非递减有序序列,采用插入排序排成非递增序列,其可能的比较次数和移动次数分别是?** 72 | 73 | 45, 44 74 | 75 | ### 9.1.4 时间复杂度下界 76 | 77 | image-20220817225121906 78 | 79 | 问题:序列{34,8,64,51,32,21}中有多少逆序对?9对 80 | 81 | image-20220817225414978 82 | 83 | 逆序对的数量跟交换元素次数是一样的,也就说明了交换两个相邻元素正好消去一个逆序对! 84 | 85 | 插入排序:T(N,I) = O(N+I) 86 | 87 | 1. 如果序列**基本有序**,则插入排序简单且非常高效 88 | 89 | image-20220817225812751 90 | 91 | 逆序对平均个数:大O(N2)数量级的,不管是冒泡排序还是插入排序,他们的平均时间复杂度是跟逆序对的个数有关系的 92 | 93 | image-20220817230020643 94 | 95 | Ω:指的是下界 96 | 97 | 这意味着:要提高算法效率,我们必须 98 | 99 | 1. 每次消去不止一个逆序对 100 | 2. 每次交换相隔较远的2个元素,这样一次性就消掉了不止一个逆序对 101 | 102 | ## 9.2 希尔排序(by Donald Shell) 103 | 104 | 基本思路:利用了插入排序的简单,同时克服插入排序每次只交换相邻两个元素的缺点 105 | 106 | image-20220817231030461 107 | 108 | image-20220817231059520 109 | 110 | image-20220817231127625 111 | 112 | image-20220817231150119 113 | 114 | 到最后使用1-间隔的排序来保证序列有序(彻底的插入排序)。但此时这个序列已经基本有序了,大多数的逆序对已经在前面两趟5-间隔和3-间隔里面被消除掉了 115 | 116 | image-20220817231759370 117 | 118 | **重要性质**:3-间隔有序的序列还保持了前面5-间隔有序的这个性质(没有把上一步的结果变坏) 119 | 120 | image-20220817231818451 121 | 122 | #### 希尔增量序列 123 | 124 | 1. 原始希尔排序image-20220817231915152 125 | 126 | 2. ```c 127 | void Shell_Sort( ElementType A[],int N ) 128 | { 129 | for(D = M/2; D > 0; D /= 2 ){//希尔增量序列 130 | for( P = D; P < N; P++ ){//插入排序,D是距离(第0张牌在我手里,下一张牌从第D张牌开始摸) 131 | Tmp = A[P]; 132 | for( i = P; i >= D && A[i - D] > Tmp;i -= D ) 133 | A[i] = A[i - D]; 134 | A[i] = Tmp; 135 | } 136 | } 137 | } 138 | ``` 139 | 140 | 最坏情况:image-20220817235114397 141 | 142 | O是一个上界(可能达不到) 143 | 144 | image-20220817235222549 145 | 146 | 增长速度跟N²一样快 147 | 148 | 149 | 150 | **坏例子**: 151 | 152 | image-20220817235503572 153 | 154 | 增量元素不互质,则小增量可能根本不起作用 155 | 156 | #### 更多的增量序列 157 | 158 | image-20220817235733415 159 | 160 | ```c 161 | 用Sedgewick增量序列 162 | void ShellSort( ElementType A[], int N ) 163 | { /* 希尔排序 - 用Sedgewick增量序列 */ 164 | int Si, D, P, i; 165 | ElementType Tmp; 166 | /* 这里只列出一小部分增量 */ 167 | int Sedgewick[] = {929, 505, 209, 109, 41, 19, 5, 1, 0}; 168 | 169 | for ( Si=0; Sedgewick[Si]>=N; Si++ ) 170 | ; /* 初始的增量Sedgewick[Si]不能超过待排序列长度 */ 171 | 172 | for ( D=Sedgewick[Si]; D>0; D=Sedgewick[++Si] ) 173 | for ( P=D; P=D && A[i-D]>Tmp; i-=D ) 176 | A[i] = A[i-D]; 177 | A[i] = Tmp; 178 | } 179 | } 180 | ``` 181 | 182 | ## 9.3 堆排序 183 | 184 | 概念 185 | 186 | ``` 187 | 大顶堆:每个节点的值都大于或者等于它的左右子节点的值。 188 | 189 | 堆排序的基本思想是: 190 | 1、将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素; 191 | 2、将堆顶元素和最后一个元素交换,然后将剩下的节点重新构造成一个大顶堆; 192 | 3、重复步骤2,如此反复,从第一次构建大顶堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大顶堆的尾部。最后,就得到一个有序的序列了。 193 | ``` 194 | 195 | 196 | 197 | ### 9.3.1 选择排序 198 | 199 | ```c 200 | void Selection_Sort( ElementType A[],int N ) 201 | { 202 | for( i = 0;i < N; i++ ){ 203 | MinPosition = ScanForMin( A,i,N-1); 204 | //从A[i]到A[N-1]中找最小元,并将其位置赋给MinPosition 205 | Swap(A[i],A[MinPosition]);//这两个元素通常情况下不是挨着的,可能跳了很远的距离做一个交换,一下子就消除掉很多逆序对 206 | //将未排序部分的最小元换到有序部分的最后位置 207 | //最坏情况就是每次都必须换一下,最多需要换N-1次 208 | } 209 | } 210 | //想要得到更快的算法取决于这个ScanForMin( A,i,N-1),也就是如何快速找到最小元 211 | ``` 212 | 213 | image-20220818001442455 214 | 215 | 最小堆的特点就是他的根结点一定存的是最小元 216 | 217 | ### 9.3.2 堆排序 218 | 219 | 算法1: 220 | 221 | ```c 222 | void Heap_Sort(ElementType A[],int N) 223 | { 224 | BuildHeap(A);//O(N) 225 | for( i = 0;i < N;i++ ) 226 | TmpA[i] = DeleteMin(A);//把根结点弹出来,依次存到这个临时数组里面。O(logN) 227 | for( i = 0;i < N;i++ )//O(N) 228 | A[i] = TmpA[i];//将TmpA里面所有的元素导回A里面 229 | } 230 | //缺点:需要额外O(N)空间,并且复制元素需要时间 231 | ``` 232 | 233 | 算法2: 234 | 235 | ```c 236 | void Heap_Sort(ElementType A[],int N ) 237 | { 238 | for(i = N/2;i >= 0;i-- ){//BuildHeap,i对应的是根节点所在的位置,N对应的是当前这个堆里一共有多少个元素 239 | PercDown(A,i,N); 240 | for( i = N-1;i > 0;i--){//堆循环 241 | Swap(&A[0],&A[i]);//DeleteMax,A[0]根节点里面存的是最大的元素,i是当前最后一个元素的下标,把根节点换到当前这个堆的最后一个元素的位置上去 242 | PercDown(A,0,i);//调整的时候是以0为根节点,i是当前这个最大堆的元素个数 243 | } 244 | } 245 | } 246 | ``` 247 | 248 | 在堆排序中,元素下标从0开始。则对于下标为i的元素,其左、右孩子的下标分别为:2i+1, 2i+2 249 | 250 | image-20220818003410802 251 | 252 | 算法2的动态变化: 253 | 254 | image-20220818003617976image-20220818003655108image-20220818003743315image-20220818003817239image-20220818003836433image-20220818003917597image-20220818003933416 255 | 256 | ```c 257 | 堆排序 258 | void Swap( ElementType *a, ElementType *b ) 259 | { 260 | ElementType t = *a; *a = *b; *b = t; 261 | } 262 | 263 | void PercDown( ElementType A[], int p, int N ) 264 | { /* 改编代码4.24的PercDown( MaxHeap H, int p ) */ 265 | /* 将N个元素的数组中以A[p]为根的子堆调整为最大堆 */ 266 | int Parent, Child; 267 | ElementType X; 268 | 269 | X = A[p]; /* 取出根结点存放的值 */ 270 | for( Parent=p; (Parent*2+1)= A[Child] ) break; /* 找到了合适位置 */ 275 | else /* 下滤X */ 276 | A[Parent] = A[Child]; 277 | } 278 | A[Parent] = X; 279 | } 280 | 281 | void HeapSort( ElementType A[], int N ) 282 | { /* 堆排序 */ 283 | int i; 284 | 285 | for ( i=N/2-1; i>=0; i-- )/* 建立最大堆 */ 286 | PercDown( A, i, N ); 287 | 288 | for ( i=N-1; i>0; i-- ) { 289 | /* 删除最大堆顶 */ 290 | Swap( &A[0], &A[i] ); /* 见代码7.1 */ 291 | PercDown( A, 0, i ); 292 | } 293 | ``` 294 | 295 | ## 9.4 归并排序 296 | 297 | ### 9.4.1 有序子列的归并 298 | 299 | 需要3个指针(这个指针不一定是C语言里面说的那个语法上的指针) 300 | 301 | **指针**:本质上他存的是位置 302 | 303 | image-20220818060835688 304 | 305 | 假设我们讨论的是数组(那位置由下标决定,那图中的指针就可以是整数,整数存的是这个元素的下标) 306 | 307 | ``` 308 | 上方图中红色跟绿色的指针指向的位置进行比大小,小的填入下方的空位置中,红绿色其他一方填入数值后指针就往后挪一位,然后继续红绿色指针所指位置对比大小,直到下方空位置填满 309 | ``` 310 | 311 | 如果两个子列一共有N个元素,则归并的时间复杂度是?**T(N) = O(N)** 312 | 313 | #### 有序子列归并的伪代码 314 | 315 | ```c 316 | //L = 左边起始位置,R = 右边起始位置,RightEnd = 右边终点位置 317 | void Merge(ElementType A[],ElementType TmpA[],int L,int R,int RightEnd)//Merge就是归并的意思 318 | {//参数意思从左到右分别是:原始的待排的序列,临时存放的数组,归并左边的起始位置(也就是上图的Aptr),归并右边的起始位置(也就是上图的Bptr),右边终点的位置 319 | 320 | LeftEnd = R - 1;//左边终点位置,假设左右两列挨着 321 | Tmp = L;//存放结果的数组的初始位置,相当于上图的Cptr 322 | NumElements = RightEnd - L + 1;//元素的总个数 323 | //上方是准备工作,下方开始归并 324 | while( L <= LeftEnd && R <= RightEnd ){//一直走到左右两边其中一方不满足之后跳出(意味着其中一个子序列已经空了,没有元素了,另一方剩下的元素直接全部导入后面就可以了) 325 | if(A[L] <= A[R] ) TmpA[Tmp++] = A[L++];//左边小,将Aptr放入 326 | else TmpA[Tmp++] = A[R++];//右边小,将Bptr放入 327 | } 328 | while( L <= LeftEnd )//直接复制左边剩下的 329 | TmpA[Tmp++] = A[L++]; 330 | while( R <= RightEnd)//直接复制右边剩下的 331 | TmpA[Tmp++] = A[R++];//TmpA只是临时存放的地方,还需要导回去 332 | for( i = 0;i < NumElements;i++,RightEnd-- )//从后面开始才能知道终点的位置具体是哪个,因为RightEnd具体多少是不固定的 333 | A[RightEnd] = TmpA[RightEnd]; 334 | } 335 | ``` 336 | 337 | ### 9.4.2递归算法 338 | 339 | 1. **分而治之** 340 | 341 | 1. ``` 342 | 先把整个一分为二,然后递归的去考虑问题,递归的去把左边排好序,再递归的把右边排好序。这样得到两个有序的子序列,而且肩并肩的放在一起,最后调用我们归并的算法,把他们归并到一个完整的数组里 343 | ``` 344 | 345 | 2. 算法的伪代码实现image-20220818072931321 346 | 347 | ```c 348 | void MSort(ElementType A[],ElementType TmpA[],int L,int RightEnd ) 349 | {//上述参数:原始待排的数组,临时的数组,L指待排序列开头的位置,RightEnd则是待排序列结尾的位置 350 | int Center;//中间的位置 351 | if( L < RightEnd ){ 352 | Center = (L + RightEnd ) / 2; 353 | MSort( A,TmpA,L,Center );//左边的递归排序 354 | MSort( A,TmpA,Center+1,RightEnd );//右边的递归 355 | Merge( A,TmpA,L,Center+1,RightEnd );//归并,传入的参数分别是原始数组A,临时数组TmpA,左边的起始点,右边的起始点吗,右边的终点。结果存在原来这个数组A里面 356 | } 357 | } 358 | //T(N) = T(N/2)+T(N/2)+O(N) => T(N) = O(NlogN) 359 | NlogN:没有最坏时间复杂度也没有最好时间复杂度,更没有平均时间复杂度,任何情况下都是NlogN,非常稳定 360 | ``` 361 | 362 | image-20220818074038141 363 | 364 | #### 统一函数接口 365 | 366 | ```c 367 | void Merge_sort( ElementType A[],int N )//参数:原始的数组A,元素的个数N 368 | { 369 | ElementType *TmpA; 370 | TmpA = malloc(N * sizeof( ElementType ));//TmpA空间在这里临时申请 371 | if( TmpA != NULL ){//检查申请的空间是否还有位置 372 | MSort(A,TmpA,0,N-1);//TmpA在这里只是一个递归的调用,真正用到TmpA的地方是在Merge(核心的那个归并函数里) 373 | free( TmpA );//把临时空间给释放掉 374 | } 375 | else Error("空间不足") 376 | } 377 | ``` 378 | 379 | ```c 380 | 如果只在Merge中声明临时数组TmpA 381 | 1.void Merge( ElementType A[],int L,int R,int RightEnd ) 382 | 2.void MSort( ElementType A[],int L,int RightEnd) 383 | ``` 384 | 385 | image-20220818083938089白色砖块一样的东西是申请的空间,要不停的申请空间再释放掉,这样做实际上是不合算的(太麻烦了,申请一个释放掉在申请下一个不停循环) 386 | 387 | 最合算的做法:一开始就声明一个数组,每次只把数组的指针传进去,只在这个数组的某一段上面做操作,就不需要重复的malloc跟free 388 | 389 | ### 9.4.3非递归算法(归并排序) 390 | 391 | image-20220818084431828 392 | 393 | 上图的深度为logN 394 | 395 | 非递归算法的额外空间复杂度是?O(N) 396 | 397 | ``` 398 | 只需要开一个临时数组就够了,没有必要每次合并都开一个 399 | 第一次我们把A给归并到临时数组里面 400 | 第二次把临时数组里面的东西归并回A里面去,然后再把A导到临时数组里,再把临时数组导回到A 401 | 最后一步运气好的话就是A,运气不好的话这最后一步可能是那个临时数组他不是A(需要再加一步导回到A里面去) 402 | ``` 403 | 404 | **一趟归并伪代码** 405 | 406 | ```c 407 | void Merge_pass( ElementType A[],ElementType TmpA[],int N,int length)//length = 当前有序子列长度(一开始为1,之后每次加倍) 408 | {//参数:原始数组,临时数组,N为待排序列长度 409 | for(i = 0; i < N-2*length;i += 2*length )//i += 2*length就是跳过两段然后去找下一对。最后尾巴可能是单个的所以先把前面成对的那一部分处理完,终止条件就是处理到倒数第二对(这个处理完了再看尾巴) 410 | Merge1( A, TempA, i, i+length, i+2*length-1 );//不做Merge最后一步导入A中,在这里意味着把A中的元素归并到TmpA里面去,最好有序的内容是放在TmpA里面 411 | if( i+length < N )//归并最后两个子列,最后如果加上一段以后还是小于N的,那就说明我最后是不止一个子列,是有两个子列的 412 | //如果这个if条件不成立意味着当前i这个位置加上一个length之后他就跳到N外面去了,也就意味着我最后只剩下一个子列 413 | Merge1(A,TmpA,i,i+length,N-1); 414 | else//最后剩下一个子列 415 | for(j = i;i < N;j++ ) TmpA[j] = A[j]; 416 | } 417 | ``` 418 | 419 | **原始统一接口** 420 | 421 | ```c 422 | void Merge_sort( ElementType A[],int N ) 423 | { 424 | int length = 1//初始化子序列长度 425 | ElementType *TmpA; 426 | TmpA = malloc( N* sizeof(ElementType)); 427 | if( TmpA != NULL ){ 428 | while( length < N ){ 429 | Merge_pass(A,TmpA,N,length); 430 | length *= 2; 431 | Merge_pass(TmpA,A,N,length);//传进来的length长度是2。前面这个TmpA是初始状态,后面A是归并以后的状态 432 | length *= 2;//这里length再次double(翻倍)变成了4 433 | //最后跳出while循环,结果都是存在A里面的,哪怕最后一步执行到Merge_pass(A,TmpA,N,length);就已经有序了,也会多执行一步Merge_pass,将TmpA原封不动的导到A里面然后自然跳出 434 | } 435 | free(TmpA); 436 | } 437 | else Error("空间不足"); 438 | } 439 | 440 | //优点:稳定 441 | //缺点:需要一个额外的空间,并且需要在数组跟数组之间来回来去的复制 导这个元素。所以实际运用中基本上不做内排序(在外排序的时候是非常有用的) 442 | ``` 443 | 444 | -------------------------------------------------------------------------------- /数据结构第五周笔记——树(下)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第五周笔记——树(下)(慕课浙大版本--XiaoYu) 2 | 3 | ## 5.1 堆(heap) 4 | 5 | ### 5.1.1 什么是堆 6 | 7 | 1. ``` 8 | 优先队列(Priority Queue):特殊的"队列",取出元素的顺序是依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序 9 | ``` 10 | 11 | 1. image-20220707112759644 12 | 13 | 2. 是否可以采用二叉树存储结构? 14 | 15 | 1. 二叉搜索树? 16 | 2. 如果采用二叉树结构,应更关注插入还是删除? 17 | 1. 树结点顺序怎么安排? 18 | 2. 树结构怎么样? 19 | 20 | 3. 优先队列的完全二叉树表示 21 | 22 | 1. image-20220707114110845 23 | 24 | 2. ``` 25 | 堆的两个特性 26 | 结构性:用数组表示的完全二叉树; 27 | 有序性:任一结点的关键字是其子树所有结点的最大值(或最小值) 28 | "最大堆(MaxHeap)",也称"大顶堆":最大值 29 | "最小堆(MinHeap)",也称"小顶堆":最小值 30 | 通俗的来说就是最大堆的上面的那个要比连接下面两个都要大。最小堆则相反,最上面那个要比连接下面两个都要小。同时需要是完全二叉树 31 | 注意:从根结点到任意结点路径上结点系列的有序性! 32 | ``` 33 | 34 | 3. image-20220707114551521 35 | 36 | #### 堆的抽象数据类型描述 37 | 38 | ``` 39 | 类型名称:最大堆(MaxHeap) 40 | 数据对象集:完全二叉树,每个结点的元素值不小于其子结点的元素值 41 | 操作集:最大堆H属于MAaxHeap,元素item属于ElementType,主要操作有: 42 | 1.MaxHeap Create(int MaxSize):创建一个空的最大堆 43 | 2.Boolean IsFull(MaxHeap H):判断最大堆H是否已满 44 | 3.Insert(MaxHeap H,ElementType item):将元素item插入最大堆H 45 | 4.Boolean IsEmpty(MaxHeap H):判断最大堆H是否为空 46 | 5.ElementType DeleteMax(MaxHeap H):返回H中最大元素(高优先级) 47 | ``` 48 | 49 | ### 5.1.2 堆的插入 50 | 51 | 最大堆的创建 52 | 53 | ```c 54 | typedef struct HeapStruct *MaxHeap; 55 | struct HeapStruct{ 56 | ElementTye *Elements;//存储堆元素的数组 57 | int Size;//堆的当前元素个数 58 | int Capacity;//堆的最大容量 59 | }; 60 | MaxHeap Create(int MaxSize) 61 | { 62 | //创建容量为MaxSize的空的最大堆 63 | MaxHeap H = malloc(sizeof(struct HeapStruct)); 64 | H->Elements = malloc((Maxsize+1)* sizeof(ElementType));//Maxsize+1是希望申请的容量 65 | H->Size = 0; 66 | H->Capacity = MaxSize; 67 | H->Elements[0] = MaxSize; 68 | H->Elements[0] = MaxData; 69 | //定义"哨兵"为大于堆中所有可能元素的值,便于以后更快操作 70 | return H; 71 | } 72 | ``` 73 | 74 | #### 最大堆的插入 75 | 76 | 1. image-20220707205756557 77 | 78 | 2. image-20220707205844776 79 | 80 | 3. image-20220707205940384 81 | 82 | 4. 如果在底下插入的数值比上面大就沿着线路一路换位置,直到上面没有在比插入的大了为止 83 | 84 | 5. 算法的实现如下:将新增结点插入到从其父结点到根结点的有序序列中 85 | 86 | 1. ```c 87 | void Insert(Maxheap H,ElementType item) 88 | { 89 | //将元素item插入最大堆H,其中H->Elements[0]已经定义为哨兵 90 | int i; 91 | if(IsFull(H)){ 92 | printf("最大堆已满"); 93 | return; 94 | } 95 | i = ++H->Size;//i指向插入后堆中的最后一个元素的位置 96 | for(;H->Elements[i/2]) < item;i/=2) 97 | H->Elements[i] = H->Elements[i/2];//向下过滤结点 98 | H->Elements[i] = item;//将item插入 99 | } 100 | 复杂度:T(N) = O(log N) 101 | ``` 102 | 103 | image-20220707212158564 104 | 105 | 问题:“哨兵”是在创建堆(Create函数)时设置的:H->Elements[0]=MaxData; 106 | 107 | 答案:这是对的 108 | 109 | 110 | 111 | ### 5.1.3 堆的删除 112 | 113 | #### 最大堆的删除 114 | 115 | 1. 取出根结点(最大值)元素,同时删除堆的一个结点 116 | 117 | 2. image-20220707213148580 118 | 119 | 3. image-20220707213217119 120 | 121 | 1. 时间复杂度就等于树的高度 122 | 123 | 4. 删除MaxHeap程序实现: 124 | 125 | 1. ```c 126 | ElementType DeleteMax(MaxHeap H) 127 | { 128 | //从最大堆H中取出键值为最大的元素,并删除一个结点 129 | int Parent,Child; 130 | ElementType MaxItem,temp; 131 | if( IsEmpty(H)){ 132 | printf("最大堆已为空"); 133 | return; 134 | } 135 | MaxItem = H->Elements[1];//取出根结点最大值,保存起来,最后是要return出去的 136 | //temp = H->Elements[H->Size--]; 137 | for(Parent = 1;Parent*2 <= H->Size;Parent=Child){//Parent指示我将来要换的位置,退出循环就说明了我们要的位置找到了 138 | Child = Parent * 2;//Child指向左儿子 139 | if((Child != H->Size) && (H->Elements[Child] < H->Elements[Child+1])) 140 | Child++;//Child指向左右子结点的较大者 141 | if(temp > H->Elements[Child])break; 142 | else//移动temp元素到下一层 143 | H->Elements[Parent] = H->Elements[Child]; 144 | } 145 | H->Elements[Parent] = temp; 146 | return MaxItem; 147 | } 148 | 149 | 在完全二叉树里面,i代表一个节点,它的左儿子在2i,右儿子在2i+1 150 | 如果左儿子2i已经超出Size的范围,说明左儿子已经在堆栈外面了,那就说明它没左儿子 151 | Parent*2 <= H->Size是判别它有没有左儿子,如果没有左儿子那右儿子肯定也没有那就退出循环了 152 | 153 | Child != H->Size:Child指向左儿子意味左儿子是最后一个元素了,那就意味着没有右儿子了。实际上这个就相当于判别有没有右儿子 154 | ``` 155 | 156 | 问题:有个堆其元素在数组中的序列为:58,25,44,18,10,26,20,12。如果调用DeleteMax函数删除最大值元素,请猜猜看:程序中的for循环刚退出时变量parent的值是多少? 157 | 158 | 是6 159 | 160 | ### 5.1.4 堆的建立 161 | 162 | #### 堆的一个应用:堆排序--需要先建堆 163 | 164 | 最大堆的建立 165 | 166 | ``` 167 | 建立最大堆:将已经存在的N个元素按最大堆的要求存放在一个一维数组中 168 | 169 | 方法1:通过插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为O(NlogN) 170 | 每次插入 它的时间复杂性是log2N 171 | 总共循环N遍,所以整个时间复杂性是Nlog2N 172 | 上方这个方法1的效率是不够的,我们可以有更好的方法 173 | 方法2:在线性时间复杂度下建立最大堆. 174 | (1)将N个元素按输入顺序存入,先满足完全二叉树的结构特性 175 | (2)调整各结点位置,以满足最大堆的有序特性 176 | ``` 177 | 178 | 问:建堆时,最坏情况下需要挪动元素次数是等于树中各结点的高度和。问:对于元素个数为12的堆,其各结点的高度之和是多少?10 179 | 180 | image-20220707221437125 181 | 182 | ### 小测验:堆 183 | 184 | ![image-20220707222337161](https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/925/image-20220707222337161.png) 185 | 186 | image-20220707223127298 187 | 188 | ### C语言代码:堆的定义和操作 189 | 190 | ```c 191 | typedef struct HNode *Heap; /* 堆的类型定义 */ 192 | struct HNode { 193 | ElementType *Data; /* 存储元素的数组 */ 194 | int Size; /* 堆中当前元素个数 */ 195 | int Capacity; /* 堆的最大容量 */ 196 | }; 197 | typedef Heap MaxHeap; /* 最大堆 */ 198 | typedef Heap MinHeap; /* 最小堆 */ 199 | 200 | #define MAXDATA 1000 /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */ 201 | 202 | MaxHeap CreateHeap( int MaxSize ) 203 | { /* 创建容量为MaxSize的空的最大堆 */ 204 | 205 | MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode)); 206 | H->Data = (ElementType *)malloc((MaxSize+1)*sizeof(ElementType)); 207 | H->Size = 0; 208 | H->Capacity = MaxSize; 209 | H->Data[0] = MAXDATA; /* 定义"哨兵"为大于堆中所有可能元素的值*/ 210 | 211 | return H; 212 | } 213 | 214 | bool IsFull( MaxHeap H ) 215 | { 216 | return (H->Size == H->Capacity); 217 | } 218 | 219 | bool Insert( MaxHeap H, ElementType X ) 220 | { /* 将元素X插入最大堆H,其中H->Data[0]已经定义为哨兵 */ 221 | int i; 222 | 223 | if ( IsFull(H) ) { 224 | printf("最大堆已满"); 225 | return false; 226 | } 227 | i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */ 228 | for ( ; H->Data[i/2] < X; i/=2 ) 229 | H->Data[i] = H->Data[i/2]; /* 上滤X */ 230 | H->Data[i] = X; /* 将X插入 */ 231 | return true; 232 | } 233 | 234 | #define ERROR -1 /* 错误标识应根据具体情况定义为堆中不可能出现的元素值 */ 235 | 236 | bool IsEmpty( MaxHeap H ) 237 | { 238 | return (H->Size == 0); 239 | } 240 | 241 | ElementType DeleteMax( MaxHeap H ) 242 | { /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */ 243 | int Parent, Child; 244 | ElementType MaxItem, X; 245 | 246 | if ( IsEmpty(H) ) { 247 | printf("最大堆已为空"); 248 | return ERROR; 249 | } 250 | 251 | MaxItem = H->Data[1]; /* 取出根结点存放的最大值 */ 252 | /* 用最大堆中最后一个元素从根结点开始向上过滤下层结点 */ 253 | X = H->Data[H->Size--]; /* 注意当前堆的规模要减小 */ 254 | for( Parent=1; Parent*2<=H->Size; Parent=Child ) { 255 | Child = Parent * 2; 256 | if( (Child!=H->Size) && (H->Data[Child]Data[Child+1]) ) 257 | Child++; /* Child指向左右子结点的较大者 */ 258 | if( X >= H->Data[Child] ) break; /* 找到了合适位置 */ 259 | else /* 下滤X */ 260 | H->Data[Parent] = H->Data[Child]; 261 | } 262 | H->Data[Parent] = X; 263 | 264 | return MaxItem; 265 | } 266 | 267 | /*----------- 建造最大堆 -----------*/ 268 | void PercDown( MaxHeap H, int p ) 269 | { /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */ 270 | int Parent, Child; 271 | ElementType X; 272 | 273 | X = H->Data[p]; /* 取出根结点存放的值 */ 274 | for( Parent=p; Parent*2<=H->Size; Parent=Child ) { 275 | Child = Parent * 2; 276 | if( (Child!=H->Size) && (H->Data[Child]Data[Child+1]) ) 277 | Child++; /* Child指向左右子结点的较大者 */ 278 | if( X >= H->Data[Child] ) break; /* 找到了合适位置 */ 279 | else /* 下滤X */ 280 | H->Data[Parent] = H->Data[Child]; 281 | } 282 | ``` 283 | 284 | 285 | 286 | ## 5.2 哈夫曼树与哈夫曼编码 287 | 288 | ### 5.2.1 什么是哈夫曼树(Huffman Tree) 289 | 290 | [例]将百分制的考试成绩转换成五分制的成绩 291 | 292 | ```C 293 | if(score < 60 ) grade = 1; 294 | else if(score < 70) grade = 2; 295 | else if(score < 80) grade = 3; 296 | else if(score < 90) grade = 4; 297 | else grade = 5; 298 | ``` 299 | 300 | 上述代码中,其实对应着就有一棵树,如下图: 301 | 302 | image-20220707234237931 303 | 304 | image-20220707234356578 305 | 306 | 优化后的效率: 307 | 308 | image-20220707234452711 309 | 310 | ```c 311 | if(score < 80) 312 | { 313 | if(score < 70 ) 314 | if(score < 60) grade = 1; 315 | else grade = 2; 316 | }else if(score < 90 )grade = 4; 317 | else grade = 5; 318 | ``` 319 | 320 | **如何根据结点不同的查找频率构造更有效的搜索树?** 321 | 322 | #### 哈夫曼树的定义 323 | 324 | image-20220707234921567 325 | 326 | **最优二叉树或哈夫曼树就是WPL最小的二叉树** 327 | 328 | 一棵二叉树每一个叶结点的频率或者权重乘以这个叶结点到根结点的这个路径的长度就是带权路径的长度 329 | 330 | **权值 = 频率** 331 | 332 | [例]有五个叶子结点,它们的权值为{1,2,3,4,5},用此权值序列可以构造出形状不同的多个二叉树 333 | 334 | image-20220708001419786 335 | 336 | image-20220708001539908是50 337 | 338 | image-20220708001741632 339 | 340 | 341 | 342 | ### 5.2.2 哈夫曼树的构造 343 | 344 | 1. 每次把权值最小的两颗二叉树合并 345 | 346 | 2. image-20220708002041082 347 | 348 | 3. 代码实现(如何选取两个最小的?) 349 | 350 | 1. ```c 351 | typedef struct TreeNode *HuffmanTree; 352 | struct TreeNode{ 353 | int Weight; 354 | HuffmanTree Left,Right; 355 | } 356 | HuffmanTree Huffman(MinHeap H) 357 | { 358 | //假设H->Size个权值已经存在H->Elements[]->Weight里 359 | int i; HuffmanTree T; 360 | BuildMinHeap(H);//将H->Elements[]按权值调整为最小堆 361 | for(i = 1;i < H->Size; i++){//做H->Size-1次合并 362 | T = malloc(sizeof(struct TreeNode));//建立新结点 363 | T->Left = DeleteMin(H);//从最小堆中删除一个结点,作为新T的左子结点 364 | T->Right = DeleteMin(H);//从最小堆中删除一个结点,作为新T的右子结点 365 | T->Weight = T->Left->Weight+T->Right->Weight;//计算新权值 366 | Insert(H,T);//将新T插入最小堆 367 | } 368 | T = DeleteMin(H); 369 | return T; 370 | } 371 | 整体复杂度为O(NlogN) 372 | ``` 373 | 374 | #### 哈夫曼树的特点: 375 | 376 | 1. 没有度为1的结点; 377 | 2. n个叶子结点的哈夫曼树共有2n-1个结点 378 | 1. n0:叶结点总数 379 | 2. n1:只有一个儿子的结点总数 380 | 3. n2:有2个儿子的结点总数 381 | 4. n2 = n0 - 1 382 | 3. 哈夫曼树的任意非叶节点的左右子树交换后仍是哈夫曼树; 383 | 4. image-20220708003623357 384 | 5. 385 | 386 | ### 5.2.3 哈夫曼编码 387 | 388 | 给定一段字符串,如何对字符进行编码,可以使得该字符串的编码存储空间最少? 389 | 390 | image-20220708003841443 391 | 392 | 分析: 393 | 394 | 1. 用等长ASCII编码:58×8 = 464位 395 | 2. 用等长3位编码:58×3 = 174位; 396 | 3. 不等长编码:出现频率高的字符用的编码短些,出现频率低的字符则可以编码长些? 397 | 398 | #### 怎么进行不等长编码? 399 | 400 | #### 如何避免二义性(就是你这个编码不止一个意思) 401 | 402 | 1. 前缀码prefix code:任何字符的编码都不是另一字符编码的前缀 403 | 1. 可以无二义地解码(你的这个编码不能是其他编码的前缀) 404 | 2. 二叉树用于编码: 405 | 1. 左右分支:0、1 406 | 2. 字符只在叶结点上 407 | 3. image-20220708004934296 408 | 4. image-20220708005100967 409 | 5. image-20220708005137719 410 | 6. 怎么构造一颗编码代价最小的二叉树? 411 | 1. image-20220708005631723 412 | 413 | 414 | 415 | ### 小测验:哈夫曼树 416 | 417 | image-20220708010907498 418 | 419 | 420 | 421 | ## 5.3 集合及运算 422 | 423 | ### 5.3.1 集合的表示及查找 424 | 425 | 1. 集合运算:交,并,补,差,判定一个元素是否属于某一集合 426 | 427 | 2. 并查集:集合并、查某元素属于什么集合 428 | 429 | 3. 并查集问题中集合存储如何实现? 430 | 431 | 1. 可以用树结构表示集合,树的每个结点代表一个集合元素 432 | 433 | 2. image-20220708125448566对这样三棵树的存储方法 434 | 435 | 1. 采用数组存储形式 436 | 2. image-20220708130212563 437 | 3. 上图中没有父节点的用负数来表示,Parent是它父节点的下标 438 | 439 | 3. ``` 440 | [例]:有10台电脑{1,2,3,.....,9,10},已知下列电脑之间已经实现了连接: 441 | 1和2、2和4、3和5、4和7、5和8、6和9、6和10 442 | 问:2和7之间,5和9之间是否是连通的? 443 | 2和7是连通的,5和9不连通 444 | ``` 445 | 446 | image-20220708124931978 447 | 448 | #### 集合运算 449 | 450 | (1)查找某个元素所在的集合(用根结点表示) 451 | 452 | ```c 453 | int Find(SetType S[],ElementType X) 454 | { 455 | //在数组S中查找值为X的元素所属的集合 456 | //MaxSize是全局变量,为数组S的最大长度 457 | int i; 458 | for(i = 0;i < MaxSize && S[i].Data != X; i++); 459 | if(i >= MaxSize) return -1;//未找到X,返回-1 460 | for(;S[i].Parent >= 0; i = S[i].Parent);//Parent的值为-1的时候就是找到根结点。i = S[i].Parent:原本指向i的位置现在跳到了s[i].Parent 461 | return i;//找到X所属集合,返回树根结点在数组S中的下标 462 | } 463 | ``` 464 | 465 | 466 | 467 | ### 5.3.2 集合的并运算 468 | 469 | (2)集合的并运算 470 | 471 | 1. 分别找到X1和X2两个元素所在集合树的根结点 472 | 473 | 2. 如果它们不同根,则将其中一个根结点的父结点指针设置成另一个根结点的数组下标 474 | 475 | 1. Union实现代码 476 | 477 | 2. ```c 478 | void Union(SetType S[ ],ElementType X1,ElementType X2 ) 479 | { 480 | int Root1,Root2; 481 | Root1 = Find(S,X1);//得到X1与X2对应的树根 482 | Root2 = Find(S,X2); 483 | if( Root1 != Root2 ) S[Root2].Parent = Root1;//判断如果不是本身就是同一个集合的,如果是同一个集合的话就不需要做这个并的操作。不同则合并 484 | } 485 | ``` 486 | 487 | 为了改善合并以后的查找性能,可以采用小的集合合并到相对大的集合中。(修改Union函数)。也许树的高度不会增加 488 | 489 | ### 小测验:集合 490 | 491 | 已知a、b两个元素均是所在集合的根结点,且分别位于数组分量3和2位置上,其parent值分别为-3,-2。问:将这两个集合按集合大小合并后,a和b的parent值分别是多少? 492 | 493 | -5,3 494 | 495 | #### 集合的定义与并查 496 | 497 | ```c 498 | #define MAXN 1000 /* 集合最大元素个数 */ 499 | typedef int ElementType; /* 默认元素可以用非负整数表示 */ 500 | typedef int SetName; /* 默认用根结点的下标作为集合名称 */ 501 | typedef ElementType SetType[MAXN]; /* 假设集合元素下标从0开始 */ 502 | 503 | void Union( SetType S, SetName Root1, SetName Root2 ) 504 | { /* 这里默认Root1和Root2是不同集合的根结点 */ 505 | /* 保证小集合并入大集合 */ 506 | if ( S[Root2] < S[Root1] ) { /* 如果集合2比较大 */ 507 | S[Root2] += S[Root1]; /* 集合1并入集合2 */ 508 | S[Root1] = Root2; 509 | } 510 | else { /* 如果集合1比较大 */ 511 | S[Root1] += S[Root2]; /* 集合2并入集合1 */ 512 | S[Root2] = Root1; 513 | } 514 | } 515 | 516 | SetName Find( SetType S, ElementType X ) 517 | { /* 默认集合元素全部初始化为-1 */ 518 | if ( S[X] < 0 ) /* 找到集合的根 */ 519 | return X; 520 | else 521 | return S[X] = Find( S, S[X] ); /* 路径压缩 */ 522 | ``` 523 | 524 | 525 | 526 | ## 小白专场:堆中的路径-C语言实现 527 | 528 | ### 堆中的路径 529 | 530 | 1. image-20220708133742876 531 | 2. 5 3的意思是给你5个数据构成一个最小堆进行3次查询。第二行就是要插入的五个数据。5 4 3代表下标 532 | 533 | image-20220708134013080 534 | 535 | ```c 536 | 堆的表示及其操作(堆是一种按一定顺序组织的完全二叉树) 537 | #define MAXN 1001 538 | #define MINH -10001 539 | 540 | int H[MAXN],size;//由于堆在存储的时候是把根结点放在数组下标为1的地方,也就是说0是空缺的 541 | //这样子按照一层层顺序逐个往数组后面存放,使得堆中的任何一个元素可以很容易的找到他的父节点在哪里,左右儿子在哪里 542 | //整数size表示当前堆大小 543 | void Create()//堆的初始化,就是建立一个空堆(size设置为0) 544 | { 545 | size = 0; 546 | H[0] = MINH;//设置岗哨 547 | } 548 | //插入操作 549 | void Insert(int X) 550 | { 551 | //将X插入H。这里省略检查堆是否已满的代码 552 | int i; 553 | for(i = ++size;H[i/2]>X;i/=2) 554 | H[i] = H[i/2];//i挪到父节点(i/2)的位置 555 | H[i] = X; 556 | } 557 | 558 | 主程序 559 | int main() 560 | { 561 | 1.读入n和m 562 | 2.根据输入序列建堆 563 | 3.对m个要求:打印到根的路径 564 | 565 | return 0; 566 | } 567 | //具体实现程序 568 | int main() 569 | { 570 | int n,m,x,i,j; 571 | 572 | scanf("%d%d",&n,&m); 573 | Create();//堆初始化 574 | for(i = 0;i < n; i++ ) {//以逐个插入方式建堆 575 | scanf("%d",&x); 576 | Insert(x);//利用Insert函数插到堆中 577 | } 578 | //m个查询 579 | for(i=0;i1是代表还没有到根的时候,根的位置是1,这时候j/2就代表了他父节点的位置 583 | j /= 2; 584 | printf("%d",H[j]); 585 | } 586 | printf("\n"); 587 | } 588 | return 0; 589 | } 590 | ``` 591 | 592 | 593 | 594 | ## 小白专场[陈越]:File Transfer (是一道经典的并查集的应用题)-C语言实现 595 | 596 | ### 小白-FT.1 集合的简化表示 597 | 598 | 集合的表示方法 599 | 600 | ```c 601 | typedef struct { 602 | ElementType Data; 603 | int Parent; 604 | }SetType; 605 | ``` 606 | 607 | 查找Find函数 608 | 609 | ```c 610 | int Find(SetType S[],ElementType X) 611 | { 612 | int i; 613 | for(i = 0; i < MaxSize && S[i].Data != X; i++);//找到X在集合中是第几个元素,这个时间复杂度是O(N2)这个数量级,可以有更好的方法来寻找 614 | if( i >= MaxSize ) return -1; 615 | for(;S[i].Parent >= 0;i = S[i].Parent); 616 | return i; 617 | } 618 | ``` 619 | 620 | image-20220708232958390 621 | 622 | 但其实这个声明这个Data专门用来存储是可以省略的,直接用数组来存储就行了,想知道有几个独立的集合就看数组里面有几个-1就可以了(-1表示处于最上方的了) 623 | 624 | image-20220708233303936 625 | 626 | #### 集合简化表示 627 | 628 | ```c 629 | typedef int ElementType;//默认元素可以用非负整数表示 630 | typedef int SetNameL;//默认用根结点的下标作为集合名称 631 | typedef ElementType SetType[MaxSize]; 632 | 633 | SetName Find(SetType S,ElementType X)//这个X直接就是数组的下标 634 | { 635 | //默认集合元素全部初始化为-1 636 | for(;S[X]>=0;X=S[X]);//这和S[X]里面存的直接就是X的父节点 637 | return X; 638 | } 639 | 640 | void Union (SetType S, SetName Root1,SetName Root2) 641 | { 642 | //这里默认Root1和Root2是不同集合的根结点 643 | S[Root2] = Root1; 644 | } 645 | ``` 646 | 647 | 648 | 649 | ### 小白-FT.2 题意理解与TSSN的实现 650 | 651 | #### 题意理解 652 | 653 | C:检测能不能连通,能就Yes不行就No 654 | 655 | I:连通 656 | 657 | S:输入结束 658 | 659 | image-20220708234749670 660 | 661 | 在这里面每台计算机都是直接或者间接连通的。在上图中1虽然是一个孤立的计算机,但他也被认为是跟自己连通的 662 | 663 | 所以这个会输出:There are 2 components (这个系统有两个连通机) 664 | 665 | image-20220709000356238 666 | 667 | 在上图的这种情况下,输出的最后一行就应该是The network is connected(这个网络是连通的) 668 | 669 | #### 程序框架搭建 670 | 671 | ```C 672 | int main() 673 | { 674 | 初始化集合; 675 | do{ 676 | 读入一条指令; 677 | 处理指令; 678 | }while(没结束); 679 | return 0; 680 | } 681 | ``` 682 | 683 | 翻译为具体代码如下 684 | 685 | ```c 686 | int main() 687 | { 688 | SetType S;//这是并查集 689 | int n;//n是集合里面元素的个数 690 | char in;//第一个字符被命名为in 691 | 692 | //初始化集合 693 | scanf("%d\n",&n); 694 | Initialization(S,n); 695 | do{ 696 | scanf("%c",&in); 697 | switch(in){ 698 | case 'I':Input_connection(S);break;//把两台需要连接的计算机的编号读进来,检查是否连接好了,没连接好就连接一下 699 | //Union和Find函数都会出现在上方那个函数当中 700 | case 'C':Check_connection(s);break;//这个函数要做的就是读入待查询的计算机的编号,检查他们在不在一个集合里面,用到Find函数 701 | case 'S':Check_network(s,n);break;//检查网络是否已经连通了 702 | } 703 | }while(in != 'S'); 704 | 705 | 706 | 707 | 708 | return 0; 709 | } 710 | ``` 711 | 712 | image-20220709000953246 713 | 714 | 答案是A 715 | 716 | #### 程序框架搭建2 717 | 718 | ```c 719 | //处理输入 720 | void Input_connection(SetType S) 721 | { 722 | ElementType u,v; 723 | SetName Root1,Root2; 724 | scanf("%d %d\n",&u,&v); 725 | Root1 = Find(S,u-1); 726 | Root2 = Find(S,v-1); 727 | if( Root1 != Root2 ) 728 | Union(S,Root1,Root2); 729 | } 730 | ----------------------------------------------------------------- 731 | //处理查询 732 | void Check_connection(SetType S) 733 | { 734 | ElementType u,v; 735 | SetName Root1,Root2; 736 | scanf("%d %d\n",&u,&v); 737 | Root1 = Find(S,u-1); 738 | Root2 = Find(S,v-1); 739 | if( Root1 == Root2 ) 740 | printf("Yes\n"); 741 | else printf("no\n") 742 | } 743 | -------------------------------------------------- 744 | //检查整个网络是否已经连通 745 | void Check_network(SetType S,int n) 746 | { 747 | int i,count = 0; 748 | for(i = 0; i < n; i++){ 749 | if( S[i] < 0 ) counter++//只要S[i] < 0就意味着这是一个根节点 750 | } 751 | if(counter == 1)//只有一个根节点表示整个集合全部连通了 752 | printf("The network is connected.\n");//输出网络连通了 753 | else 754 | printf("There are %d conponents.\n",counter);//输出一共有剁谁个连通集 755 | } 756 | 757 | 运行效率取决于Find函数与Union函数怎么实现的 758 | ``` 759 | 760 | TSSN(to simple sometime neive)实现 761 | 762 | ### 小白-FT.3 按秩归并 763 | 764 | #### 为什么需要按秩归并? 765 | 766 | 1. ``` 767 | 1. Union(Find(2),Find(1))//注意2在前,1在后。前面输入集合1后面输入集合2。让集合2指向集合1。其实也就是1指向2 768 | 2. Union(Find(3),Find(1))//这里1指向3了,由于上面那个,相当于2也指向3了 769 | ``` 770 | 771 | image-20220710091606477但这样显然会让树越来越高,有几个数,树就有多高,退化成单链表了,而且每次都绑定在1身上就意味着每次都需要从1开始找,直到找到他的根结点 772 | 773 | 刚才一系列Union(Find(![img](https://img-ph-mirror.nosdn.127.net/QQG8BZi1xVjMpernpk1GVQ==/6608211117981750295.png)), Find(1))(其中![img](https://img-ph-mirror.nosdn.127.net/Or5-lxUEtwwNfcxG__CzJg==/6631765955584149961.png))操作的时间复杂度是:T(n) = O(n²) 774 | 775 | #### 为什么树的高度会越来越高 776 | 777 | image-20220710092019371 778 | 779 | 1. 只要把矮树贴到高树上就可以避免这种情况。但这样就势必要判断树的高度再决定谁贴到谁身上 780 | 781 | #### 树的高度存哪里? 782 | 783 | ```C 784 | S[Root] = -1//这是根结点,设置为-1就不会是任何一个元素的下标了,这样就把他和其他的非根结点区别开了。写-1或者-100都是没有区别的都可以 785 | //由这个想法向后延伸 786 | S[Root] = -树高//依旧初始化为-1,这棵树的高度一开始就是1,每个计算机都是单独的一个结点 787 | 788 | //代码演示(伪代码) 789 | if( Root2高度 > Root1高度 ) 790 | S[Root1] = Root2;//集合1指向集合2 791 | else{ 792 | if(两树一样高) 树高++;//存在新的结点里面 793 | S[Root2] = Root1; 794 | } 795 | 796 | //实际代码,比高度的做法 797 | if( S[Root2] < S[Root1] )//因为存的是负数,所以需要反着来 798 | S[Root1] = Root2;//集合1指向集合2 799 | else{ 800 | if( S[Root1] == S[Root2] ) Root1--;//存在新的结点里面,是负数负数负数,需要--而不是++ 801 | S[Root2] = Root1; 802 | } 803 | ``` 804 | 805 | #### 另一种做法:比规模 806 | 807 | 1. 把小树贴在大树上 808 | 809 | 2. S[Root] = -元素个数 810 | 811 | 3. 最后那个结果树的规模都会改变,变为两个树的规模之和 812 | 813 | 4. ```c 814 | void Union(SetType S,SetName Root1,SetName Root2) 815 | { 816 | if(S[Root2] < S[Root1] ){ 817 | S[Root2] += S[Root1]; 818 | S[Root1] = Root2;//把集合小的贴到大的上面去,在这里Root2更大,条件中只是因为是负数表示反过来了而已 819 | } 820 | else{ 821 | S[Root1] +=S[Root2]; 822 | S[Root2] = Root1; 823 | } 824 | } 825 | ``` 826 | 827 | **上述的两种方法都统称按秩归并** 828 | 829 | 按秩归并:最坏情况下树高 = O(logN) 830 | 831 | ### 小白-FT.4 路径压缩 832 | 833 | 按秩归并是对Union的一个改进 834 | 835 | 路径压缩是对Find这个函数的一个改进 836 | 837 | ```C 838 | SetName Find (SetType S,ElementType X) 839 | { 840 | if(S[X] < 0 )//找到集合的根,X如果小于0的话那就意味着X本身就已经是根了,直接返回就行了 841 | return X; 842 | else 843 | return S[X] = Find( S,S[X]);//先找到根,把根变为X的父结点,再返回根 844 | } 845 | ``` 846 | 847 | image-20220710103232692 848 | 849 | image-20220710103259091 850 | 851 | 路径压缩好处: 852 | 853 | 1. 第一次调用find会稍微复杂一点,但只要后续还需要调用就会非常合算 854 | 2. Find函数的递归调用是一个伪递归调用(不会像递归一样把堆栈压爆掉,因为是非常容易转换成循环的,编辑器可能直接执行优化过后的循环代码) 855 | 856 | image-20220710104353745 857 | 858 | 做不做路径压缩的本质区别: 859 | 860 | 1. 在查找次数M前面,到底是要乘一个常数还是要乘一个logN的问题。logN是N的一个递增函数,当N趋向无穷大的时候,logN也会趋向于无穷大。常数是不会变的 861 | 2. 当N充分大的时候 862 | 863 | -------------------------------------------------------------------------------- /数据结构第八周笔记——图(下)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第八周笔记——图(下)(慕课浙大版本--XiaoYu) 2 | 3 | ## 8.1最小 生成 树 问题(Minimum Spanning Tree) 4 | 5 | 1. 是一棵树 6 | 1. 无回路 7 | 2. 有|V|个顶点的话一定刚好有|V|-1条边(不多不少) 8 | 2. 是生成树 9 | 1. 包含了全部的顶点(图全部的顶点都一定要在这棵树里) 10 | 2. |V|-1条边都在原始的图里 11 | 12 | image-20220803215420096向这个生成树中任加一条边都一定构成回路 13 | 14 | 15 | 16 | 3. 边的权重和最小(指这个数里面这些边的权重和是最小的) 17 | 4. 最小生成树存在的话图一定是连通的,图连通的话那最小生成树也一定是存在的 18 | 19 | #### 贪心算法 20 | 21 | 1. 什么是"贪":解决问题是一步一步来解决的,每一步都要最好的(眼前最好的) 22 | 2. 什么是"好":权重最小的边 23 | 3. 需要约束: 24 | 1. 只能用图里面有的边(原图里面没有的是不能用的) 25 | 2. 只能正好用掉|V|-1条边 26 | 3. 不能有回路 27 | 28 | ### 8.1.1 Prim算法——让一棵小树长大 29 | 30 | 树的初始样子:image-20220803222042856 收到一半:image-20220803222226809 31 | 32 | 因为为了不直接形成回路,所以接下来不能走v2到v4的那条边,不然就直接结束了 33 | 34 | 最后生成的样子为:image-20220803222434567 35 | 36 | 上图的收集算法就是Prim算法,收集的过程有点像Dijkstra算法 37 | 38 | 回顾**Dijkstra算法** 39 | 40 | ```c 41 | void Dijkstra(Vertex s) 42 | { 43 | while(1){ 44 | V = 未收录顶点中dist最小者; 45 | if( 这样的V不存在 ) 46 | break; 47 | collected[V] = true; 48 | for( V 的每个邻接点 W ) 49 | if(collected[W] == false) 50 | if( dist[V]+E < dist[W] ){ 51 | dist[W] = dist[V] + E; 52 | path[W] = V; 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | **Prim算法** 59 | 60 | ```c 61 | void Prim() 62 | { 63 | MST = {s};//跟上方不同的是这里需要先生成一个最小生成树,开始的时候随便选一个根结点s收录进来。这个树怎么存?未必需要真的定义树节点,把树构建出来。可以parent[s] = -1,就是他的父节点的编号,跟并查集里面的概念差不多。这样当我们发现他的parent的 64 | while(1){ 65 | V = 未收录顶点中dist最小者;//dist在这个最小生成树的问题里被定义成一个顶点v到这棵生成树的最小距离(他跟这个生成树里面已经被收罗进去的顶点之间,所有的距离里面最小的那个,把它定义成是v到这棵树的距离,我们每一次是要从这个距离里面找一个最小的) 66 | //Prim算法中的dist[V]应该初始化为:E(s,V)或者正无穷 67 | //E(s,V):顶点到这个树的距离是这个边的权重 68 | //如果V跟s之间没有直接的边的话,那我们一定要把它定义成正无穷(跟Dijkstra类似) 69 | if( 这样的V不存在 )//v不存在就直接跳出来,这时候有两种情况:一种就是全部顶点收录了,如果是这种情况那程序顺利结束 70 | //第二种情况就是所有未收录的顶点的dist全部都是无穷大,意味着剩下那些顶点到这棵树之间都没有边,整个图是不连通的 71 | break; 72 | 将V收录进MST:把V收进最小生成树意味着这个顶点到这棵树的距离就变成0了(因为他已经是这棵树的一部分了) 73 | dist[V] = 0;//设成0就相当于已经收进来了 74 | for( V的每个邻接点 W ) 75 | if( W未被收录 )//dist不是0就意味着没被收录 76 | if(E(v,w) < dist[W] ){//v到w之间有一条直接的边,而这个边的距离是小于原始的dist 77 | dist[W] = E(v,w);//更新一下这个w的距离。v可能是边直接指向w的 78 | parent[W] = V; 79 | } 80 | } 81 | if( MST中收的顶点不到|V|个 ) 82 | Error("生成树不存在 or 图不连通") 83 | }//这一步取决于V= 未收录顶点中dist最小者的做法。粗暴的做法得出来的整个时间复杂度就是T = O(|V|²),稠密图这样处理好 84 | ``` 85 | 86 | 总结:Prim算法对稠密图效果好 87 | 88 | ### 8.1.2 Kruskal算法(将森林合并成树) 89 | 90 | 直接了当的贪心(直接找权重最小的边把它收进来) 91 | 92 | image-20220804170722722 93 | 94 | 什么叫做把森林合并成树呢?就是在初始的状况下认为每一个顶点都是一颗树,然后通过不断的把边收进来,就把两棵树合并成一棵树了,最后就是把所有的7个节点并成一棵树 95 | 96 | image-20220804170944493边为1收完收2,2收完看3和4和5,3跟部分4和5会形成回路所以跳过 97 | 98 | 到6的时候形成了最小生成树:image-20220804171148363 以上就是Kruskal算法的基本思想 99 | 100 | **Krustal算法的伪代码** 101 | 102 | ```c 103 | void Kruskal( Graph G ) 104 | { 105 | MST = { };//刚开始为空集 106 | while( MST 中不到|V|-1 条边 && E中还有边 ){ 107 | 从 E 中取一条权重最小的边E(v,w);//(v,w)是下标,上面用小括号括起来的v,w也都是。时间复杂度取决于这一步(是所有边都搜集一遍还是其他的方法,采用最小堆的方法是最好的)/*最小堆*/ 108 | 将 E(v,w)从 E 中删除;//E(v,w)是边集合 109 | if( E(v,w)不在 MST 中构成回路)//检查这条边加到最小生成树之后是否构成回路/*并查集*/ 110 | 将E(v,w)加入MST;//不构成的话边就被加进来了 111 | else 112 | 彻底无视 E(v,w); 113 | } 114 | //while判定的时候不到|v|-1条边就跳出来的情况是:还没收满,原图里面的边就都已经被删光了,没有边了 115 | if( MST 中不到|V|-1条边 ) 116 | Error("生成树不存在"); 117 | } 118 | 当图非常稀疏的时候,也就是E跟v差不多同一个数量级的时候 119 | T = O(|E|log|E|) 120 | 121 | 并查集:一开始认为每个顶点都是独立的一棵树(集合),当我们把一条边收进去的时候就意味着把两棵树并成一棵 122 | ``` 123 | 124 | #### 并查集 125 | 126 | ``` 127 | 信息补充 128 | 原理: 129 | 初始化 130 | 把每个点所在集合初始化为其自身。 131 | 通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为。 132 | 133 | 查找 134 | 查找元素所在的集合,即根节点。 135 | 136 | 合并 137 | 将两个元素所在的集合合并为一个集合。 138 | 通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。 139 | 140 | ``` 141 | 142 | 143 | 144 | ## 8.2 拓扑排序 145 | 146 | ### 8.2.1 拓扑排序 147 | 148 | 例:计算机专业课排课 149 | 150 | image-20220806011815543 151 | 152 | 把课程列表转换为图,其中顶点代表课程,则从V到W有一条边代表:**V是W的预修课程** 153 | 154 | 以下是专业课的依赖图(也叫做**AOV**,是Activity On Vertex的缩写) 155 | 156 | image-20220806015109058 157 | 158 | 以上抽象成一个**拓扑排序**的问题 159 | 160 | 1. 拓扑序:如果图中从V到W有一条有向路径,则V一定排在W之前。满足此条件的顶点序列称为一个拓扑序。(意思就是要学习W之前必须要学习V的话,那么在输出的时候V一定要在W之前被输出) 161 | 2. 获得一个拓扑序的过程就是拓扑排序 162 | 3. AOV如果有合理的拓扑序,则必定是有向无环图(Directed Acyclic Grap,简称DAG) 163 | 164 | 165 | 166 | 如图:image-20220806015719757什么是合理的拓扑序?有环意味着这个活动他自己是他自己的前驱节点(也就是他在他开始之前就必须要已经结束了),而V必须在V开始之前结束是不合理的。所以这个是**不行的**,得不到合理的拓扑序 167 | 168 | 经过排版得出:image-20220806020150508 规律是:每次我们要输出哪个顶点?输出没有前驱顶点的那个顶点 169 | 170 | 怎么只是没有前驱顶点?对于顶点来说,我们有两个度可以记得,一个是**入度**一个是**出度** 171 | 172 | 没有前驱顶点的那些课程的特点:他们的入度都是0,没有任何一条边指向他。 173 | 174 | 拓扑序步骤: 175 | 176 | 1. 在输出的时候我们要选择入度为0的那个顶点 177 | 2. 在输出的同时我们要把这个顶点从原始的图里面彻底抹掉,当我们把原来的图抹光的时候,一个正常的拓扑序就产生了 178 | 179 | #### 拓扑排序的算法 180 | 181 | ```c 182 | void TopSort() 183 | { 184 | for( cnt = 0; cnt < |V|; cnt++ ){ 185 | //在TopSort函数中,如果外循环还没结束,就已经找不到“未输出的入度为0的顶点”,则说明:图中必定存在回路 186 | V = 未输出的入度为0的顶点;//程序复杂度的复杂程度主要取决于这一步。 187 | //方法1:简单粗暴的把所有点都扫描一遍然后去找那个没有输出过的且入度又为0的顶点,复杂度就为O(|V|)。 188 | //整体复杂度:T = O(|V|²) 这是不聪明的做法 189 | //方法2(聪明的算法):随时将入度变为0的顶点放到一个容器(可以理解为任何东西,数组、链表、堆栈、队列之类里都可以,总之放到一个特殊的地方)里面。这样就不用重新去扫描所有的顶点集合,直接从容器里面取一个出来就行了,这样复杂度就直接变成常数级别的了 190 | if( 这样的V不存在 ){ 191 | Error("图中有回路"); 192 | break; 193 | } 194 | 输出V,或者记录V的输出序号; 195 | for( V 的每个邻接点 W ) 196 | Indegree[W]--;//意味着把V到W的这条边给减掉了,W少了一条进来的边,所以入度就会减少1。就是把V抹掉的意思(抹掉不是删掉) 197 | } 198 | } 199 | ``` 200 | 201 | #### 聪明的算法 202 | 203 | 方法2(聪明的算法):随时将入度变为0的顶点放到一个容器(以下是用队列做排序) 204 | 205 | ```c 206 | void TopSort() 207 | { 208 | for ( 图中每个顶点 V ) 209 | if( Indegree[V]==0)//检查有没有入度为0的 210 | Enqueue( V,Q );//有的都放到容器里面 211 | while(!IsEmpty(Q)){ 212 | V = Dequeue( Q ); 213 | 输出V,或者记录V的输出序号;cnt++//输出后就需要抹掉,就通过下面那个for循环解决,入度减一 214 | for( V 的每个邻接点 W) 215 | if( --Indegree[W]==0)//剪完入度为0就放入下面那个容器里面,下次可以取出来用,否则就说明都不做,继续去容器里取下一个顶点 216 | Enqueue( W,Q ); 217 | } 218 | if( cnt !=|V|)//检查顶点有没有输出完,没有输出完就意味着遇到回路了 219 | Error("图中有回路"); 220 | } 221 | 222 | 此时时间复杂度为:T = Q(|V|+|E|) 223 | 如果是稀疏图的话是QV的复杂度,稠密图的话|V|²数量级的 224 | ``` 225 | 226 | ### 8.2.2 关键路径 227 | 228 | **AOE**(Activity On Edge)网络 229 | 230 | 1. 一般用于安排项目的工序 231 | 232 | ``` 233 | Earliest:所有的任务最早的完成时间 234 | Latest:所有任务最晚完成时间 235 | ``` 236 | 237 | image-20220806025653865 238 | 239 | ------------------ 240 | 241 | ``` 242 | 0顶点表示开始,8顶点表示结束 243 | 每一条边代表一件事情 244 | 每件事情按照相互依赖的顺序完成了之后到达顶点8为结束 245 | 边上的权重代表持续时间 246 | 边之间的关系:两个小组之间开工就必须两个小组都完工才能往下走 247 | 248 | 假设不仅得1、2组结束,同时还需要等3一起全部完工才能往下走 249 | ``` 250 | 251 | image-20220806030102372 252 | 253 | image-20220806030345634 254 | 255 | 问题1:整个工期有多长 256 | 257 | 问题2:哪几个组有机动时间?(就是可以随时拉出去干活的,不需要赶工的) 258 | 259 | image-20220806032153371 260 | 261 | 绿色部分为机动时间: 262 | 263 | image-20220806032329936 264 | 265 | 什么是关键路径:整个manager最需要关注的那些组,哪些组是一天都不能耽误的(耽误一条整个工期都得往后耽误)。所以关键路径就是指**绝对不允许延误**的活动组成的路径 266 | 267 | ## 图之习题选讲-旅游规划 268 | 269 | ### 图习题.1 核心算法 270 | 271 | #### 题意理解 272 | 273 | 1. 城市为结点 274 | 2. 公路为边 275 | 1. 权重1:距离 276 | 2. 权重2:收费 277 | 278 | image-20220806032808917红色是起点,蓝色是终点。;绿色字体是距离,紫色字体是收费 279 | 280 | 3. 单源最短路 281 | 1. Dijkstra算法 ——距离 (一个结点一个结点往那个集合里面去收集,每收进来一个就要检查以下其他结点距离有没有被新进来的结点影响,得到更短的距离就刷新掉,得不到就保持原样) 282 | 2. 等距离时按收费更新 283 | 284 | **核心算法** 285 | 286 | ```c 287 | 最基础版本的Dijkstra算法 288 | void Dijkstra( Vertex s ) 289 | { 290 | while(1){ 291 | V = 未收录顶点中dist最小者; 292 | if( 这样的V不存在 ) 293 | break; 294 | collected[V] = true; 295 | for( V 的每个邻接点 W ) 296 | if( dist[V] + E < dist[W] ){//Vd点的加入使我们得到一个更短的w的距离 297 | dist[W] = dist[V] + E;//更新距离 298 | path[W] = V;//更新最短路径 299 | } 300 | } 301 | } 302 | ``` 303 | 304 | ```c 305 | 根据基础进行改良过后的 306 | 最基础版本的Dijkstra算法 307 | void Dijkstra( Vertex s ) 308 | { 309 | while(1){ 310 | V = 未收录顶点中dist最小者; 311 | if( 这样的V不存在 ) 312 | break; 313 | collected[V] = true; 314 | for( V 的每个邻接点 W ) 315 | if( dist[V] + E < dist[W] ){//Vd点的加入使我们得到一个更短的w的距离 316 | dist[W] = dist[V] + E;//更新距离 317 | path[W] = V;//更新最短路径,s走到V的所有费用加上V走到W这条边的费用 318 | } 319 | else if((dist[V]+E == dist[W]) && (cost[V] + C < cost[W])){//等长最短路径并且这条路径上面的新费用比原来的费用小的话那也要更新路径 320 | cost[W] = cost[V] + C;//更新费用 321 | path[w] = V;//更新路径 322 | } 323 | } 324 | } 325 | ``` 326 | 327 | 328 | 329 | ### 图习题.2 其他推广 330 | 331 | #### 其他类似问题 332 | 333 | 1. 要求最短路径有多少条? 334 | 1. count[s] = 1;//计数器的作用 335 | 2. 在要求数最短路径有多少条的问题中,如果找到更短路,则count[W]应该更新为要求边数最少的最短路:count[W]=count[V] 336 | 3. 在要求数最短路径有多少条的问题中,如果找到等长路,则count[W]应该更新为:count[W]+=count[V] 337 | 2. 要求边数最少的最短路 338 | 1. 费用初始化怎么做?初始化为0 339 | 2. count[s] = 0; 340 | 3. 如果找到更短路:count[W] = count[V] + 1; 341 | 4. 如果找到等长路:count[W] = count[V] + 1;//count[V]加上新的权重 -------------------------------------------------------------------------------- /数据结构第六周笔记——图(上)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第六周笔记——图(上)(慕课浙大版本--XiaoYu) 2 | 3 | ## 6.1 什么是图 4 | 5 | ### 6.1.1 什么是图——定义 6 | 7 | **表示多对多的关系** 8 | 9 | 包含 10 | 11 | 1. 一组顶点:通常用V(Vertex)表示顶点集合 12 | 2. 一组边:通常用E(Edge)表示边的集合 13 | 1. 边是顶点对:(v,w)属于E,其中v,w属于V 14 | 2. 有向边表示从v指向w的边(单行线) 15 | 3. 不考虑重边和自回路 16 | 17 | #### 抽象数据类型定义 18 | 19 | 1. 类型名称:图(Graph) 20 | 21 | 2. 数据对象集:G(V,E)由一个非空的有限顶点集合V和一个有限边集合E组成(可以一条边都没有,但不能一个顶点都没有) 22 | 23 | 3. 操作集:对于任意图G属于Graph,以及v属于V,e属于E 24 | 25 | 1. ```c 26 | 1. Graph Create():建立并返回空图 27 | 2. Graph InsertVertex(Graph G, Vertex v):将v插入G 28 | 3. Graph InsertEdge(Graph G, Edge e):将e插入G; 29 | 4. void DFS(Graph G,Vertex v):从顶点v出发深度优先遍历图G; 30 | 5. void BFS(Graph G,Vertex v):从顶点v出发宽度优先遍历图G; 31 | 6. void ShortestPath(Graph G,Vertex v,int Dist[]):计算图G中顶点v到任意其他顶点的最短距离 32 | 7. void MST(Graph G):计算图G的最小生成树 33 | ``` 34 | 35 | #### 常见术语 36 | 37 | 1. 无向图:无所谓方向的 38 | 2. 有向图:在图中,若用箭头标明了边是有方向性(单向或者双向)的,则称这样的图为有向图,否则称为无向图。 39 | 3. 权重:边上显示的数字,可以有各种各样的现实意义 40 | 4. 网络:有带权重的图 41 | 42 | ### 6.1.2 什么是图——邻接矩阵表示法 43 | 44 | #### 怎么在程序中表示一个图 45 | 46 | 1. image-20220715110405629 47 | 48 | 2. image-20220715110835844 49 | 50 | #### 邻接矩阵——有什么好处? 51 | 52 | 1. 直观、简单、好理解 53 | 2. 方便检查任意一对顶点间是否存在边 54 | 3. 方便找任一顶点的所有"邻接点"(有边直接相连的顶点) 55 | 4. 方便计算任一顶点的"度"(从该点出发的边数为"出度",指向该点的边数为"入度")有向图的概念 56 | 57 | #### 邻接矩阵——有什么不好? 58 | 59 | 1. 浪费空间——存稀疏图(点很多而边很少)有大量无效元素 60 | 61 | 但对稠密图(特别是完全图)还是很合算的 62 | 63 | 2. 浪费时间——统计稀疏图中一共有多少条边 64 | 65 | #### 无向图 66 | 67 | 对应行(或列)非0元素的个数 68 | 69 | #### 有向图 70 | 71 | 对应行非0元素的个数是"出度";对应列非0元素的个数是"入度" 72 | 73 | ### 6.1.3 什么是图——邻接表表示法 74 | 75 | 邻接表:G[N]为指针数组,对应矩阵每行一个的链表,只存非0元素 76 | 77 | image-20220715120351402 78 | 79 | 上图的顺序是无所谓的,可以随意排列。使用这个表需要足够稀疏才合算 80 | 81 | 优点: 82 | 83 | 1. 方便找任一顶点的所有"邻接点" 84 | 85 | 2. 节约稀疏图的空间 86 | 87 | 需要N个头指针+2E个结点(每个结点至少2个域) 88 | 89 | 3. 方便计算任一顶点的"度" 90 | 91 | 对无向图:是的 92 | 93 | 对有向边:只能计算"出度";需要构造"逆邻接表"(存指向自己的边)来方便计算"入度" 94 | 95 | 4. 方便检查任意一对顶点间是否存在边?NO 96 | 97 | 对于网络,结构中要增加权重的域 98 | 99 | ## 6.2 图的遍历 100 | 101 | ### 6.2.1 图的遍历——DFS 102 | 103 | 遍历:把图里面每个顶点都访问一遍而且不能有重复的访问 104 | 105 | #### 深度优先搜索(DFS) 106 | 107 | 当访问完了一个节点所有的灯后,一定原路返回对应着堆栈的出栈入栈的一个行为 108 | 109 | 深度优先搜索的算法描述 110 | 111 | ```c 112 | void DFS(Vertex V)//从迷宫的节点出来 113 | { 114 | visited[V] = true;//给每个节点一个变量,true相当于灯亮了,false则是熄灭状态 115 | for(V的每个邻接点W)//视野看得到的灯 116 | if(!visited[W])//检测是否还有没点亮的 117 | DFS(W);//递归调用 118 | } 119 | 120 | //类似树的先序遍历 121 | 若有N个顶点、E条边,时间复杂度是 122 | 用邻接表存储图,有O(N+E)//对每个点访问了一次,每条边也访问了一次 123 | 用邻接矩阵存储图,有O(N²)//V对应的每个邻接点W都要访问一遍 124 | ``` 125 | 126 | image-20220715122831029 127 | 128 | ### 6.2.2 图的遍历——BFS 129 | 130 | #### 广度优先搜索(Breadth First Search,BFS) 131 | 132 | image-20220715122953905 133 | 134 | ```c 135 | void BFS(Vertex V) 136 | { 137 | visited[V] = true; 138 | Enqueue(V,Q);//压到队列里 139 | while(!IsEmpty(Q)){ 140 | V = Dequeue(Q);//每次循环弹出一个节点 141 | for(V的每个邻接点W) 142 | if(!visited[W]){//没有访问过的去访问将其压入队列中 143 | visited[W] = true; 144 | Enqueue(W,Q); 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | 若有N个顶点,E条边,时间复杂度是 151 | 152 | ​ 用邻接表存储图,有O(N+E) 153 | 154 | ​ 用邻接矩阵存储图,有O(N²) 155 | 156 | 157 | 158 | 下面进行说明: 159 | 160 | image-20220715124837990 161 | 162 | 163 | 第1步:访问A。 164 | 165 | 第2步:访问(A的邻接点)C。在第1步访问A之后,接下来应该访问的是A的邻接点,即"C,D,F"中的一个。但在本文的实现中,顶点ABCDEFG是按照顺序存储,C在"D和F"的前面,因此,先访问C。 166 | 167 | 第3步:访问(C的邻接点)B。在第2步访问C之后,接下来应该访问C的邻接点,即"B和D"中一个(A已经被访问过,就不算在内)。而由于B在D之前,先访问B。 168 | 169 | 第4步:访问(C的邻接点)D。在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到访问C的另一个邻接点D。 170 | 171 | 第5步:访问(A的邻接点)F。 前面已经访问了A,并且访问完了"A的邻接点B的所有邻接点(包括递归的邻接点在内)";因此,此时返回到访问A的另一个邻接点F。 172 | 173 | 第6步:访问(F的邻接点)G。 174 | 175 | 第7步:访问(G的邻接点)E。 176 | 177 | 因此访问顺序是:A -> C -> B -> D -> F -> G -> E。 178 | 179 | 当然,上图是基于无向图,具体的代码在文章后面实现。 180 | 181 | 广度优先搜索 182 | 183 | 广度优先搜索算法(Breadth First Search),又称为"宽度优先搜索"或"横向优先搜索",简称BFS。 184 | 185 | 它的思想是:从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。 186 | 187 | 换句话说,广度优先搜索遍历图的过程是以v为起点,由近至远,依次访问和v有路径相通且路径长度为1,2…的顶点。 188 | 189 | image-20220715124941418 190 | 191 | 第1步:访问A。 192 | 193 | 第2步:依次访问C,D,F。在访问了A之后,接下来访问A的邻接点。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,C在"D和F"的前面,因此,先访问C。再访问完C之后,再依次访问D,F。 194 | 195 | 第3步:依次访问B,G。在第2步访问完C,D,F之后,再依次访问它们的邻接点。首先访问C的邻接点B,再访问F的邻接点G。 196 | 197 | 第4步:访问E。在第3步访问完B,G之后,再依次访问它们的邻接点。只有G有邻接点E,因此访问G的邻接点E。 198 | 199 | 因此访问顺序是:A -> C -> D -> F -> B -> G -> E。 200 | 201 | ### 6.2.3 图的遍历——为什么需要两种遍历 202 | 203 | 在不同的情况下效率不同 204 | 205 | 广度跟深度的区别 206 | 207 | 1. 深度是直接一条路走到黑,碰壁没路走了在返回 208 | 2. 广度是一圈一圈的扫描过去,虽然前面还有路也不会强行深入 209 | 210 | ### 6.2.4 图的遍历——图不连通怎么办 211 | 212 | **连通**:如果从V到W存在一条(无向)路径,则称V与W是连通的 213 | 214 | **路径**:V到W路径是一系列顶点{V,v1,v2,....,vn,W}的集合,其中任一对相邻的顶点间都有图中的边。**路径的长度**是路径中的边数(如果带权(带权图),则是所有边的权重和)。如果V到W之间的所有顶点都不同,则称为**简单路径**(有回路就不是简单路径) 215 | 216 | **回路**:起点等于终点的路径 217 | 218 | **连通图**:图中任意两顶点均连通 219 | 220 | **连通分量**:无向图的极大连通子图 221 | 222 | 1. 极大顶点数:再加1个顶点就不连通了 223 | 2. 极大边数:包含子图中所有顶点相连的所有边 224 | 225 | **对有向图**: 226 | 227 | ```c 228 | //强连通:有向图中顶点V和W之间存在双向路径,则称V和W是强连通的(路径可以不同同一条,但是一定是连通的) 229 | //强连通图:有向图中任意两顶点均强连通 230 | //强连通分量:有向图的极大强连通子图 231 | //弱连通图:将强连通图的所有边的方向抹掉变成无向图就是连通的了 232 | 233 | ``` 234 | 235 | 每调用一次DFS(V),就把V所在的连通分量遍历了一遍,BFS也一样 236 | 237 | ```c 238 | void DFS(Vertex V) 239 | { 240 | visited[V] = true; 241 | for(V的每个邻接点W) 242 | if(!visited[W]) 243 | DFS(W); 244 | } 245 | ``` 246 | 247 | 遍历分量 248 | 249 | ```c 250 | void ListComponents(Graph G) 251 | { 252 | for(each V in G) 253 | if(!visited[V]){ 254 | DFS(V);//or BFS(V) 255 | } 256 | } 257 | ``` 258 | 259 | ## 6.3 应用实例:拯救007 260 | 261 | ```c 262 | void Save007(Graph G) 263 | { 264 | for(each V in G){ 265 | if(!visited[V] && FirstJump(V)){//这个FirstJump(V)是007第一跳有没有可能从孤岛跳到V上有没有可能,有且没踩过就跳上去 266 | answer = DFS(V);//or BFS(V) 267 | if(answer == YES) break;0 268 | } 269 | } 270 | if(answer == YES) output("Yes"); 271 | else output("No"); 272 | } 273 | ``` 274 | 275 | #### DFS算法 276 | 277 | ```c 278 | void DFS(Vertex V) 279 | { 280 | visited[V] = true;//表示鳄鱼头踩过了 281 | for(V的每个邻接点W) 282 | if(!visited[W]) 283 | DFS(W);//递归 284 | } 285 | ``` 286 | 287 | 改良版本 288 | 289 | ```c 290 | void DFS(Vertex V) 291 | { 292 | visited[V] = true;//表示鳄鱼头踩过了 293 | if(IOsSafe(V)) answer = YES; 294 | else{ 295 | for(each W in G ) 296 | if(!visited[W] && Jump(V,W)){//可以从V jump跳到这个w上面,作用是算V到W之间的距离是不是小于007可以跳跃最大距离 297 | answer = DFS(W);//递归 298 | if(answer == YES) break; 299 | } 300 | } 301 | return answer; 302 | } 303 | ``` 304 | 305 | 306 | 307 | ## 6.4 应用实例:六度空间(Six Degrees of Separation) 308 | 309 | 理论: 310 | 311 | 1. 你和任何一个陌生人之间所间隔的人不会超过6个 312 | 2. 给定社交网络图,请对每个节点计算符合"六度空间"理论的结点占结点总数的百分比 313 | 314 | ```c 315 | 算法思路 316 | 1.对每个节点进行广度优先搜索 317 | 2.搜索过程中累计访问的节点数 318 | 3.需要记录"层"数,仅计算6层以内的节点数 319 | 320 | void SDS() 321 | { 322 | for(each V in G){ 323 | count += BFS(V); 324 | Output = (count/N); 325 | } 326 | } 327 | 328 | //结合最初的BFS 329 | void BFS(Vertex V) 330 | { 331 | visited[V] = true;count = 1; 332 | Enqueue(V,Q);//压到队列里 333 | while(!IsEmpty(Q)){ 334 | V = Dequeue(Q);//每次循环弹出一个节点 335 | for(V的每个邻接点W) 336 | if(!visited[W]){//没有访问过的去访问将其压入队列中 337 | visited[W] = true; 338 | Enqueue(W,Q);count++; 339 | } 340 | }return count; 341 | } 342 | ``` 343 | 344 | 另外的解决方案 345 | 346 | ```c 347 | int BFS(Vertex V) 348 | { 349 | vistex[V] = true;count = 1; 350 | level = 0;last = V; 351 | Enqueue(V,Q); 352 | while(!IsEmpty(Q)){ 353 | V = Dequeue(Q); 354 | for( V的每个邻接点W) 355 | if(!visited[W]){ 356 | visited[W] = true; 357 | Enqueue(W,Q);count++; 358 | tail = W; 359 | } 360 | if(V == last ){ 361 | level++;last = tail; 362 | } 363 | } 364 | return count++; 365 | } 366 | ``` 367 | 368 | ​ image-20220716095328150 369 | 370 | ## 小白专场:如何建立图:C语言实现 371 | 372 | ### 小白BG.1 邻接矩阵表示的图结点的结构 373 | 374 | ```c 375 | typedef struct GNode *PtrToGNode;//PtrToGNode是指向GNode的一个指针 376 | struct GNode{ 377 | int Nv;//顶点数 378 | int Ne;//边数 379 | WeightType G[MaxVertexNum][MaxVertexNum]; 380 | DataType Data[MaxVertexNum];//存顶点的数据 381 | }; 382 | typedef PtrToGNode MGraph;//以邻接矩阵存储的图类型。定义为指向节点的指针。因为要用到的时候一个指针远远比一整个图来的快捷 383 | ``` 384 | 385 | 386 | 387 | ### 小白BG.2 邻接矩阵表示的图——初始化 388 | 389 | 初始化一个有VertexNum个顶点但没有边的图 390 | 391 | ```c 392 | typedef int Vertex;//用顶点下标表示顶点,为整型 393 | MGraph CreateGraph(int VertexNum)//VertexNum这个顶点数真的是整数, 394 | { 395 | Vertex V , W;//我们在说V跟W的时候不是在说整数,而是顶点 396 | MGraph Graph; 397 | 398 | Graph = (MGraph)malloc(sizeof(struct GNode)); 399 | Graph->Nv = VertexNum; 400 | Graph->Ne = 0; 401 | 402 | //注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) 403 | for(V=0;VNv;V++) 404 | for((W=0;WNv;W++)) 405 | Graph->G[V][M] = 0;//或者INFINITY,表示这两个顶点之间是没有边的 406 | 407 | return Graph 408 | } 409 | ``` 410 | 411 | ### 小白BG.3 邻接矩阵表示的图——插入边 412 | 413 | ```c 414 | typedef struct ENode *PtrToENode; 415 | struct ENode{ 416 | Vertex V1,V2;//有向边,V1V2两个顶点一个出发点一个终点 417 | WeightType Weight;//权重,有权图才需要。权重的类型是WeightType 418 | }; 419 | typedef PtrToENode Edge; 420 | 421 | void InsertEdge(MGraph Graph,Edge E) 422 | { 423 | //插入边,这是一条边 424 | Graph->G[E->V1][E->V2] = E->Weight; 425 | 426 | //无向图的话还需要一条边(一共两条), 427 | Graph->G[E->V2][E->V1] = E->Weight; 428 | } 429 | ``` 430 | 431 | ### 小白BG.4 邻接矩阵表示的图——建立图 432 | 433 | **完整的建立一个MGraph** 434 | 435 | 输入格式 436 | 437 | 1. Nv Ne 438 | 2. V1 V2 Weight 439 | 3. ...... 440 | 441 | ```c 442 | MGraph BuildGraph() 443 | { 444 | MGraph Graph; 445 | 446 | 447 | 448 | 449 | 450 | scanf("%d",&Nv); 451 | Graph = CreateGraph(Nv); 452 | //读入边数 453 | scanf("%d",&(Graph->Ne)); 454 | if(Graph -> Ne = 0){//有边就还需要经过这里,没有边直接结束 455 | E = (Edge)malloc(sizeof(struct ENode));//临时存一下边 456 | for(i = 0; i < Graph->Ne; i++){ 457 | scanf("%d %d %d",&E->V1,&E->V2,&E->Weight); 458 | InsertEdge(Graph,E); 459 | } 460 | } 461 | //如果顶点有数据的话,读入数据 462 | for(V=0;VNv;V++) 463 | scanf("%c",&(Graph->Data[V])); 464 | return Graph; 465 | } 466 | ``` 467 | 468 | 简易建法 469 | 470 | ```c 471 | int G[MAXN][MAXN],Nv,Ne;//声明为全局变量 472 | void BuildGraph() 473 | { 474 | int i,j,v1,v2,w; 475 | 476 | scanf("%d",&Nv); 477 | //CreateGraph 478 | for(i=0;iNv = VertexNum; 536 | Graph->Ne = 0; 537 | 538 | 539 | //没有边的意思是每个顶点跟着的那个链表都是空的 540 | //注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) 541 | for(V=0;VNv;V++) 542 | Graph->G[V].FirstEdge = NULL; 543 | 544 | return Graph; 545 | } 546 | ``` 547 | 548 | 向LGraph中插入边 549 | 550 | ```c 551 | void InsertEdge(LGraph Graph,Edge E) 552 | { 553 | PtrToAdjVNode NewNode; 554 | //-------------------插入边------------------------------ 555 | //为V2建立新的邻接点 556 | NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjNode)); 557 | NewNode->AdjV = E->V2; 558 | NewNode->Weight = E->Weight 559 | //将V2插入到V1的表头 560 | NewNode->Next = Graph->G[E->V1].FirstEdge; 561 | Graph->G[E->V1].FirstEdge = NewNode; 562 | 563 | //-------------------若是无向图,还需插入边------------------ 564 | //为V1建立新的邻接点 565 | NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjNode)); 566 | NewNode->AdjV = E->V1; 567 | NewNode->Weight = E->Weight 568 | //将V2插入到V1的表头 569 | NewNode->Next = Graph->G[E->V2].FirstEdge; 570 | Graph->G[E->V2].FirstEdge = NewNode; 571 | } 572 | ``` 573 | 574 | ![image-20220716114326873](https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/925/image-20220716114326873.png) 575 | 576 | ![image-20220716114644431](https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/925/image-20220716114644431.png) 577 | 578 | **完整建立一个LGraph** 579 | 580 | ```c 581 | LGraph BuildGraph() 582 | { 583 | LGraph Graph; 584 | ............... 585 | } 586 | ``` 587 | 588 | -------------------------------------------------------------------------------- /数据结构第十一周笔记—— 散列查找 (慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第十一周笔记—— 散列查找 (慕课浙大版本--XiaoYu) 2 | 3 | ## 11.1 散列表 4 | 5 | ### 11.1.1 引子:散列的基本思路 6 | 7 | C语言变量名必须: 8 | 9 | 1. 先定义(或者声明)后使用 10 | 11 | 编译处理时,涉及变量及属性(如:变量类型)的管理: 12 | 13 | 1. 插入:新变量定义(将变量名及其定义插到我们要管理的这个集合里面去) 14 | 2. 查找:变量的引用(在编译的时候,会查找你使用的这个变量是否定义,在根据变量的类型去进行判别看能不能这么使用) 15 | 16 | 编译处理中对变量管理: 17 | 18 | 1. 动态查找问题 19 | 20 | 利用查找树(搜索树)进行变量管理? 21 | 22 | 1. 两个变量名(字符串)比较效率不高,因为变量名比较比整数的比较要复杂。 23 | 2. 整数比较一次比较大小相等就行了,而变量是字符串,需要一个字符一个字符的比过去 24 | 3. 是否可以先把字符串转换为数字再处理?这样就是进行比较两个数字的方式了,这就是散列查找 25 | 26 | 已知的几种查找方法: 27 | 28 | 1. 顺序查找:要查找的放数组列表,从头到尾慢慢找,效率差 29 | 2. 二分查找(静态查找):从小到大排好然后用二分查找这样,查找效率高 30 | 3. 二叉搜索树,平衡二叉树 31 | 32 | image-20220824223055677 33 | 34 | 问题:如何快速搜索到需要的关键词?如果关键词不方便比较怎么办? 35 | 36 | 1. 查找的本质:已知对象找位置 37 | 1. 有序安排对象:全序(典型的二分查找,从小到大排好),半序(某些关键词之间存在一个秩序,例如查找树,任何一个结点都比左边左子树的所有结点要来得大,比右边右子树的所有结点要来得小,但并不像二分查找一样完全有序) 38 | 2. 直接"算出"对象位置:散列 39 | 2. 散列查找法的两项基本工作: 40 | 1. 计算位置:构造散列函数确定关键词存储位置 41 | 2. 解决冲突:应用某种策略解决多个关键词位置相同的问题 42 | 3. 时间复杂度几乎是常量:O(1),即查找时间与问题规模无关! 43 | 1. 散列如果在函数的计算方面还有冲突策略提得好,效率就会非常高 44 | 45 | ### 11.1.2 什么是散列表 46 | 47 | ```C 48 | 类型名称:符号表(SymbolTable) 49 | 数据对象集:符号表是“名字(Name)-属性(Attribute)”对的集合。 50 | 操作集:Table∈SymbolTable,Name∈NameType,Attr∈AttributeType 51 | 1.SymbolTable InitializeTable(int TableSize )://表的初始化 52 | 创建一个长度为TableSize的符号表; 53 | 2.Boolean IsIn(SymbolTable Table,NameType Name)://判别一个对象是不是在这个表里 54 | 查找特定的名字Name是否在符号表Table中; 55 | 3.AttributeType Find(SymbolTable Table,NameType Name)://在表里查找属性 56 | 获取Table中指定名字Name对应的属性; 57 | 4.SymbolTable Modefy(SymbolTable Table,NameType Name,AttributeType Attr)://把表中属性改掉 58 | 将Table中指定名字Name的属性修改为Attr; 59 | 5.SymbolTable Insert(SymbolTable Table,NameType Name,AttributeType Attr)://在表里插入一个新的对象 60 | 向Table中插入一个新名字Name及其属性Attr; 61 | 6、SymbolTable Delete(SymbolTable Table,NameType Name)://从表里删除 62 | 从Table中删除一个名字Name及其属性。 63 | 64 | 主要操作:上面的3、5、6 65 | ``` 66 | 67 | image-20220824234520780 68 | 69 | 装填因子:散列表在外面这里是个数组,这个数组被充满的程度 70 | 71 | 72 | 73 | 冲突的设计:采用二维数组,同一个地址的就放在同一行,有冲突的在所在那行后一列继续放,如图所示: 74 | 75 | image-20220824235827460 76 | 77 | 哈希函数设计: 78 | 79 | ``` 80 | 哈希函数指将哈希表中元素的关键键值映射为元素存储位置的函数。 81 | 一般的线性表,树中,记录在结构中的相对位置是随机的,即和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较。这一类查找方法建立在“比较“的基础上,查找的效率依赖于查找过程中所进行的比较次数。 理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立一个确定的对应关系,使每个关键字和结构中一个唯一的存储位置相对应。 82 | ``` 83 | 84 | 只要不冲突的话效率还是很高的 85 | 86 | image-20220825000238029 87 | 88 | ``` 89 | ▣“散列(Hashing)”的基本思想是: 90 | (1)以关键字key为自变量,通过一个确定的函数h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址。 91 | (2)可能不同的关键字会映射到同一个散列地址上,即h(keyi)=h(keyj)(当keyi不等于keyj),称为“冲突(Collision)”。---需要某种冲突解决策略 92 | ``` 93 | 94 | ## 11.2 散列函数的构造方法 95 | 96 | ### 11.2.1 数字关键词的散列函数构造 97 | 98 | 一个"好"的散列函数一般应考虑下列两个因素: 99 | 100 | 1. 计算简单,以便提高转换速度 101 | 2. 关键词对应的地址空间分布均匀,以尽量减少冲突 102 | 103 | 数字关键词的散列函数构造 104 | 105 | 1. 直接定址法:取关键词的某个线性函数值为散列地址,即h(key) = a×key+b (a、b为常数)image-20220825132959714 106 | 2. 除留余数法(常用)://其实就是把一个大的整数把它转换成一个小的整数,这个小的整数就相当于散列表里面的地址 107 | 1. 散列函数为:h(key) = key mod pimage-20220825133843470 108 | 3. 数字分析法:分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址 109 | 1. 比如:取11位收集号码key的后4位作为地址:散列函数为:h(key)=atoi(key+7) (char*key)image-20220825135138135 110 | 2. int atoi(char*s):将类似"5678"的字符串转换为整数5678 111 | 3. 如果关键词key是18位的身份证号码:image-20220825135313221 112 | 4. image-20220825135441040 113 | 4. 折叠法:把关键词分割成位数相同的几个部分,然后叠加image-20220825140542094 114 | 5. 平方取中法:image-20220825140630018 115 | 116 | 如果仅改变56793542的最后一位,观察散列值会有什么变化(原散列值为641)。 117 | 118 | 请计算一下,按照平方取中法,key为56793543的散列值是多少=652 119 | 120 | ### 11.2.2 字符串关键词的散列函数构造 121 | 122 | **字符关键词的散列函数构造** 123 | 124 | 1. 一个简单的散列函数——ASCII码加和法(ASCII码相加在求个余数) 125 | 1. image-20220826005023076 126 | 2. 假如每个字符的变化范围在0-127,我们把它∑起来,∑后的值在0-1270之间,而变量名实际上的这种变化是非常多的,如果以很窄的计算结果来对付一个很广的这个变量范围就会使得这样的一个哈希函数很容易产生聚集,所以这不是一个很好的办法 127 | 2. 简单的改进——前3个字符移位法 128 | 1. h(key)=(key[0] x 27² + key[1] x 27 + key[2])mod TableSize 129 | 2. 这种方法仍然会产生冲突:string、street、strong、structure等等(前三位是一样的);空间浪费:3000/26³≈30 130 | 3. 空间浪费原因:字符一共有26种变化,所以三个字符的变化一共就26的三次方。而前三位一般出现的情况是3000种,而我们实际上考虑到26³去了,通过上面可以知道浪费的空间足足有30倍 131 | 3. 好的散列函数——移位法 132 | 1. image-20220826101017287 133 | 2. 将数值巨大化,采用32进制相乘image-20220826101121601 134 | 3. 优化方法:(a*32+b) =>((a*32+b)*32+c)=>(((a*32+b)*32+c)*32+d) 135 | 136 | ``` 137 | 如果直接计算'a'*32^4+'b'*32^3+'c'*32^2+'d'*32+'e'所需要的乘法总次数是4+3+2+1=10次。 138 | 139 | 采用 ((('a'*32+'b')*32+'c')*32+'d')*32+'e'的计算方法,乘法总次数是多少? 140 | 141 | (顺便思考一下两者时间效率的差别)4 142 | ``` 143 | 144 | ```c 145 | Index Hash(const char*Key,int TableSize) 146 | { 147 | unsigned int h = 0;//散列函数值,初始化为0 148 | while(*Key != ‘\0’)//位移映射,这里就是值不为空的意思 149 | h = ( h << 5 ) + *Key++;//h << 5是左移五位的意思 150 | return h % TableSize;//最后求余得到地址 151 | } 152 | ``` 153 | 154 | ## 11.3 冲突处理方法 155 | 156 | ### 11.3.1 开放定址法 157 | 158 | **处理冲突的方法** 159 | 160 | > 常用冲突的思路: 161 | > 162 | > 1. 换个位置:开放地址法 163 | > 2. 同一位置的冲突对象组织在一起:链地址法 164 | 165 | 开放地址法(Open Addressing) 166 | 167 | 1. 一旦产生了冲突(该地址已有其他元素),就按某种规则去寻找另一空地址 168 | 2. image-20220826103852201 169 | 3. di决定了不同的解决冲突方案:线性探测、平方探测、双散列(三种典型的开放地址法) 170 | 1. 线性探测:di = i(di的i是下标),第一次探测i就是1,第二次探测再上次探测基础上加1,往后持续 171 | 2. 平方探测:跟上方类似,只不过i变成了正负i²(在原来位置基础上偏移量升级层次,加一平方减一平方,加二平方减二平方...) 172 | 3. 双散列:两个散列函数,第一个散列函数作为他的最早即时找的这个位置,第二个散列函数用来计算偏移量 173 | 174 | ### 11.3.2 线性探测 175 | 176 | > 线性探测法(Linear Probing):以增量序列1,2,.....,(TableSize-1)循环试探下一个存储地址 177 | 178 | image-20220826122157322 179 | 180 | image-20220826122945504 181 | 182 | 会形成聚集现象 183 | 184 | 如果按照与刚才例子输入相反的顺序插入各个元素,这些元素在散列表中的位置还是一样的?不一样 185 | 186 | **散列表查找性能分析** 187 | 188 | > 1. 成功平均查找长度(ASLs) => 就是我要找的对象最后被我找到了 189 | > 2. 不成功平均查找长度(ASLu) =>找对象找不着 190 | > 191 | > 什么是成功的 => 在散列表里找到的是成功的,不在散列表的元素就是不成功的 192 | 193 | image-20220826133953107 194 | 195 | ASL u = 值(值取决于哈希余数) 196 | 197 | 余数为0要比较3次,为1要比较2次,余数为2要比较1次,余数为3的时候要比较2次,余数为4、5、6都是比较一次就够了,余数为7则要比较9次才能知道,余数为8则是8次 198 | 199 | -------------- 200 | 201 | **余数-哈希函数** 202 | 203 | 1. 余数的思想 204 | 所谓余数,就是程序中取模运算中的“模”。 205 | 余数具有一个非常重要的特性:可以将无限的数据归一到有限的范围(余数总是小于除数) 206 | 207 | ``` 208 | 你知道,整数是没有边界的,它可能是正无穷,也可能是负无穷。但是余数却可以通过某一种关系,让整数处于一个确定的边界内。我想这也是人类发明星期或者礼拜的初衷吧,任你时光变迁,我都是以 7 天为一个周期,“周”而复始地过着确定的生活。因为从星期的角度看,不管你是哪一天,都会落到星期一到星期日的某一天里。 209 | ``` 210 | 211 | 2. 同余定理 212 | 在上边的例子中,第一天与第八天都是周一,第二天与第九天都是周二,即 213 | 214 | ``` 215 | 1%7=8%7=1 216 | 2%7=9%7=2 217 | ``` 218 | 219 | 这就引出了余数的另一个重要概念:同余定理 220 | 221 | ``` 222 | 口语化表述: 223 | 两个整数 a 和 b,如果它们除以正整数 m 得到的余数相等,我们就可以说 a 和 b 对于模 m 同余 224 | ``` 225 | 226 | 其实,奇数与偶数的确定就是同余定理的应用。将一个数字模2,得0为偶数,得1为奇数 227 | 复杂算法拆解后的原理并不一定复杂,同余定理也可以作为有用的应用,就是哈希函数 228 | 229 | 3. **哈希函数(散列函数)** 230 | 231 | 将任意长度的输入,通过哈希算法,压缩为某一固定长度的输出,所得存储位置为散列地址 232 | 233 | 散列过程 234 | 235 | ``` 236 | (1)存储记录时,通过散列函数记录散列地址,按地址存储记录 237 | (2)查找记录时,通过同样的散列函数计算记录的散列地址,按散列地址访问记录 238 | ``` 239 | 240 | 散列技术通过散列函数建立了从记录的关键码集合到散列表的地址集合的一个映射,显然,会出现两个不同记录存放在同一位置的情况,这种现象称为冲突,此时相同位置的记录称为同义词 241 | 242 | 散列函数中最常采用的方案是除留余数法,其基本思想: 243 | 244 | ``` 245 | 选择适当的正整数P,以关键码除以P的余数作为散列地址 246 | 通常P为小于或等于表长(最好接近)的最小质数或不包含小于 20 质因子的合数 247 | ``` 248 | 249 | ### 11.3.3 线性探测—字符串的例子 250 | 251 | > 前面5个都是没有冲突的,第六个冲突1次(查找次数变为2),第七个冲突4次,最后一个冲突2次 252 | > 253 | > image-20220826135742628 254 | > 255 | > 与例子相似,如果已知散列表的前8个位置有元素(但元素内容与例子不一样)而且后面18个位置也全是空位,那么平均不成功查找次数还是一样的吗?一样的 256 | 257 | ### 11.3.4 平方探测法(Quadratic Probing) 258 | 259 | > 也可以叫做:二次探测 260 | 261 | image-20220826143648650 262 | 263 | image-20220826145722062 264 | 265 | ------ 266 | 267 | image-20220826150859973 268 | 269 | 2. 平方探测法(Quadratic Probing) 270 | 1. 是否有空间,平方探测(二次探测)就能找得到? 271 | 2. image-20220827125834414 272 | 273 | 还有一种平方探测的方式是:![img](https://img-ph-mirror.nosdn.127.net/SNWRrXvZtkCJgshKHuxl_w==/1063130987053874834.png)。也就是增量序列为![img](https://img-ph-mirror.nosdn.127.net/RMjHE_zjyegNn21mHvsSjA==/2446580497586752872.png)。 274 | 275 | 如果采用前面讲的![img](https://img-ph-mirror.nosdn.127.net/7-JqsZPN0NsBXEkeFJV6aA==/6630549895721824529.png)增量序列找不到空位置,意味着采用![img](https://img-ph-mirror.nosdn.127.net/SNWRrXvZtkCJgshKHuxl_w==/1063130987053874834.png)的增量序列也一定找不到空位置。 276 | 277 | **线性探测的缺陷**:容易聚集,二次探测虽然也有但不严重 278 | 279 | >有定理显示:如果散列表长度TableSize是某个4k+3(k是正整数)形式的素数时,平方探测法就可以探测到整个散列表空间 280 | 281 | ### 11.3.5 平方探测法的实现 282 | 283 | ```c 284 | typedef struct HashTbl*HashTable; 285 | struct HashTbl{ 286 | int TableSize;//当前表的实际大小 287 | Cell*TheCells;//代表自己是一个数组,实际上是一个指针 288 | }H; 289 | 290 | HanshTable InitializeTable(int TableSize) 291 | { 292 | HashTable H; 293 | int i; 294 | if( TableSize < MinTableSize ){//判别散列表的大小,太小就没必要做散列,直接放在数组就行了 295 | Error("散列表太小"); 296 | return NULL; 297 | } 298 | //分配散列表 299 | H = (HashTable)malloc(sizeof(struct HashTbl));//申请空间赋给H 300 | if( H == NULL ) 301 | FatalError("空间溢出!!!");//判断有没有申请成功 302 | H->TableSize = NextPrime(TableSize);//申请成功的话希望表的size是素数,NextPrime就是这个目的,产生一个比表大一点的素数 303 | //分配散列表Cells 304 | H->TheCells=(Cell*)malloc(sizeof(Cell)*H->TableSize);//为真正的TableSize分配一个空间,就相当于指向一个数组了 305 | if(H->TheCells == NULL) 306 | FatalError("空间溢出!!!"); 307 | for(i=0;iTableSize;i++) 308 | H->TheCells[i].Info = Empty; 309 | return H; 310 | } 311 | ``` 312 | 313 | > 友情小提示:typedef struct 的typedef是用来取别名的,比如上方 HashTbl 的别名就是H 314 | 315 | image-20220827132244014实际删除的元素不能真的从表中拿掉,不然查找的时候会有问题的。如果我们要删除可以先做个记号。这样在后续的查找与插入的好处有:首先在查找的时候碰到被删掉的元素就说这个位置他做了个记号被删掉了,我们就知道这还不是空位还可以继续找,如果真拿掉变成空位的话就会产生误判。然后插入的时候发现这个元素是被删掉了,他不是空位而是原来有元素占着,现在被删掉了,这个时候插入元素就可以来替代原来删掉的元素。这样我们插入删除的操作都可以做并且不影响我们的查找过程 316 | 317 | ```c 318 | //表的初始化 319 | Position Find(ElementType Key,HashTable H)//平方探测 320 | { 321 | Position CurrentPos,NewPos; 322 | int CNum;//记录冲突次数 323 | CNum = 0; 324 | NewPos = CurrentPos = Hash(Key,H->TableSize);//要算哈希函数,所以先调用一个哈希函数。CurrentPos是我们值真正要放的位置 325 | while(H->TheCells[NewPos].Info != Empty && H->TheCells[NewPos].Element != Key){//Info位置不空且Element值不等于我要找的Key,那就需要继续找,而循环的条件就是我们要找的位置,被别人占了但是不空 326 | //字符串类型的关键词需要strcmp函数 327 | if(++CNum % 2){//判断冲突的奇偶次 328 | NewPos = CurrentPos +(CNum+1)/2*(CNum+1)/2;//探测方法:在原来的位置上(CurrentPos,也就是最早的哈希函数值)加减i²获得新的地址。因为一会加一会减,所以需要在上方用上if来判别是奇数是偶来决定该加还是该减 329 | while(NewPos >= H->TableSize)//上方NewPos加上后面的大小可能超出大于TableSize了,所以需要通过不断循环减去TableSize,一直到NewPos不大于他(不大于他就落在0-TableSize之间了) 330 | NewPos -= H->TableSize; 331 | }else{//如果是偶数就走这条路啦,减去一个i平方 332 | NewPos = CurrentPos - CNum/2*CNum/2; 333 | while(NewPos < 0)//跟上面那个while类似,为了不负到突破地板,需要将值拉回来 334 | NewPos += H->TableSize; 335 | } 336 | } 337 | return NewPos; 338 | } 339 | ``` 340 | 341 | image-20220827135442103将Cnum映射为i的平方。CNum**加**1除以2就是这个i的值。所以举个例子 342 | 343 | >例子:1加1除以2为1,3加1除以2为2,5加1除以2为3 344 | > 345 | >如果是**减**少的话,4除2为2,6除2为3 346 | 347 | ```c 348 | void Insert(ElementType Key,HashTable H) 349 | { 350 | //插入操作 351 | Position Pos; 352 | Pos = Find(Key,H);//通过Find return出来一个position值 353 | if(H->TheCells[Pos].Info != Legitimate){//需要判断Pos的状态,如果Pos不属于被占用的状态,那我们这个元素就可以放进去(什么情况不是属于被别人占用:空位或者被删除了) 354 | //确认在此插入 355 | H->TheCells[Pos].Info = Legitimate;//将Info设为被我占用的状态,然后下一步将Key放进去 356 | H->TheCells[Pos].Element = Key;//字符串类型的关键词需要strcpy函数 357 | } 358 | } 359 | ps:在开放地址散列表中,删除操作要很小心。通常只能"懒惰删除",即需要增加一个“删除标记(Deleted)”,而并不是真正删除它。以便查找时不会"断链"。其空间可以在下次插入时重用 360 | ``` 361 | 362 | #### 双散列探测法(Double Hashing) 363 | 364 | >image-20220827152627815 365 | 366 | #### 再散列(Rehashing) 367 | 368 | 1. 当散列表元素太多(即装填因子α太大)时,查找效率会下降(因为冲突在不断增加); 369 | 1. 怎么解决这个问题?扩大散列表。散列表扩大时,原有元素需要重新计算放置到新表中 370 | 2. 实用最大装填因子一般取0.5 <= α <= 0.85(通常控制在0.5以内) 371 | 2. 当装填因子过大时,解决的方法是加倍扩大散列表,这个过程叫做"再散列(Rehashing)" 372 | 373 | ### 11.3.6 分离链接法(Separate Chaining) 374 | 375 | > 将相应位置上有冲突的所有关键词存储在**同一单链表中** 376 | 377 | ^表示空指针 378 | 379 | image-20220827153508063 380 | 381 | **链表实现** 382 | 383 | ```c 384 | typedef struct ListNode*Position,*List; 385 | struct ListNode{ 386 | ElementType Element; 387 | Position Next;//把Next分量分给P,P是下方代码块的一个指针,指向单项链表的第一个元素 388 | }; 389 | typedef struct HashTbl*HashTable; 390 | struct HashTbl{ 391 | int TableSize; 392 | List TheLists; 393 | }; 394 | ``` 395 | 396 | ```c 397 | Position Find(ElementType Key,HashTable H)//哈希表来表示 398 | { 399 | Position P; 400 | int Pos; 401 | 402 | Pos = Hash(Key,H->TableSize);//初始散列位置,第一步算哈希函数值,得到散列函数散列地址,散列地址就代表他在这个数组里的位置 403 | P = H->TheLists[Pos].Next;//获得链表头,这个P就是上方代码块说的那个指针P,指向单项链表的第一个元素 404 | while(P != NULL && strcmp(P->Element,Key))//典型的遍历单项链表的循环,只要P不等于NULL,P所指向的这个元素跟我要找的这个元素不相等就一个个往后找,P的Next赋给P。意思就是只要P不空(后面还有元素),那么循环就一直做,同时循环的另一个条件是当前的这个元素值不等于我要找的元素值,如果列表不空再找下一个,再下一个就P的Next赋给P 405 | //等循环退出来要么P空了,就return NULL(没找到)。要么就strcmp返回值等于0,等于0就相等了,那这个时候所在的这个节点就是我们找到了,也就是return P 406 | P = P->Next; 407 | return P; 408 | } 409 | ``` 410 | 411 | image-20220827154233888 412 | 413 | #### 创建开放地址法的散列表 414 | 415 | ```c 416 | #define MAXTABLESIZE 100000 /* 允许开辟的最大散列表长度 */ 417 | typedef int ElementType; /* 关键词类型用整型 */ 418 | typedef int Index; /* 散列地址类型 */ 419 | typedef Index Position; /* 数据所在位置与散列地址是同一类型 */ 420 | /* 散列单元状态类型,分别对应:有合法元素、空单元、有已删除元素 */ 421 | typedef enum { Legitimate, Empty, Deleted } EntryType; 422 | 423 | typedef struct HashEntry Cell; /* 散列表单元类型 */ 424 | struct HashEntry{ 425 | ElementType Data; /* 存放元素 */ 426 | EntryType Info; /* 单元状态 */ 427 | }; 428 | 429 | typedef struct TblNode *HashTable; /* 散列表类型 */ 430 | struct TblNode { /* 散列表结点定义 */ 431 | int TableSize; /* 表的最大长度 */ 432 | Cell *Cells; /* 存放散列单元数据的数组 */ 433 | }; 434 | 435 | int NextPrime( int N ) 436 | { /* 返回大于N且不超过MAXTABLESIZE的最小素数 */ 437 | int i, p = (N%2)? N+2 : N+1; /*从大于N的下一个奇数开始 */ 438 | 439 | while( p <= MAXTABLESIZE ) { 440 | for( i=(int)sqrt(p); i>2; i-- ) 441 | if ( !(p%i) ) break; /* p不是素数 */ 442 | if ( i==2 ) break; /* for正常结束,说明p是素数 */ 443 | else p += 2; /* 否则试探下一个奇数 */ 444 | } 445 | return p; 446 | } 447 | 448 | HashTable CreateTable( int TableSize ) 449 | { 450 | HashTable H; 451 | int i; 452 | 453 | H = (HashTable)malloc(sizeof(struct TblNode)); 454 | /* 保证散列表最大长度是素数 */ 455 | H->TableSize = NextPrime(TableSize); 456 | /* 声明单元数组 */ 457 | H->Cells = (Cell *)malloc(H->TableSize*sizeof(Cell)); 458 | /* 初始化单元状态为“空单元” */ 459 | for( i=0; iTableSize; i++ ) 460 | H->Cells[i].Info = Empty; 461 | 462 | return H; 463 | } 464 | ``` 465 | 466 | #### 平方探测法的查找与插入 467 | 468 | ```c 469 | Position Find( HashTable H, ElementType Key ) 470 | { 471 | Position CurrentPos, NewPos; 472 | int CNum = 0; /* 记录冲突次数 */ 473 | 474 | NewPos = CurrentPos = Hash( Key, H->TableSize ); /* 初始散列位置 */ 475 | /* 当该位置的单元非空,并且不是要找的元素时,发生冲突 */ 476 | while( H->Cells[NewPos].Info!=Empty && H->Cells[NewPos].Data!=Key ) { 477 | /* 字符串类型的关键词需要 strcmp 函数!! */ 478 | /* 统计1次冲突,并判断奇偶次 */ 479 | if( ++CNum%2 ){ /* 奇数次冲突 */ 480 | NewPos = CurrentPos + (CNum+1)*(CNum+1)/4; /* 增量为+[(CNum+1)/2]^2 */ 481 | if ( NewPos >= H->TableSize ) 482 | NewPos = NewPos % H->TableSize; /* 调整为合法地址 */ 483 | } 484 | else { /* 偶数次冲突 */ 485 | NewPos = CurrentPos - CNum*CNum/4; /* 增量为-(CNum/2)^2 */ 486 | while( NewPos < 0 ) 487 | NewPos += H->TableSize; /* 调整为合法地址 */ 488 | } 489 | } 490 | return NewPos; /* 此时NewPos或者是Key的位置,或者是一个空单元的位置(表示找不到)*/ 491 | } 492 | 493 | bool Insert( HashTable H, ElementType Key ) 494 | { 495 | Position Pos = Find( H, Key ); /* 先检查Key是否已经存在 */ 496 | 497 | if( H->Cells[Pos].Info != Legitimate ) { /* 如果这个单元没有被占,说明Key可以插入在此 */ 498 | H->Cells[Pos].Info = Legitimate; 499 | H->Cells[Pos].Data = Key; 500 | /*字符串类型的关键词需要 strcpy 函数!! */ 501 | return true; 502 | } 503 | else { 504 | printf("键值已存在"); 505 | return false; 506 | } 507 | } 508 | ``` 509 | 510 | #### 分离链接法的散列表实现 511 | 512 | ```c 513 | #define KEYLENGTH 15 /* 关键词字符串的最大长度 */ 514 | typedef char ElementType[KEYLENGTH+1]; /* 关键词类型用字符串 */ 515 | typedef int Index; /* 散列地址类型 */ 516 | /******** 以下是单链表的定义 ********/ 517 | typedef struct LNode *PtrToLNode; 518 | struct LNode { 519 | ElementType Data; 520 | PtrToLNode Next; 521 | }; 522 | typedef PtrToLNode Position; 523 | typedef PtrToLNode List; 524 | /******** 以上是单链表的定义 ********/ 525 | 526 | typedef struct TblNode *HashTable; /* 散列表类型 */ 527 | struct TblNode { /* 散列表结点定义 */ 528 | int TableSize; /* 表的最大长度 */ 529 | List Heads; /* 指向链表头结点的数组 */ 530 | }; 531 | 532 | HashTable CreateTable( int TableSize ) 533 | { 534 | HashTable H; 535 | int i; 536 | 537 | H = (HashTable)malloc(sizeof(struct TblNode)); 538 | /* 保证散列表最大长度是素数,具体见代码5.3 */ 539 | H->TableSize = NextPrime(TableSize); 540 | 541 | /* 以下分配链表头结点数组 */ 542 | H->Heads = (List)malloc(H->TableSize*sizeof(struct LNode)); 543 | /* 初始化表头结点 */ 544 | for( i=0; iTableSize; i++ ) { 545 | H->Heads[i].Data[0] = '\0'; 546 | H->Heads[i].Next = NULL; 547 | } 548 | 549 | return H; 550 | } 551 | 552 | Position Find( HashTable H, ElementType Key ) 553 | { 554 | Position P; 555 | Index Pos; 556 | 557 | Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */ 558 | P = H->Heads[Pos].Next; /* 从该链表的第1个结点开始 */ 559 | /* 当未到表尾,并且Key未找到时 */ 560 | while( P && strcmp(P->Data, Key) ) 561 | P = P->Next; 562 | 563 | return P; /* 此时P或者指向找到的结点,或者为NULL */ 564 | } 565 | 566 | bool Insert( HashTable H, ElementType Key ) 567 | { 568 | Position P, NewCell; 569 | Index Pos; 570 | 571 | P = Find( H, Key ); 572 | if ( !P ) { /* 关键词未找到,可以插入 */ 573 | NewCell = (Position)malloc(sizeof(struct LNode)); 574 | strcpy(NewCell->Data, Key); 575 | Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */ 576 | /* 将NewCell插入为H->Heads[Pos]链表的第1个结点 */ 577 | NewCell->Next = H->Heads[Pos].Next; 578 | H->Heads[Pos].Next = NewCell; 579 | return true; 580 | } 581 | else { /* 关键词已存在 */ 582 | printf("键值已存在"); 583 | return false; 584 | } 585 | } 586 | 587 | void DestroyTable( HashTable H ) 588 | { 589 | int i; 590 | Position P, Tmp; 591 | 592 | /* 释放每个链表的结点 */ 593 | for( i=0; iTableSize; i++ ) { 594 | P = H->Heads[i].Next; 595 | while( P ) { 596 | Tmp = P->Next; 597 | free( P ); 598 | P = Tmp; 599 | } 600 | } 601 | free( H->Heads ); /* 释放头结点数组 */ 602 | free( H ); /* 释放散列表结点 */ 603 | } 604 | ``` 605 | 606 | ### 11.4 散列表的性能分析 607 | 608 | > 1. 平均查找长度(ASL)用来度量散列表查找效率:成功、不成功 609 | > 1. 成功:查找的元素再散列表里 610 | > 2. 不成功:查找的元素不在散列表里 611 | > 2. 关键词的比较次数,取决于产生冲突的多少。影响产生充裕多少有以下三个因素: 612 | > 1. 散列函数是否均匀(不均匀的话冲突会多,性能就会差) 613 | > 2. 处理冲突的方法 614 | > 3. 散列表的装填因子α(装了多少元素,装的元素少那么冲突少。装的元素多则冲突多) 615 | 616 | 分析:不同冲突处理方法、装填因子对效率的影响 617 | 618 | 1. 线性探测法的查找性能 619 | 620 | 可以证明,线性探测法的期望探测次数满足下列公式:image-20220827214026405 621 | 622 | image-20220827214455451 623 | 624 | 对于线性探测,如果当前装填因子值为0.654321, 此时不成功情况下的期望探测次数小于成功情况下的期望探测次数。错误 625 | 626 | 2. 平方 探测法和双散列探测法的查找性能 627 | 628 | 可以证明,平方探测法和双散列探测法探测次数 满足下列公式:image-20220827214621567 629 | 630 | image-20220828210649498 631 | 632 | ASCu:成功 ASLs:失败 633 | 634 | **期望探测次数与装填因子α的关系** 635 | 636 | > 横坐标:装填因子α 637 | > 638 | > 纵坐标:期望探测次数 639 | 640 | 当装填因子α<0.5的时候,各种探测法的期望探测次数都不大,也比较接近 641 | 642 | 合理的最大装入因子α应该不超过0.85(对线性探测来说) 643 | 644 | image-20220828220419341 645 | 646 | 3. **分离链接法的查找性能** 647 | 648 | 所有地址链表的平均长度定义成**装填因子α**,α有可能超过1 649 | 650 | 不难证明:其期望探测次数p为:image-20220828224355504 651 | 652 | 653 | 654 | ### 总结 655 | 656 | #### 哈希查找(散列查找)特点 657 | 658 | 选择合适的h(key),散列法的查找效率期望是常数O(1),它几乎与关键词的空间的大小n无关!也适合于还建瓷直接比较计算机大的问题 659 | 660 | > 优点: 661 | > 662 | > 1. 不像搜索树一样或者平衡二叉树一样,他的查找基本上是跟问题的规模有关系。所以不发生冲突的话基本上是一次成功 663 | 664 | >特点: 665 | > 666 | >1. 跟问题规模无关,是一个常量时间 667 | >2. 散列查找在很多情况下,用于字符串的管理(例如web地址名关键词搜索这种字符串管理),关键词查找 668 | >3. 它是以较小的α为前提。因此,散列方法是一个以空间换时间 669 | >4. 散列方法的存储对关键字是随机的,不便于顺序查找关键字,也不适合于范围查找,或最大值最小值查找 670 | 671 | #### 开放地址法 672 | 673 | > 优:散列表是一个数组,存储效率高,随机查找 674 | > 675 | > 缺:散列表有"聚集"现象 676 | 677 | #### 分离链法 678 | 679 | > 优:关键字删除不需要"懒惰删除"法,从而没有存储"垃圾" 680 | > 681 | > 缺:散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低 682 | > 683 | > 太小的α可能导致空间浪费,大的α又将付出更多的时间代价。不均匀的链表长度导致时间效率的严重下降 684 | 685 | ### 11.5 应用实例:词频统计 686 | 687 | ``` 688 | 【例】给定一个英文文本文件,统计文件中所有单词出现的频率,并输出词频最大的前10%的单词及其词频。 689 | 假设单词字符定义为大小写字母、数字和下划线,其它字符均认为是单词分隔符,不予考虑。 690 | 691 | 【分析】关键:对新读入的单词在已有单词表中查找,如果已经存在,则将该单词的词频加1,如果不存在,则插入该单词并记词频为1。 692 | 693 | 如何设计该单词表的数据结构才可以进行快速地查找和插入?散列表 694 | ``` 695 | 696 | ```c 697 | int main(){ 698 | int TableSize = 10000;//散列表的估计大小 699 | int wordcount = 0,length; 700 | HashTable H; 701 | ElementType word; 702 | FILE*fp; 703 | char document[30] = "HarryPotter.txt";//要被统计词频的文件名 704 | H = Initialize Table( TableSize );//建立散列表(也就是初始化散列表) 705 | if((fp = fopen(document,"r")) == NULL) FatalError("无法打开文件!\n"); 706 | while(!feof(fp)){//对文件进行处理,读到不是字母跟数字或者下划线而是分隔符的话,那就返回,获得到一个完整的word 707 | length = GetAWord(fp,word);//从文件中读取一个单词 708 | if(length > 3){//只考虑适当长度的单词 709 | wordcount++;//统计文件中单词总数 710 | InsertAndCount(word,H);//插入哈希表 711 | //InsertAndCount这个函数作用:到哈希表里去找这个元素单词在不在,不在就插入,如果在就词频加1 712 | } 713 | } 714 | fclose(fp); 715 | printf("该文档共出现%d个有效单词,",wordcount); 716 | Show(H,10.0/100);//显示词频前10%的所有单词 717 | //Show一共两个参数,一个是我们的散列表,另外一个是我们的要求词频前10% 718 | //这个函数一共做4件事情:1.统计最大词频;2.用一组数统计从1到最大词频的单词数;3.计算前10%的词频应该是多少;4.输出前10%词频的单词 719 | DestroyTable(H);//销毁散列表 720 | return 0; 721 | } 722 | ``` 723 | 724 | 725 | 726 | ## 小白专场[陈越]:电话聊天狂人-C语言实现 727 | 728 | ### 小白-PM.1 题意理解与解法分析 729 | 730 | > 所有电话号码统计一下,打电话或者接电话的总次数是最多的,那这个人就叫做电话聊天狂人 731 | 732 | image-20220831124929950 733 | 734 | > **解法1:-排序** 735 | > 736 | > 第1步:读入最多2×10五次方个电话号码,每个号码存为长度为11的字符串 737 | > 第2步:按字符串非递减顺序排序 738 | > 第3步:扫描有序数组,累计同号码出现的次数,并且更新最大次数 739 | > 740 | > 优势:编程简单快捷 741 | > 742 | > 缺点:如果这是在现实应用场合,这些号码不是一次性出现给你的,而是每天没分每秒的不断的在进来,每时每刻我们都可能被停下来问这个聊天狂人是谁。那每次都需要做一个NlogN的排序,要做N次这种事情,所以整个算法的复杂度就变成了N2logN 743 | > 744 | > 所以这个算法不好拓展解决动态插入的问题 745 | 746 | >**方法2:-直接映射** 747 | > 748 | >为什么是2x10的10次方个单位,因为每个电话有11位,而第一位都是1,把1排除掉 749 | > 750 | >image-20220831130043024 751 | > 752 | >优势:编程简单快捷,动态插入快 753 | > 754 | >缺点:下标超过了unsigned long,因为我们平常用到的最长长度的是10位的 755 | > 756 | >直接映射需要存储![img](https://img-ph-mirror.nosdn.127.net/uZtL3cn7cILodVYIJivfKg==/6619284299584833484.png)个短整型数字,大约是多少字节空间?差不多40GB,image-20220831134221137 757 | > 758 | >我们需要扫描多少个单位才能找到我们需要的那个电话狂人? 759 | > 760 | >=>为了要找2x10的五次方个号码,必须要扫描2x10的10次方个单元,太浪费啦 761 | 762 | > **解法3-带智商的散列** 763 | > 764 | > 注意:散列表的头节点的个数一共比2N要略大一点 765 | > 766 | > image-20220831134934368 767 | > 768 | > 769 | 770 | ### 小白-PM.2 程序框架搭建 771 | 772 | ```C 773 | int main() 774 | { 775 | 创建散列表; 776 | 读入号码插入表中; 777 | 扫描表输出狂人; 778 | return 0; 779 | } 780 | ``` 781 | 782 | ```c 783 | int main() 784 | { 785 | int N,i; 786 | ElementType Key;//声明这个为了配合我们HashTable里面声明的类型 787 | HashTable H; 788 | 789 | scanf("%d",&N); 790 | H = CreateTable(N*2);//创建一个散列表 791 | for(i=0;i 创建表需要的流程 802 | 803 | image-20220831144530212要调用插入函数的话肯定会用到其他两个函数,一个是散列函数Hash,另一个是要找到合适的位置把它插入,Find 804 | 805 | ScanAndOutput要做的事情:image-20220831144828537 806 | 807 | ### 小白-PM.3 输出狂人 808 | 809 | ```c 810 | void ScanAndOutput( HashTable H) 811 | { 812 | int i,MaxCnt = PCnt = 0; 813 | ElementType MinPhone; 814 | List Ptr; 815 | MinPhone[0] = '\0';//最小电话号码初始化为一个空的字符串 816 | for(i=0;iTableSize;i++){//扫描链表 817 | Ptr = H->Heads[i].Next; 818 | while(Ptr){ 819 | if(Ptr->Count >MaxCnt){//更新最大通话次数 820 | MaxCnt = Ptr->Count; 821 | strcpy(Minphone,Ptr->Data);//狂人的电话号码copy到我们将要输出的最小的电话号码里 822 | PCnt = 1;//最狂的只能有一个,初始化次数 823 | } 824 | else if(Ptr->Count == MaxCnt ){//狂人不止一个的情况,并列的狂人,最大通话次数一样 825 | PCnt ++;//狂人计数 826 | if(strcmp(Minphone,Ptr->Data)>0)//Ptr->Data>0意味着前面号码大后面号码小,那我们就要把最小号码复制到下方这个更新字符串里面去 827 | strcpy(Minphone,Ptr->Data);//更新狂人的最小手机号码 828 | } 829 | Ptr = Ptr->Next; 830 | } 831 | } 832 | printf("%s %d",MinPhone,MaxCnt);//输出一个是最小的电话号码,另一个是记录这个最大的通话次数 833 | id(PCnt > 1) printf("%d",PCnt);//狂人如果大于1的话我还得把这个统计数据输出出来 834 | printf("\n"); 835 | } 836 | ``` 837 | 838 | ### 小白-PM.4 模块的引用与裁剪 839 | 840 | image-20220831151057159 841 | 842 | ```c 843 | #define KEYLENGTH 11//关键词字符串的最大长度 844 | //关键词类型用字符 845 | typedef char ElementType[KEYLENGTH+1];//长度要加1是因为在C语言中,里面字符串的结尾还占了一个位置 846 | typedef int Index;//散列地址类型 847 | 848 | //分离链接法的部分 849 | typedef struct LNode *PtrToLNode; 850 | struct LNode{ 851 | ElementType Data;//电话号码 852 | PtrToLNode Next;//指向下一个节点的指针 853 | int Count;//计数器,我们定义为一个整型的Count 854 | }; 855 | typedef PtrToLNode Position; 856 | typedef PtrToLNode List; 857 | 858 | typedef struct TblNode *HashTable; 859 | struct TblNode{//散列表结点定义 860 | int TableSize;//表的最大长度 861 | List Heads;//指向链表头结点的数组 862 | } 863 | ``` 864 | 865 | NextPrime模块 866 | 867 | ```c 868 | #define MAXTABLESIZE 1000000 869 | int NextPrime(int N) 870 | {/*返回大于N且不超过AXTABLESIZE的最小素数*/ 871 | int i,p = ( N % 2 ) ? N+2 : N+1;/*从大于N的下一个奇数开始*/ 872 | while( p <= MAXTABLESIZE ){ 873 | for( i=(int)sqrt(p);i>2;i--) 874 | if(!(p%i))break;//p不是素数 875 | if(i == 2) break;//for正常结束,说明p是素数 876 | else p += 2;//否则试探下一个奇数 877 | } 878 | return p; 879 | } 880 | 881 | HashTable CreateTable( int TableSize ) 882 | { 883 | HashTable H; 884 | int i; 885 | H = (HashTable)malloc(sizeof(struct TblNode)); 886 | H->TableSize = NextPrime(TableSize); 887 | H->Heads = (List)malloc(H->TableSize*sizeof(struct LNode)); 888 | for( i = 0;i < H->TableSize; i++){ 889 | H->Heads[i].Data[0] = '\0';H->Heads[i].Next = NULL; 890 | H->Heads[i].Count = 0;//头节点定义为0,虽然不写也没关系,它本身是一个空节点。但写上是个好习惯,表示这个节点里所有的变量都有一个初始化的值,不要有个节点空着的不知道这是干嘛的 891 | } 892 | return H; 893 | } 894 | int Hash(int Key,int P)//Key是整数,当我们把它传给这个Hash函数去处理的时候我们应该已经把那电话号码后五位截取出来了并且把它变成一个整数了,而不是直接把11位电话号码传进来 895 | { 896 | //除留余数法,散列函数 897 | return Key%P; 898 | } 899 | ``` 900 | 901 | > 原始的Find函数 902 | 903 | ```c 904 | #deine MAXD 5//参与散列映射计算的字符个数,从后往前数5位 905 | Position Find( HashTable H,ElementType Key ) 906 | { 907 | Position P; 908 | Index Pos; 909 | //初始散列位置 910 | Pos = Hash(Key,H->TableSize );//在原始的这个是直接把Key传到Hash函数里面去算的 911 | //但在我们这个问题里就不能这么做,在Key传进去之前得先把它后五位截出来变成一个整数,变成 912 | Pos = Hash(atoi(Key+KEYLENGTH-MAXD),H->TableSize)//把数传给atoi转化为整数再把整数传给散列函数去计算 913 | 914 | P = H->Heads[Pos].Next;//从该链表的第一个结点开始 915 | //当未到表尾,并且Key未找到时 916 | while(P && strcmp(P->Data,Key)) 917 | P = P->Next; 918 | 919 | return P;//此时P或者指向找到的结点,或者为NULL 920 | } 921 | ``` 922 | 923 | > 插入函数 924 | 925 | ```c 926 | bool Insert(HashTable H,ElementType Key) 927 | { 928 | Position P,NewCell; 929 | Index Pos; 930 | 931 | P = Find(H,Key); 932 | if(!P){//关键词未找到,可以插入 933 | NewCell = (Position)malloc(sizeof(struct LNode)); 934 | strcpy(NewCell->Data,Key); 935 | //初始散列位置 936 | //Pos = Hash(Key,H->TableSize);这里进行改动 937 | NewCell->Count = 1;//当我们要插入新的电话号码的时候,这个新的电话号码节点的计数器要初始化为1 938 | Pos = Hash(atoi(Key+KEYLENGTH-MAXD),H->TableSize); 939 | //将NewCell插入为H->Heads[Pos]链表的第一个结点 940 | NewCell->Next = H->Heads[Pos].Next; 941 | H->Heads[Pos].Next = NewCell; 942 | return true; 943 | } 944 | else{//关键词已存在 945 | //printf("键值已存在");//表示我找到了这个电话号码,但在实际显示中我们不能显示这一行,需要修改一下 946 | P->Count++;//找到这个电话号码计数器要加1,表示我们多打了一次电话或多接了一次电话 947 | return false; 948 | } 949 | } 950 | ``` 951 | 952 | -------------------------------------------------------------------------------- /数据结构第十二周笔记—— 综合习题选讲 (慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第十二周笔记—— 综合习题选讲 (慕课浙大版本--XiaoYu) 2 | 3 | ## 习题选讲 - Insert or Merge 4 | 5 | ### 习题-IOM.1 插入排序的判断 6 | 7 | #### 题意理解 8 | 9 | > 如何区分简单插入和非递归的归并排序 10 | 11 | 1. 插入排序:前面有序,后面没有变化 12 | 2. 归并排序:分段有序 13 | 14 | image-20220901225159180 15 | 16 | #### 捏软柿子算法 17 | 18 | ps:在插入和归并两种算法里,哪种算法比较容易判断?**插入排序** 19 | 20 | 判断是否插入排序 21 | 22 | 1. 从左向右扫描,直到发现顺序不对,跳出循环 23 | 24 | 2. 从跳出地点继续向右扫描,与原始序列比对,发现不同则判断为"非"(如果是插入排序的话,所有的元素都是跟原始序列一模一样的) 25 | 26 | 如果在**对比**的过程中发现不同,则跳出循环 27 | 28 | 3. 循环自然结束,则判断为"是",返回跳出地点 29 | 30 | 1. 解析:(我们可以返回一个布尔值,例如非为0、是为1,但如果我返回的是"是"的话,插入排序要往下进行一步,简单返回一个1在我进行插入排序下一步的时候,还得从左向右扫描去找那个要执行下一步的那个点。所以我们返回"是"的)同时返回一个跳出的地点 31 | 32 | 如果是插入排序,则从**跳出地点**开始进行一趟插入 33 | 34 | ### 习题-IOM.2 归并段的判断 35 | 36 | #### 判断归并段的长度 37 | 38 | 错误的想法: 39 | 40 | 1. 从头开始连续有序的子列长度?image-20220901232611803 41 | 42 | 2. 所有连续有序子列的最短长度?image-20220901232637687 43 | 44 | 1. 这个其实是四个一段的,但前8个刚好都是有序的 45 | 46 | 2. 保险正确的判断方法:从原始序列出发,真的在做归并排序,每归并一趟就把归并的中间结果跟这个结果的序列做一个比对。什么时候每一个数都对上了就再把当前的归并多执行一次然后输出结果 47 | 48 | 3. ```c 49 | for (l=2;l<=N;l*=2) 50 | //在保证了l是4的情况下,要检查看能不能是8,我们要重复前面的步骤看两段之间的衔接点是不是有序 51 | ``` 52 | 53 | image-20220901234730452 54 | 55 | 红色位置没有序了跳出循环(此时l为4,我们直接以4为归并段继续执行下一趟的归并就可以了) 56 | 57 | image-20220901234939114 58 | 59 | #### 其他数据测试 60 | 61 | 最小N(应该是多大?) 62 | 63 | ps:边界测试是每道题里面测试非常重要的一个组成部分 64 | 65 | > N会是1吗?N等于1会出现什么情形? 66 | > 67 | > N等于1就意味着整个序列里面只有一个数字,在排序前它是一个数字,在排序之后他仍然是同一个数字,在这种情况下我不管是使用插入排序还是归并排序得到的都会是同样的结果,这样解就不是唯一的。我们题目输出的要求是插入排序或者归并排序的其中一个,所以N=1是绝对不可以的 68 | 69 | 保证可以区分两种算法的最小N应该是:4(区分插入排序与归并排序最小要求) 70 | 71 | 1. 插入排序第一步,什么都没变 72 | 2. 归并排序第一步,什么都变了 73 | 74 | 尾部子列无变化,但前面变了(归并) 75 | 76 | 最大N 77 | 78 | ## 习题选讲 - Sort with Swap(0,*) 79 | 80 | ### 习题-SWS.1 环的分类 81 | 82 | #### 题意理解 83 | 84 | 1. 给定N个数字的排列,如何仅利用与0交换达到排序目的? 85 | 86 | 0在里面扮演了空位的问题 87 | 88 | image-20220902094457658 89 | 90 | > 环的分类 91 | > 92 | > image-20220902111012945 93 | > 94 | > 95 | 96 | ### 习题-SWS.2 算法示例 97 | 98 | image-20220902111758466 99 | 100 | image-20220902112532941 101 | 102 | 对于不包含0的swap操作次数为n+1,包含0则是n-1次 103 | 104 | image-20220902112547320 105 | 106 | ## 习题选讲 - Hashing - Hard Version 107 | 108 | ### 习题-HHV 算法思路概述 109 | 110 | 这是哈希问题的逆问题 111 | 112 | #### 题意理解 113 | 114 | 1. 已知H(x) = x%N以及用线性探测解决冲突问题,**模大小取决于目的有多少个下标** 115 | 2. 先给出散列映射的结果,反求输入顺序 116 | 1. 当元素x被映射到H(x)位置,发现这个位置已经有y了,则y一定是在x之前被输入的 117 | 118 | > 样例 119 | 120 | image-20220902113725989 121 | 122 | 限制:为了保证解是唯一的,当有几个元素都有可能是同时被插入的时候,我们是从小到大去插入的 123 | 124 | 因为12模11,余数为1,所以跟12冲突,放在12下面。后面都是类型的操作 125 | 126 | 依次输入顺序为image-20220902113843819 127 | 128 | ## 串的模式匹配(KMP算法) 129 | 130 | ### KMP-1. 问题及简单解决方案 131 | 132 | #### 什么是串 133 | 134 | 1. 线性存储的一组数据(默认是字符) 135 | 136 | 2. 特殊操作集 137 | 138 | ``` 139 | 1.求串的长度 140 | 2.比较两串是否相等 141 | 3.两串相接 142 | 4.求子串 143 | 5.插入子串 144 | 6.匹配子串(有难度) 145 | 7.删除子串 146 | ``` 147 | 148 | > 什么是串的模式匹配 149 | > 150 | > 目标:给定一段文本,从中找出某个指定的关键字 151 | > 152 | > 例如从一本Thomas Love Peacock写于十九世纪的小说《Headlong Hall》中找到那个最长的单词: 153 | > osseocarnisanguineoviscericartilaginonervomedullary 154 | > 155 | > 或者从古希腊喜剧《Assemblywomen》中找到一道菜的名字: 156 | > Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphioparaomelitokatakechymenokichlepikossyphophattoperisteralektryonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon 157 | 158 | 当我们文本是很长的时候,而指定的关键字也是一个很长的字符串的时候,模式匹配就不再是一件简单的事情了 159 | 160 | image-20220902115311611 161 | 162 | ```c 163 | Position PatterMatch(char *string,char *pattern)//position指位置 164 | //模式匹配就是给定一段文本(string),给定一个模式(*pattern),我们要通过PatterMatch函数来返回这个pattern里string第一次出现的位置 165 | ``` 166 | 167 | #### **简单实现** 168 | 169 | 方法1:C语言的库函数strstr 170 | 171 | ```c 172 | 接口:char *strstr(char *string,char *pattern) 173 | //返回的是char *这个类型的变量(指向某个字符的指针),变量里面存的是pattern这个字符串第一个字母在string出现的时候那个字母所在的位置 174 | //一个小Demo 175 | #include 176 | #include //库要记得包含进来 177 | 178 | typedef char* Position;//给char*重新起个名字,让不懂的人也可以知道返回的是一个位置 179 | 180 | int main() 181 | { 182 | char string[] = "This is a simple example."; 183 | char pattern[] = "simple"; 184 | Position p = strstr(string,pattern); 185 | if( p == NotFound ) printf("Mot Found.\n");//能不能找到进行一个判断 186 | else printf("%s\n",p); 187 | return 0; 188 | } 189 | //输出:simple example. 190 | //如果输入的找不到,就会输出一个空指针(#define NotFound NULL) 191 | ``` 192 | 193 | strstr的复杂度怎么样?要想知道这个问题我们就得了解一下strstr是怎么运行的 194 | 195 | image-20220902122153661 196 | 197 | ``` 198 | 如上图,是两个指针指向两个变量的内容开头进行比对,第一个对上了对下一个,直到全部对上或者中途失败的时候将pattern的a与string下一个字符继续比对,一直循环下去,直到比对完都没成功或者中途成功了就退出循环 199 | ``` 200 | 201 | 若给定文本长度为 n,模式长度为 m,则库函数 strstr 的最坏时间复杂度是:**T = O(n*m)** 202 | 203 | 当我们的pattern比较小的时候,我们这个strstr库函数还是很好用的,当两者都不小的时候就得慎重了 204 | 205 | #### **简单改进** 206 | 207 | > 方法2:从末尾开始比 208 | 209 | image-20220902122725157 210 | 211 | ```c 212 | 时间复杂度:T = O(n)//仅仅是根据上方的例子进行的改动,如果pattern = "aab"换成"baa"一样要芭比Q 213 | 所以这个改进是没啥作用的 214 | ``` 215 | 216 | ### KMP-2. KMP 算法思路 217 | 218 | #### **大师改进** 219 | 220 | 方法3:**KMP(Knuth、Morris、Pratt)算法** 221 | 222 | T = O(n+m) 223 | 224 | > 简单的往前错一位的比较是完全没有必要的没有意义的,如下图 225 | > 226 | > image-20220902213138406 227 | > 228 | > KMP算法的想法: 229 | > 230 | > image-20220902213251531 231 | > 232 | > 指针指向x不会回退(回溯)了,直接继续从x开始,继续往前比较 233 | > 234 | > image-20220902213429523 235 | 236 | **match的具体例子** 237 | 238 | image-20220902213628863 239 | 240 | ```c 241 | 下标从0到9 242 | 第0个字符对应的是一个长度为1的子串,所以他不可能产生匹配,match就永远是-1 243 | 从0到1:a跟b是配不上的,match也为-1 244 | 0-2:a和c配不上,ab和bc也配不上,所以match还是为-1 245 | 0-3:ab和ca是配不上的,abc跟bca也配不上,a对应的j为0,所以match也为0 246 | //此时限制条件是最大i是小于j的,如果i=j的话那就相当于自己等于自己就没有意义了(p0...pj = p0...pj) 247 | //所以我们考虑他的真子串 248 | 0-4:a跟b配不上,abc跟cab配不上,ab跟ab能配上,match值为1... 249 | ``` 250 | 251 | 对于 pattern = abcabcacab,最后 3 个字符的 match 值是多少?**-1, 0, 1** 252 | 253 | 在早期的教科书上被叫做failure(失败的意思) 254 | 255 | > match值的含义: 256 | > 257 | > 例子:从0到6的子串,首跟尾能配上的小串,从0开始他的尾部下标为3,abca跟abca能配上。这就是match的含义 258 | 259 | ```c 260 | 此代码块内容来自百度百科: 261 | KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) 262 | 263 | KMP算法是三位学者在 Brute-Force算法的基础上同时提出的模式匹配的改进算法。Brute- Force算法在模式串中有多个字符和主串中的若干个连续字符比较都相等,但最后一个字符比较不相等时,主串的比较位置需要回退。KMP算法在上述情况下,主串位置不需要回退,从而可以大大提高效率 264 | 265 | ``` 266 | 267 | 模式匹配类型 268 | 269 | ``` 270 | (1)精确匹配 271 | 如果在目标T中至少一处存在模式P,则称匹配成功,否则即使目标与模式只有一个字符不同也不能称为匹配成功,即匹配失败。给定一个字符或符号组成的字符串目标对象T和一个字符串模式P,模式匹配的目的是在目标T中搜索与模式P完全相同的子串,返回T和P匹配的第一个字符串的首字母位置 。 272 | (2)近似匹配 273 | 如果模式P与目标T(或其子串)存在某种程度的相似,则认为匹配成功。常用的衡量字符串相似度的方法是根据一个串转换成另一个串所需的基本操作数目来确定。基本操作由字符串的插入、删除和替换来组成 274 | ``` 275 | 276 | ### KMP-3. KMP 算法实现 277 | 278 | ```c 279 | #include 280 | #include 281 | 282 | typedef int Position;//Position对应的是一个整型变量,指的是数组的下标 283 | #define NotFound -1//NotFound就应该定义成一个不可能是数组下标的东西 284 | 285 | int main() 286 | { 287 | char string[] = "This is a simple example.";//默认指字符串,但这个串可以是任何类型的 288 | char pattern[] = "simple"; 289 | Position p = KMP(string,pattern);//返回的是一个字符指针的话就只能处理字符串,如果返回的是数组下标的话那可以处理任何字符的串 290 | if (p == NotFound ) printf("Not Found.\n"); 291 | else printf("%s\n",string+p);//因为这里返回的是整数,这个整数就没办法被当作字符串的头指针了。如果我们要打印整个字符串的话,我们这里就只能写成string+p这样的形式 292 | return 0; 293 | } 294 | ``` 295 | 296 | #### KMP算法实现 297 | 298 | image-20220903190348366 299 | 300 | 一直走到指针不匹配 301 | 302 | image-20220903190416709 303 | 304 | ```c 305 | Position KMP(char *string,char *pattern ) 306 | { 307 | int n = strlen(string);//strlen得到string的长度,下方也是一样 复杂度:O(n) 308 | int m = strlen(pattern);//复杂度:O(m) 309 | int s,p,*match;//声明两个指针 310 | if(n < m) return NotFound;//找的n不可能比m短 311 | match = (int *)malloc(sizeof(int) *m); 312 | BuildMatch(pattern,match);//Tm = O(?) 313 | s = p =0; 314 | while( s0) p = match[p-1]+1;//为了防止得到段错误,这里加上条件p>0 317 | //如果p = 0的话,意味着pattern从第一个字符就不匹配,这个时候p不动,s向前走一格 318 | else s++;//当string[s] == pattern[p]不匹配,我们s++,继续下一轮匹配 319 | } 320 | //在我们跳出while循环的时候,p指针已经碰到pattern的末尾(p==m),那就是完全的匹配上了 321 | //反之p还没有到结尾,而string已经到p的结尾了,就意味着我们找不到这个模式 322 | return (p == m) ? (s-m) : NotFound; 323 | } 324 | ``` 325 | 326 | image-20220903191526409 327 | 328 | image-20220903191718124 329 | 330 | KMP的整体时间复杂度:T = O(n+m+Tm) 331 | 332 | ### KMP-4. BuildMatch 的实现原理 333 | 334 | image-20220903195432050 335 | 336 | image-20220903195522833 337 | 338 | 如果采用这种方法实现的话,时间复杂度将会达到Tm = O(m³) 339 | 340 | > 新想法:如果我们要算j的match值的话先考虑他跟j-1的match值有什么关系 341 | 342 | 假如我们这是从0到j-1的字段 343 | 344 | image-20220903195814250 345 | 346 | image-20220903195902393 347 | 348 | > match[j] >= match[j-1] + 1(是否正确?) 349 | 350 | 如果 match[j-1]+1 这个位置上的字符与 j 位置上的字符相等,match[j] 会有可能比 match[j-1]+1 更大吗?**没可能** 351 | 352 | image-20220903200043175 353 | 354 | >match[j] = match[j-1] + 1 (最多持平啦,利用反证法证明) 355 | > 356 | >且能得到这个结果的前提是运行很好image-20220903200332283 357 | 358 | 当 pattern[match[j-1]+1] != pattern[j] 时,下一个待与 pattern[j] 比较的元素下标是:match[match[j-1]]+1 359 | 360 | image-20220903200538879 361 | 362 | ### KMP-5. BuildMatch的编程实现 363 | 364 | ```c 365 | void BuildMatch(char *pattern,int *match) 366 | { 367 | int i,j; 368 | int m = strlen(pattern);//复杂度O(m) 369 | match[0] = -1; 370 | for(j=1;j=0) && (pattern[i+1] != pattern[j]))//每次都考虑最坏情况的话复杂度可能就为O(j)了,每次都退到底的话 373 | i = match[i];//让i做了一个回退,回退到while条件两者有其中之一不发生的时候 374 | if(pattern[i+1]==pattern[j]) 375 | match[j] = i+1;//i回退总次数不会超过i增加的总次数 376 | else match[j] = -1; 377 | } 378 | } 379 | ``` 380 | 381 | 整个算法复杂度:image-20220903203859011 -------------------------------------------------------------------------------- /数据结构第十周笔记——排序(下)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | --- 2 | typora-copy-images-to: ..\..\..\AppData\Roaming\Typora\typora-user-images 3 | --- 4 | 5 | # 数据结构第十周笔记——排序(下)(慕课浙大版本--XiaoYu) 6 | 7 | ## 10.1 快速排序 8 | 9 | ### 10.1.1 算法概述 10 | 11 | 快速排序的算法跟归并函数的算法差不多,策略都是分而治之的策略 12 | 13 | 1. 分而治之 14 | 1. 主元(pivot)=>中枢枢纽的意思 15 | 16 | image-20220818103011759 17 | 18 | 伪码描述 19 | 20 | ```c 21 | void Quicksort( ElementType A[],int N ) 22 | { 23 | if( N < 2 ) return; 24 | pivot = 从A[]中选一个主元;//主元的选择决定了快速排序到底快不快 25 | 将S = { A[] \ pivot }将除了主元以外的元素分成两个独立子集://怎么分 26 | A1 = {a属于S | a <= pivot }和A2 = {a属于S | a >= pivot };//一部分由小于等于pivot元素来组成的,另一部分由大于等于pivot元素组成 27 | A[] = Quicksort(A1,N1) U {pivot} U Quicksort(A2,N2); 28 | } 29 | ``` 30 | 31 | 什么是快速排序算法的最好情况?每次正好中分 32 | 33 | image-20220818103818288 34 | 35 | ### 10.1.2 选主元 36 | 37 | image-20220819213549001 38 | 39 | 1. 随机取pivot?rand()函数不便宜=相当花费时间 40 | 41 | 2. 取头、中、尾的中位数 42 | 43 | 1. 例如8、12、3的中位数就是8 44 | 45 | 2. 测试一下pivot()不同的取法和堆运行速度有多大影响? 46 | 47 | ```c 48 | ElementType Median3( ElementType A[],int Left,int Right ) 49 | { 50 | int Center = ( Left + Right ) / 2; 51 | if( A[ Left ] > A[ Center ] )//三步的比较跟交换(保证从左到右的大小顺序)。左边比中间大 52 | Swap( &A[ Left ],&A[ Center ] ); 53 | if( A[ Left ] > A[ Right ] )//左边比右边大 54 | Swap( &A[ Left ],&A[ Right ] ); 55 | if( A[ Center ] > A[ Right ] )//中间比右边大 56 | Swap( &A[ Center ],&A[ Right ] ); 57 | //这样三步交换下来,左边一定是最小的那个 58 | Swap( &A[ Center ],&A[ Right-1 ] );//将pivot藏到右边(为了之后方便,先将Center放到现在需要考虑的子列的最右边),然后就只需要考虑A[Left + 1]....A[ Right - 2] 59 | return A[ Right - 1];//返回pivot 60 | } 61 | ``` 62 | 63 | ``` 64 | 字符串交换(swap)swap操作实现交换两个容器内所有元素的功能。要交换的容器的类型必须匹配: 必须是相同类型的容器,而且所存储的元素类型也必须相同。调用了swap函数后,右操作数原来存储的元素被存放在左操作数中,反之亦然。 65 | ``` 66 | 67 | ### 10.1.3 子集划分 68 | 69 | image-20220819221831219 70 | 71 | 1. i和j不是C语言的指针意义,而是指向存放位置的意思 72 | 2. 6是主元,被藏到了最右边的位置 73 | 3. 将主元和i与j进行比较(比较完指针i与j当前的位置后向里靠),发现大小符号相反的之后i与j当前的数值对调,如下图 74 | 1. image-20220819222228534 75 | 2. image-20220819222252440 76 | 3. image-20220819222420312 77 | 4. image-20220819222524423 78 | 5. image-20220819222539880 79 | 80 | 以上就是快速排序为什么快的原因: 81 | 82 | 1. 每次他选定主元之后,这个主元在子集划分完成以后,他就被一次性的放在他最终的正确位置上 83 | 2. 不像插入排序,每次做了元素交换以后,这个元素所待的位置只是临时的,当下一张新的扑克牌插进来的时候,这些牌所有的都要往后错,一张牌的插入就可能要牵扯到多次的移动 84 | 85 | 上述快速排序需要考虑的问题: 86 | 87 | 1. 如果有元素正好等于pivot怎么办? 88 | 1. 停下来做交换? 当所有元素都相等的时候会做很多很多次完全没有用处的交换。但做了很多次无用的交换后最终我们的主元会被换到中间的位置(好处:每次递归的时候这个原始的序列都被基本上等分成两个等长的序列)复杂度NlogN 89 | 2. 不理他,继续移动指针? 避免了很多次无用的交换,但每次子集划分的时候,主元都是被放在某一个端点的(就复杂度又变成了N²了) 90 | 3. 结论:还是选择停下来交换划算点 91 | 92 | **小规模数据的处理** 93 | 94 | 1. 快速排序的问题 95 | 1. 用递归.... 96 | 2. 对小规模的数据(例如N不到100)可能还不如插入排序快 97 | 2. 解决方案 98 | 1. 当递归的数据规模充分小,则停止递归,直接调用简单排序(例如插入排序) 99 | 2. 在程序中定义一个Cutoof的阈值(决定什么时候不递归) 100 | 101 | ### 10.1.4 算法实现 102 | 103 | ```c 104 | void Quicksort( ElementType A[],int Left,int Right ) 105 | { 106 | if( Cutoff <= Right - Left ){ 107 | Pivot = Median3( A,Left,Right );//pivot是主元的意思,在这里返回的不仅仅只是一个主元的值 108 | //这里的Left参数是最小值,Right参数是最大值。真正的主元被藏在了Right-1的地方 109 | i = Left; j = Right - 1; 110 | for(;;){ 111 | while(A[ ++i ] < Pivot ){} 112 | while(A[ --j ] < Pivot ){} 113 | if( i < j) 114 | Swap( &A[i],&A[j] );//i < j则证明中间还有其他元素,这时候就可以调换 115 | //如果i > j则这个子集划分应该结束了 116 | else break; 117 | } 118 | Swap( &A[i],&A[Right-1]);//把藏在right-1这个位置的主元换到A[i]的位置上面去 119 | Quicksort(A,Left,i-1);//递归的左半部分 120 | Quicksort(A,i+1,Right);//递归的右半部分 121 | } 122 | else 123 | Insertion_Sort(A+Left,Right-Left+1);//Right-Left+1:待排序列的总个数;A+Left:开始的地方 124 | } 125 | ``` 126 | 127 | #### 快速排序的标准接口应该怎么写? 128 | 129 | ```c 130 | void Quick_Sort(ElementType A[], int N) 131 | { 132 | /* 这里写什么?如下*/ 133 | Quicksort( A, 0, N-1 ); 134 | } 135 | ``` 136 | 137 | ## 10.2 表排序 138 | 139 | ### 10.2.1 算法概述 140 | 141 | 什么时候会用到表排序: 142 | 143 | 1. 待排元素不是一个简单的整数,而是一个庞大的结构(比如说是一本书) 144 | 2. 表排序在实际上是不需要移动原始数据的,移动的是指向他们位置的指针 145 | 146 | 间接排序: 147 | 148 | 1. 不移动元素本身,只移动指针 149 | 2. 定义一个指针数组作为"表"(table)image-20220820012809524 150 | 3. 交换的只是table的整数(指针),得到image-20220820013032729 151 | 152 | ### 10.2.2 物理排序 153 | 154 | N个数字的排列由若干个独立的环组成,意思如下: 155 | 156 | image-20220820210611116table值的3跳到1再跳到5最后跳到0形成一个环。而环与环之间是独立互不交集(不相干)的 157 | 158 | **如何判断一个环的结束**?每访问一个空位i后,就令table[i]=i。当发现table[i]==i时,环就结束了。 159 | 160 | **复杂度分析**: 161 | 162 | 1. 最好情况:初始即有序 163 | 2. 物理排序过程的最坏情况是:有N/2个环,每个环包含2个元素。需要3N/2次元素移动 164 | 3. T = O(mN),m是每个A元素的复制时间 165 | 166 | ## 10.3 基数排序 167 | 168 | 仅仅基于比较进行的排序所有的这些算法他的最坏时间复杂度下界是O(NlogN),也就是说不管有多快我们总能制造出一个最坏的情况让他用最快的算法跑他也只能跑到NlogN。 169 | 170 | 这个时候除了比较之外有没有更快的排序呢?有,那就是基数排序 171 | 172 | ### 10.3.1 桶排序 173 | 174 | 基数排序是桶排序的升级版 175 | 176 | image-20220821101651278 177 | 178 | count是数组,这个数组的每一个元素都是一个指针,一开始被初始化为空链表的头指针,所以一开始有101个空链表(对应了101个空的桶 ) 179 | 180 | 假设一个学生考88分:先找到88这个桶,然后把学生信息插到这个链表的表头里image-20220821101749960 181 | 182 | ```c 183 | 伪码描述 184 | void Bucket_Sort(ElementType A[],int N) 185 | { 186 | count[]初始化; 187 | while(读入一个学生成绩grade) 188 | 将该生插入count[grade]链表; 189 | for( i=0;i>N的话怎么办? 201 | 202 | image-20220821102329119 203 | 204 | 值是0-999之间,最多一共就三位数。我们在考虑三位数的时候每一位数只有十种可能。 205 | 206 | 基数就是这个进制的基数,二进制的基数就是2,八进制基数就是8,所以十进制的基数自然就是10 207 | 208 | ```c 209 | 输入序列:64,8,216,512,27,729,0,1,343,125 210 | 用"次位优先"(Least Significant Digit)=>简称LSD算法 211 | //什么是次位优先?假设目前手里是216,这个时候6 个位数是最次位,2 百位数是主位(有一种算法是主位优先) 212 | //比较先从个位数开始 213 | ``` 214 | 215 | 第一步:建立十个桶image-20220821111151812 216 | 217 | 第二步:根据**个位数**把他们放入相应的桶里面image-20220821111259360 218 | 219 | 第三步:根据十位数放入相应的桶里面:image-20220821111430643 220 | 221 | 最后一步:根据百位数放入相应的桶里面:image-20220821112558973 222 | 223 | 设元素个数为N,整数进制为B,LSD的趟数为P,则最坏时间复杂度是:T=O(P(N+B)) 224 | 225 | #### 次位优先LSD算法 226 | 227 | ``` 228 | 对于给定范围在0-999之间的10个关键字{64,8,216,512,27,729,0,1,343,125} 229 | ① 先为最次位关键字建立桶(10个),将关键字按最次位分别放到10个桶中 230 | ② 然后将①中得到的序列按十位放到相应的桶里 231 | ③ 做一次收集,扫描每一个桶,收集到一个链表中串起来 232 | ④ 将③中得到的序列按最主位放到桶中 233 | ⑤ 最后做一次收集,这样就得到一个有序的序列了 234 | ``` 235 | 236 | ### 10.3.3 多关键字的排序 237 | 238 | 基数排序不仅仅用于处理整数的基数,还可以用于处理有多关键字的排序 239 | 240 | image-20220821112839874 241 | 242 | ```c 243 | 采用"主位优先"(Most Significant Digit)排序:为花色建4个桶 244 | 在每个桶内分别排序,最后合并结果 245 | =>其实我们还是需要在每个桶内调用相应的排序算法来解决,但是总体的时间复杂度会比我们把它完整的看成一个待排数组来排稍微好一点 246 | 247 | 采用"次位优先"(Least Significant Digit)排序:为面值建13个桶 248 | 这个方便就不需要再排序了,直接将结果合并,然后再为花色建4个桶就ok了 249 | 250 | 从上面两个例子来看,我们可以看出次位优先比主位优先要聪明得多,时间复杂度也快不少 251 | ``` 252 | 253 | ## 10.4 排序算法的比较 254 | 255 | 前三个是简单排序,时间复杂度都是比较差的。优点在于程序非常好写,很短且不需要额外的空间 256 | 257 | 冒泡排序跟直接插入排序因为每次都交换两个相邻的元素,虽然慢,但是稳定 258 | 259 | 简单选择排序是跳着交换的,导致它是不稳定的 260 | 261 | 希尔排序是最先打破下界的算法,d最坏情况下是2,这个还是取决于增量排序,因为这是跳着排,所以也不稳定 262 | 263 | 堆排序跟归并排序理论来说,他们的时间复杂度都是最好的为NlogN 264 | 265 | 归并排序的缺点:需要一个额外的空间。当我们要排的数据量非常大的时候,归并排序会导致我们只能排一半的数据,本来可以排下的数据因为空间的问题而排不下。但好处在于稳定 266 | 267 | image-20220821114318866 268 | -------------------------------------------------------------------------------- /数据结构第四周笔记——树(中)(慕课浙大版本--XiaoYu).md: -------------------------------------------------------------------------------- 1 | # 数据结构第四周笔记——树(中)(慕课浙大版本--XiaoYu) 2 | 3 | ## 4.1 二叉搜索树 4 | 5 | ### 4.1.1 二叉搜索树及查找 6 | 7 | ``` 8 | 查找问题: 9 | 1.静态查找与动态查找 10 | 2.针对动态查找,数据如何组织 11 | 12 | 什么是二叉查找树:直接把元素放在树上,不要放在数组里面。 13 | 好处:树的动态性比较强,要插入删除比在线性里面做要方便 14 | ``` 15 | 16 | #### 二叉搜索树(BST) 17 | 18 | ``` 19 | BST=>Binary Search Tree 20 | 21 | 也称为二叉排序树或者二叉查找树 22 | 23 | 二叉搜索树:一颗二叉树,可以为空;如果不为空,满足以下性质: 24 | 1.非空左子树的所有键值小于其根结点的键值。 25 | 2.非空右子树的所有键值大于其根结点单独键值 26 | 3.左、右子树都是二叉搜索树 27 | ``` 28 | 29 | image-20220702012825394 30 | 31 | image-20220702012726729 32 | 33 | #### 二叉搜索树操作的特别函数: 34 | 35 | image-20220702012904895 36 | 37 | #### 插入(新结点x)删除(x这个结点) 38 | 39 | image-20220702013012616 40 | 41 | #### 二叉搜索树的查找操作:Find 42 | 43 | ``` 44 | 查找从根结点开始,如果树为空,返回NULL 45 | 46 | 若搜索树非空,则根结点关键字和X进行比较,并进行不同处理: 47 | 1.若X小于根结点赋值,只需在左子树中继续搜索; 48 | 2.如果X大于根结点的键值,在右子树中进行继续搜索; 49 | 3.若两者比较结果是相等,搜索完成,返回指向此结点的指针 50 | ``` 51 | 52 | ```c 53 | 代码实现 54 | Position Find(ElementType X,BinTree BST) 55 | { 56 | if(!BST) return NULL;//查找失败 57 | if( X > BST -> Data)//这是尾递归,下面的两个Find的也是同理 58 | return Find(X,BST->Right);//在右子树中继续查找 59 | Else if(X < BST -> Data) 60 | return Find(X,BST->Left);//在左子树中继续查找 61 | else//X == BST->Data 62 | return BST;//查找成功,返回结点的找到结点的地址 63 | } 64 | 65 | 66 | //由于非递归函数的执行效率高,可将"尾递归"函数改为迭代函数 67 | Position IterFind(ElementType X,BinTree BST) 68 | { 69 | while(BST){ 70 | if(X > BST->Data) 71 | BST = BST -> Right;//向右子树中移动,继续查找 72 | else if(X < BST->Data) 73 | BST = BST ->Left;//向左子树中移动,继续查找 74 | else//X == BST ->Data 75 | return BST;//查找成功,返回结点的找到结点的地址 76 | } 77 | return NULL;//查找失败 78 | } 79 | 80 | //查找的效率决定于树的高度 81 | ``` 82 | 83 | #### 查找最大和最小元素 84 | 85 | ``` 86 | 1.最大元素一定是在树的最右分枝的端结点上 87 | 2.最小元素一定是在树的最左分枝的端结点上 88 | 89 | 对于搜索树的最大元素结点,下面哪个说法是正确的? 90 | A. 91 | 一定是叶结点 92 | B. 93 | 一定没有左儿子 94 | C. 95 | 一定没有右儿子 96 | D. 97 | 是后序遍历的最后一个结点 98 | 答案是:C 99 | ``` 100 | 101 | ##### 查找最小元素的递归函数 102 | 103 | ```c 104 | Position FindMin( BinTree BST) 105 | { 106 | if(!BST) return NLULL;//空的二叉树,返回NULL 107 | else if(!BST->Left) 108 | return BST;//找到最左结点并返回 109 | else 110 | return FindMin(BST->Left);//沿左分支继续查找 111 | } 112 | ``` 113 | 114 | ##### 查找最大元素的迭代函数 115 | 116 | ```c 117 | Position FindMax( BinTree BST) 118 | { 119 | if( BST ) 120 | while( BST -> Right ) BST = BST->Right;//沿右分支继续查找,直到最右叶结点 121 | return BST; 122 | } 123 | ``` 124 | 125 | ### 4.1.2 二叉搜索树的插入 126 | 127 | 【分析】关键是要找到元素应该插入的位置,可以采用与Find类似的方法 128 | 129 | image-20220702015511291 130 | 131 | #### 二叉搜索树的插入算法 132 | 133 | ```C 134 | BinTree Insert(ElementType X,BinTree BST) 135 | { 136 | if(!BST){ 137 | //若原树为空,生成并返回一个结点的二叉搜索树 138 | BST = malloc(sizeof(struct TreeNode)); 139 | BST -> Data = X; 140 | BST -> Left = BST -> Right = NULL; 141 | }else //开始找要插入元素的位置 142 | if(X < BST->Data ) 143 | BST -> Left = Insert(X,BST->Left);//递归插入左子树 144 | else if(X > BST->Data) 145 | BST->Right = Insert(X,BST->Right);//递归插入右子树 146 | //else X已经存在,什么都不做 147 | return BST; 148 | } 149 | ``` 150 | 151 | image-20220702021901357 152 | 153 | image-20220702021921682 154 | 155 | 156 | 157 | ### 4.1.3 二叉搜索树的删除 158 | 159 | **考虑三种情况**: 160 | 161 | 1. 要删除的是叶结点:直接删除,并再修改其父节点指针---置为NULL 162 | 163 | 2. 要删除的结点只有一个孩子结点: 164 | 165 | 1. 将其父节点的指针指向要删除结点的孩子结点 166 | 2. image-20220702025452840 167 | 3. image-20220702025522322 168 | 169 | 3. 要删除的结点有左、右两颗子树: 170 | 171 | 1. 用另一结点代替被删除结点:右子树的最小元素或者左子树的最大元素 172 | 2. image-20220702025716832 取右子树中的最小元素替代 173 | 3. image-20220702025939253 174 | 4. image-20220702030039570 175 | 5. 代码实现 176 | 177 | ```c 178 | BinTree Delete (ElementType X,BinTree BST) 179 | { 180 | Position Tmp; 181 | if(!BST) printf("要删除的元素未找到"); 182 | else if(X < BST ->Data) 183 | BST->Left = Delete(X,BST->Left);//左子树递归删除,返回左子树删除了x这个结点之后,新的左子树根结点的地址 184 | else if(X > BST->Data) 185 | BST->Right = Delete( X,BST->Right);//右子树递归删除 186 | else//找到要删除的结点 187 | if(BST->Left && BST->Right ){//被删除结点有左右两个子节点 188 | Tmp = FindMin(BST->Right);//在右子树中找最小的元素填充删除结点 189 | BST->Data = Tmp->Data; 190 | BST->Right = Delete(BST->Data,BST->Right);//在删除结点的右子树中删除最小元素 191 | }else{//被删除结点有一个或无子结点 192 | Tmp = BST; 193 | if(!BST->Left)//有右孩子或无子结点 194 | BST = BST->Right; 195 | else if(!BST->Left)//有左孩子或无子结点 196 | BST = BST->Left; 197 | free(Tmp); 198 | } 199 | } 200 | return BST; 201 | ``` 202 | 203 | 204 | 205 | ## 4.2 平衡二叉树 206 | 207 | ### 4.2.1 什么是平衡二叉树 208 | 209 | image-20220702032658211 210 | 211 | 怎么样子算基本上平衡:1.左右结点差不多2.左右高度差不多 212 | 213 | #### **平衡因子**: 214 | 215 | image-20220702032941579 216 | 217 | 平衡因子是对结点来说的,左右的一个高度差我们就称为平衡因子 218 | 219 | ``` 220 | 平衡二叉树(Balanced Binary Tree)(AVL树) AVL是提出这个的科学家名字的第一个字母 221 | 空树,或者任一结点左、右子树高度差的绝对值不超过1,即|BF(T)|<=1 222 | ``` 223 | 224 | image-20220702040217888 225 | 226 | image-20220702040552776 227 | 228 | 画画看,至少需要多少个结点才能构造出一棵4层(h=3)的平衡二叉树?7个 229 | 230 | image-20220702041010879 231 | 232 | image-20220702041413070 233 | 234 | 235 | 236 | ### 4.2.2 平衡二叉树的调整 237 | 238 | #### RR旋转 239 | 240 | image-20220702050945421 241 | 242 | image-20220702051127034 243 | 244 | image-20220702051214039 245 | 246 | #### LL旋转 247 | 248 | image-20220702051315347 249 | 250 | image-20220702051544173 251 | 252 | #### LR旋转 253 | 254 | image-20220702051645639 255 | 256 | image-20220702051833406 257 | 258 | image-20220702052115268 259 | 260 | #### RL旋转 261 | 262 | image-20220702052247419 263 | 264 | image-20220702052510229 265 | 266 | 267 | 268 | ### 小测试 269 | 270 | image-20220702042025929 271 | 272 | ## 小白专场:是否同一棵二叉搜索树 - C实现 273 | 274 | ### 题意理解及搜索树表示 275 | 276 | #### 是否是同一棵二叉搜索树 277 | 278 | 求解思路 279 | 280 | 两个序列是否对应相同搜索树的判别 281 | 282 | 1. **分别建两棵搜索树的判别方法**:根据两个序列分别建树,再判别树是否一样 283 | 284 | 2. **不建树的判别方式** 285 | 286 | 1. image-20220702053319839 287 | 2. 先看根结点也就是第一个数一样吗?再把比根结点小的放左边(顺序不动),比根结点大的放右边(顺序不动),然后再进行对比左右树,上图就是一样的,下图为反面例子 288 | 3. image-20220702053522047 289 | 290 | 3. 建一颗树,再判别其他序列是否与该树一致 291 | 292 | 1. 求解思路 293 | 294 | 2. 搜索树表示 295 | 296 | 3. 建搜索树T 297 | 298 | 4. 判别一序列是否与搜索树T一致 299 | 300 | 5. ```c 301 | 搜索树表示 302 | typedef struct TreeNode*Tree; 303 | struct TreeNode{ 304 | int v;//v来表示基本信息 305 | Tree Left,Right; 306 | int flag;//flag是一个阈(用来判别一个序列是不是跟树一致,实际效果就是如果这个结点没有被访问过flag设为0,被访问过了就设为1) 307 | }; 308 | ``` 309 | 310 | 311 | 312 | ### 程序框架及建树 313 | 314 | #### 程序框架搭建 315 | 316 | ```c 317 | int main() 318 | { 319 | 对每组数据 320 | 1.读入N和L 321 | 2.根据第一行序列建树T 322 | 3.依据树T分别判别后面的L个序列是否能与T形成同一搜索树并输出结果//分别读入L个序列来跟T做比较,看是不是一致的,如果说一致的,他所对应的搜索树是一样的,一样输出yes,不一样输出no 323 | 324 | return 0; 325 | } 326 | 根据上方框架,我们需要设计的主要函数: 327 | 1.读数据建搜索树T//读入N个数据来建我们的搜索树,将来所有序列都会跟这个T去比较,所以以T基准 328 | 2.判别一序列是否与T构成一样的搜索树 329 | int main() 330 | { 331 | int N,L,i; 332 | Tree T; 333 | 334 | scanf("%d",&N); 335 | while(N){//N如果为0输出就结束了 336 | scanf("%d",&L); 337 | T = Make Tree(N);//读入后面N个数来建我们的树T 338 | for(i = 0; i < L;i++ ){//比较是不是跟T一致的这样的序列 339 | if(Judge(T,N))printf("Yes\n");//用函数Judge读入后面的N个数然后跟T去做比较,所以这样Judge有两个参数,一个是树T,一个是序列的个数。通过Judge来判别,如果一致就print yes 反之print no 340 | else printf("No\n"); 341 | ResetT(T);//清除T中的标记flag 342 | } 343 | Free Tree(T);//释放掉树占有的空间,因为已经处理完了准备处理下一组数据了,下一组数据需要的空间可能不需要这么多也可能远远不够 344 | scanf("%d",&N); 345 | } 346 | return 0; 347 | } 348 | ``` 349 | 350 | #### 如何建搜索树 351 | 352 | ```C 353 | Tree Make Tree(int N) 354 | { 355 | Tree T; 356 | int i,V; 357 | 358 | scanf("%d",&V);//读入第一个数,然后把数放到V里面去 359 | T = NewNode(V);//为V构造一个对应的节点,然后赋给T,T只含一个节点 360 | for(i=1;iv = V; 371 | T->Left = T->Right = NULL; 372 | T->flag = 0; 373 | return T; 374 | } 375 | 376 | 怎么把树插到T里面去呢?引用insert这个函数 377 | Tree Insert(Tree T, int V) 378 | { 379 | if(!T)T = NewNode(V);//如果T空了,那就调用NewNode来产生这样一个节点 380 | else{ 381 | if(V > T->v)//如果不空就做比较 382 | T->Right = Insert(T->Right,V); 383 | else 384 | T->Left = Insert(T->Left,V); 385 | } 386 | return T; 387 | } 388 | ``` 389 | 390 | 391 | 392 | ### 搜索树是否一样的判别 393 | 394 | image-20220706083408133 395 | 396 | ```c 397 | 方法:在树T中按顺序搜索序列 398 | 1.如果每次搜索所经过的结点在前面均出现过,则一致 399 | 2.否则(某次搜索中遇到前面未出现的结点),则不一致 400 | 去在T中搜索我们序列中的每一个整数,实际上就是个查找过程 401 | int check(Tree T,int V) 402 | { 403 | if(T->flag){ 404 | if(Vv)return check(T->Left,V); 405 | else if(V->T->v)return check(T->Right,V); 406 | else return 0; 407 | } 408 | else{ 409 | if(V == T->v ){ 410 | T->flag = 1; 411 | return 1; 412 | } 413 | else return 0; 414 | } 415 | } 416 | ``` 417 | 418 | #### 如何辨别 419 | 420 | ```c 421 | //有bug版本 422 | int Judge(Tree T,int N) 423 | { 424 | int i,V; 425 | scanf("%d",&V); 426 | if(V!=T->v)return 0; 427 | else T->flag = 1; 428 | 429 | for(i=1;iv)flag = 1; 442 | else T->flag = 1;//T->flag是结点上的标记,flag是整个的标记,是不一样的 443 | for(i = 1;i < N;i++){ 444 | scanf("%d",&V); 445 | if((!flag)&&(!check(T,V)))flag = 1; 446 | } 447 | if(flag)return 0; 448 | else return 1; 449 | } 450 | 451 | void ResertT(Tree T)//清除T中各结点的flag标记 452 | { 453 | if(T->Left) ResetT(T->Left); 454 | if(T->Right) ResetT(T->Right); 455 | T->flag = 0; 456 | } 457 | 458 | void FreeTree(Tree T)//释放T的空间 459 | { 460 | if(T->Left) FreeTree(T->Left); 461 | if(T->Right)FreeTree(T->Right); 462 | free(T);//递归方法 463 | } 464 | ``` 465 | 466 | 467 | 468 | ## 线性结构之习题讲选[陈越]:Reversing Linked List 469 | 470 | ### 线性结构习题1:什么是抽象的链表 471 | 472 | **链表是一个抽象的数据结构** 473 | 474 | 1. 有块地方存数据 475 | 2. 有块地方存指针——下一个结点的地址(一个抽象的指针指向的就是地址,任何一种形式去存了下一个结点的位置这东西就叫做指针) 476 | 3. image-20220706093854900 477 | 4. image-20220706094019412 478 | 5. 479 | 480 | ### 线性结构习题2:链表逆转算法 481 | 482 | #### 单链表的逆转 483 | 484 | image-20220706094116841 485 | 486 | 第一个加上头结点虽然会浪费一个空间,但是会使后面的操作变为更加简单 487 | 488 | **逆序之后如下:** 489 | 490 | image-20220706094241649 491 | 492 | 接下来是逆序的过程: 493 | 494 | 1. 首先需要对图中定义的词汇进行解释 495 | 496 | 1. new:指的是新的已经逆转好的链表,他的头结点的位置 497 | 2. old: 指的是旧的还没有逆转好的老链表,他的头结点位置 498 | 499 | 2. 我们首先想要把2跟1进行逆转,但是在逆转之前需要先把3这个位置记住,所以设定一个tmp指针去指向3。否则当2的指针一转向,2后面的链表就丢失了 500 | 501 | 1. image-20220706095027756 502 | 503 | 2. image-20220706095101249 504 | 505 | 3. image-20220706095134083 506 | 507 | 4. image-20220706095200258 508 | 509 | 5. ```c 510 | 以下是伪代码实现 511 | Ptr Reverse(Ptr head,)//head是一个指针 512 | { 513 | cnt = 1 514 | new = head->next;//刚开始指向1的 515 | old = new->next;//还没有逆转的那个头结点 516 | while(cnt < K){ 517 | tmp = old->next;//记住下一个的位置 518 | old->next = new;//指针逆转指向新的结点 519 | new = old;old = tmp;//往前位移一段 520 | cnt++; 521 | } 522 | head->next->next = old;//把开头那个空结点指向结尾的(因为逆转了,最开头的现在变为最末尾的了) 523 | return new; 524 | } 525 | //取巧:用顺序表存储,先排序,再直接逆序输出 526 | ``` 527 | 528 | 529 | 530 | ### 线性结构习题3:测试数据 531 | 532 | 1. 有尾巴不反转 533 | 2. 地址取到上下界 534 | 3. 正好全反转 535 | 4. K = N是全反转 536 | 5. K = 1不用反转 537 | 6. 最大(最后剩K-1不反转)、最小N 538 | 7. 有多余结点 539 | 540 | #### 2-6是属于边界测试 541 | 542 | 1. 边界测试 543 | 1. 意思就是给你一个数值,你就一定要取到那个范围上界和下界 --------------------------------------------------------------------------------