8、排序

基于比较的排序,n个关键字,比较次数为 log2(n!)\lceil \log_2(n!) \rceil

1、插入排序

  • 每次把一个元素插入到前面的已排序序列中

  • 可以用折半查找快速找到插入位置

    • 对当前插入节点前面的顺序队列使用折半查找

    • 先确定位置再插入

//直接插入排序
void InsertSort(int A[],int n){
    int i,j, temp ;
    for(i=1;i<n;i++){		//将各元素插入已排好序的序列中
        if(A[i]<A[i-1]){	//若A[i]关键字小于前驱
        	temp=A[i];		//用temp暂存A[i]
            for(j=i-1;j>=0 && A[j]>temp;-- j){ //检查所有前面已排好序的元素
                A[j+1]=A[j];//所有大于temp的元素都向后挪位
            }
            A[j+1]=temp;	//复制到插入位置
        }
    }
}
  • 空间复杂度:O(1)O(1),使用常数辅助单元

  • 时间复杂度:O(n2)O(n^2)

    • 最好情况:O(n)O(n)

      • 本身正序

      • 比较次数:n1n-1,只需要各与前面一个比较一次

      • 无需移动

    • 最坏情况:O(n2)O(n^2)

      • 本身倒序

      • 比较次数:n(n1)/2n(n-1)/2,不带哨兵,若带哨兵则每轮多一次与哨兵的比较

      • 移动次数:n(n1)/2n(n-1)/2,不带哨兵,若带哨兵则每轮多两次存取哨兵的值

    • 折半插入排序:O(n2)O(n^2)

      • 比较次数少了,但是移动次数不变

      • 比较次数:O(nlog2n)O(n\log_2n)

  • 稳定

  • 可用于链表,但不再能使用折半查找确定插入位置

2、希尔排序

  • 按照不同的间隔分为几个子表

  • 对每个子表进行插入排序

  • 间隔每轮缩短一次

    • 距离每次缩小一半

    • 表现为每次的子表中元素数量为2、4、6……

  • 直到间隔变为1

  • 空间复杂度:O(1)O(1),使用常数辅助单元

  • 时间复杂度:最坏情况下为O(n2)O(n^2)

  • 不稳定

  • 不适用于链表

3、冒泡排序

  • 最后开始,相邻的两两对比

    • 决定是否交换顺序

  • 每次确定最小的一个(即最前面的一个)

  • 下一次对比不再管已经对了的

  • 共需要n-1次冒泡

  • 空间复杂度:O(1)O(1),不使用辅助单元

  • 时间复杂度:O(n2)O(n^2)

    • 最好情况:O(n)O(n)

      • 本身有序

      • 比较次数:n1n-1

      • 无需移动

    • 最坏情况:O(n2)O(n^2)

      • 本身逆序

      • 比较次数:n(n1)/2n(n-1)/2,每个数与前面的比较一次

      • 移动次数:3n(n1)/23n(n-1)/2,每次交换需要3次赋值操作

  • 稳定

  • 可用于链表

4、快速排序

  • 每一趟确定一个点的最终位置

  • 左边全部比他小,右边全部比他大

  • 将目标点的位置腾空,之后low和high哪一个指向不空的就移动哪一个

  • 对左右两部分再次做如上处理

快速排序

快速排序:

划分左右子表:

递归次数:

快排递归次数

递归次数=二叉树的深度 (log2n1,n)\in (\log_{2}n -1,n)

  • 当表本身有序或逆序时,效率最低

  • 当每次的关键字都把表几乎等分时,效率最高

  • 空间复杂度:O(递归层数)

    • 最好空间复杂度:O(log2n\log_{2}n)

    • 最坏空间复杂度:O(n)

  • 时间复杂度:O(n*递归层数)

    • 最好时间复杂度:O(nlog2nn\log_{2}n)

    • 最坏时间复杂度:O(n2n^{2})

5、简单选择排序

  • 每一次扫描未排序队列,找到其中最小的

  • 将最小的移动到开头

  • 下一次再遍历新的未排序队列

  • 只剩下一个时代表排序结束

  • 空间复杂度:O(1)O(1)

  • 时间复杂度:O(n2)O(n^2)

  • 不稳定

