C语言实现八大排序算法(一)

本文主要介绍数据结构中常见的八大排序算法,冒泡排序、快速排序、直接插入排序、希尔排序、简单选择排序、堆排序、归并排序和基数排序。

排序相描述

  1. 排序分类:若排序过程中,所有的文件都是放在内存中处理的,不涉及数据的内外存交换,则称该排序算法是内部排序算法; 若排序过程中涉及内外存交换,则是外部排序。内部排序适合小文间,外部排序适用于不能一次性把所有记录放入内存的大文件。常见的分类算法还可以根据排序方式分为两大类:比较排序非比较排序
  2. 排序算法的稳定性: 若排序对象中存在多个关键字相同的记录,经过排序后,相同关键字的记录之间的相对次序保持不变,则该排序方法是稳定的,若次序发生变化(哪怕只有两条记录之间),则该排序方法是不稳定的。关于哪些算法是稳定的,哪些不稳定,下面会详细介绍。
  3. 时间复杂度:一般情况下,算法中基本操作重复执行的次数是问题规模 n n n的某个函数,用 T ( n ) T(n) T(n)表示,若有某个辅助函数 f ( n ) f(n) f(n),使得 T ( n ) / f ( n ) T(n)/f(n) T(n)/f(n)的极限值(当 n n n趋近于无穷大时)为不等于零的常数,则称 f ( n ) f(n) f(n) T ( n ) T(n) T(n)的同数量级函数。记作 T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n)),称 O ( f ( n ) ) O(f(n)) ​ O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
  4. 空间复杂度:空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,它是问题规模 n n n的函数,记做 S ( n ) = O ( f ( n ) ) S(n)=O(f(n)) S(n)=O(f(n))。比如直接插入排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2),空间复杂度是 O ( 1 ) O(1) O(1)。而一般的递归算法就要有 O ( n ) O(n) O(n)的空间复杂度了,因为每次递归都要存储返回信息,需要辅助空间的大小随着 n n n的增大线性增大。
  5. 就地排序:若一个排序算法所需的辅助空间并不依赖于问题的规模n,即时间复杂度是 O ( 1 ) O(1) O(1),则称该排序算法为就地排序。非就地排序算法的时间复杂度一般为 O ( n ) O(n) O(n)

注:算法是否稳定并不能衡量一个算法的优劣,排序算法主要包含2种操作:比较和移动,但并不是所有的排序都基于比较操作,比如基数排序。

下图为排序算法体系结构图:

各大排序算法的时间复杂度、空间复杂度及稳定性见下表:

直接插入排序(Insertion Sort)

基本思想

将待排序的无序数列看成是一个仅含有一个元素的有序数列和一个无序数列,将无序数列中的元素逐次插入到有序数列中,从而获得最终的有序数列。

算法流程

  1. 初始时, a [ 0 ] a[0] a[0]自成一个有序区, 无序区为 a [ 1 , . . . , n 1 ] a[1, ... , n-1] a[1,...,n1], 令 i = 1 i=1 i=1;
  2. a [ i ] a[i] a[i]并入当前的有序区 a [ 0 , . . . , i 1 ] a[0, ... , i-1] a[0,...,i1];
  3. i + + i++ i++并重复步骤2,直到 i = n 1 i=n-1 i=n1, 排序完成。

详细描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置中
  6. 重复步骤2

复杂度分析:
第1个元素,需要进行 0 次比较;
第2个元素,需要进行 1 次比较;
第3个元素,需要进行 2 次比较;
第n个元素,需要进行n-1次比较;

所以插入排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2),空间复杂度是 O ( 1 ) O(1) O(1),稳定

动态实例

算法实现

//直接插入法一
void InsertSort1(int a[], int n)
{
    int i, j;
    for(i=1; i<n; i++)
        if(a[i] < a[i-1])   
        {
            int temp = a[i];                       //保存要比较的值
            for(j=i-1; j>=0 && a[j]>temp; j--)    //从后向前查找待插入位置
                a[j+1] = a[j];                    //挪位
            a[j+1] = temp;                       //复制到插入位置
        }
}

//直接插入法二:用数据交换代替法一的数据后移(比较对象只考虑两个元素)
void InsertSort2(int a[], int n)
{
    for(int i=1; i<n; i++)
        for(int j=i-1; j>=0 && a[j]>a[j+1]; j--)
            Swap(a[j], a[j+1]);
}
void Swap(int a,int b){
    int temp;
    temp=a;
    a=b;
    b=temp;
}

拓展(折半插入排序)

仅适用于顺序存储的线性表
直接插入排序是边比较,边移动元素;
折半插入排序是先折半查找到插入位置,再统一移动元素

