通俗易懂的C语言快速排序和归并排序的时间复杂度分析

目录
  • 快速排序和归并排序的时间复杂度分析——通俗易懂
    • 归并排序的时间复杂度分析
    • 快速排序的时间复杂度
  • 快速排序的最坏情况O(n^2)
  • 总结

快速排序和归并排序的时间复杂度分析——通俗易懂

今天面试的时候,被问到归并排序的时间复杂度,这个大家都知道是O(nlogn),但是面试官又继续问,怎么推导出来的。这我就有点懵了,因为之前确实没有去真正理解这个时间复杂度是如何得出的,于是就随便答了一波(理解了之后,发现面试的时候答错了......)。

归并排序和快速排序,是算法中,非常重要的两个知识点,同时也是在面试中被问的非常频繁的内容,我明知如此,却没有彻底理解,真是太不应该了。所以,今天这篇博客就来分析一下这两种排序算法的时间复杂度是如何得出的。我查了许多篇博客,很多都是通过公式进行分析,十分难理解,下面我就结合自己的理解,使用通俗易懂的方式进行描述(为了好理解,可能会有些啰嗦)。

归并排序的时间复杂度分析

了解归并排序的应该都知道,归并排序的时间复杂度是O(nlogn),且这个时间复杂度是稳定的,不随需要排序的序列不同而产生波动。那这个时间复杂度是如何得来的呢?我们可以这样分析,假设我们需要对一个包含n个数的序列使用归并排序,并且使用的是递归的实现方式,那么过程如下:

  • 递归的第一层,将n个数划分为2个子区间,每个子区间的数字个数为n/2
  • 递归的第二层,将n个数划分为4个子区间,每个子区间的数字个数为n/4
  • 递归的第三层,将n个数划分为8个子区间,每个子区间的数字个数为n/8;

......

  • 递归的第logn层,将n个数划分为n个子区间,每个子区间的数字个数为1

我们知道,归并排序的过程中,需要对当前区间进行对半划分,直到区间的长度为1。也就是说,每一层的子区间,长度都是上一层的1/2。这也就意味着,当划分到第logn层的时候,子区间的长度就是1了。而归并排序的merge操作,则是从最底层开始(子区间为1的层),对相邻的两个子区间进行合并,过程如下:

  • 在第logn层(最底层),每个子区间的长度为1,共n个子区间,每相邻两个子区间进行合并,总共合并n/2次。n个数字都会被遍历一次,所有这一层的总时间复杂度为O(n)

......

  • 在第二层,每个子区间长度为n/4,总共有4个子区间,每相邻两个子区间进行合并,总共合并2次。n个数字都会被遍历一次,所以这一层的总时间复杂度为O(n)
  • 在第一层,每个子区间长度为n/2,总共有2个子区间,只需要合并一次。n个数字都会被遍历一次,所以这一层的总时间复杂度为O(n)

通过上面的过程我们可以发现,对于每一层来说,在合并所有子区间的过程中,n个元素都会被 操作一次,所以每一层的时间复杂度都是O(n)。而之前我们说过,归并排序划分子区间,将子区间划分为只剩1个元素,需要划分logn次。每一层的时间复杂度为O(n),共有logn层,所以归并排序的时间复杂度就是O(nlogn) 。

上面的描述算是非常详细了,应该不会太难理解。如果上面的过程还是不太理解,那么我们通过另外一种更直观的方式进行分析。上面描述的是递归的过程,下面我们通过非递归(迭代)方式实现的归并排序,再来分析一波,这种方式更加直观(为什么不直接通过非递归的方式描述,而是先通过递归的方式分析,是因为上面的过程也可以用来分析快速排序)。下面是通过非递归方式实现的归并排序代码,其中有两处分析时间复杂度的关键点,我标注出来了(重点关注注释):

**

/**
 * 此方法用来定义子区间大小,子区间大小从1->2->4->8 ... ->n/2
 * 可以近似地认为进行了logn次
 */
public static void merge(int[] arr) {
    // 关键点1:划分子区间,每一次的子区间长度是上一次的两倍,所以这个循环需要执行logn次
    for(int i = 1;i<arr.length;i *= 2){
        // 关键点2:此方法每次执行的时间复杂度为O(n),具体看下方
        mergeSort(arr,i);
    }
}
/**
 * 以下方法,每次执行的时间复杂度都是O(n),
 * 因为需要将arr数组的每gap个数子,作为一个子区间,
 * 然后对相邻的两个子区间执行归并排序的merge操作,
 * 所以在这个方法中,arr数组中的每一个数都会在merge操作中,
 * 被处理一次,所以下面这个方法的时间复杂度为O(n)
 */
