冒泡排序、插入排序、选择排序这三种算法的时间复杂度都为 O ( n 2 ) O(n^2) O(n2),只适合小规模的数据。今天,我们来认识两种时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 的排序算法——归并排序(Merge Sort)和快速排序(Quick Sort),他们都用到了分治思想,非常巧妙。

1. 归并排序(Merge Sort)?

1.1. 归并排序算法实现

  • 归并排序的核心思想其实很简单,如果要排序一个数组,我们先把数组从中间分成前后两部分,然后分别对前后两部分进行排序,再将排好序的两部分数据合并在一起就可以了。

  • 归并排序使用的是分治思想,分治也即是分而治之,将一个大问题分解为小的子问题来解决。分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧

  • 如果要对数组区间 [p, r] 的数据进行排序,我们先将数据拆分为两部分 [p, q] 和 [q+1, r],其中 q 为中间位置。对两部分数据排好序后,我们再将两个子数组合并在一起。当数组的起始位置小于等于终止位置时,说明此时只有一个元素,递归也就结束了。

递推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))终止条件:
p >= r 不用再继续分解
  • 对两个子数组进行合并的过程如下所示,我们先建立一个临时数组,然后从两个子数组的起始位置开始比较,将较小的元素一个一个放入临时数组,直到其中一个子数组比较完毕,再将剩下的另一个子数组余下的值全部放到临时数组后面。最后我们需要将临时数组中的数据拷贝到原数组对应的位置。

  • 代码实现
// O(n(logn))
void Merge_Sort(float data[], int left, int right, float sorted_data[])
{if(left < right){int mid = (left + right) / 2;Merge_Sort(data, left, mid, sorted_data);Merge_Sort(data, mid+1, right, sorted_data);Merge_Array(data, left, mid, right, sorted_data);}
}void Merge_Array(float data[], int left, int mid, int right, float temp[])
{int i = left, j = mid + 1;int k = 0;// 从子数组的头开始比较while(i <= mid && j <= right){if (data[i] <= data[j]){temp[k++] = data[i++];}else{temp[k++] = data[j++];}}// 判断哪个子数组还有元素,并拷贝到 temp 后面while(i <= mid){temp[k++] = data[i++];}while(j <= right){temp[k++] = data[j++];}// 将 temp 中的数据拷贝到原数组对应位置for(i = 0; i < k; i++){data[left+i] = temp[i];}
}/*哨兵简化*/
void Merge_Array(float data[], int left, int mid, int right, float temp[])
{int max_num = INT_MAX;int len = right - left + 1;int data_left = new int[mid-left+2];int data_right = new int[right-mid+1];int i = 0, j = 0, k = 0;// 复制左半部分元素,放置哨兵在末尾for(int k = left; k <= mid; k++){data_left[k-left] = data[k];}data_left[k-left] = max_num;// 复制右半部分元素,放置哨兵在末尾for(int k = mid + 1; k <= right; k++){data_right[k-mid-1] = data[k];}data_right[k-mid-1] = max_num;for (int k = 0; k < len; k++){if (data_left[i] <= data_right[j]){data[k+left] = data_left[i++];}else{data[k+left] = data_right[j++];}}
}

1.2. 归并排序算法分析

  • 归并排序是一个稳定的排序算法,在进行子数组合并的时候,我们可以设置当元素大小相等时,先将前半部分的数据放入临时数组,这样就可以保证相等元素在排序后依然保持原来的顺序。

  • 不仅递归求解的问题可以写成递推公式,递归代码的时间复杂度也可以写成递归公式

  • 如果我们对 n n n 个元素进行归并排序所需要的时间是 T ( n ) T(n) T(n),那分解成两个子数组排序的时间都是 T ( n 2 ) T(\frac{n}{2}) T(2n​),而合并两个子数组的时间复杂度为 O ( n ) O(n) O(n)。所以,归并排序的时间复杂度计算公式为:

T ( 1 ) = C T(1) = C T(1)=C
T ( n ) = 2 ∗ T ( n 2 ) + n , n > 1 T(n) = 2*T(\frac{n}{2}) + n, n>1 T(n)=2∗T(2n​)+n,n>1

  • n = 1 时,只需要常量级的执行时间,所以表示为 C。

