数据结构(8)-内部排序

本文详细介绍了插入排序、折半插入排序、希尔排序、冒泡排序、快速排序、选择排序和堆排序的基本原理、代码实现及性能分析,探讨了它们的空间和时间复杂度,稳定性特点。此外,还对比了这些排序算法在实际场景中的优劣和适用性。
摘要由CSDN通过智能技术生成

排序的基本概念

排序:重新排列,使表中元素满足按关键字有序的过程。
根据元素在排序过程是否完全在内存中,将排序分为(1)内部排序(2)外部排序
内部排序两种操作:(1)比较(2)移动 (基数排序不基于比较
下面介绍几种基本的内部排序算法思路,代码实现(大多为伪代码),性能分析。

插入排序

基本思想:每次将一个待排序的记录按关键字大小插入前面已经排好的序列。
三类插入排序算法:直接插入排序,折半插入排序,希尔排序。

直接插入排序

1)查找L(i)在L[1…i-1]中的插入位置k;
2) 将L[k…i-1]中所以元素依次后移;
3) 将L(i)复制到L[k];

伪代码:

void insertSort(ElemType A[],int n){
    int i,j;
    for(int i=1;i<n;i++){ //将A[1]~A[n-1]依次插入到前面的有序序列
        ElemType temp=A[i];  //记录待排序的值
        for(j=i-1;j>=0&&temp<=A[j];j--) A[j+1]=A[j]; //从后往前作比较,并挪位
        A[j+1]=temp; //复制到插入位置
    }
}

性能分析:
空间效率:使用常数辅助单元,O(1)。
时间效率:插入操作n-1趟,每趟比较最少1次,最多i次。最好时间复杂度O(n),最差O( n 2 n^2 n2),平均时间复杂度O( n 2 n^2 n2)。
稳定性:稳定。每次插入从后向前先比较再移动。

折半插入排序

对直接插入排序算法做改进,查找有序子表用折半查找来实现。
伪代码:

void InsertSort(ElemType A[],int n){
    int i,j,low,high;
    for(int i=1;i<n;i++){
        //折半查找
        ElemType temp=A[i];
        low=0;high=i-1;
        while(low<=high){
            mid=(low+high)/2;
            if(A[mid]>temp) high=mid-1; //查找左半子表
            else low=mid+1;    //查找右半子表
        }
        //移位,插入
        for(j=i-1;j>high+1;j--){
            A[j+1]=A[j];
        }
        A[high+1]=temp;
    }
}

性能分析:
空间和稳定性同直接插入算法。
时间:减少比较元素的次数,约为O( n l o g 2 n nlog_2n nlog2n),该比较次数仅取决于元素个数n。元素移动次数并未改变,它依赖于表的初始状态,故时间复杂度仍为O( n 2 n^2 n2)。

希尔排序

基本思想:将待排序表分割成若干形如L[i,i+d,i+2d,…,i+kd]的子表,对各个子表进行插入排序。
步长: d 1 < n , d 2 < d 1 , . . . , d t = 1 d_1<n,d_2<d_1,...,d_t=1 d1<n,d2<d1,...,dt=1
伪代码:

void ShellSort(ElemType A[],int n){
    for(dk=n/2;dk>0;dk=dk/2)    //步长变化
        for(int i=dk+1;i<=n;i++){   //从子表中的第二个元素进行比较
            if(A[i]<A[i-dk]){
                A[0]=A[i];
                for(j=i-dk;j>0&&A[0]<A[j];j=j-dk) A[j+dk]=A[j]; //记录后移
                A[j+dk]=A[0]; //插入
            }
        }
}

性能分析:
空间:仍为O(1)。
时间:复杂度依赖于增量序列的函数,最坏情况O( n 2 n^2 n2)。
稳定性:不稳定。相同关键字划分到不同子表时,可能改变相对次序。

交换排序

基本思想:根据元素关键字的比较结果对换两个记录在序列中的位置。

冒泡排序

从后往前(从前往后)依次比较相邻两个元素的值,若为逆序,则交换,直到比较完。这是第一趟冒泡,结果使最小元素到第一个位置(或最大元素到最后一个位置)。前一趟排序确定的最小元素(或最大)不再参与比较,每趟排序把序列中的一个元素放到序列的最终位置。最多n-1趟把所以元素排好序。
伪代码:

void BubbleSort(ElemType A[],int n){
    for(int i=0;i<n-1;i++){
        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==false) return; //本趟遍历没有发生交换,说明表已经有序
    }
}

