第八章 排序

排序

框架

基础知识

插入排序

交换排序

选择排序

归并排序

基数排序

各种内部排序算法的比较

外部排序

框架

image-20220614103418744

基本知识

算法的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持 不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法 是稳定的;否则称为不稳定的。

内部排序:待排序记录存放在计算机随机存储器中(说简单点,就是内存)进行的排序过程。

外部排序:待排序记录的数量很大,内存不能一次容纳全部记录,所以在排序过程中需要对外存进行访问的排序过程。

一般情况下,内部排序算法在执行过程中都要进行两种操作:比较和移动。但不是所有,例如基数排序。

插入排序

1、直接插入排序

image-20220614105618588
//0相当于哨兵
void InsertSort(int a[],int n){
    int i,j;
    for(i=2;i<=n;i++)
        if(a[i]<a[i-1]){              //判断当前数字是否小于前驱,如果不是,就直接不用操作。
            a[0] = a[i];
        	for(j=i-1;a[0]<a[j];j--)
            	a[j+1] = a[j];
        	a[j+1] = a[0];
        }
}

此算法以第一个为基准,从第二个数开始运行,如果当前数字小于前驱,就要用“哨兵”存储好当前数,在逐一与前面的数进行比较,如果当前数小,则让它的前面的数往后挪,直到小于等于当前数的值 j 出现,则让哨兵的值放到 j 后面。

如果待排序元素顺序相反(逆序),则比较次数达到最大。

2、折半插入排序

本质上是折半查找+移动+插入

//0相当于哨兵
void InsertSort(int a[],int n){
    int i,j,low,high,mid;
    for(i=2;i<=n;i++){
        a[0] = a[i];
        low = 1;high = i-1;
        while(low<=high){
            mid = (low+high)/2;
            if(a[mid]>a[0]) high = mid-1;
            else low = mid+1;
        }
        for(j=i-1;j>=high+1;j--){
            a[j+1] = a[j];
        }
        a[high+1] = a[0];
    }
}

//优化...
void InsertSort(int a[],int n){
    int i,j,low,high,mid;
    for(i=2;i<=n;i++){
        a[0] = a[i];
        low = 1;high = i-1;
        while(low<=high){
            mid = (low+high)/2;
            if(a[mid]>a[0]) high = mid-1;
            else if(a[mid]==a[0]){//突然发现这种方法好像会让它变成不稳定算法。。。
              high=mid;
              break;
            } 
            else low = mid+1;
        }
        for(j=i-1;j>=high+1;j--){
            a[j+1] = a[j];
        }
        a[high+1] = a[0];
    }
}

3、希尔排序

插入排序更适用于数据量不大基本有序的排序表,希尔排序正是基于这两点改进而来的,又称缩小增量排序。

希尔排序算法仅适用于线性表为顺序存储的情况。

很好奇,这样两两的排,到后面能排好吗?下图告诉我们答案。。。到最后就是一次所有的直接插入排序。

image-20220614132334218

//0相当于哨兵
void ShellSort(int a[],int n){
    int i,j,k,d;
    for(d=n/2;d>=1;d/=2){                   //增量 d=1时就是基本有序的表进行直接插入排序
        for(i=d+1;i<=n;++i){                 //按照增量分为d个子表
            if(a[i]<a[i-d]){
                a[0]=a[i];
                for(j=i-d;j>0&&a[0]<a[j];j-=d){ //找到插入位置
                    a[j+d]=a[j];
                }
                a[j+d]=a[0];
            }
        }
    }
}

交换排序

1、冒泡排序(优化)

冒泡排序比较简单。冒泡排序所产生的有序子序列一定是全局有序的,即每趟排序都会将一个元素放置到其最终的位置上。(位置定好不再改变)

在这里插入图片描述

//冒泡排序,元素下标从1开始,从后往前冒泡
void BubbleSort1(int a[],int n){
    for(int i=1;i<n-1;i++){
        bool flag = false;
        for(int j=n-1;j>i;j--){
            if(a[j-1]>a[j]){
                swap(a[j-1],a[j]);
                flag = true;
            }
        }
        if(!flag) return ; //本趟遍历后没有发生交换,说明表已经有序,这就是优化。。。
    }
}

2、快速排序

快速排序的基本思想是基于分治法的。

每一趟至少有一个会在它最终位置。之后再在两边分别进行快排。它可以从前面或者后面选择pivot

