排序算法总结

名称就地性内部/外部稳定性时间复杂度
插入排序就地内部稳定O(n^2)
希尔排序就地内部不稳定最坏O(n^2)
冒泡排序就地内部稳定O(n^2)
快速排序就地内部不稳定

O(nlogn)

选择排序就地内部不稳定O(n^2)
堆排序不一定内部不稳定O(nlogn)
归并排序非就地

2-路归并内部排序

稳定

O(nlogn)

计数排序非就地内部使用前缀和数组保证稳定

O(n+maxx)

基数排序非就地内部使用前缀和数组保证稳定O(d * (n+b))
桶排序非就地内部取决桶内排序的稳定性不唯一

        排序算法主要有以下几类:插入排序(直接插入排序,希尔排序),交换排序(冒泡排序,快速排序),选择排序(简单选择排序,堆排序),归并排序(2-路归并),统计排序(桶排序,基数排序)

一.插入排序

1.1直接插入排序

        插入排序很简单,和“斗地主”时整理牌序的过程很像。主要的实现思想是将数据按照⼀定的顺序⼀个⼀个的插⼊到有序的表中,最终得到的序列就是已经排序好的数据。而直接插⼊排序是插⼊排序算法中的⼀种,采⽤的⽅法是:在添加新的记录时,使⽤顺序查找的⽅式 找到其要插⼊的位置,然后将新记录插⼊。

直接插⼊排序(Straight Insertion Sort)的基本思想是:
· 把n个待排序的元素看成为⼀个有序表和⼀个⽆序表。
· 开始时有序表中只包含1个元素,⽆序表中包含有n-1个元素,排序过程中每次从⽆序表中取出第⼀ 个元素,将它插⼊到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。
直接上代码就知道怎么一回事了
#include<stdio.h>
#include<stdlib.h>
//就地排序,内部排序,稳定,时间复杂度O(n^2)
int n,a[105];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    int k,t;//k用于倒序遍历找第一个比a[i]小或等于(保证稳定性)的数,t用于保存a[i],避免移动时将a[i]值覆盖
    // for(int i=2;i<=n;i++)//枚举趟数以及每趟排序的乱序区的左边界
    // {//把a[i]加入到有序区中
    //     //遍历i-1~1,找到第一个比a[i]小或等于(保证稳定性)的数。这里找位置的部分可以优化使用折半查找,但优化效果不明显。
    //     for(k=i-1;k>=1;k--)
    //     {
    //         if(a[k]<=a[i])
    //         {
    //             break;
    //         }
    //     }
    //     //把a[i]插入到k+1位置。在此折半查找基础上,还可以引入一个循环数组,实现2-路插入排序,但同样效果来看也是鸡肋的
    //     t=a[i];
    //     for(int j=i-1;j>=k+1;j--)
    //     {
    //         a[j+1]=a[j];
    //     }
    //     a[k+1]=t;

    // }
    //查找和插入移动精简版代码(边查找边替换)
    for(int i=2;i<=n;i++)
    {
        t=a[i];
        for(k=i-1;k>=1;k--)
        {
            if(a[k]>t)//边比较边移动
            {
                a[k+1]=a[k];
            }else
            {
                break;
            }
        }
        a[k+1]=t;
    }
    for(int i=1;i<=n;i++)
    {
        printf("%d ",a[i]);
    }
}

        对于直接插入排序,还有两种作用不是特别大的优化方法。就是折半插⼊排序和2-路插⼊排序。

        在查找插⼊位置时,采⽤的是顺序查找的⽅式,⽽在查找表中数据本身有序的前提下,可以使⽤折半查找来代替顺序查找,这种排序的算法就是折半插⼊排序算法。2-路插⼊排序算法是在折半插⼊排序的基础上对其进⾏改进,减少其在排序过程中移动记录的次数 从⽽提⾼效率。 具体实现思路为:另外设置⼀个同存储记录的数组⼤⼩相同的数组 d,将⽆序表中第⼀个记录添加 进 d[0] 的位置上,然后从⽆序表中第⼆个记录开始,同 d[0] 作⽐较:如果该值⽐ d[0] ⼤,则添加到其右侧;反之添加到其左侧。

        但因这两种方法并不能从本质改变直接插入排序O(n^2)的量级,因此不采用。对于直接插入法的优化,我们选择shell排序,也就是希尔排序。