空间:常数辅助单元O(1)。
时间:有序,一趟排序,比较n-1,交换0,最好时间复杂度O(n)。逆序,n-1趟排序,每趟n-i次比较,每次比较移动元素3次,这种情况下最坏时间复杂度O( n 2 n^2 n2)。平均时间复杂度O( n 2 n^2 n2)。
稳定性:稳定。

快速排序

基本思想:基于分治。在待排序表中选取一个元素pivot作为枢轴(或基准),通过一趟排序将待排序表分为独立的两部分L[1…k-1] (所有元素小于pivot) 和L[k+1…n] (所有元素大于pivot)。这个过程称为一趟快速排序。然后分别对两个子表重复上述过程,直至每一部分内只有一个元素或空为止。
伪代码:

void QuickSort(ElemType A[],int low,int high){
    if(low<high){
        int pivotpos=Partition(A,low,high);
        QuickSort(A,low,pivotpos-1);
        QuickSort(A,pivotpos+1,high);
    }
}
int Partition(ElemType A[],int low,int high){
    ElemType 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; //返回最终位置
}

空间:需要借助递归工作栈来保存每层递归调用的必要信息,其容量与递归调用的最大深度一致。最好情况O( l o g 2 n log_2n log2n),最坏情况O(n),平均为O( l o g 2 n log_2n log2n)。
时间:运行时间与划分是否对称有关。最坏情况下时间复杂度为O( n 2 n^2 n2)。在理想情况下,划分对称,时间复杂度为O( n l o g 2 n nlog_2n nlog2n)。
稳定性:不稳定。右端两个相同关键字且均小于基准值的记录,在交换到左端点区间后,它们的相对位置会发生变化。

选择排序

基本思想:每一趟(如第i趟)在后面n-i+1个待排元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟做完。

简单选择排序

第i趟排序即从L[i…n]中选择关键字最小的元素与L(i)交换。
伪代码:

void SelectSort(ElemType A[],int n){
    for(int i=0;i<n-1;i++){     //一共n-1趟
        int min=i;          //记录最小元素位置
        for(j=i+1;j<n;j++){
        if(A[j]<A[min]) min=j;  //更新最小元素位置
        }
        if(min!=i) swap(A[i],A[min]);
    }
}

性能分析:
空间:常数个辅助单元O(1);
时间:移动-[0,3(n-1)]次,元素间比较次数与序列初始状况无关,始终是n(n-1)/2次,时间复杂度O( n 2 n^2 n2)。
稳定性:不稳定。在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其含有相同关键字元素的相对位置发生改变。

堆排序

大根堆:L(i)>=L(2i)且L(i)>=L(2i+1)
小根堆:L(i)<=L(2i)且L(i)<=L(2i+1)
堆排序思路:
(1)构造初始堆
(2)输出堆顶元素,将剩余元素调整为新的堆
性能分析:
空间:O(1)
时间:建堆时间为O(n),之后n-1次向下调整,每次调整O(h),故堆排序的时间复杂度( n l o g 2 n nlog_2n nlog2n)
稳定性:不稳定

归并排序

归并:将两个或两个以上的有序表组合成一个新的有序表

void Merge(ElemType A[],int low,int mid,int high){
    for(int k=low;k<=high;k++)
        B[k]=A[k];
    for(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++];
}

一趟归并:将L[1…n]中前后相邻长度为h的有序段进行两两归并整个归并需要进行 ⌈ l o g 2 n ⌉ \lceil log_2n \rceil log2n趟。
递归形式的2路算法是基于分治的。

void MergeSort(ElemType A[],int low,int high){
    if(low<high){
        int mid=(low+high)/2;
        MergeSort(A,low,high);
        MergeSort(A,mid+1,high);
        Merge(A,low,mid,high);
    }
}

性能分析:
空间:辅助数组B,故为O(n)。
时间:每趟归并O(n),共 ⌈ l o g 2 n ⌉ \lceil log_2n \rceil log2n趟,时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
稳定性:稳定

基数排序

基于关键字各位的大小进行排序。
通常有两种方法:(1)最高位优先法(MSD)(2)最低位优先法(LSD)
性能分析:
空间:需要辅助存储空间r个队列,所以空间复杂度O®。
时间:进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(d(n+r))。
稳定性:稳定。按位排序时必须是稳定的。

各种内部排序算法的比较

排序算法的主要步骤还是比较和交换,比较和交换的方法会影响其空间,时间复杂度。分析复杂度,也主要从算法比较和交换的角度分析。

空间复杂度