6、堆排序

  • 堆:属于完全二叉树(顺序存储的完全二叉树)

    • 小根堆:根 ≤ 左、右

    • 大根堆:根 ≥ 左、右

  • 建立大根堆

    • 检查各个根节点是否大于叶子

    • 完全二叉树中,下标i < n/2向下取整的节点为根节点

    • 左孩子:2i

    • 右孩子:2i+1

    • 将根节点与较大的孩子互换

    • 从现有树调整时,从底向上依次调整

      • 先进行交换调整

      • 对于每一次交换,对换下来的节点再次进行判断,并进行调整

      • 之后回到之前的地方继续往上走

  • 堆排序

    • 每次将最上层的根节点与无序队列最后一个元素互换

      • 每次确定一个最终最大值

      • 从队尾开始是正确序列

    • 对新的换上来的根进行调整,形成新的大根堆

    • 得到递增序列

    • 最终的树是层序的

    堆排序

  • 节点每下坠一层,最多对比2次

  • 树高h,节点在第i层,则

    • 最多下坠 h-i 层

    • 最多对比 2(h-i) 次

  • n个元素(节点)的完全二叉树高:log2n+1\lfloor\log_2n\rfloor+1

  • 第i层最多有2(i1)2^(i-1)个节点(根为0层)

  • 只有非底层(1~(h-1))层节点可能下坠

  • 空间复杂度O(1)O(1),没有递归

  • 时间复杂度O(nlog2n)O(n\log_2n)

    • 建堆:O(n)O(n)

      • 关键字对比次数不超过4n

    • 排序:O(nlog2n)O(n\log_2n)

      • n-1趟

      • 每趟时间等于树高

  • 不稳定

7、归并排序

将两个有序的序列合成一个

K路归并排序

  • 将序列分为多个小序列

  • 小序列的大小从1开始

  • 相邻的K个小序列归并成新序列

  • 直到归并为1个

image-20221001111552762
  • 空间复杂度O(n)O(n),同样大小的辅助数组

  • 时间复杂度O(nlog2n)O(n\log_2n)

    • 归并的趟数:log2n\log_2n

    • 每趟的时间复杂度:O(n)O(n)

8、基数排序

设关键字有d位,分成r组

  • 空间复杂度O(r)O(r),r个辅助队列

  • 时间复杂度O(d(r+n))O(d(r+n))

    • 需要d趟排序

    • 每趟分配n次,收集r次

9、内部排序

算法性能总结

算法选择

  • 数量较小时:直接插入排序、简单排序

  • 数量较大时:快速排序、堆排序、归并排序

  • 文件本身基本有序:直接插入排序、冒泡排序

  • 数量非常大且关键字可分解:基数排序

10、外部排序

  • 构造初始归并段

  • 合并归并段时,两个输入缓冲区分别存放来自两个归并段的元素

  • 读写磁盘次数:文件块数×(1+归并趟数)×2文件块数\times (1+归并趟数) \times 2

  • 时间开销:读写磁盘时间 + 内部排序时间 + 内部归并时间

11、败者树

  • 是一颗满二叉树

  • 叶子节点为比较的元素

  • 每一层的非叶节点代表该次对比输的一方所属的归并段

  • 对于K路归并,构造完败者树需要比较k-1次

  • 构造完成后每次选出最小元素需要对比:log2k\log_2k

12、置换-选择排序

  • 用于构建初始归并段

  • 工作区满了则构造下一个归并段

13、最佳归并树

  • 就是一颗类哈夫曼树

  • 一定是严格的K叉树

  • 磁盘读写次数=2*带权路径长度

  • 若初始归并段的数量无法恰好满足,补充长度为0的“虚段”

  • 设采用K路归并,初始归并段数量为n,若

    • (n1)%(k1)=0(n-1)\%(k-1)=0,则不用补充虚段

    • (n1)%(k1)=u0(n-1)\%(k-1)=u\neq0,补充(k1)u(k-1)-u个虚段

最后更新于

这有帮助吗?