1.2Shell排序

        Shell排序又称缩小增量排序。这里又引出了增量这个概念。首先我们知道,数据量越小,排序肯定越快,数据本身越有序,排的也会更快。那么希尔排序 就是从这两点出发对算法进⾏改进得到的排序算法。

        希尔排序的具体实现思路是:先将整个记录表分割成若⼲部分,分别进⾏直接插⼊排序,然后再对整个记录表进⾏⼀次直接插⼊排序。其中组数,其实就是所谓的增量。规定同一组中相邻两个数的下标相差就是增量d,并且这个增量也是变化的。那么如何确定d呢。就是使用增量序列,shell增量序列规定为:n/2,n/4,n/8...1

#include<stdio.h>
#include<stdlib.h>
//特点:就地,内部,不稳定(如果重复数据被分在不同组中就会不稳定),时间复杂度:最坏O(n^2),主要是取决于增量序列设计
int n,a[105];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    //shell排序:基于shell增量序列的缩小增量排序
    int t,k,j=0;
    for(int d=n/2;d>=1;d=d/2)//枚举增量
    {
        for(int i=1+d;i<=n;i++)//枚举每次排序的位置
        {
            //找到i位置前面同组中的第一个小于等于a[i]的位置
            t=a[i];
            for(k=i-d;k>0;k-=d)
            {
                if(a[k]>t)
                {
                    a[k+d]=a[k];//移动到同组的下一个位置
                }
                else
                {
                    break;
                }
            }
            //k的同组的下一个位置就是插入的位置
            a[k+d]=t;
        }
        j++;//趟数
        printf("第%d趟,增量为%d, 排序后的结果:\n",j,d);
        for(int i=1;i<=n;i++)
        {
            printf("%d ",a[i]);
        }
        printf("\n");
    }
}
//测试样例
10
8 9 1 7 2 3 5 4 6 0

二.交换排序

        交换排序也非常简单,就是在排序的过程中不断比较两个关键字,根据比较的结果进行关键字位置的交换,直到排序为止

2.1冒泡排序

       这个很熟悉了,c语言第一个算法。指对n个数据进行排序,排n-1趟,同样是把数组分为有序和乱序区。在每一趟排序过程中,从前往后不断比较乱序区中相邻的两个数据,如果a[i]>a[i+1].就交换两个数,最终会把乱序区中最大的数交换到乱序区最后面,在这个过程中有序区则+1,乱序区-1。

#include<stdio.h>
#include<stdlib.h>
//冒泡排序:就地排序,内部排序,稳定,时间复杂度O(n^2)
int n,a[105];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    int t=0;
    int flag=0;//标记是否真的实现了交换,优化一下
    for(int i=1;i<=n-1;i++)//枚举趟数
    {
        //枚举乱序区数据 1~(n-i+1)
        for(int j=1;j<=n-i;j++)
        {
            flag=0;
            if(a[j]>a[j+1])
            {
                t=a[j];
                a[j]=a[j+1];
                a[j+1]=t;
                flag=1;
            }
            if(flag==0)
            {
                break;
            }
        }

    }


    for(int i=1;i<=n;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
}

 2.2快速排序

        通过找一个基准数,把比基准数小的放在基准数前面,大的放后面。然后从基准数的位置分开成前后两部分,对前后两部分分别进行快速排序。

        那么问题就来了,如何选择基准数以及如何实现把比基准数小的放在基准数前面,大的放后面。