void ZhebanInsertSort(int a[],int n){
    int i,j,low,high,mid;
    for(i=1;i<n;i++){
        temp=a[i];
        low=1,high=i;            //设置折半查找范围
        while(low<=high){
            mid=(low+high)/2;  
            if(a[mid]>temp) 
                high=mid-1;        //查找左半部分
            else
                low=mid+1;        //查找右半部分
        }
        for(j=i;j>=high+1;--j)
            a[j+1]=a[j];          //统一后移元素,空出插入位置
        a[high+1]=temp;           //插入
    }
}

希尔排序(Shell Sort)

希尔排序是第一个突破 O ( n 2 ) O(n^2) O(n2)的排序算法,是简单插入排序的优化,实质是分组的简单插入排序。它与插入排序的不同之处在于,它会优先比较距离较远的元素(每次取相隔一定间隔gap的元素作为一组,在组内执行简单插入排序)。希尔排序又叫缩小增量排序不断减小间隔gap的数组,直到gap=1)。

基本思想

  1. 希尔排序是把记录按下标的一定量分组,对每组使用直接插入算法排序;
  2. 随着增量逐渐减少,每组包1含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法被终止。

算法流程

  1. 选择一个增量序列 t 1 t 2 t k t_1,t_2,…,t_k t1t2tk,其中 t i &gt; t j t k = 1 t_i&gt;t_j,t_k=1 ti>tjtk=1
  2. 按增量序列个数 k k k,对序列进行$k $趟排序;
  3. 每趟排序,根据对应的增量 t i t_i ti,将待排序列分割成若干长度为 m m m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

时间复杂度为 O ( n 1 + e ) O(n^{1+e}) O(n1+e)(其中0<e<1),空间复杂度 O ( 1 ) O(1) O(1),不稳定

动态实例

算法实现


//希尔排序法一
void shellSort1(int a[],int n){
    int i,j,gap,temp;
    for(gap = n/2;gap>0;gap/=2){
        for(i=gap;i<n;i+=gap){
            temp = a[i];
            for(j = i-gap;j>=0&&a[j]>temp;j-=gap){
                a[j+gap] = a[j];
            }
            a[j+gap] = temp;
        }
    }
}
//希尔排序法二,和上面的直接插入排序类似,用数据交换代替法一的数据后移(比较对象只考虑两个元素)
void ShellSort2(int a[], int n)
{
    int i, j, gap;
    //分组
    for(gap=n/2; gap>0; gap/=2)
        //直接插入排序
        for(i=gap; i<n; i++)
            for(j=i-gap; j>=0 && a[j]>a[j+gap]; j-=gap)
                Swap(a[j], a[j+gap]);
}

冒泡排序(Bubble Sort)

基本思想

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。每一趟排序后的效果都是讲没有沉下去的元素给沉下去。

算法流程 (递增为例)

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数
  3. 针对所有的元素重复以上的步骤,除了最后一个
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

复杂度分析
第一次排序的比较次数:$ n-1$ n个元素相邻两两比较,需要n-1次比较;
第二次排序的比较次数:$ n-2$ 第一次排序后最后一个元素可以确定为最大值,不再需要参与第二次排序;
第三次排序的比较次数: n 3 n-3 n3 同理,最后两个数已经确定,不再需要参与排序;
第 n-1 次排序的比较次数: 1 1 1 ;

所以冒泡排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。 空间复杂度是 O ( 1 ) O(1) O(1),稳定

动态实例

算法实现

//冒泡排序
void BubbleSort(int a[], int n)
{
    int i, j;
    for(i=0; i<n; i++){
        bool flag=false;              //表示本趟冒泡是否发生交换的标志
        for(j=1; j<n-i; j++){         //j的起始位置为1,终止位置为n-i 
            if(a[j]<a[j-1]){
               Swap(a[j-1], a[j]);
                flag=true;
            }
        }
        if(flag==false)             //未交换,说明已经有序,停止排序
            return;
    }          
}

拓展 (冒泡排序的改进 ---- 鸡尾酒排序)

鸡尾酒排序与冒泡排序的不同处在于排序时是以首尾双向在序列中进行排序

  1. 先对数组从左到右进行升序的冒泡排序;
  2. 再对数组进行从右到左的降序的冒泡排序;
  3. 以此类推,持续的、依次的改变冒泡的方向,并不断缩小没有排序的数组范围;

鸡尾酒排序的优点是能够在特定条件下(如集合中大部分元素有序),减少排序的回合数

实现鸡尾酒排序需要分别定义一个从最左边开始的index_left和从最右边开始的index_reight,当两个index相等的时候循环结束。