快速排序的运行时间与划分是否对称有关。

当每次pivot把表等分为长度相近的两个子表时,速度最快;当表本身已经有序或者逆序时,速度最慢。

在这里插入图片描述

int Partition(int a[],int low,int high){
    int pivot = a[low];
    while(low<high){
        while(low<high&&a[high]>=pivot) high--;
        a[low] = a[high];
        while(low<high&&a[low]<=pivot) low++;
        a[high] = a[low];
    }
    a[low] = pivot;
    return low;
}
 
//快速排序
void QuickSort(int a[],int low,int high){
    if(low<high){
        int pivotpos = Partition(a,low,high);
        QuickSort(a,low,pivotpos-1);
        QuickSort(a,pivotpos+1,high);
    }
}

选择排序

1、简单选择排序

//下标从1开始
void SelectSort(int a[],int n){
    int i,j,min;
    for(i=0;i<n-1;i++){
        min=i;
        for(j=i+1;j<n;j++){
            if(a[j]<a[min])	min=j;
        }
        swap(a[min],a[i]);
    }
}

2、堆排序

在这里插入图片描述

//将元素k为根的子树进行调整(大根堆)
void HeadAdjust(int a[],int k,int n){
    a[0] = a[k];
    for(int i=k*2;i<=n;i*=2){ //下一层
        if(i<n&&a[i]<a[i+1])
            i++;
        if(a[0]>=a[i]) break;
        else{
            a[k]=a[i];
            k=i;
        }
    }
    a[k] = a[0];
}
 
//建立大根堆
void BuildMaxHeap(int a[],int n){
    for(int i=n/2;i>0;i--){      //从 n/2 ~ 1反复调整堆
        HeadAdjust(a,i,n);
    }
}

//堆排序
void HeapSort(int a[],int n){
    BuildMaxHeap(a,n);
    for(int i=n;i>1;i--){
        swap(a[i],a[1]);
        HeadAdjust(a,1,i-1);
 	   }
}

从随机的堆到建立大顶堆,从n/2~1反复调整堆,先将下面的排好,再上去(BuildMaxHeap函数);但是对于每次调整时,它又必须延伸至它的最下层(HeadAdjust函数)。就是说 i会从n/2开始向上到1,而在这里的每次调整都必须延伸到对应子树的最底层。之后就是最后的排序,将大顶堆的根拿出去,再用最后一个叶节点补上,再重新开始调整。(虽然我感觉挺麻烦的。。。)

堆排序适合关键字较多的情况。例如1亿个数里选出最大的100个数。

归并排序

感觉有点像希尔排序。

在这里插入图片描述

void Merge(int a[],int low,int mid,int high){
    for(int k=low;k<=high;k++)
        b[k] = a[k];
    for(int i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
        if(b[i]<=b[j]){
            a[k++] = b[i++];
        }else{
            a[k] = b[j++];
        }
    }
    while(i<=mid) a[k++] = b[i++];		//若第一个表未检查完,复制
    while(j<=high) a[k++] = b[j++];
}
 
void MergeSort(int a[],int low,int high){
    if(low<high){
        int mid = (low+high)/2;
        MergeSort(a,low,mid);
        MergeSort(a,mid+1,high);
        Merge(a,low,mid,high);
    }
}

基数排序

先让优先级低的排序,最后让优先级最高的排。

在这里插入图片描述

各种内部排序算法的比较

在这里插入图片描述

在这里插入图片描述

1、算法复杂度与初始状态无关的有:选择排序、堆排序、归并排序、基数排序

​ (一堆(堆排序)乌龟(归并排序)选(选择排序)基(基数排序)友 )

2、元素总比较次数与初始状态无关的有:选择排序基数排序折半插入排序

3、元素总移动次数与初始状态无关的有:归并排序基数排序

外部排序

1、多路平衡归并与败者树

外部排序过程中的时间代价主要考虑访问磁盘的次数。

外部排序的总时间=内部排序所需的时间+外存信息读写的时间+内部归并所需的时间。 (可以看成单纯的外村读写时间,因为内部排序就是I/O数据+排序,而排序不需要什么时间,主要用在I/O读取。内部归并也一样。)

多路平衡归并通过适度增大k来达到提高外部排序速度的目的。(这样可以减少外存读写次数)