T ( n ) = 2 ∗ T ( n 2 ) + n T(n) = 2*T(\frac{n}{2}) + n T(n)=2∗T(2n​)+n
= 2 ∗ [ 2 ∗ T ( n 4 ) + n 2 ] + n = 4 ∗ T ( n 4 ) + 2 ∗ n = 2*[2*T(\frac{n}{4}) + \frac{n}{2}] + n = 4*T(\frac{n}{4}) + 2*n =2∗[2∗T(4n​)+2n​]+n=4∗T(4n​)+2∗n
= 4 ∗ [ 2 ∗ T ( n 8 ) + n 4 ] + 2 ∗ n = 8 ∗ T ( n 8 ) + 3 ∗ n = 4*[2*T(\frac{n}{8}) + \frac{n}{4}] + 2*n = 8*T(\frac{n}{8}) + 3*n =4∗[2∗T(8n​)+4n​]+2∗n=8∗T(8n​)+3∗n
. . . . . . ...... ......
= 2 k ∗ T ( n 2 k ) + k ∗ n = 2^k * T(\frac{n}{2^k}) + k * n =2k∗T(2kn​)+k∗n
. . . . . . ...... ......
当 n 2 k = 1 \frac{n}{2^k} = 1 2kn​=1时, k = l o g 2 n k = log_2n k=log2​n,代入上式得:
T ( n ) = n ∗ C + n l o g 2 n T(n) = n * C + nlog_2n T(n)=n∗C+nlog2​n
用大 O 标记法来表示,归并排序的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。

  • 从我们的分析可以看出,归并排序的执行效率与原始数据的有序程度无关,其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn)

  • 归并排序有一个缺点,那就是它不是原地排序算法。在进行子数组合并的时候,我们需要临时申请一个数组来暂时存放排好序的数据。因为这个临时空间是可以重复利用的,因此归并排序的空间复杂度为 O ( n ) O(n) O(n),最多需要存放 n n n 个数据。


2. 快速排序(Quick Sort)?

1.1. 快速排序算法实现

  • 快速排序的思想是这样的,如果要对数组区间 [p, r] 的数据进行排序,我们先选择其中任意一个数据作为 pivot(分支点),一般为区间最后一个元素。然后遍历数组,将小于 pivot 的数据放到左边,将大于 pivot 的数据放到右边。接着,我们再递归对左右两边的数据进行排序,直到区间缩小为 1 ,说明所有的数据都排好了序。
递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)终止条件:
p >= r
  • 归并排序是由下向上的,先处理子数组然后再合并。而快速排序正好相反,它的过程是由上向下的,先分出两个子区间,再对子区间进行排序。归并排序是稳定的时间复杂度为 O ( n ) O(n) O(n),但它是非原地算法,而快排则是原地排序算法。

  • 快速排序的分区过程如下所示,从左到右依次遍历数组,如遇到小于 pivot 的元素,则进行数据交换 ,否则继续往前进行,最后再放置 pivot。

  • 代码实现

// O(n(logn))
void Quick_Sort(float data[], int left, int right)
{if (left < right){int i = left, j = left;int pivot = data[right];for (j = left; j < right; j++){if (data[j] < pivot){int temp = data[i];data[i] = data[j];data[j] = temp;i++;}}data[j] = data[i];data[i] = pivot;Quick_Sort(data, left, i-1);Quick_Sort(data, i+1, right);}
}
  • 快速排序的另一种实现方式如下所示,先取出一个元素作为 pivot(假设是最后一个),这时 pivot 位置可以看作为空,然后从左到右查找第一个比 pivot 大的元素放在 pivot 的位置,此时空的地方变成了这第一个比 pivot 大的元素位置。然后从右到左查找第一个比 pivot 小的元素放在刚才空的位置,依次循环直到从左到右和从右到左都查找到了同一位置,这时候再把 pivot 放置在最后一个空位。这个过程可以形象的被称为“挖坑填坑”。

  • 代码实现
