本篇博客是考研期间学习王道课程 传送门 的笔记,以及一整年里对数据结构知识点的理解的总结。希望对新一届的计算机考研人提供帮助!!!
关于对 第八章 排序
知识点总结的十分全面,涵括了《王道数据结构》课程里的全部要点(本人来来回回过了三遍视频),其中还陆陆续续补充了许多内容,所以读者可以相信本篇博客对于考研数据结构 “排序” 章节知识点的正确性与全面性;
但如果还有自主命题的学校,还需额外读者自行再观看对应学校的自主命题材料。
数据结构与算法
笔记导航
- 第一章 绪论
(无)
- 第二章 线性表
- 第三章 栈和队列
- 第四章 串-KMP(看毛片算法)
- 第五章 树和二叉树
- 第六章 图
- 第七章 查找(B树、散列表)
- 第八章 排序 (内部排序:八大排序动图演示与实现 + 外部排序)
⇦当前位置
-
数据结构与算法 复试精简笔记 (未完成)
- 408 全套初复试笔记汇总 传送门
如果本篇文章对大家起到帮助的话,跪求各位帅哥美女们,求赞 、求收藏 、求关注!
你必考上研究生!
我说的,耶稣来了也拦不住!
精准控时:
如果不实际操作代码,只是粗略过一下知识点,需花费 70 分钟左右过一遍
这个70分钟是我在后期冲刺复习多次尝试的时间,可以让我很好的在后期时间紧张的阶段下,合理分配复习时间;
但是刚开始看这份博客的读者也许会因为知识点陌生、笔记结构不太了解,花费许多时间,这都是正常的。
重点!!!学习一定要多总结多复习!重复、重复、再重复!!!
食用说明书:
第一遍学习王道课程时,我的笔记只有标题和截图,后来复习发现看只看图片,并不能很快的了解截图中要重点表达的知识点。
所以再第二遍复习中,我给每一张截图中标记了重点,以及每张图片上方总结了该图片对应的知识点以及自己的思考。
最后第三遍,查漏补缺。
所以 ,我把目录放在博客的前面,就是希望读者可以结合目录结构去更好的学习知识点,之后冲刺复习阶段脑海里可以浮现出该知识结构,做到对每一个知识点熟稔于心!
请读者放心!目录展示的知识点结构是十分合理的,可以放心使用该结构去记忆学习!
注意(⊙o⊙)!,每张图片上面的文字,都是该图对应的知识点总结,方便读者更快理解图片内容。
第8章 排序
文章目录
- 第8章 排序
-
- 8.1 排序的基本概念
-
- 8.1.1 排序的定义
- 8.1.1 排序的定义小结
- 8.2 插入排序
-
- 8.2.1 直接插入排序
-
- 8.2.2 折半插入排序
- 8.2.1 小结
- 8.2.3 希尔排序
-
- 1、算法思想
- 2、具体实现
- 3、算法性能 - 不适应于链表
- 8.3 交换排序
-
- 8.3.1 冒泡排序
-
- 1、算法思想
- 2、算法实现
- 3、拓展 - 适用链表
- 8.3.1 冒泡排序小结
- 8.3.2 快速排序
-
- 8.3.2 快速排序小结
- 8.4 选择排序
-
- 8.4.1 简单选择排序
-
- 1、算法思想
- 2、算法实现
- 3、算法性能分析 - 好坏都O(n^2)
- 8.4.1 简单选择排序小结
- 8.4.2 堆排序
-
- 1、算法思想
- 2、建立大根堆
- 3、建立大根堆的具体代码
- 4、基于大根堆的排序过程
- 5、完整的堆排序代码
- 6、算法效率分析
- 8.4.2 堆排序小结
- 8.4.3 堆的插入与删除
-
- 8.4.3 堆的插入与删除小结
- 8.5 归并排序和基数排序
-
- 8.5.1 归并排序
-
- 8.5.1 归并排序小结
- 8.5.2 基数排序
-
- 1、算法思想
- 2、算法效率分析
- 3、基数排序的应用
- 8.5.2 基数排序小结
- 8.7 外部排序
-
- 8.7.1 外部排序的基本概念
- 8.7.2 外部排序的方法
- 8.7.2 外部排序小结
- 8.7.3 败者树
-
- 8.7.3 败者树小结
- 8.7.4 置换-选择排序(生成初始归并段)
-
- 1、外部排序的局限性
- 2、置换-选择排序(选择最小或最大元素置换出去)
- 8.7.4 置换-选择排序小结
- 8.7.5 最佳归并树
8.1 排序的基本概念
8.1.1 排序的定义
8.1.1 排序的定义小结
- 在学习下面的各种排序方法之前,我想说的是,在你学习的过程中,你需要养成一种习惯,每学习一种排序方法,你需要注意那些关键指标呢?
- 时间、空间复杂度(最好、最坏情况)
- 稳定性
- 比较次数、交换次数
8.2 插入排序
8.2.1 直接插入排序
1、算法思想
2、算法效率分析
// ! 直接插入排序
void InsertSort(int arrList[], int n)
{
int temp, i, j;
for (i = 1; i < n; i++) // ! 有a[0],下图代码是从a[1]开始的
{
if (arrList[i] < arrList[i - 1])
{
temp = arrList[i];
for (j = i - 1; j >= 0 && arrList[j] > temp; --j)
{
arrList[j + 1] = arrList[j];
}
arrList[j + 1] = temp;
}
}
}
3、优化
- 下面当mid60low==high时,为了保证算法的稳定性,折半查找还是要接着运行,下一步之后就会low
- 看了三个例子,你就会发现,当折半查找结束之后,你就会发现,low会比high大1,high会在要插入位置的后面一个位置
8.2.2 折半插入排序
- 折半插入排序还是插入排序,不是新的排序算法。时间复杂度还是O(n^2)
8.2.1 小结
8.2.3 希尔排序
1、算法思想
- 第一趟:d为4(n/2),分成n/2个子表,每个子表里直接插入排序
- 第二趟,d=2,分成2个子表,各子表在直接插入排序
- 第三趟,d=1,就相当于对整个表进行插入排序,而此时数据表处于一个基本有序的状态,所以插入排序的效率很可
2、具体实现
- 直接插入排序也是从第二个元素开始循环,所以下面i=d+1
3、算法性能 - 不适应于链表
- 不稳定,因为划分区间后,各区间进行插入排序,可能使得后面关键字相同的元素跑到前面去
- !!!仅适应于顺序表,不适应于链表
8.3 交换排序
8.3.1 冒泡排序
1、算法思想
- 在某一趟循环里没有发生元素交换,说明已经有序,不需要再循环了。
2、算法实现
3、拓展 - 适用链表
8.3.1 冒泡排序小结
8.3.2 快速排序
1、算法思想
- 上一小节里,冒泡排序之后每次循环之后,就会确认当前循环内最大(小)d额元素,并且使其到对应位置。
- 快速排序,在每轮循环里,都确认了 “枢轴元素” 的对应位置
- ① 先选择49为基准(枢轴)元素,进行一次元素划分。
- ② 设置好low,high指针
- ③ 因为设置49为枢轴元素,所以位置0为空,故low所指向的位置0空,high先遍历
- ④ high遇到49,不比49小,high–。然后high遇到27,小于49,将27送到low指向的空位置,初始high指向的位置6空了,low开始遍历。
- ⑤ 一直遍历到low、high相遇,此时将枢轴元素49放到该位置。第一轮快排划分结束。
- ⑥ 各小区间再进行各自的快排划分
2、具体实现
- 注意代码细节: low < high,在折半查找里是: low <= high
3、算法效率分析
- 每一层的时间复杂度不超过O(n),最麻烦的也就是第一层对n-1个元素分区间,第二层就n-1-2个元素,第三层n-1-2-4
- 快排适用于,每次划分结束,可以分出两个均匀的子序列的情况
8.3.2 快速排序小结
8.4 选择排序
8.4.1 简单选择排序
1、算法思想
2、算法实现
- 一共n-1趟,选出n-1个最小的,剩下那个不就是最大的嘛,所以不需要n趟
3、算法性能分析 - 好坏都O(n^2)
- 无论元素是否有序,最好,最坏时间复杂度都是O(n^2)
- 学习到目前位置,不稳定算法:希尔,快排,简单选择
- 稳定算法:插入排序,冒泡排序
8.4.1 简单选择排序小结
8.4.2 堆排序
1、算法思想
2、建立大根堆
- 从数组(n/2)位置开始往左扫描,不满足条件的结点,将该结点与更大的一个孩子互换。
- 也就是说下图从arr[4]开始到arr[1]进行扫描;不满足什么条件呢?—— 根 > 左、右
- 为什么是n/2呢?因为大于(n/2)位置的结点,是叶子结点,不用管。只看分支节点
3、建立大根堆的具体代码
4、基于大根堆的排序过程
- 核心思想:每一趟循环都是在待排序元素中选取关键字最大的元素加入有序子序列
- 对于大根堆来说,待排序元素中关键字最大的元素就是堆顶元素
-
在我解释下面大根堆的排序过程之前,我要先点名两个点
- 1、下图的数组表是真实的存储结构
- 2、下图的大根堆是我们自己想的逻辑结构(两者要区分开)
- 3、我们建立大根堆的结果,就是下图中的数组表,那个树形结构的大根堆只是便于我们理解
-
开始:
-
① 将堆顶元素和待排序序列中最后一个元素交换,也就是87和09交换。(看上面的图,下图是调整完毕的大根堆)
- 在数组表里,就只是arr[1]和arr[8]交换了元素
- 在树形大根堆里,09跑到了树根,87去了09原来的位置。
-
完成①之后,所达到的效果:1、大根堆失效了,需要调整。2、找了待排序序列中最大的元素,加入了有序序列
-
② 调整大根堆,09不断下坠,78成为新堆顶。
HeadAdjust(int A[], int k, int len); // 此时len的值取8-1=7
- ③ 交换堆顶元素和“待排序序列”中最后一个元素,也就是78和53交换。
- ④ 调整大根堆
5、完整的堆排序代码
#include
using namespace std;
// ? 下面两个函数是用来建立大根堆的
// ! 将以 k 为根的子树调整为大根堆
void HeadAdjust(int arrList[], int k, int len)
{
arrList[0] = arrList[k]; // ! a[0]暂存子树的根结点
for (int i = 2 * k; i <= len; i *= 2)
{
if (i < len && arrList[i] < arrList[i + 1]) // ! 有时可能只有一个子结点,所以i= arrList[i]) // ! 当子树根结点大于子结点时,筛选结束
{
break;
}
else
{
arrList[k] = arrList[i]; // ! 将a[i]调整到双亲结点上
k = i; // ! 修改k值,以边继续向下筛选
}
}
arrList[k] = arrList[0]; // ! 被筛选的点的值被放入最终的位置
}
// ! 建立大根堆
void BuildMaxHeap(int arrList[], int len)
{
// ! i=len/2,此时i就指向最后一个非终端结点
for (int i = len / 2; i > 0; i--) // ! 从后往前去调整所有非终端结点
{
HeadAdjust(arrList, i, len);
}
}
// ! 堆排序
void HeapSort(int arrList[], int len)
{
BuildMaxHeap(arrList, len); // 初始建堆
for (int i = len; i > 1; i--) // n-1趟的 交换 和 建堆过程
{
swap(arrList[i], arrList[1]); // 堆顶元素 和堆底元素 交换
HeadAdjust(arrList, 1, i - 1); // 把剩余的待排序元素整理成堆
}
}
int main()
{
int a[] = {0, 38, 49, 65, 97, 76, 13, 27, 49};
HeapSort(a, 8);
cout << "数组的元素个数: " << sizeof(a) / 4 << endl;
for (int i = 1; i <= 8; i++)
{
cout << "a[" << i << "]: " << a[i] << " ";
}
return 0;
}
6、算法效率分析
- 下图是关键,对比两次:
- 1、两个孩子之间对比,找到更大的那个
- 2、更大的那个孩子和根元素对比
8.4.2 堆排序小结
8.4.3 堆的插入与删除
1、插入
- ① 新元素放表尾
- ② 新元素和其父节点对比,满足条件上升
- ③ 重复②的操作,直到无法上升
2、删除
- ② 46不断下坠调整,直到无法下坠(选择更小的结点)
8.4.3 堆的插入与删除小结
8.5 归并排序和基数排序
8.5.1 归并排序
1、算法思想
2、代码实现
3、算法效率分析
- 树高h,归并h-1趟
- 空间复杂度还受递归深度的影响,也就是树高,但还是小于O(n),所以空间复杂度取O(n)
8.5.1 归并排序小结
8.5.2 基数排序
1、算法思想
2、算法效率分析
3、基数排序的应用
8.5.2 基数排序小结
8.7 外部排序
8.7.1 外部排序的基本概念
- 磁盘容量大,内存容量小,需要对磁盘内的数据进行排序
- 借用归并排序的思想
8.7.2 外部排序的方法
- 初始构造“归并段”:进行归并排序的要求就是两个序列有序,所以初始化的目标就是使得每一个要归并的各数据块内的内容有序
过程原理:根据输入缓冲区的数量,例如上面数量为2;那么每次都可以读入2块的内容,把这两块内容在内存中进行内部排序,再写入磁盘。
- 上面的步骤是外部排序的准备条件,构建基本有序的归并段
- 接下来开始归并排序
8.7.2 外部排序小结
8.7.3 败者树
1、算法思想
2、算法实现
8.7.3 败者树小结
8.7.4 置换-选择排序(生成初始归并段)
1、外部排序的局限性
- 土办法的局限性:
- 由下面的式子可以知道,初始归并段的数量由内存工作区的大小来决定
2、置换-选择排序(选择最小或最大元素置换出去)
- 内存工作区现在规定只能容纳3个位置。(假设要构造递增的归并段)
- ① 先把内存工作区填满:4 6 9,把最小的记录输出加入到归并段中,并用关键字MINIMAX记录出去的关键字4
- ② 待排序记录补上,也就是7去了4的位置,此时最小元素6输出加入归并段,MINIMAX为6
- ③ 13在补上6的空位,依次类推……
- 此时10 < MINIMAX==13,故输出第二小的元素14
- 此时内存工作区的三个元素都比MINIMAX更小,第一个归并段的构造结束(注意!每次对比都是和MINIMAX进行的)
- 置换-选择排序优点:初始归并段的长度可以超过内存工作区的限制,下面例子的归并段长度都超过了3
- 归并段的长度越长,那么归并段的数量r就越小
8.7.4 置换-选择排序小结
- 内存读写数据都是以块为单位的。
- 上面置换-选择排序的步骤看似是输出一个,读入一个,但是实际I/O都是凑齐一个块的大小在进行的。
- 这里解释一下,避免产生误解
8.7.5 最佳归并树
- 上一小节里使用置换-选择排序构造初始归并段,各归并段的长度也许是各不相同的
- 在使用2路以上的归并时,归并段数量并不能保证每次都刚好就是最佳归并树要求的数量,例如下图
- n0 == 初始归并段+虚段 == 最佳排序树的叶子结点(都是度为0的结点)
- 下面的式子除得尽才表示是“严格的”k叉树