O ( 1 ) O(1) O(1)简单选择排序,插入排序(直接or折半),希尔排序,冒泡排序,堆排序仅需借助常数个辅助空间(基于插入的排序算法借助一个temp临时变量存储待排序元素,和其他元素作比较;基于交换的排序算法使用swap()做交换时,借助临时变量)。
O ( l o g 2 n ) O(log_2n) O(log2n):快速排序(使用一个小的辅助栈,用于实现递归,最坏情况可能达到 O ( n ) O(n) O(n)
O ( n ) O(n) O(n):2路归并排序在合并操作时,需要借助辅助空间,用于元素复制。
O ( r ) O(r) O(r):基数排序,辅助存储空间r个队列。

时间复杂度

O ( n 2 ) O(n^2) O(n2):简单选择,直接插入,冒泡排序平均情况下。直接插入和冒泡最好情况可以达到 O ( n ) O(n) O(n)(顺序)。简单选择与序列初始状态无关。
希尔排序:作为插入排序的拓展,对较大规模的排序可以达到很高的效率,未得其精确渐进时间。
O ( n l o g 2 n ) O(nlog_2n) O(nlog2n):堆排序,快速排序,归并排序。堆排序使用堆的数据结构,快排和归并都基于分治的思想。但快排最坏情况可以达到 O ( n 2 ) O(n^2) O(n2),归并最好,最坏和平均都是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

稳定性

稳定:插入排序,冒泡排序,归并排序,基数排序
不稳定:简单选择,快速排序,希尔排序和堆排序

参考:
王道数据结构考研复习指导

1.实验目的 掌握内排序,比较各种排序的优、缺点。 2 需求分析 2.1原理 2.1.1、直接排序 算法描述:经过i-1遍处理后,L[1..i-1]己排好序。第i遍处理仅将L[i]插入L[1..i-1]的适当位置,使得L[1..i]又是排好序的序列。要达到这个目的,我们可以用顺序比较的方法。首先比较L[i]和L[i-1],如果L[i-1]≤ L[i],则L[1..i]已排好序,第i遍处理就结束了;否则交换L[i]与L[i-1]的位置,继续比较L[i-1]和L[i-2],直到找到某一个位置j(1≤j≤i-1),使得L[j] ≤L[j+1]时为止。 2.1.2、冒泡排序 算法描述:核心思想是扫描数据清单,寻找出现乱序的两个相邻的项目。当找到这两个项目后,交换项目的位置然后继续扫描。重复上面的操作直到所有的项目都按顺序排好。 2.1.3、快速排序 算法描述:首先检查数据列表中的数据数,如果小于两个,则直接退出程序。如果有超过两个以上的数据,就选择一个分割点将数据分成两个部分,小于分割点的数据放在一组,其余的放在另一组,然后分别对两组数据排序。通常分割点的数据是随机选取的。这样无论你的数据是否已被排列过,你所分割成的两个字列表的大小是差不多的。而只要两个子列表的大小差不多。 2.1.4、选择排序 算法描述:首先找到数据清单中的最小的数据,然后将这个数据同第一个数据交换位置;接下来找第二小的数据,再将其同第二个数据交换位置,以此类推。 2.1.5、堆排序 (1) 基本思想:堆排序是一树形选择排序,在排序过程中,将R[1..N]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择最小的元素。 (2) 堆的定义: N个元素的序列K1,K2,K3,...,Kn.称为堆,当且仅当该序列满足特性: Ki≤K2i Ki ≤K2i+1(1≤ I≤ [N/2]) 2.1.6、希尔排序 算法描述:在直接插入排序算法中,每次插入一个数,使有序序列只增加1个节点,并且对插入下一个数没有提供任何帮助。如果比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。 2.2要求 1.本程序对以下六种常用内部排序算法进行实测比较:冒泡排序,插入排序,选择排序,希尔排序,快速排序,堆排序。 2.排序的元素的关键字为整数。用正序,逆序,不同乱序的数据作测试比较。比较的指标为有关键字参加的比较次数和关键字的移动次数。 3.程序以人机对话的形式进行,每次测试完毕显示各种比较指标值 。 2.3任务 设计一个测试程序比较几种内部排序算法的关键字比较次数和移动次数以取得直观感受。 2.4运行环境 (1)WINDOWSXP系统 (2)C++ 编译环境 3.实验方法 本实验主要是内排序,通过比较的次数和移动的次数判断排序的好坏。主要子函数的说明如下。 1.简单选择排序XuanzePaixu(); 2.冒泡排序MaopaoPaixu(); 3. 直接插入排序CharuPaixu(); 4. 快速排序KuaisuPaixu(); 5. 堆排序DuiPaixu(); 6. 希尔排序 XierPaixu(); 以上的排序算法均采用书中所用的算法。程序采用输入的时候仅输入所要的个数,具体的输入数据由程序随机产生个数,并且输出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值