public static void mergeSort(int[] arr, int gap) {
    int[] tmp = new int[arr.length];
    int index = 0;
    int start1 = 0;
    int end1 = start1 + gap - 1;
    int start2 = end1 + 1;
    int end2 = (start2 + gap - 1)>=arr.length?arr.length-1:start2+gap-1;
    while(start2<arr.length){
        while(start1<=end1&&start2<=end2){
            if(arr[start1]<arr[start2]){
                tmp[index++] = arr[start1++];
            }else{
                tmp[index++] = arr[start2++];
            }
        }
        while(start1<=end1){
            tmp[index++] = arr[start1++];
        }
        while(start2<=end2){
            tmp[index++] = arr[start2++];
        }
        start1 = end2+1;
        end1 = start1 + gap - 1;
        start2 = end1 + 1;
        end2 = (start2 + gap - 1)>=arr.length?arr.length-1:start2+gap-1;
    }
    while(start1<arr.length){
        tmp[index++] = arr[start1++];
    }
    for(int j = 0;j<tmp.length;j++){
        arr[j] = tmp[j];
    }
}

上面的代码,merge方法中的循环需要循环logn次,每次循环都调用一次mergeSort方法,mergeSort方法的时间复杂度为O(n),所以很容易得出归并排序的时间复杂度为O(nlogn)

快速排序的时间复杂度

了解快速排序的应该知道,快速排序的时间复杂度在O(nlogn)~ O(n^2)之间,下面我就来分别分析这两种情况:

(一)快速排序的最好情况O(nlogn)

这种情况下,其实和上面通过递归分析的归并排序很类似,理解了归并排序的时间复杂度分析,那这里应该也很好理解。快速排序的实现方式,就是在当前区间中选择一个轴,区间中所有比轴小的数都需要放到轴的左边,而比轴大的数则放到轴的右边。在理想的情况下,我们选取的轴刚好就是这个区间的中位数。也就是说,在操作之后,正好将区间分成了数字个数相等的左右两个子区间。此时就和归并排序基本一致了:

  • 递归的第一层,n个数被划分为2个子区间,每个子区间的数字个数为n/2
  • 递归的第二层,n个数被划分为4个子区间,每个子区间的数字个数为n/4
  • 递归的第三层,n个数被划分为8个子区间,每个子区间的数字个数为n/8;

......

  • 递归的第logn层,n个数被划分为n个子区间,每个子区间的数字个数为1

以上过程与归并排序基本一致,而区别就是,归并排序是从最后一层开始进行merge操作,自底向上;而快速排序则是从第一层开始,交换区间中数字的位置,也就是自顶向下。但是,merge操作和快速排序的调换位置操作,时间复杂度是一样的,对于每一个区间,处理的时候,都需要遍历一次区间中的每一个元素。这也就意味着,快速排序和归并排序一样,每一层的总时间复杂度都是O(n),因为需要对每一个元素遍历一次。而且在最好的情况下,同样也是有logn层,所以快速排序最好的时间复杂度为O(nlogn)

快速排序的最坏情况O(n^2)

下面我们再来说一说快速排序的最坏情况,这种情况就比较好理解了。什么是快速排序的最坏情况,那就是,对于每一个区间,我们在处理的时候,选取的轴刚好就是这个区间的最大值或者最小值。比如我们需要对n个数排序,而每一次进行处理的时候,选取的轴刚好都是区间的最小值。于是第一次操作,在经过调换元素顺序的操作后,最小值被放在了第一个位置,剩余n-1个数占据了2到n个位置;第二次操作,处理剩下的n-1个元素,又将这个子区间的最小值放在了当前区间的第1个位置,以此类推......每次操作,都只能将最小值放到第一个位置,而剩下的元素,则没有任何变化。所以对于n个数来说,需要操作n次,才能为n个数排好序。而每一次操作都需要遍历一次剩下的所有元素,这个操作的时间复杂度是O(n),所以总时间复杂度为O(n^2)

其实上面的过程,我们可以换一个角度理解:每次操作,找出最小值放到剩余区间的第一个位置,这不就是选择排序的实现方式吗?而选择排序的时间复杂度就是O(n^2),所以上面的过程也就O(n^2)

总结

以上内容,就是我基于自己的理解,对快速排序和归并排序时间复杂度的分析。为了更好理解,我的描述都尽可能的详细,所以可能会有点啰嗦,但是我认为还是很通俗易懂的。希望这篇博客能够为之前对这两种排序算法理解不是特别清晰的人提供帮助,同时,若上面的内容存在错误或不足,欢迎指正或补充。

以上就是通俗易懂的C语言快速排序和归并排序的时间复杂度分析的详细内容,更多关于C语言排序时间复杂度的资料请关注我们其它相关文章!

(0)