//鸡尾酒排序
void CocktailSort(int a[], int n){
    int left = 0, right=n-1;
    while(left<right){
        for(int i=left;i<right-1;i++){       //从前往后排
            if(a[i]>a[i+1]){
                Swap(a[i],a[i+1]);
            }           
            right-=1;
            for(int j=right;j>left;j--){     //从后往前排
                if(a[j]<a[j-1]){
                    Swap(a[j],a[j-1]);
                }
            }
            left+=1;
        }
    }    
}

快速排序(Quick Sort)

快速排序是目前所有内部排序算法中平均性能最优的排序算法。

基本思想

快速排序算法的基本思想为分治思想。

  1. 先从数列中取出一个数作轴值(基准数)pivot
  2. 根据基准数将数列进行分区,小于基准数的放左边,大于基准数的放右边;
  3. 重复分区操作,知道各区间只有一个数为止。

算法流程(递归+挖坑填数)

  1. i = L j = R i=L,j=R i=Lj=R,将基准数挖出形成第一个坑 a [ i ] a[i] a[i]
  2. j j-- j由后向前找出比它小的数,找到后挖出此数 a [ j ] a[j] a[j]填到前一个坑 a [ i ] a[i] a[i]中;
  3. i + + i++ i++从前向后找出比它大的数,找到后也挖出此数填到前一个坑 a [ j ] a[j] a[j]中;
  4. 再重复2,3,直到 i = j i=j i=j,将基准数填到 a [ i ] a[i]​ a[i]

复杂度分析:
时间复杂度为 O ( n l o g 2 n ) O(nlog_{2}^{n}) O(nlog2n),空间复杂度为 O ( l o g 2 n ) O(log_2^n) O(log2n),不稳定

下面是一个我手写的详细分析实例:

动态实例

算法实现

//快速排序
void QuickSort(int a[],int left,int right){
    if(left<right){
        int i=left,j=right;
        int base=a[left];             //基准
        while(i<j){
            while(i<j&&a[j]>=base)    //从右往左找小于base的元素
                j--;
            if(i<j)
                a[i++]=a[j];
            while(i<j&&a[i]<base)    //从左往右找大于base的值
                i++;
            if(i<j)
                a[j--]=a[i];
        }
        a[i]=base;                  //将基准base填入最后的坑中
        QuickSort(a,left,i-1);      //递归调用,分治
        QuickSort(a,i+1,right);
    }
}
    

注意:如果基准不是选第一个数,可以用Swap()函数将基准调整至第一个位置,再执行上述程序。

总结:本文主要介绍了2大类排序算法:插入排序(直接插入、希尔排序)和交换排序(冒泡排序、快速排序)。在下一篇博文中,我将介绍剩下的4种排序(简单选择排序、堆排序、归并排序、基数排序),并对所有的排序算法做一个比较。

全部评论

相关推荐