对于 k-路平衡归并中 k 值得选择,增加 k 可以减少归并的次数,从而减少外存读写的次数,最终达到提高算法效率的目的。除此之外,一般情况下对于具有 m 个初始归并段进行 k-路平衡归并时,归并的次数为:s=⌊logk⁡m ⌋(其中 s 表示归并次数)。

**从公式上可以判断出,想要达到减少归并次数从而提高算法效率的目的,可以从两个角度实现:

**

  • 增加 k-路平衡归并中的 k 值;[但不能影响内部归并的效率]
  • 尽量减少初始归并段的数量 m,即增加每个归并段的容量;

其增加 k 值的想法引申出了一种外部排序算法:多路平衡归并算法;增加数量 m 的想法引申出了另一种外部排序算法:置换-选择

1、内部排序

image-20220615091805123

2、内部归并

image-20220615091817590

3、实例

image-20220615091734397 image-20220615091657164

多路平衡归并时k过度增大会减少外存访问次数所得到的收益。因此不能使用普通的内部归并排序算法。

为了使内部归并不受k的增大影响,引入败者树。可视为一颗完全二叉树。

b0 b1 b2 b3 b4 就是5路归并。ls[i]它是固定位置,里面存的值在圆圈里面,它表示是哪路输了,最后ls[0]是剩下的一个冠军。

在这里插入图片描述

2、置换-选择排序

为什么用这个?

上一节介绍了增加 k-路归并排序中的 k 值来提高外部排序效率的方法,而除此之外,还有另外一条路可走,即减少初始归并段的个数,也就是本章第一节中提到的减小 m 的值。

m 的求值方法为:m=⌈n/l⌉(n 表示为外部文件中的记录数,l 表示初始归并段中包含的记录数)

如果要想减小 m 的值,在外部文件总的记录数 n 值一定的情况下,只能增加每个归并段中所包含的记录数 l。而对于初始归并段的形成,就不能再采用上一章所介绍的内部排序的算法,因为所有的内部排序算法正常运行的前提是所有的记录都存在于内存中,而内存的可使用空间是一定的,如果增加 l 的值,内存是盛不下的。所以要另想它法,探索一种新的排序方法:置换—选择排序算法。

置换—选择排序算法的具体操作过程为:

  1. 首先从初始文件中输入 6 个记录到内存工作区中;
  2. 从内存工作区中选出关键字最小的记录,将其记为 MINIMAX 记录;
  3. 然后将 MINIMAX 记录输出到归并段文件中;
  4. 此时内存工作区中还剩余 5 个记录,若初始文件不为空,则从初始文件中输入下一个记录到内存工作区中;
  5. 从内存工作区中的所有比 MINIMAX 值大的记录中选出值最小的关键字的记录,作为新的 MINIMAX 记录;[使用败者树或者堆排序实现]
  6. 重复过程 3—5,直至在内存工作区中选不出新的 MINIMAX 记录为止,由此就得到了一个初始归并段;
  7. 重复 2—6,直至内存工作为空,由此就可以得到全部的初始归并段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAxjbH2q-1655259252757)(https://cdn.jsdelivr.net/gh/2339539741/tuchuang/img/202206151010867.png)]

3、最佳归并树

文件经过置换-选择排序后,得到的是长度不等的初始归并段。如何组织长度不等的初始归并段的归并顺序,使得I/O次数最少?

利用哈夫曼树。。。

若初始归并段不足以构成一棵严格k叉树,需添加长度为0的“虚段”。

对于如何判断是否需要增加虚段,以及增加多少虚段的问题,有以下结论直接套用即可:在一般情况下,对于 k–路平衡归并来说,若 (m-1) MOD (k-1) = 0,则不需要增加虚段;否则需附加 k - (m-1)MOD(k-1) - 1 个虚段。

易错

1、拓扑排序不属于内部排序。

2、对同一线性表使用不同的排序方法进行排序,得到的排序结果可能不同。

3、对于任意序列进行基于比较的排序,求最少的比较次数应考虑最坏情况。对任意n个关键字排序的比较次数至少为log2(n!)向上取整。

4、交换类的排序,其趟数和原始序列状态有关。

小思

1、折半查找能不能改进成当 a[mid]=x 时,直接退出?折半查找应该分 大于等于小于,使得查找次数减少。

​ 可能会造成其不稳定。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值