相关推荐

  • C语言嵌入式实现支持浮点输出的printf示例详解

    目录 简介 背景 C语言可变参数函数 踩坑 功能实现 简介 mr-printf 模块为 mr-library 项目下的可裁剪模块,以C语言编写,可快速移植到各种平台(主要以嵌入式mcu为主). mr-printf 模块用以替代 libc 中 printf, 可在较小资源占用的同时支持绝大部分 printf 功能,于此同时还支持对单独功能模块的裁剪以减少用户不需要功能的资源占用. 背景 printf 大家应该使用的比较多,但是在嵌入式平台中,尤其是单片机中,libc中的printf对内存的占用较高

  • C语言利用goto语句设计实现一个关机程序

    目录 前言 一.什么是goto语句 二.goto语句的作用是什么 三.goto语句的缺点 四.goto语句的结构与用法 五.goto语句的巧用实例——关机小程序 总结撒花 前言 goto语句其实在平常中我们 除了学习分支语句和循环语句时,介绍循环语句时,才会知道有goto语句这个用法,那读者可能会问:我们还有学习的必要吗? 答案是显而易见的,正如黑格尔所说的:存在即合理!既然存在,就会有存在的必要!虽然我们现在不会遇到且用到 ,当在搞Linux硬件驱动等的时候,其内核含有较多的goto语句,如果

  • C语言软件iic虚拟总线中间层设计详解

    目录 简介 IIC-协议 接线方式 总线 工作本质 虚拟总线(中间层)设计 使用示例 简介 mr-soft-iic 模块为 mr-library 项目下的可裁剪模块,以C语言编写,可快速移植到各种平台(主要以嵌入式mcu为主). mr-soft-iic 模块通过 io 模拟实现 iic 协议. IIC-协议 SPI 一般为一主多从设计.由2根线组成:CLK(时钟).SDA(数据). 接线方式 主机 从机 CLK CLK SDA SDA 主机从机一 一对应相接. 总线 IIC 通过地址码识别设备,

  • C语言中的字符型数据与ASCII码表

    目录 1.字符型常量 2.字符型变量 3.字符型数据的输入输出 (1)scanf()和printf()函数输入输出字符 (2)字符输入函数getchar() 总结 1.字符型常量 字符型常量指单个字符,是用一对单引号及其所括起来的字符表示. 例如:‘A’.‘a’.‘0’.’$‘等都是字符型常量. C语言的字符使用的就是ASCII字符集,总共有128个,每个相应的ASCII码都表示一个字符: (1)每一个字符都有唯一的次序值,即ASCII码. (2)数字’0’,‘1’,‘2’,…,‘9’.大写字母

  • C语言实现求解素数的N种方法总结

    目录 前言 必备小知识 C语言详解<试除法>求解素数 试除法境界1 试除法境界2 试除法境界3 试除法境界4 C语言详解<筛选法>求解素数 筛选法境界5 前言 哈喽各位友友们,我今天又学到了很多有趣的知识,现在迫不及待的想和大家分享一下!我仅已此文,手把手带领大家探讨利用试除法.筛选法求解素数的n层境界!都是精华内容,可不要错过哟!!! 必备小知识 质数又称素数.一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数:否则称为合数(规定1既不是质数也不是合数).这里

  • C语言非递归算法解决快速排序与归并排序产生的栈溢出

    目录 1.栈溢出原因和递归的基本认识 2.快速排序(非递归实现) 3.归并排序(非递归实现) 建议还不理解快速排序和归并排序的小伙伴们可以先去看我上一篇博客​​​​​​哦!C语言超详细讲解排序算法下篇 1.栈溢出原因和递归的基本认识 我们先简单来了解下内存分布结构: 栈区:用于存放地址.临时变量等:                                                                            堆区:程序运行期间动态分配所使用的场景: 静态区

  • C 语言快速排序实例代码

    快速排序是对冒泡法排序的一种改进. 快速排序算法 的基本思想是:将所要进行排序的数分为左右两个部分,其中一部分的所有数据都比另外一 部分的数据小,然后将所分得的两部分数据进行同样的划分,重复执行以上的划分操作,直 到所有要进行排序的数据变为有序为止. 可能仅根据基本思想对快速排序的认识并不深,接下来以对n个无序数列A[0], A[1]-, A[n-1]采用快速排序方法进行升序排列为例进行讲解. (1)定义两个变量low和high,将low.high分别设置为要进行排序的序列的起始元素和最后一个元

  • 通俗易懂讲解C语言与Java中二叉树的三种非递归遍历方式

    详解二叉树的三种非递归遍历方式(附C.java源码) 前言 二叉树的递归遍历方式很简单,三种递归遍历方式的区别,只是printf放的位置不一样而已,这里就不多讲了.把前序遍历代码贴在这里: //结点 struct Node { int val; struct Node* left, * right; }; //前序遍历 void pre(Node* root) { if (root == null) return; printf("%d ",root->val); pre(roo

  • c语言排序之归并排序(递归和非递归)

    目录 递归代码流程 非递归代码流程 两者比较 时间复杂度 代码(递归) 代码(非递归) 递归代码流程 归并就是把两个或多个序列合并,这里只介绍二路归并,就是不断的把序列分为2组,直到每个组有一个元素为止,然后再比较合并,直到合为1个序列,完成. 非递归代码流程 与递归不断分解数组相反,非递归直接从长度为1的子序列开始合并,直到全并为1个整个序列,复用了merge函数 两者比较 代码用非递归的方式效率更高一些: ​ 空间复杂度:从O(log2n)变为1个临时数组O(n) ​ 时间复杂度:少了递归的

  • C语言简明讲解归并排序的应用

    目录 一.归并排序 1.1归并排序引入 1.2归并排序的概念 1.3归并排序的原理 1.4实例说明 1.5具体步骤说明 1.6代码实现 1.7性能分析 一.归并排序 1.1归并排序引入 对于堆排序来说,因为用到了完全二叉树的深度是(log2n+1)的特性,所以效率就比较高,但是堆结构的设计比较复杂,现在我们想要可以直接利用完全二叉树来排序的方法,这个方法就是归并排序. 1.2归并排序的概念 归并排序是建立在归并操作上的一种有效的排序算法,归并排序对序列的元素进行逐层折半分组,然后从最小分组开始比

  • 详解C语言快速排序三种方法的单趟实现

    目录 交换排序的思想 冒泡排序的思想 快速排序的整体框架 快速排序单趟实现逻辑 1. hoare版本单趟实现(左右指针法) 2.挖坑法单趟排序实现 3.前后指针法 交换排序的思想 基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动. 冒泡排序的思想 冒泡排序比较简单,我们之前使用也很多,简单讲解,就是比较两个数,如果比他大就交换,没有他大就接着向后比,一直到数组结束,这是单趟

  • Java快速排序与归并排序及基数排序图解示例

    目录 一.快速排序 1.基本介绍 2.代码实现 二.归并排序 1.基本介绍 2.代码实现 三.基数排序 1.基本介绍 2.代码实现 一.快速排序 1.基本介绍 以上面的数组为例分析快速排序. 首先要传入三个值,数组arr[ ] ,最左边下标left ,最右边下标 right.然后将根据左右的下标值计算出中间值mid. 我们要做的就是将左边的值大于mid的放到右边,将右边小于mid的值放到左边. 左右两边分别单独循环,左边找到比mid大的数,右边找到比mid小的数. 两边分别找到符合条件的数后,进

  • GO语言实现的http抓包分析工具pproxy介绍

    引言 web 开发和 API 开发中难免要详细分析 http 请求和响应信息.web 开发的话,浏览器提供了便利的工具,比如 chrome 和 IE 都带了 develop tool,而 firefox 更是有十分强大的 firebug,可以让 http 请求的所有秘密一览无遗.目前是 app 大流行的时代,想要观察 app 中得 http 请求的秘密,浏览器的工具和插件都无能为力,有不少本地化的软件可以很好的解决这个问题,Windows 平台下有大名鼎鼎的 Fiddler 和 HttpWatc

  • R语言字符串知识点总结及实例分析

    在R语言中的单引号或双引号对中写入的任何值都被视为字符串. R语言存储的每个字符串都在双引号内,即使是使用单引号创建的依旧如此. 在字符串构造中应用的规则 在字符串的开头和结尾的引号应该是两个双引号或两个单引号.它们不能被混合. 双引号可以插入到以单引号开头和结尾的字符串中. 单引号可以插入以双引号开头和结尾的字符串. 双引号不能插入以双引号开头和结尾的字符串. 单引号不能插入以单引号开头和结尾的字符串. 有效字符串的示例 以下示例阐明了在 R 语言中创建字符串的规则. a <- 'Start

  • R语言矩阵知识点总结及实例分析

    矩阵是其中元素以二维矩形布局布置的R对象. 它们包含相同原子类型的元素. 虽然我们可以创建一个只包含字符或只包含逻辑值的矩阵,但它们没有太多用处. 我们使用包含数字元素的矩阵用于数学计算. 使用matrix()函数创建一个矩阵. 语法 在R语言中创建矩阵的基本语法是 matrix(data, nrow, ncol, byrow, dimnames) 以下是所使用的参数的说明 数据是成为矩阵的数据元素的输入向量. nrow是要创建的行数. ncol是要创建的列数. byrow是一个逻辑线索. 如果

随机推荐