排序(内部排序)+外部排序

排序可视化过程

插入排序

三种插入排序的时间复杂度均为 O ( n 2 ) O(n^2) O(n2)

直接插入排序

无哨兵

//直接插入排序  没有哨兵,数组0的位置也存放的是待排元素
void InsertSort(int A[], int n){
    int i,j,temp;//因为A[0]也可以存放数据元素,所以i从1开始
    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;//复制到插入位置
        }
    }

}

无哨兵的示意图如下:
在这里插入图片描述

有哨兵

//直接插入排序 带哨兵,A[0]为哨兵
void InsertSort0(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];
            }//for
            A[j+1]=A[0];
        }//if
    }//for
}

折半插入排序

即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。
折半插入仅较少了比较元素的次数,约为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n), 而元素移动的次数并未改变,仍为 O ( n 2 ) O(n^2) O(n2)

//折半插入排序,  A[0]空出来当中转站
void InsertSort2(int A[],int n){
    int i,j,low,high,mid;
    for(i=2;i<=n;i++){//依次将A[2]~A[n]插入到前面的已排序序列
        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;//查找右半边
        }//当high<low时退出循环,此时low=high-1;
        //且low的位置即为要插入的位置
        for(j=i-1;j>=low;--j){//统一后移元素,空出插入位置A[low]
            A[j+1]=A[j];
        }
        A[low]=A[0];
    }
}

希尔排序

也叫缩小增量排序
先将待排序表分割为若干形如L[i, i+d, i+2d,…, i+kd]的特殊子表,即把相隔某个增量的记录组成一个子表,对各个子表分别进行直接插入排序。当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
希尔提出的方法是 d 1 = n / 2 , d i + 1 = ⌊ d i / 2 ⌋ d_1=n/2, d_{i+1}=\lfloor d_i/2 \rfloor d1=n/2,di+1=di/2, 并且最后一个增量等于1
稳定性:当相同的关键字的记录被划分到不同的子表中时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。且希尔排序仅适用于线性表为顺序存储的情况。

//希尔排序,A[0]为中转站,暂存元素
void ShellSort(int A[], int n){
    //A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
    int i,j,dk;
    for(dk=n/2;dk>=1;dk=dk/2){//步长变换
        for(i=1+dk;i<=n;++i){//i=1+dk即指第1个元素+步长dk
        //++i指每次轮流地切换着来处理不同的子表
            if(A[i] < A[i-dk]){
                A[0]=A[i];
                for(j=i-dk;j>=1 && A[j]>A[0];j=j-dk){
                    A[j+dk]=A[j];//记录后移,查找插入的位置
                }
                A[j+dk]=A[0];//插入
            }
        }
    }
}

交换排序

冒泡排序

从下往上冒泡,即从数组尾部开始比较,这里每次将最小的往上冒.
是一种稳定的排序

void Swap(int &a,int &b){
    int t;
    t=a;
    a=b;
    b=t;
}

void BubbleSort(int A[], int n){
    bool flag;
    for(int i=0; i<n-1; i++){//n个数最多要比较n-1趟
        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;//本趟遍历后没有发生交换,说明表已经有序
        }
    }
}

快速排序

快排的思想基于分治法
**空间效率:**由于快排是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为 O ( l o g 2 n ) O(log_2n) O(log2n); 最坏情况下,因为要进行n-1次递归调用,所以栈的深度为 O ( n ) O(n) O(n); 平均情况下,栈的深度为 O ( l o g 2 n ) O(log_2n) O(log2n)
时间效率:快排的运行时间与划分是否对称有关,快排的最快情况发生在两个区域分别包含n-1个元素和0个元素时,这种最大限度的不对称性若发生在每层递归上,即对应于初始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2);
快排是所有内部排序算法中平均性能最右的排序算法,平均情况下时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

//快速排序
/*i,j指针轮流搜索,j先搜索,因为枢轴定在A[low]处,
即A[low]的位置先给了pivot,空了出来,所以j先搜索,
搜索到的符合条件的值放到A[low]的位置,然后i再搜索......
*/
int Partition(int A[], int low,int high){
    int pivot = A[low];//将当前表中第一个元素设为pivot,对表进行划分
    while(low < high){//当low>=high时跳出循环
        while(low<high && A[high]>=pivot)
            high--;
        A[low]=A[high];//将比pivot小的元素移动到左端

        //上下两个while不能颠倒,因为在最开始是将A[low]的值给了pivot,
        //所以这时A[low]空出来了

        while(low<high && A[low]<=pivot)
            low++;
        A[high] = A[low];//将比pivot大的元素移动到右端
    }
    //最后这里low必定等于high,low==high时跳出循环,
    //并且此位置即为pivot元素要放的位置,即枢轴的位置
    A[low]=pivot;//pivot元素存放到最终位置
    return low;//返回存放枢轴的最终位置
}

