常见排序算法的简单总结

本文对几种常见的排序算法做一个简单总结

基本概念
  • 排序稳定性:若排序的序列中,有多个相同的值,且假设A在B前面;若排序后可能使得B在A的前面,那么则称这个排序是不稳定的。

  • 大O法则:用于分析算法的复杂度,有以下约定:用常数1取代算法复杂度表达式的加法常数;算法复杂度表达式中,只保留最高阶;算法复杂度表达式最高阶项的相乘系数修改为1。
    在这里插入图片描述

冒泡排序
  • 流程:

    • 外循环控制排序轮数,排序的轮数为序列长度n
    • 内循环从序列第一/最后序列项开始,与前一个序列项比较;若小于/大于前一序列项,则交换位置;第i轮排序内循环比较次数为 (n – 1 – 第k轮排序)。
    • 若某一次排序轮数为发生交换,则表示此时已完成排序。
  • 时间复杂度:

    • 最好情况:进行n-1次比较,0次交换,复杂度为O(n)
    • 最坏情况:进行( (n-1) … + 2 + 1 ) = n(n-1)/2次比较,并进行n(n-1)/2次的交换,复杂度为O(n2)
  • 空间复杂度:O(1)

  • 代码实现:

    int bubbling_Sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        int i, j, flag = 1;
    
        for (i = 0 ; i < (len-1) && flag --; i ++) {
            for (j = 0; j < (len-1-i); j ++) {
                if (arr[j] > arr[j+1]) {
                    swap(&arr[j], &arr[j+1]);
                    flag = 1;
                }
            }
        }
    
        return 0;
    }
    
简单选择排序
  • 流程:

    • 选择第i个序列项,与后续的 (n-i) 个序列项进行 (n-i) 次比较,找出这 (n-i+1) 个子序列中最小的序列项,并与第 i 个序列项交换。
    • 总计需要循环的轮数为 (n-1) 轮。
  • 时间复杂度:

    • 最好情况:比较( (n-1) … + 2 + 1 ) = n(n-1)/2次,交换0次,复杂度为O(n2)
    • 最坏情况:比较n(n-1)/2次,交换 (n-1) 次,复杂度为O(n2)
  • 空间复杂度:O(1)

  • 代码实现:

    int select_sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        int i, j, min_index;
    
        for (i = 0; i < (len-1); i ++) {
            min_index = i;
            for (j = (i+1); j < len; j ++) {
                if (arr[j] < arr[min_index]) {
                    min_index = j;
                }
            }
            swap(&arr[i], &arr[min_index]);
        }
    
        return 0;
    }
    
直接插入排序
  • 流程:

    • 从第2个序列项开始,第n个序列项结束。

    • 判断当前第i (i≥2) 个序列项是否小于前一个序列项,是则启动插入程序。

    • 前面的 (i-1) 个序列项,只要比第i个序列项小,就往后移一位。

    • 直到遇到不比第i个序列大的序列,则停止,该位置即为第i个序列项的插入位置。

  • 时间复杂度:

    • 最好情况:比较 (n-1) 次,交换0次,复杂度为O(n)
    • 最坏情况:比较(2 + 3 + … + n) = (n-1)(n+2)/2次,交换 ( 1 + 2 + … + (n-1) ) = n(n-1)/2次。
  • 空间复杂度:O(1)

  • 代码实现:

    int direct_insert_sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        int i, j, temp;
    
        for (i = 1; i < len; i ++) {
            if (arr[i] < arr[i-1]) {    /*begin insert*/
                temp = arr[i];
                /*find insert index and move the node which bigger then index node*/
                for (j = i-1; arr[j] > temp && j >= 0; j --) {
                    arr[j+1] = arr[j];
                }
                arr[j+1] = temp;
            }
        }
    
        return 0;
    }
    