// O(n(logn))
void Quick_Sort(float data[], int left, int right)
{if (left < right){int i = left, j = right;int pivot = data[j];while(i < j){while(i < j && data[i] <= pivot) // 从左往右找到第一个比 pivot 大的数{i++;}if(i < j){data[j--] = data[i];}while(i < j && data[j] >= pivot) // 从右往左找到第一个比 pivot 小的数{j--;}if(i < j){data[i++] = data[j];}}data[i] = pivot; // i=jQuick_Sort(data, left, i-1);Quick_Sort(data, i+1, right);}
}

2.2. 快速排序算法分析

  • 如果快速排序每次都将数据分成相等的两部分,则快排的时间复杂度和归并排序相同,也是 O ( n l o g n ) O(nlogn) O(nlogn),但这种情况是很难实现的。如果数据原来已经是有序的,则每次的分区都是不均等的,我们需要进行 n 次分区才能完成整个排序,此时快排的时间复杂度就退化成了 O ( n 2 ) O(n^2) O(n2)。

  • 平均时间复杂度的求解也可以通过递归树来分析,这个问题留待我们以后再解决。我们现在只需要知道,在大部分情况下,快速排序的时间复杂度都可以做到 O ( n l o g n ) O(nlogn) O(nlogn),只有在极端情况下,才会退化成 O ( n 2 ) O(n^2) O(n2)

  • 快速排序是一个原地排序算法,是一个不稳定的排序算法,因为其在数据交换过程中可能会改变相等元素的原始位置。


3. 小结

  • 归并排序和快速排序都是利用分治的思想,代码都通过递归来实现,过程非常相似。
  • 归并排序非常稳定,时间复杂度始终都是 O ( n l o g n ) O(nlogn) O(nlogn),但不是原地排序;快速排序虽然最坏情况下时间复杂度为 O ( n 2 ) O(n^2) O(n2),但平均情况下时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),最坏情况发生的概率也比较小,而且是原地排序算法,因此应用得更加广泛。

参考资料-极客时间专栏《数据结构与算法之美》

获取更多精彩,请关注「seniusen」!

排序算法之——归并排序和快速排序相关推荐

  1. 排序算法:归并排序、快速排序

    相关博客: 排序算法:冒泡排序.插入排序.选择排序.希尔排序 排序算法:归并排序.快速排序 排序算法:桶排序.计数排序.基数排序 排序算法:堆排序 十大排序算法小结 一.归并排序: 1.工作原理: 归 ...

  2. NOI提高级:排序算法之归并排序、快速排序

    图解排序算法(四)之归并排序 图解排序算法(四)之归并排序 - dreamcatcher-cx - 博客园 小学生图解排序算法:⑥归并排序 小学生图解排序算法:⑥归并排序_纯文笔记-CSDN博客_图解 ...

  3. 排序算法中——归并排序和快速排序

    冒泡排序.插入排序.选择排序这三种算法的时间复杂度都为 $O(n^2)$,只适合小规模的数据.今天,我们来认识两种时间复杂度为 $O(nlogn)$ 的排序算法--归并排序(Merge Sort)和快 ...

  4. [Alg]排序算法之归并排序

    [Alg]排序算法之归并排序 作者:屎壳郎 miaosg01@163.com 日期:Aug 2021 版次:初版 简介: 归并排序是一类在任何情况下都能保证Nlg⁡(N)N\lg(N)Nlg(N)的排 ...

  5. 【排序算法】归并排序(C语言)

    [排序算法]-- 归并排序(C语言) 目录 一.归并排序的原理 二.两个有序数组排序和合并 1. 原地排序 2. 创建临时空间 二.递归实现 三.非递归实现 1. 实现思路 2. 数组边界问题 3. ...

  6. 排序算法之--归并排序(好玩的一个算法o。o)快速入门

    排序算法之--归并排序(好玩的一个算法o.o) 下面是归并操作的基本思路(注意:是归并操作哦,不是归并排序哦) 归并操作的工作原理如下: 第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存 ...

  7. 数据结构与算法:十大排序算法之归并排序

    数据结构与算法:十大排序算法之归并排序 package TopTenSortingAlgorithms;/*** 归并排序:Java** @author skywang* @date 2014/03/ ...

  8. c++ 快速排序_C语言必学的12个排序算法:归并排序(第8篇)

    题外话很多童鞋感受算法比较难度,的确,算法其实和C语言本身关系不大,算法是计算机科学家利用丰富的数学和算法设计知识研究出来,如今我们只需拿来主义,学习使用即可,当然这需要一定的努力过程. [C语言必学 ...

  9. C语言各种排序算法(冒泡排序、快速排序、插入排序、希尔排序、快速排序、 归并排序)

    不知名排序 #include <stdio.h>int main() {int a[18] = {11,5,53,10,26,3,89,32,9,10,1,24,35,56,43,23,7 ...

最新文章

  1. java同步与死锁_Java多线程 - 线程同步与死锁
  2. Ubuntu下压缩解压文件
  3. debian清除无用的库文件(清理系统,洁癖专用)
  4. WinSock网络编程实用宝典(一)
  5. ffplay.c学习-1-框架及数据结构
  6. Asp.net mvc 知多少(六)
  7. numpy的基本使用 附python代码详细讲解(numpy函数,创建数组,常用属性,索引切片,数组的复制,修改维度,数组的分割、拼接、转置)
  8. ASP.NET Core默认注入方式下如何注入多个实现(多种方式) - sky 胡萝卜星星 - CSDN博客...
  9. AcWing 1855. 愤怒的奶牛(枚举+暴力)
  10. Django 入门初探
  11. java常见基础面试题
  12. 关于java开发阿里云视频直播的使用及掉坑、爬坑,欢迎入坑交流
  13. 承受自律的苦,不要承受自责的悔
  14. 北理在线作业答案c语言,18秋北理工《计算机组成原理》在线作业答案
  15. 中兴C300主控升级到V2.1.0要谨慎,(针对ONU光猫生产企业)
  16. selenium验证码登录_用 Python 登录主流网站
  17. 读赵凯华之《新概念物理教程.光学》
  18. 开始VT-x各种电脑进入BIOS界面按什么?
  19. 双目立体匹配算法:SGM
  20. VMware 丢失vmx文件恢复后报错:找不到磁盘文件,“另一个程序已锁定文件的一部分,进程无法访问”解决办法

热门文章

  1. 华硕服务器进入重装系统界面怎么办,进不了桌面怎么重装系统,华硕电脑卡在欢迎界面...
  2. 敏捷-细说敏捷建模思想与实践(转)
  3. DP方法(动态规划) 寻找最长公共子序列 LCS问题(c++)
  4. Springcloud微服务概述
  5. 2022Android春招,2022最新阿里Android高级面试题及答案
  6. python中,ttk.Combobox的background、foreground和font属性的设置问题
  7. 袖珍电子书:一元实函数的微分定义
  8. 人脸检测的测试程序(视频和摄像头)
  9. 【MySQL】数据库配置
  10. Java 8 流过滤List