8.1 排序的基本概念
排序定义

排序算法的评价指标

排序算法的分类

总结
 https:/www.cs.usfca.edu/~galles/visualization/Algorithms.html
8.2-1 插入排序
插入排序举例

插入排序算法实现
插入排序一般实现
  
插入排序带哨兵实现

算法效率分析
空间复杂度  时间复杂度   下图:比如第一趟:查找70,对照着对待,A[2]=70<A[1]=80,对比关键字一次,A[0]=A[i]=70,移动元素一次,for循环,A[0]=70<A[1]=80,又对比关键字一次,A[2]=A[1]=80,又移动元素一次,最A[1]=A[0]=70,又移动元素一次。共对比关键字2次,移动元素3次  平均时间复杂度O(n2) 
优化—折半插入排序
  
8.2-2 希尔排序
希尔排序示例
第一趟:     第二趟:    第三趟:  
希尔建议设置每次将来增量缩小一半

算法实现
下图只是实现希尔排序的一种方法,但是这种方法并不是按照上面的思路:先分成多个子表,每个子表排完序再放回数组,更换d的大小,继续这样的操作。但下图的方法仍然能实现希尔排序。由于时间原因暂时没能实现上述思路的希尔排序的代码。 下图方法的思路是:逐步的遍历每个子表,在逐步遍历过程中使得每个子表都更有序一点,循环往复。   第一趟分别比较76和49,13和39,27和65,49和97. 第一趟结束时,第二趟开始时如下:  第二趟:d=4/2=2,从27开始比较,++i,顺次比较49和之前的13,比较76跟之前的27,38和之前的49,65和之前的76,97和之前38。  第二趟结束时,第三趟开始时如下:  第三趟结束时如下 
算法性能分析
  因为需要用增量d来快速的找到与之相邻的从属于同一个子表的元素,所以只有具有随机访问的特性才能完成这个事情。所以必须用顺序表。
8.3-1冒泡排序

算法实现
升序排列,从后往前冒泡,将小的值冒泡到前面。 当然也可以从前往后冒泡。  
算法性能分析

冒泡排序适用于链表

8.3-2 快速排序
快速排序算法思想

快速排序代码

算法效率分析
时间复杂度
 下面的分析是不严谨的,因为比较一次元素可能带来元素的移动,也可能不带来元素的移动,这里我们按照比较一次元素就按O(1)计算。那么比如下图这个初始序列,要比较每一个元素,所有我按O(n)处理。 
空间复杂度
QuickSort()函数在执行时,不断地递归会占用多层递归工作栈,所以这里空间复杂度和递归层数有关。空间复杂度=O(递归层数)

把快速排序过程梳理成二叉树
       
最坏的情况
若给的序列是有序的,那么每次划分都很不均匀 
比较好的情况
优化思路②随机选一个元素作为枢轴元素:因为是随机的,所以不太可能每次都选到最大的或者最小的,所以划分比较均匀。 
实际应用中
在实际应用当中,快速排序算法的平均时间复杂度其实要接近于最好的时间复杂度,而不是接近最坏,所以在实际应用当中,快排这个算法是所有内部排序算法当中平均性能最优的排序算法。 
稳定性
  1比2小,转移到数组编号0的位置。    
总结

8.4-1简单选择排序
 下图有两个49,当49是最小的时,我们优先将最前面的49移动到头部的位置使之有序。 
算法性能分析
n个元素的简单选择排序需要n-1趟处理。 
稳定性分析

8.4-2 堆排序
什么是堆?
 回忆二叉树的顺序存储 大根堆  小根堆 
如何基于堆进行排序
 建立大根堆  从n/2 的位置 8/2即4的位置开始逐个往前检查。 9不大于他的左孩子32,互换位置。  互换后符合大根堆  检查78,78小于右孩子87,互换位置。  交换位置后符合大根堆  检查17,17小于右孩子45,互换位置  交换位置后符合大根堆  检查53,53小于其右孩子87  交换位置后,53引起了其下层子树不符合大根堆要求,  采取同样的方式检查53,53小于其右孩子78,交换位置。  调整完成 
建立大根堆的代码
 这里我们直接快进到代码执行到53这个元素。        
基于大根堆进行排序
将87与待排序序列的最后一个元素9交换  87和09交换完成,87不需要再移动  将待排序元素序列再次调整为大根堆  小元素09下坠第一次  小元素09下坠第二次  经过调整后待排序元素序列再次构成一个“大根堆”  将78跟待排序序列的最后一个元素交换。  53被交换到第一个元素的位置  53下坠一次,由于78已经被排好序了,不在待排序序列中了,所以不用在比较53和78了,下图虚线的就不用再比较了。  经过调整后待排序元素序列再次构成一个“大根堆"  将65和待排序序列中的最后一个元素交换。  9被换到第一个位置,继续小元素下坠   将53与待排序序列中的最后一个元素交换,  将17进行下坠  17下坠一次,还没形成大根堆  17下坠第二次,形成大根堆  将45和17进行交换,  17下坠一次,形成大根堆  将来32和9交换,  9下坠一次形成大根堆  将17和待排序元素序列中的元素9交换,只剩下一个元素9,不用再调整。  
基于大根堆进行排序(代码)
 
算法效率分析
 下方有两个孩子,则“下坠”一层,需对比左右孩子各1次  下方只有1个孩子,则“下坠”一层,需对比左孩子1次。i<len说明没有右孩子。 