(1)基准数选择:一般选择第一个数,最后一个数或者中间位置的数做基准数。这里以选择第一个数做基准数为例

(2)如何实现把比基准数小的放在基准数前面,大的放后面:有三种方法,分别是挖空法,左右指针法,前后指针法。这里以挖空法为例。

#include<stdio.h>
#include<stdlib.h>
//快速排序:就地排序(空间复杂度O(logn)),内部排序,不稳定,时间复杂度O(nlogn),但存在退化情况,如果待排序的数据本身就是逆序的,快速
//排序就会退化成近似冒泡排序的量级O(n^2)
int n,a[105];
void Quick_Sort(int l,int r)
{
    if(l>=r) return;
    //l<r
    int i=l,j=r;
    int p=a[l];//区间第一个位置的数做基准数
    while(i<j)
    {
        //最开始最左边i位置是空的,要用j从后找一个比p小的数
        while(i<j&&a[j]>=p) j--;
        if(i<j)
        {//a[j]<=p
            a[i]=a[j];
            i++;
        }
        //j位置空了,用i从前找一个比p大的数
        while(i<j&&a[i]<=p) i++;
        if(i<j)
        {
            //a[i]>=p
            a[j]=a[i];
            j--;
        }
    }
    a[i]=p;//i==j 该位置就是基准数p所在位置
    Quick_Sort(l,i-1);
    Quick_Sort(i+1,r);
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }

    Quick_Sort(1,n);
    for(int i=1;i<=n;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
}
//测试样例
6
5 1 4 2 8 4

         同时注意一下快速排序的退化现象即可。

三.选择排序

3.1简单选择排序

        执行n-1趟排序,第i趟排序的乱序区是i~n。每趟排序都在乱序区中找到最小的数据,放到乱序区第一个位置即可。这就是选择排序,非常简单。

#include<stdio.h>
#include<stdlib.h>
//简单选择排序:就地排序,内部排序,不稳定,时间复杂度O(n^2)
int n,a[105];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }

    int minn;
    int k;//最小数的下标

    for(int i=1;i<=n-1;i++)//排序的趟数,也是乱序区的左边界
    {//乱序区i~n中找到最小的数,放到i位置
        minn=a[i];
        k=i;
        for(int j=i+1;j<=n;j++)
        {
            if(a[j]<minn)
            {
                minn=a[j];
                k=j;
            }
        }
        if(k!=i)
        {
            a[k]=a[i];
            a[i]=minn;
        }

    }

    for(int i=1;i<=n;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
}

3.2堆排序

        用堆对选择排序进行优化就是堆排序。

        那这里就要介绍什么是堆。堆是基于完全二叉树的数据结构。根据其约束条件不同,可分为两种:

大顶堆:根结点的值必须⼤于他的孩⼦结点的值,对于⼆叉树中所有⼦树都满⾜此规律。

⼩顶堆:在⼩顶堆中,根结点的值必须⼩于他的孩⼦结点的值,对于⼆叉树中所有⼦树都满⾜此规律。

既然是数据结构,那么也有一些关于堆的操作:

        以小顶堆为例,用数组来存储完全二叉树。这里需要回顾应该性质,就是使用数组存储完全二叉树,树的某个节点下标为i,则其父亲下标为i/2,左孩子下标是i*2,右孩子下标为i*2+1.

(1)自初始化法:直接认为存树的数组就是堆,只是开始不满足小顶堆的约束。我们需要在数组本身进行调整。从最小的子树开始调整,从以a[n/2]为根的子树开始倒着遍历调整每棵子树。对于每棵子树,采取自上而下调整,具体过程如下:从当前子树根节点p开始调整,p去和两个孩子比较,如果孩子比p小,则交换p和最小孩子,然后再把进行交换的孩子做新的调整节点,直到p比两个孩子都小或者p为叶子节点为止。

(2)插入建堆(堆的插入):开始认为是空堆,不断插入数据进行调整:先把数据插入到堆的最后面,此时该数据是叶子节点。向上进行调整,直到调整到根节点或者满足小顶堆约束为止。