2025-11-26 11:21
已编辑
武汉理工大学 Java
个人bg:&nbsp;211本,一段京东实习,一段xhs实习,一段小厂实习。互联网只有美团一个带薪意向。转正失败情况:京东实习了四个月,感觉收获比较少,做的事情偏基础,第三个月底答辩,离职后两个月被告知转正失败。对此我只能说,零售卡硕。xhs实习两个月,反而感觉收获更多,被安排了有挑战的事情,大模型在业务场景中的运用,最后一个星期通知有转正机会,边做需求边匆忙准备,答辩采取一票否决制,四个领导三过一否,也失败。(早知道xhs今年开这么高我就熬夜赶材料了)不过在这个过程中,也push自己了解了一定rag&nbsp;mcp&nbsp;大模型的相关知识,对于后续面阿里和美团很有帮助。个人基础情况:hot100能默写。去年12底学完jvm&nbsp;juc。2月入职京东前小林coding&nbsp;guide就差不多看完了。后面实习的时候也有继续补面筋,场景题。秋招情况:8月初就投了,也不晚。滴滴:&nbsp;笔试a了没面,可能投的岗位太小众了?(抱着拿了也不去&nbsp;用于a价的想法)一直卡着。携程:&nbsp;不发笔。发官方邮件也不回。京东:笔试挂了。嗯,很耻辱,那天在外面玩但确实很久没复习笔试考试范围了,全忘光了。腾讯:从来没约过,可能暑期面了十几次面太多了。阿里控股:一面挂。阿里国际:hr面后一个月挂。字节:国际电商三面挂-&gt;星图一面挂(面的时候已经有很多候选人了)-&gt;&nbsp;安全风控二面挂(业务不是很好,面试过程说漏嘴说业务会影响我选择,场景题没答好)-&gt;&nbsp;中台一面后无消息快手:二面挂。xhs:hr面后无消息,排序应该很靠后。虾皮:hr面两个月无消息,应该还在泡池子。百度:一面挂。pdd:笔试a3后笔试挂。难绷。个人反思总结:for&nbsp;后来者。1.&nbsp;笔试一定要把握好,虽然面试中都是hot100,有些甚至不考面试题,但是大厂笔试题是有acm难度的,挂了就是挂了,很多没有第二次机会,约面也没机会了。建议时间充裕情况下,还是要把灵神的题单多刷点。顺序可以参考:代码随想录视频+题&nbsp;-&gt;&nbsp;灵神视频+题&nbsp;-&gt;hot100&nbsp;-&gt;灵神题单(可以每个part挑难度低的前几道写)2.&nbsp;一段深入长的实习经历一定是大于两段短的,不过现在再让我选到底是继续在jd还是去xhs我还是选不出来。在面试的过程中,有些面试官也会认为我实习的太浅,没有做什么有深度的事情,对多种方案的调研不全面。如果实习做的事情比较有挑战最好,如果没有,也要尽量往多种方案调研最后选择了哪个方案,达到了当初定的业务指标/技术指标方面包装。3.&nbsp;还是得早投。身边除了bg特别好的朋友,投的晚的无一例外秋招情况会差很多。8月前投能赶上提前批。最晚不要8月中旬过了还没投完。有投的早的没有实习的朋友秋招结果也可以。没有面试的同学一定要尝试官网,boss直聘多种途径投。4.&nbsp;对于有实习的同学,基础没有那么重要了,更多还是专注于对实习的考察,可以以金字塔的形式进行论述,避免在最开始的时候就展开大量细节。如果实在没有实习,bg够硬,投的够早也会有面,只需要一个比较深入的项目应该就没问题,把项目当作自己在实习要投入生产的心态去调研包装。5.&nbsp;有的时候真的看运气。即使是同一个部门甚至是同一个组的同学,做的事情也会有差异,这主要看导师被分配到什么样的活。for&nbsp;me:大二的时候绩点排名前10%,但还是决定放弃保研,开始学java,这一路走来,经历迷茫踏实的反复,也想和自己说句幸苦了,谁想得到当初给自己定的目标是有份工作不饿死就行。可能差点运气,可能在关键节点上做的还是不够,对于实习的包装,对于面试表现还是差点。会后悔自己没读研吗?其实我也有考雅思,申请了港大计算机,但估计大概率还是工作(实则也没港大offer)。人不能既要又要还要,我不能既要早点工作赚钱,实现我财富自由支配,带不舍得花钱的家人去旅游的想法,又要长期来看高学历晋升的优势,还要在大环境变差一届比一届卷我也能找到差强人意的工作。所以,至少现在,我不后悔。如果我更倾向于国企而不是互联网,比起技术挑战更偏爱稳定的生活我大概率会读研。如果我本科没有211,我还想进大厂,我也大概率会读研。会后悔自己没选其他的方向吗?java确实相对卷一点,但也只是相对的,因为其他方向的人也很多,并不是换方向就一定会更好。计算机这一行本就短命,能干到35就算成功,大家都是为了赚钱,基于此,在背景没那么硬时,选择一个相对人少的方向进大厂是对的。看自己怎么理解了。最好的还是参考直系学长学姐的选择,一定要多沟通交流。一些安慰自己的话,秋招是人生的起点,不一定是高费阵容才能吃鸡,低费阵容早点发育也有吃鸡的上限。(随便乱说的)。最后还想再写一段话给学妹们,程序员这一行,女生确实会相对少一点,但比起传统工科非常直接的偏向男生,计算机这一行认为菜是原罪,性别的因素会少很多,更多看个人技术和水平。在京东实习的时候,我的小组长在我进去第一天就和我说,我们部门女生虽然少,但是水平都至少是中上的,都很能吃苦很能干。无论是我们组干活巨快的A姐,还是总能很快解答我问题的B姐,又或者是其他总能给我提供建议的其他姐姐们,都使我对这一点坚信不疑,她们高学历,专业,细心,耐心。如果你也热爱技术,虽然有时会被bug折磨,但喜欢学到知识时候的踏实,喜欢bug&nbsp;fix的爽感,你就是适合这一行的。我的秋招结束了,但我大概率不会甘心,还是会想试试春招,但我也真的觉得到现在这一步已经很棒了。欢迎同校学妹学弟们找我沟通交流~
疲倦的牛马还在上班:再冲一次,春招不留遗憾吧!
投递美团等公司9个岗位
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务