void QuickSort(int A[], int low,int high){
    if(low < high){//递归跳出的条件
        //Partition就是划分操作,将表A[low...high]
        //划分为满足上述条件的两个子表
        int pivotpos=Partition(A,low,high);
        QuickSort(A,low,pivotpos-1);
        QuickSort(A,pivotpos+1,high);
    }
}

选择排序

简单选择排序

一视同仁,无论数的初始状态如何,均要n-1趟处理

void Swap(int &a,int &b){
    int t;
    t=a;
    a=b;
    b=t;
}

//简单选择排序
void SelectSort(int A[], int n){
    for(int i=0;i<n-1;i++){//一共进行n-1趟
        int minpos=i;//记录最小元素的位置
        for(int j=i+1;j<n;j++){//在A[i...n-1]中选择最小的元素
            if(A[j]<A[minpos])
                minpos=j;//更新最小元素的位置
        }
        if(minpos != i)
            Swap(A[i],A[minpos]);//封装的swap()函数
    }
}

堆排序

可以将该一维数组视为一棵完全二叉树:满足条件 (根>=左,右) 的称为大根堆; 满足条件**(根<=左右)**的称为小根堆;
从堆的定义中可知,堆的根节点一定是堆中所有结点的最大值或最小值
构造大根堆的思路: 把所有非终端结点(即 i < ⌊ n / 2 ⌋ i<\lfloor n/2\rfloor i<n/2则为非终端结点)都检查一遍,看是否满足大根堆的要求(根>=左,右),如果不满足则进行调整:将当前结点与更大的一个孩子互换(非叶子结点i的左右孩子为2i , 2i+1)
注意: 非终端叶子结点的检查顺序为i,i-1,i-2…即从编号最大的非终端叶子结点开始依次检查并调整。也即从后往前调整所有非终端结点
空间效率:O(n)
在最好,最坏和平均情况下,堆排序的时间复杂度为: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
稳定性:不稳定

#include<stdio.h>
//  建立大根堆
//函数HeadAdjust将元素k为根的子树进行调整
void HeadAdjust(int A[],int k,int len){
    A[0]=A[k];//A[0]暂存子树的根节点
    for(int i=2*k;i<=len;i=i*2){//沿key较大的子节点向下筛选
        if(i<len && A[i]<A[i+1])//i<len是为了保证i有右兄弟
            i++;//取key较大的子节点的下标

        if(A[0] >= A[i])
            break;//筛选结束
        else{
            A[k]=A[i];//将A[i]调整到双亲节点上
            k=i;//修改k的值,以便继续向下筛选
        }
    }
    A[k]=A[0];//被筛选结点的值放入最终位置
}

void BuildMaxHeap(int A[],int len){
    for(int i=len/2;i>=0;i--)//从i=n/2 ~ 1,从后往前依次调整所有非终端结点
        HeadAdjust(A,i,len);
}

//堆排序算法
void HeapSort(int A[], int len){
    BuildMaxHeap(A,len);//初始建堆  O(n)
    for(int i=len;i>1;i--){//n-1趟的交换和建堆过程  O(nlog2^n)
        Swap(A[i],A[1]);//输出堆顶元素(和堆底元素交换,即和完全二叉树的最后一个结点交换)
        printf("%d ",A[i]);//输出堆顶元素
        HeadAdjust(A,1,i-1);//调整,把剩余的i-1个元素整理成堆
    }
}

int main(){
    int a[9]={0,53,17,78,9,45,65,87,32};//a[1]~a[8]
    int n=8;
    printf("HeapSort  堆排: ");
    HeapSort(a,n);
    return 0;
}

一个结点每下坠一层,最多只需对比关键字2次
若树高为h,而结点在第i层,则将这个结点向下调整,最多只需要下坠h-i层,关键字对比次数不超过2(h-i)次
n个结点的完全二叉树的树高为 h = ⌊ l o g 2 n ⌋ + 1 h=\lfloor log_2n\rfloor+1 h=log2n+1
第i层最多有 2 i − 1 2^{i-1} 2i1个结点,而只有第1~(h-1)层的结点才有可能下坠调整
将整棵树调整为大根堆,关键字对比次数不超过 ∑ i = h − 1 1 2 i − 1 2 ( h − i ) = ∑ i = h − 1 1 2 i ( h − i ) = ∑ j = 1 h − 1 2 h − j j < = 2 n ∑ j = 1 h − 1 j 2 j < = 4 n \sum_{i=h-1}^12^{i-1}2(h-i)=\sum_{i=h-1}^12^i(h-i)=\sum_{j=1}^{h-1}2^{h-j}j<=2n\sum_{j=1}^{h-1}\frac{j}{2^j}<=4n i=h112i12(hi)=i=h112i(hi)=j=1h12hjj<=2nj=1h12jj<=4n
部分计算过程:
∑ j = 1 h − 1 2 h − j j = ∑ j = 1 h − 1 2 ⌊ l o g 2 n ⌋ + 1 . 2 − j . j \sum_{j=1}^{h-1}2^{h-j}j=\sum_{j=1}^{h-1}2^{\lfloor log_2n\rfloor+1}.2^{-j}.j j=1h12hjj=j=1h12log2n+1.2j.j
2 ⌊ l o g 2 n ⌋ < = 2 l o g 2 n = 2^{\lfloor log_2n\rfloor}<=2^{log_2n} = 2log2n<=2log2n=
在这里插入图片描述