希尔排序 — 增强版插入排序
  • 流程:将大序列按照增量序列跳跃式地分割为多个小序列,每轮对这些小序列进行(直接)插入排序;这些小序列排序完成后,大序列就会逐渐趋向于基本有序(大的基本在前面,小的基本在后面);在最后一轮时,进行一次(直接)插入排序就可以了。

    • 初始化incre为序列长度,选取增量序列increincre[k] = incre[k-1] / 3 + 1
    • incre = incre / 2 + 1
    • 从第incre+1个序列项开始,到第n个序列项结束。
    • 判断当前第i(i≥incre+1) 个序列项是否小于第 (i-incre) 序列项,是则启动插入程序。
    • 前面的 (i-1)/incre 个序列项,只要比第i个序列项小,就往后移一位。
    • 直到遇到不比第i个序列大的序列,则停止,该位置即为第i个序列项的插入位置。
    • incre大于1,从第2步开始重复(incre为1时,相当于做了一次直接插入排序)。
  • 时间复杂度:当增量序列选取得好,复杂度为O(nlog n) — O(n2)

  • 空间复杂度:O(1)

  • 不稳定性:由于希尔排序过程中存在跳跃式的子序列进行独立插入排序的情况,所以希尔排序是不稳定的排序算法。

  • 代码实现:

    int shell_sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        int i, j, temp, incre = len;
    
        do {
            incre = incre / 3 + 1;
            for (i = incre; i < len; i ++) {
                if (arr[i] < arr[i-incre]) {
                    temp = arr[i];
                    for (j = i-incre; j >= incre-1 && arr[j] > temp; j -= incre) {
                        arr[j+incre] = arr[j];
                    }
                    arr[j+incre] = temp;
                }
            }
        } while (incre > 1);   /*incre == 1, which mean direct insert sort*/
    
        return 0;
    }
    
堆排序 — 增强版选择排序
  • 堆是完全二叉树,若每个节点值都大于或等于其左右孩子的值,则为大顶堆;反之若每个节点值都小于或等于其左右孩子的值,则为小顶堆

  • 流程:

    • 调整序列结构,使其为大/小顶堆。
    • 交换大/小顶堆的头尾元素,同时将尾元素剔除出堆结构(此时序列中最大与最小元素完成交换)。
    • 重新调整为大/小顶堆结构。
    • 重复2,3步操作n-1次,此时完成排序。
  • 时间复杂度:O(nlog n)

  • 空间复杂度:O(1)

  • 不稳定性:由于存在节点间的比较和交换是跳跃进行的现象,所以堆排序是不稳定的排序算法。

  • 代码实现:

    static void heap_adjust(int *arr, int s, int m)
    {
        int j, temp = arr[s];
    
        for (j = 2*s+1; j <= m; j = 2*j+1) {
            if (j < m && arr[j] < arr[j+1]) /* right node bigger than left node */
                j ++;                       /* use right node */
            if (temp >= arr[j])             /* father node bigger than child node , exit*/
                break;
            arr[s]  = arr[j];               /* swap father node and child node */
            s       = j;                    /* index old father node */
        }
        arr[s] = temp;                      /* set old father node value */
    }
    
    int heap_sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        int i;
    
        for ( i = (len/2-1); i >= 0; i-- )         /* make a big head */
            heap_adjust(arr, i, len-1);
    
        for (i = (len-1); i > 0; i --) {
            swap(&arr[0], &arr[i]);                 /* swap biggest and minimal */
            heap_adjust(arr, 0, i-1);               /* make a big head */
        }
    
        return 0;
    }
    
    
归并排序
  • 流程

    • 将序列中间开始拆分子序列,当n/2小于等于1时停止拆分(拆分log2n次)。

    • 对每个拆分的子序列排序(有些子序列存在两个元素)。

    • 使用两个游标分别指向相邻子序列的头部,比较结点大小,较小的结点输出到缓冲序列中,同时游标移动;当一方游标到底后,另一方剩余数据全部输出,此时归并后的子序列是有序的。

    • 重复第三步直至序列合并完成,此时完成排序。
      在这里插入图片描述

  • 时间复杂度:O(nlog n)

  • 空间复杂度:O(n)

  • 代码实现:

    static void merge(int *sr, int *tr, int i, int m, int n)
    {
        int j, k, r;
    
        /*use two index to sort two orderly list*/
        for (j = m+1, k = i; i <= m && j <= n; k ++)
        {
            if (sr[i] < sr[j])
                tr[k] = sr[i++];
            else
                tr[k] = sr[j++];
        }
    
        /*left have node, push them*/
        if (i <= m) {
            for (r = 0; r <= m-i; r++) {
                tr[k+r] = sr[i+r];
            }
        }
        /*right have node, push them*/
        if (j <= n) {
            for (r = 0; r <= n-j; r++) {
                tr[k+r] = sr[j+r];
            }
        }
    }
    
    static void m_sort(int *sr, int *tr1, int s, int t)
    {
        int m;
        int tr2[26];
    
        if (s == t) {
            tr1[s] = sr[s];
        } else {
            m = (s + t) / 2;			/* get mid index */
            m_sort(sr, tr2, s, m);      /* sort sr[s ...... m] to tr2 */
            m_sort(sr, tr2, m+1, t);    /* sort sr[m+1 ...... t] to tr2 */
            merge(tr2, tr1, s, m, t);   /* merge and sort tr2(which have two orderly list) to tr1, */
        }
    }
    
    int merge_sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        m_sort(arr, arr, 0, len-1);
    
        return 0;
    }
    