(3)堆的删除:每次删除堆顶元素,删除之后把最后一个位置数据放到堆顶,从堆顶向下调整

关于堆排序,一般升序用大顶堆,降序用小顶堆(满足就地排序)但不唯一。关于使用小顶堆实现升序(基于堆的删除,是非就地排序),具体过程如下:

        1.先根据待排序数据建立小顶堆,堆顶就是最小的

        2.循环n次:输出堆顶(堆顶放到有序数组中),执行堆的删除操作

当然也可以用小顶堆实现降序(基于交换,就地排序)

#include<stdio.h>
#include<stdlib.h>
//堆排序:就地排序(不一定,看如何实现,一般采取就地的),内部排序,不稳定,时间复杂度O(nlogn)
void DownAdjust(int a[],int i,int n)
{//向下调整
    int now=i;//正在调整的节点
    int next;//下一个要调整的节点
    int t;
    //now是next的父亲
    while(now*2<=n)//now*2存在,即now的左孩子存在
    {
        next=now*2;//next是now的左孩子
        if(next+1<=n&&a[next+1]<a[next])
        {
            next=next+1;//如果右孩子存在并且值比左孩子小,next执行右孩子
        }
        if(a[next]<a[now])
        {//父亲比最小的孩子小,不满足小顶堆
            t=a[next];
            a[next]=a[now];
            a[now]=t;

            now=next;//交换之后,孩子成为新的要被调整的节点
        }
        else
        {
            break;
        }
    }
}
//插入建堆,也就是堆的插入
//向上调整
void h_Adjust(int a[],int i)
{
    int now=i;//目前要调整的节点
    int next;//下一个要调整的节点
    int t;
    //next是now的父亲
    while(now/2>=1)
    {
        next=now/2;//next是now的父亲
        if(a[now]<a[next])
        {
            t=a[next];
            a[next]=a[now];
            a[now]=t;

            now=next;
        }
        else
        {
            break;
        }
    }
}
//删除堆顶
void delet_Adjust(int a[],int n)
{
    // printf("%d\n",a[1]);
    a[1]=a[n];
    n--;
    DownAdjust(a,1,n);
}
int main()
{
    int n,a[105],ans[105],k=0;
    scanf("%d",&n);
    int size=n;//堆中实际元素的个数
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);//边读入边插入
        h_Adjust(a,i);
    }
    //自初始化建堆:从最后一个叶子节点父亲为跟的子树开始调整
    // for(int i=n/2;i>=1;i--)
    // {
    //     DownAdjust(a,i,n);
    // }
    // delet_Adjust(a,size);
    // size--;

    int t;
    for(int i=1;i<=n;i++)
    {
        //升序
        // ans[++k]=a[1];
        // delet_Adjust(a,size);
        // size--;

//小顶堆实现降序
        t=a[size];
        a[size]=a[1];
        a[1]=t;
        size--;
        DownAdjust(a,1,size);
    }

    for(int i=1;i<=n;i++)
    {
        // printf("%d ",ans[i]);
        printf("%d ",a[i]);
    }
    printf("\n");
}
//测试样例
9
53 17 78 9 45 65 87 32 8

 最后总结一下堆的常见应用,包括堆排序,优先队列,图的算法优化(如Dijkstra)

四.归并排序(2-路归并)

        不断将序列分成两半,直到不能分割为止,每个序列只有一个数据是就停止,在不断的将两个序列合并成一个有序的序列,在合并的过程中进行排序。合并的具体过程:一个序列,从中间位置分开,如果其前后两部分分别有序(和最终结果的顺序一样),只需要将前后两部分重新合并为整体有序即可。这就是归并排序。