在这里插入图片描述
建立小根堆的练习
在这里插入图片描述

归并排序

空间复杂度为O(n)
时间复杂度为 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
稳定的

//2路归并排序
int n=10;
int *B=(int *)malloc(sizeof(int)*(n+1));//辅助数组B
//Merge函数的功能是将前后相邻的两个有序表归并为一个有序表
void Merge(int A[],int low,int mid,int high){
//表A的两段A[low...mid]和A[mid...high]各自有序,将它们合并成一个有序表
    int i,j,k;
    for(k=low;k<=high;k++)
        B[k]=A[k];//将A中所有元素复制到B中

    for(i=low,j=mid+1,k=i; i<=mid && j<=high;k++){
        if(B[i]<=B[j])//比较B的左右两段中的元素
            A[k]=B[i++];//将较小值复制到A中
        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);//归并
    }
}

在这里插入图片描述

基数排序

基数排序不基于比较和移动进行排序,而基于关键字各位的大小进行排序。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

基数排序的应用

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

外存,内存之间的数据交换

外部排序的原理

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

增加归并的路数,减少归并的趟数,这样就可以减少读写磁盘的趟数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

多路平衡归并

在这里插入图片描述

在这里插入图片描述

败者树

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
正确的结点记录内容如下,即灰色结点中记录的是竞争失败者是来自哪个归并段,而根节点记录冠军(竞争最终胜利者)是来自哪个归并段
在这里插入图片描述
第一次归并,对比了7次找到了最小的元素来自哪个归并段。接下来按照归并排序的规则,我们还需要在归并段1~8中选出下一个最小的元素。接下来我们会让归并段3的下一个元素替代1这个元素的原有的位置
在这里插入图片描述

接下来要从余下的叶子结点中选出新的最小的元素,我们只需要让6和第四个归并段中最小的元素进行对比
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
只要构造好了败者树,接下来每次选最小的元素时,只需要对比3次,即败者树的灰色结点的层数

在这里插入图片描述

假设现在构造好了一棵败者树,树高为h(h不包含蓝色结点,而是灰色和绿色结点构成的树)。则第h层一共有 2 h − 1 2^{h-1} 2h1个结点
k路归并的败者树会有k个叶子结点。所以应有 k < = 2 h − 1 k <= 2^{h-1} k<=2h1
解得 h − 1 = ⌈ log ⁡ 2 k ⌉ h -1=\lceil\log_2k\rceil h1=log2k .而 h-1 刚好代表分支结点有多少层。而之前说分支结点有多少层就需要对比多少次,所以有了败者树后,选出最小元素,只要对比关键字 ⌈ log ⁡ 2 k ⌉ \lceil\log_2k\rceil log2k
如果是1024路归并,则传统方法,每次都要进行1023次比较,而败者树只要对比 ⌈ log ⁡ 2 1024 ⌉ \lceil\log_21024\rceil log21024 即10次对比

在这里插入图片描述

数组下标为1的元素对应传统意义上的根节点,数组下标为0的对应新增加的小头头ls[0]。而叶子结点在实际的数组当中是不对应任何一个数据的。在逻辑上,每个绿色的叶子结点对应一个归并段,而实际上这些叶子结点是我们脑补上去的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

置换选择排序

在这里插入图片描述

此时发现内存工作区中最小的是10,但是记录的MINMAX=13,13>10,所以此时不能将10放入归并段1后面(归并段1是要保证内部递增的)

在这里插入图片描述
除了10之外,最小的是14,14>MINMAX=13,所以此时可把14放入归并段1后面,紧接着读入下一个元素22
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

若WA内的所有元素都比MINMAX更小,则归并段应在此截止,并开启下一个归并段的构造
在这里插入图片描述

解冻红色的,找出最小的元素为2,输出,并将MINMAX置为2。每当内存工作区有空位时就读入下一个记录
在这里插入图片描述
在这里插入图片描述

输出文件FO是存放在磁盘中的,演示中为每次输出一个,实际上是先将要输出的元素放到输出缓冲区中,凑成一整块后再写入磁盘。对磁盘的读写是以块为单位的。同理于输入文件FI
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最佳归并树

绿色结点上的权值代表归并段的长度,即归并段所占磁盘的块数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
进行3路归并时,会在内存中开辟3个缓冲区(缓冲区的大小和磁盘的一块的大小相等),而对于2,3 和虚节点0来说,2和3分别放在一个缓冲区中,而第三缓冲区中什么也不放

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值