快速排序 — 升级版冒泡排序
  • 流程:通过一轮排序将序列项分为独立的两个子序列——较大序列项的子序列和较小序列项的子序列;再分别对两个子序列排序,最终达到有序目的。

    • 使用两个游标指向序列的头部和尾部,选取序列中某个节点作为基准k
    • 将比基准k小的数放在左边,比k大的数放在右边,返回最终基准k的位置
    • k的位置为基准,分割成两个子序列
    • 重复上诉步奏,直至所有子序列个数为1,此时排序完成
  • 时间复杂度:情况介于O(nlog n) — O(n2),但平均情况是O(nlogn)

  • 空间复杂度:情况介于O(nlog n) — O(n2),但平均情况是O(nlogn)

  • 不稳定性:由于存在跳跃式的比较和交换,所以快速排序算法是不稳定

  • 代码实现

    static int partition(int *arr, int low, int high)
    {
        int pivot_val = arr[low];       /* init pivot value */
    
        while (low < high) {            /* loop until find the pivot */
            /* find the node which lower than pivot value */
            while (low < high && arr[high] >= pivot_val)
                high --;
            swap(&arr[low], &arr[high]);    /* swap lower node to left */
            /* find the node which bigger than pivot value */
            while (low < high && arr[low] <= pivot_val)
                low ++;
            swap(&arr[low], &arr[high]);    /* swap bigger node to right */
        }
    
        return low;
    }
    
    static void q_sort(int *arr, int low, int high)
    {
        int pivot;
    
        if (low < high) {
            pivot = partition(arr, low, high);  /* find the pivot, divide list */
            q_sort(arr, low, pivot-1);          /* sort low to pivot-1 */
            q_sort(arr, pivot+1, high);         /* sort pivot+1 to high */
        }
    }
    
    int quick_sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        q_sort(arr, 0, len-1);
    
        return 0;
    }
    
桶排序
  • 流程:按照数据的特点,规定几个范围称之为桶,将数据按照范围装进桶中;然后在桶里面在进行分别排序(可使用别的排序算法或递归使用桶排序),最后再将桶中数据分别输出,完成排序。

  • 代码实现:假设数据为正整数类型,且数据范围小。

    int bucket_sort(int *arr, int len)
    {
        if (arr == NULL || len <= 1)
            return -1;
    
        int *bucket = (int*)malloc(MAX_VAL*sizeof(int));
        if (bucket == NULL)
            return -1;
        memset(bucket, 0, MAX_VAL*sizeof(int));
        int i, j;
    
        for (i = 0; i < len; i ++)
            bucket[arr[i]] ++;
    
        for (i = 0, j = 0; i < MAX_VAL; i ++) {
            if (bucket[i] > 0) {
                while (bucket[i] --)
                    arr[j++] = i;
            }
        }
    		
    	free(bucket);
        return 0;
    }
    
结语
  • 分类:冒泡排序快速排序属于交换排序类;简单选择排序堆排序属于选择排序类;直接插入排序希尔排序属于插入排序类;归并排序属于归并排序类。

  • 各排序算法比较
    在这里插入图片描述

  • 由上表和前面分析可推测算法的选取准则:

    • 若是序列长度短且序列元素简单,选择直接插入排序;序列长度短但序列元素复杂,需要减少交换次数选择简单选择排序;折中选取冒泡排序
    • 序列复杂但给足空间选择归并排序;序列复杂但空间不足选取堆排序希尔排序;多次优化后快速排序整体性能最好。
参考资料
  • 《大话数据结构》
  • 《数据结构与算法分析 — C语言描述》

以上是对常见排序算法的简单总结,不当之处请在评论区指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值