时间复杂度1:建堆的过程(每个关键字都可能下坠)
第1层有1个结点,最多下坠(h-1)层,每个结点最多对比关键字不超过2(h-1) 第2层有2个结点,最多下坠(h-2)层,每个结点最多对比关键字不超过2(h-2),第二层最多对比关键字不超过22(h-2), 第h层不下坠, 第h-1层有2的h-2次方个结点,最多下坠1层,每个结点最多对比关键字不超过2,第二层最多对比关键字不超过22的h-2次方,  
时间复杂度2:每一趟调整的过程(由于已经建完堆了,双亲结点大于孩子结点,所以将一个元素下坠,无需担心被调整上去的元素造成大根堆的破坏,只需要考虑下坠元素即可,而下坠元素最多下坠次数 不会超过树的高度h)

空间复杂度:只用到了几个 i 这样的变量,所以是常数级O(1)

稳定性分析
  2杠和最后一个2交换,  未破坏大根堆,将第一个2和1交换,  还剩一个元素1,算法结束。结果显示是不稳定的。 
8.4-3 堆的插入删除
插入
插入13
 13和32比较一次,上升一层,  13和17比较一次,上升一层,17和9比较一次,17>9,无法上升,程序停止。共比较了3次关键字。 
插入46

删除
 删除13,  用堆底元素46替代被删除元素的位置。  46下坠第一次,比较左右孩子各一次  46下坠第二次,比较左右孩子各一次。以上共对比关键字次数4次 
关键字对比次数

8.5-1 归并排序
什么是归并?
 
2路归并和4路归并
  
代码实现Merge函数
 归并排序有稳定性   
代码递归实现MergeSort

算法效率分析
第一次归并对比关键字次数3次,相当于n/2,是O(n)数量级。 第三次归并:每对比关键字一次,就能挑出一个当前还剩余的更小的元素。最多进行n-1次关键字的对比,就能完成一趟归并。 所以 不管是那一次的归并,都是O(n)的数量级的时间。 
8.5-2 基数排序
基数排序示例
第一趟
  靠近队头的连在前面,靠近队尾的连在后面。 
第二趟
  
第三趟
  
基数排序总体顺序

基数排序得到递减序列的过程

基数排序得到递增序列的过程

算法效率分析
每新增一个队列就是新增两个指针域的空间。 下图r就是10,代表0-9 10个辅助队列, d就是关键字能被拆成几个部分。 每次分配其实就是从头到尾把链表元素扫一遍,有n个元素,扫一遍需要有O(n)的时间。 一次收集就是逐个的扫描这些队列,把这些队列上的元素都给拆下来,合成一条新链表。  收集一个队列只需O(1)的时间,如下图, 后,这一队列的全部元素就串到下面的链表中了,只需要O(1)的时间。 
稳定性分析

8.7-1 外部排序
外部排序使用的是归并排序
磁盘的读/写以“块"为单位数据读入内存后才能被修改修改完了还要写回磁盘。 “归并排序”要求各个子序列有序,每次读入两个块的内容,进行内部排序后写回磁盘。所以先进行一次内部排序将各个初始归并块有序,后面才能进行归并排序。 若要进行k路归并排序,则需要在内存中分配k个输入缓冲区和1个输出缓冲区。 
时间开销分析
读写次数是主要消耗时间的步骤。 “归并排序”要求各个子序列有序,所以先进行一次内部排序使初始归并块有序,然后才能进行归并排序。 
两个优化方法
优化1:多路归并
若要进行k路归并排序,则需要在内存中分配k个输入缓冲区和1个输出缓冲区。 下图是4个输入缓冲区和1个输出缓冲区  
优化2:减少初始归并段数量
 每个初始归并段越长,初始归并段的个数r就越少,归并趟数就越少,读写磁盘的次数就越少。 
总结
若要进行k路归并排序,则需要在内存中分配k个输入缓冲区和1个输出缓冲区 
多路平衡归并
 
8.7-2 败者树
问题引入
8路平衡归并,从八个归并段中选出一个最小元素需要对比关键字7次,每次都要对比很多元素,造成时间的大量消耗。 
败者树的构建

败者树在多路平衡归并中的应用
   将归并段3的这个最小的元素1 弹出,进入归并序列。  从归并段3弹出一个元素填补空位置。   有了败者树,选出最小元素,只需对比关键字[log2(k)]次。 k路归并,就是叶子节点是k个。 下图树高4,要对比关键字3次,那么树高h,要对比关键字h-1次。 
败者树的思路实现
 对于k路归并,第一次构造败者树需要对比关键字k-1次,有了败者树,除了第一次构造败者树需要对比关键字k-1次外,其他的时候选出最小元素,只需对比关键字[log2(k)]次。  
8.7-3 置换_选择排序
传统方法的初始归并段数量计算方法如下,可用“置换-选择排序”进步减少初始归并段数量

用于内部排序的内存工作区WA可容纳 l 个记录,则每个初始归并段也只能包含 l 个记录,若文件共有n个记录,则初始归并段的数量 r = n / l  用于内部排序的内存工作区WA可容纳 6 个记录,则每个初始归并段也只能包含 6 个记录。 
可用“置换-选择排序”进步减少初始归并段数量(示例)
生成归并段的过程

生成归并段1
初始待排序文件FI在磁盘中, 初始待排序文件FO在磁盘中, 内存工作区WA在内存中。                         
生成归并段2
  循环往复, 
生成归并段3
  循环往复,  算法结束。
8.7-4 最佳归并树
归并树的性质
   构建哈夫曼树,首先要挑选权值最小的两个结点使之成为兄弟。 
多路归并的情况
第一个归并,得到蓝色的数字,读或写的次数为51+38+32=121, 第二次归并,得到红色的数字,这次读或写的次数121, 两次一共读或写的次数为242。 
多路归并的最佳排序树
 每次都选择3个最小的进行归并。 
不是多路归并的最佳排序树
 下图注意:k叉归并的最佳归并树一定是严格k叉树,即树中只有度为k、度为0的结点     
添加虚段的数量
 
|