#include<stdio.h>
#include<stdlib.h>
//归并排序:非就地排序,2-路归并内部排序,稳定,时间复杂度O(nlogn)
void Merg(int a[],int l,int mid,int r)
{
    //合并a数组的[l,mid] [mid+1,r]
    int i=l;//前一半的第一个位置
    int j=mid+1;//后一半的第一个位置
    int t[105];
    int k=0;
    while(i<=mid&&j<=r)
    {
        if(a[i]<=a[j])
        {
            t[k++]=a[i];//从t[0]开始存数
            i++;
        }
        else
        {
            t[k++]=a[j];
            j++;
        }
    }
    //放剩下的
    while(i<=mid)
    {
        t[k]=a[i];
        k++;
        i++;
    }
    while(j<=r)
    {
        t[k]=a[j];
        k++;
        j++;
    }
    for(int i=0;i<k;i++)
    {
        a[l+i]=t[i];
    }
}
void MergSort(int a[],int l,int r)
{
    if(l<r)//如果[l,r]至少有两个数就进行排序
    {
        int mid=(l+r)/2;
        //[l,mid] [mid+1,r]
        MergSort(a,l,mid);//先对前一部分排序
        MergSort(a,mid+1,r);//再对后一部分进行排序
        Merg(a,l,mid,r);//合并前后两部分
    }
}
int main()
{
    int n,a[105];
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    MergSort(a,1,n);
    for(int i=1;i<=n;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
}
//测试样例
8
2 1 7 6 8 4 3 5

五.统计排序

5.1 计数排序

        n个非负整数,统计每个数的个数和这n个数的最大值。就是基数排序。这种排序是线性,但其实就是以空间换时间的排序,会造成极大的空间浪费,并且对小数和负数进行排序必须转为正整数。小数乘相同数量积,负数找到最小的负数,每个数都加其相反数。

#include<stdio.h>
#include<stdlib.h>
//计数排序:非就地排序 O(n+maxx)(取决于数组),内部排序,稳定性无法确定(需要使用一个前缀和数组实现优化),时间复杂度O(n+maxx)
int n,a[1005],t[1005],count[1005],sumcount[1005];
int main()
{
    scanf("%d",&n);
    int maxx=0;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        count[a[i]]++;
        if(a[i]>maxx) maxx=a[i];
    }
    //使用前缀和数组实现稳定
    sumcount[0]=count[0];
    for(int i=1;i<=maxx;i++)
    {
        sumcount[i]=sumcount[i-1]+count[i];
    }
    // for(int i=0;i<=maxx;i++)
    // {
    //     int s=count[i];
    //     for(int j=1;j<=s;j++)
    //     {
    //         printf("%d ",i);
    //     }
    // }
    int k;
    for(int i=n;i>=1;i--)
    {
        k=sumcount[a[i]];
        t[k]=a[i];
        sumcount[a[i]]--;
    }

    for(int i=1;i<=n;i++)
    {
        printf("%d ",t[i]);
    }

    return 0;
}

5.2 基数排序

        当元素的的范围在 1 到 n^2 ,此时就不能⽤计数排序了,因为这种情况下,计数排序的时间复杂度达到了O(n^2)量级。 ⽐如对数组 [170, 45, 75, 90, 802, 24, 2, 66] 这个⽽⾔,数组总共包含 8 个元素,⽽数 组中的最⼤值和最⼩值之差为 802 - 2 = 800 ,这种情况下,计数排序就失灵了。这是为了维持线性时间的排序,可以使用基数排序。

        在计数排序的基础上,通过对数位分别排序(从低位到高位)从而得到有序序列。具体过程详见代码,这里主要分析一下基数排序的时间复杂度。

        设d表示输⼊的数组当中最⼤值的位数(⽐如 8023位,d = 3 ),那么基数排序的时间复杂度就是 O(d x (n+b)),其中n表示数组的⻓度,⽽b则是表示⼀个数的进制,对⼗进制⽽⾔ b = 10 ;
        当数字⽤n进制表示的时候,我们就可以对 1 n^c 范围之内的数组进⾏线性排序。 对于元素的跨度(范围)⽐较⼤的数组⽽⾔,基数排序的运⾏时间可能⽐快速排序要好。但是对 于基数排序⽽⾔,其渐近时间复杂度中隐含了更⾼的常量因⼦,并⾮完全的线性;⽽快速排序更有效地
