王道408---DS---排序
外部排序与内部排序
内部排序指排序期间元素全部存放在内存的排序
外部排序指排序期间元素无法同时存放在内存,必须在排序的过程中根据要求不断地在内、外存之间移动的排序。
插入排序---直接插入排序
算法思想
每遍历到一个元素,就拿这个元素一直向左比较,直到比左边元素小或相等或者直到数组下标为0时终止
算法性能
算法时间复杂度 : O(n) ~ O(n^2) 平均来看是O(n^2)
算法空间复杂度 : O(1)
算法稳定性
算法稳定
由于每次插入元素总是从后向前比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序算法是一个稳定的算法
代码实现
插入排序---折半插入排序
算法思想
该算法与直接插入排序唯一的区别是: 直接插入排序向左比较的时候是逐个比较的,而折半插入排序是借助折半查找完成比较的
所以该算法比较次数少一些,但由于每次循环都要把元素右移,这导致直接插入排序与折半插入排序的时间复杂度差不多
算法性能
时间复杂度: O(n^2)
空间复杂度: O(1)
算法稳定性
稳定算法
实际上,在使用二分法的时候,会产生两种情况:
1、结果左偏
2、结果右偏
如我们要二分获取 值为3的数在该数组的位置 1,2,3,3,4,5
第一种会使获取的结果偏左,也就是得到下标为2的3
第二种会使获取的结果偏右,也就是下标为3的3
我们只需根据比较次序,看情况选出左偏或右偏的代码即可
代码如下:
代码实现
插入排序---希尔排序
算法思想
-
希尔排序本质上是分步长的插入排序,第一轮选择步长 , 则对$ (1,1+dk_11,1+dk_12 \quad ...)$ 、 、等不同的组内进行直接插入排序
-
第二轮选择步长 (必须小于,一般选为 的 一半),进行分组 、 组内进行排序
...
-
直到选择的步长为1时,其组内排序的结果即为最后的排序结果
算法性能
时间复杂度: O(n) ~ O(n^2) 平均 O(n*log^2(n)) 或 O(n^1.5)
空间复杂度: O(1)
算法稳定性
不稳定
当相同关键字记录被划分到不同子表时,可能会改变他们的相对次序
代码实现
交换排序---冒泡排序
经典排序
总感觉是低配版的选择排序
算法思路
-
遍历所有元素,比如遍历到了第一个元素
-
从1到n两两比较相邻元素,若逆序就交换位置
-
比较完第一轮之后进行第二轮,遍历到第二个元素时,两两比较第二个元素与除第一、二元素的所有元素,若逆序则交换元素值,则第二轮遍历结束后,第二个元素上就是第二大的元素
...
倒是有点像简陋版的选择排序,选择排序与冒泡唯一的区别就是,它找到较大的元素时不直接交换,而是先标记,看有没有更大的,直到循环结束才交换,因此算法效率比冒泡排序高不少,不过它是稳定排序算法
算法性能
时间复杂度: O(n^2)
空间复杂度: O(1)
算法稳定性
稳定排序算法
代码实现
交换排序---快速排序
算法思想
快速排序采用的是分治思想,即在一个无序的序列中选取一个任意的基准元素pivot,利用pivot将待排序的序列分成两部分,前面部分元素均小于或等于基准元素,后面部分均大于或等于基准元素,然后采用递归的方法分别对前后两部分重复上述操作,直到将无序序列排列成有序序列
算法性能
时间复杂度: O(nlogn) ~ O(n^2) 平均O(nlogn)
空间复杂度: O(logn) ~ O(n) 平均O(logn)
算法稳定性
不稳定
在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,他们的相对位置会发生变化
代码实现
选择排序---简单选择排序
算法思想
每次循环都取出剩余数组中最小的元素,并采用交换的策略使其交换出剩余数组(这也是该算法不稳定的源头)
算法性能
时间复杂度: 恒为O(n^2)
空间复杂度: O(1)
算法稳定性
不稳定
在第i躺找到最小元素后,和第i个元素交换,可能会使第i个元素与其含有相同关键字元素的相对位置发生改变。如L={2,2,1},经过一趟排序后L={1,2,2},最终排序序列也是L={1,2,2},但位置已经发生了变化
注意,1与2的值的改变是交换改变,而不是直接复写,这就是算法不稳定的根本原因
代码实现
选择排序---堆排序
堆是具有特殊性质的完全⼆叉树
算法思路
不断的输出堆顶元素...
算法性能
时间复杂度: 建堆O(n),堆排序O(nlog2n)
空间复杂度: O(1)
算法稳定性
不稳定
进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。例如,表L={1,2,2}构造初始堆时可能将2交换到堆顶,此时L={2,1,2},最终排序序列为L={1,2,2},显然,2与2的相对次序己发生变化。
代码实现
建堆:
排序:
需要注意的是这个HeadAdjust函数
他每次都是先比较两个孩子结点的大小,然后交换较大的结点(小根堆是交换小的结点)
一直到底(底是指最后一个未排好序的位置)
王道上没有实现堆排序的插入,遂从网上找了一份代码,需要注意的是,堆排序的调整分为了向上调整与向下调整,王道书上只给了向下调整,而堆排序的插入需要用到向上调整
堆排序的插入:
很明显的可以发现,向上调整的时候,是不需要比较兄弟结点的,而向下调整的时候需要比较,想想就能明白。
另外,堆排序的插入,只需一次向上调整即可,因为堆的某一结点一定小于/大于其父/叔结点的!!!
堆排序的插入流程如下:
小结
假设待排序数组长度为n
1、初始建堆时,从[n/2] ~ 1的位置按顺序调用HeadAdjust函数进行建堆
2、删除时,每删除一个元素就从下标为1的地方调用一次HeadJust函数进行向下调整
3、插入时,每插入一个元素,从插入位置调用一次AdjustUp函数进行向上调整
归并排序
算法设计思路
算法本质就是分治后,把两个有序表合并为一个有序表
我觉得没有比这个更清楚了;
算法性能
时间复杂度: O(nlog2n)
空间复杂度: O(n)
算法稳定性
稳定算法
代码实现
基数排序
算法设计思想
通常有两种方法:
1、最高位优先(MSD)
按关键字权重递减依此排序
2、最低位优先(LSD)
按关键字权重递增依此排序
权重是指个位、十位、百位之类的
下面以LSD为例,其中基数r = 10 ,权值为0,1,2
需要注意的是,我们每趟排序包含两种操作,第一种是分配,第二种是收集
分配需要O(n)的时间复杂度,收集需要O(r),故每趟需要O(n+r)的时间复杂度
或者我们也可以使用下面这个比较清晰直观的图来理解
算法性能
时间复杂度: O(d(n+r)) 其中r代表基数,d代表趟数,也代表权重
空间复杂度: O(r) 需要r个辅助队列
算法稳定性
稳定
内部排序算法总结
需要注意的是,希尔排序的最好情况与最坏情况无法判断
比较次数与序列初态有关的算法
1、快速排序
快速排序 的排序趟数就是它的递归深度。当 快排 的数据是有序时候,会退化为冒泡排序,所以快排趟数也与初始序列顺序有关了
2、冒泡排序
其主要优化就是记录了前一趟是否冒泡,如果没有产生冒泡就说明数组已经有序,直接return
。如果产生了冒泡,才继续执行
3、直接插入排序
如果全部有序,则只需要遍历一趟就完成了排序,比较次数为 n-1
,并且在这个过程中没有发生元素的移动。因此,比较次数 与序列初态 有关 。初始序列基本有序时,移动元素最少(效率最高)
简单插入排序随着数据变成正序时,执行效率最好,每次插入都不用移动前面的元素,时间复杂度为O(N)。当数据是反序时,执行效率最差,此时时间复杂度为O(N*N).
4、希尔排序
因为其内部使用了插入排序,所以原因如上
5、堆排序
比如元素下沉的操作,虽然一个元素是从底部拉上来的,但这不代表这个元素一定会接着沉到底部,如果沉到中间就停止下沉的话,比较次数就少了。而这个过程的比较次数自然和下沉的深度是相关的。
比较次数与序列初态无关的算法
1、二路归并排序
2、简单选择排序
3、基数排序
排序趟数与初态有关的算法
1、冒泡排序
直接看源码就知道了
2、快速排序
这俩都是交换排序捏
排序趟数与初态无关的算法
1、直接插入排序
2、折半插入排序
3、希尔排序
4、简单选择排序
5、归并排序
6、基数排序
算法稳定性
选择排序、希尔排序、快速排序、堆排序都是不稳定的
速记: 堆选希块
外部排序
将待排序的记录存储在外存上,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间的交换的方法称为外部排序。
外部排序一般使用归并算法
外部排序的总时间=内部排序所需的时间+外存信息读写的时间+内部归并所需的时间
一般来说,外存信息读写的时间远大于内部排序和内部归并的时间,因此应着力减少I/O次数。
下面我们先介绍归并方法,再介绍如何进行优化
外部排序的方法
以二路归并为例:
(自己写了好久,到最后发现还是书上原来的例子好,那就直接copy过来吧)
优化一---增加归并路数k
显然,外存信息读写的时间远大于内部排序和内部归并的时间,因此应着力减少/O次数。由于外存信息的读/写是以“磁盘块”为单位的,可知每一趟归并需进行16次“读”和16次“写”,3趟归并加上内部排序时所需进行的读/写,使得总共需进32×3+32=128次读写。
若改用4路归并排序,则只需2趟归并,外部排序时的总读/写次数便减至32×2+32=96。因此,增大归并路数,可减少归并趟数,进而减少总的磁盘I/O次数,
优化二---败者树---消除增加k(归并路数)带来的副作用
增加归并路数k能减少归并趟数S,进而减少I/O次数。然而,增加归并路数k时,内部归并的时间将增加。做内部归并时,在k个元素中选择关键字最小的记录需要比较k-1次。每趟归并n个元素需要做(n-1)(k-1)次比较,S趟归并总共需要的比较数为
因此内部归并时间亦随k的增长而增长。这将抵消由于增大k而减少外存访问次数所得到的效益。因此,不能使用普通的内部归并排序算法。
下面引入败者树
败者树
它是一棵完全二叉树 ,可以快速得到n个数中最小的元素,在n个记录中选择最小的关键字,最多需要 次
内部结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。若比较两个数,大的为失败者、小的为胜利者,则根结点指向的数为最小数。
优化三---置换选择排序---减少初始归并段的个数r---生成初始归并段(大小不等的归并段)
显然,减少初始归并段的个数也可以减少归并趟数S
上面采用内部排序方法得到的各个初始归并段长度都相同(除最后一段外),它依赖于内部排序时可用内存工作区的大小。因此,必须探索新的方法用来产生更长的初始归并段,这就引出了置换选择排序
参考:
另外,若我们的工作区较大(n),每次比较n个元素都会耗费很长的时间,为此,我们可以引入败者树来提高寻找最小数的时间
优化四---最佳归并树(m叉哈夫曼树)---组织长度不等的初始归并段的归并顺序
假设由置换-选择得到9个初始归并段,其长度(记录数)依次为9,30,12,18,3,17,2,6,24。现做3路平衡归并,其一般归并树:
(倒过来看更好
其I/O次数=2xWPL = 484 (好像没有算置换选择排序时的I/O次数,不过无所谓,毕竟下面的哈夫曼树也没算这一步)
下面将第4章中哈夫曼树的思想推广到m叉树的情形,在归并树中,让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,就可以建立总的I/O次数最少的最佳归并树。上述9个初始归并段可构造成一棵如下图所示的归并树,按此树进行归并,仅需对外存进行446次读/写,这棵归并树便称为最佳归并树 (由此看来,最佳归并树是哈夫曼树的一种,且是"最佳"哈夫曼树)。
上图中的哈夫曼树是一棵严格3叉树,即树中只有度为3或0的结点。若只有8个初始归并段,如上例中少了一个长度为30的归并段。若在设计归并方案时,缺额的归并段留在最后,即除最后一次做2路归并外,其他各次归并仍是3路归并,此归方案的外存读/写次数为386。显然,这不是最佳方案
正确的做法是:若初始归并段不足以构成一棵严格k叉树时,需添加长度为0的“虚段”,按照哈夫曼树的原则,权为0
的叶子应离树根最远。因此,最佳归并树应如下图所示。
小总结
外部排序时间 = 内部排序所需的时间+外存信息读写的时间+内部归并所需的时间
理想状态下的外部排序= 较大的k路归并 + 较少的初始归并个数r
具体化一下就是 : 败者树(减少多路k中内部排序的时间)+置换选择排序(减少r) + 哈夫曼树(优化比较顺序)
需要注意的是
1、败者树是完全二叉树
2、最佳归并树是哈夫曼树的一种,它是"最优的" 哈夫曼树
3、实际在置换选择排序中使用败者树的时候与常规的败者树略有不同:
https://c.biancheng.net/view/3454.html
__EOF__

本文链接:https://www.cnblogs.com/lordtianqiyi/p/17777119.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南