利⽤硬件缓存,提⾼其效率。此外,基数排序使⽤计数排序作为⼦过程,计数排序占⽤额外的空间来对 数组进⾏排序。 但是基数排序解决我们最开始所提出的问题,当数据范围在 1 n^2 时,计数排序的复杂度将变 为O(n^2)量级,⽽基数排序依旧可以在线性时间进⾏排序!
#include<stdio.h>
#include<stdlib.h>
//基数排序:非就地排序 O(n+maxx)(取决于数组),内部排序,稳定性无法确定(需要使用一个前缀和数组实现优化),时间复杂度O(d*(n+b))
int n,a[1005],t[1005],count[1005],sumcount[1005];
void countSort(int d)
{
    for(int i=1;i<=n;i++)
    {
        count[(a[i]/d)%10]++;
    }
    //使用前缀和数组实现稳定
    sumcount[0]=count[0];
    for(int i=1;i<10;i++)
    {
        sumcount[i]=sumcount[i-1]+count[i];
    }
    int k;
    for(int i=n;i>=1;i--)
    {
       k=sumcount[(a[i]/d)%10];
       t[k]=a[i];
       sumcount[(a[i]/d)%10]--;
    }
}
int main()
{
    scanf("%d",&n);
    int maxx=0;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        if(a[i]>maxx) maxx=a[i];
    }
    for(int i=1;maxx/i>0;i*=10)
    {
        countSort(i);
    }
    for(int i=1;i<=n;i++)
    {
        printf("%d ",t[i]);
    }

    return 0;
}
//测试样例
9
1 4 1 2 5 2 4 1 8

5.3桶排序

        桶排序重要的是它的思想,⽽不是具体实现,桶排序从字⾯的意思上看:

1、若⼲个桶,说明此类排序将数据放⼊若⼲个桶中。
2、每个桶有容量,桶是有⼀定容积的容器,所以每个桶中可能有多个元素。
3、从整体来看,整个排序更希望桶能够更匀称,即既不溢出(太多)⼜不太少。
        既然是排序,那么最终的结果肯定是从⼩到⼤的,桶排序借助桶的位置完成⼀次初步的排序—— 将待排序元素分别放⾄各个桶内。 ⽽我们通常根据待排序元素整除的⽅法将其较为均匀的放⾄桶中,如 8 5 22 15 28 9 45 42 39 19 27 47 12 这个待排序序列,假设放⼊桶编号的规则为: n/10 。这样⾸先各个元素就可以直接通 过整除的⽅法放⾄对应桶中。⽽右侧所有桶内数据都⽐左侧的要⼤!这样就实现了桶间有序。那如何实现各个桶内有序呢?很简单,就是调用前面的任意排序算法。
        但是桶排序并且像常规排序那样没有限制,桶排序有相当的限制。因为桶的个数和⼤⼩都是我们⼈为设置的。⽽每个桶⼜要避免空桶的情况。所以我们在使⽤桶排序的时候即需要对待排序数列要求偏均,⼜要要求桶的设计兼顾效率和空间。
        关于桶排序的时间复杂度,是啥不一定的。以桶内快排为例:假设有 n 个待排序数字。分到 m 个桶中,如果分配均匀这样平均每个桶有 n/m 个元素。桶排序的算法时间复杂度有两部分组成:1.遍历处理每个元素,O(n)级别的普通遍历;2.每个桶内再次排序的时间复杂度总和。每个桶内的时间复杂度为 (n/m) log(n/m) 。有m个桶,那么时间复杂度为 m * (n/m)log(n/m) = n (log n-log m) .最后再将两部分相加即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值