Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >递归树:借助树来求解递归算法时间复杂度

递归树:借助树来求解递归算法时间复杂度

作者头像
acc8226
发布于 2022-05-17 07:36:10
发布于 2022-05-17 07:36:10
1.7K00
代码可运行
举报
文章被收录于专栏:叽叽西叽叽西
运行总次数:0
代码可运行

递归树与时间复杂度分析

我们前面讲过,递归的思想就是,将大问题分解为小问题来求解,然后再将小问题分解为小小问题。这样一层一层地分解,直到问题的数据规模被分解得足够小,不用继续递归分解为止。

如果我们把这个一层一层的分解过程画成图,它其实就是一棵树。我们给这棵树起一个名字,叫作递归树。我这里画了一棵斐波那契数列的递归树,你可以看看。节点里的数字表示数据的规模,一个节点的求解可以分解为左右子节点两个问题的求解。

通过这个例子,你对递归树的样子应该有个感性的认识了,看起来并不复杂。现在,我们就来看,如何用递归树来求解时间复杂度。

归并排序的原理我就不详细介绍了,如果你忘记了,可以回看一下第 12 节的内容。归并排序每次会将数据规模一分为二。我们把归并排序画成递归树,就是下面这个样子:

因为每次分解都是一分为二,所以代价很低,我们把时间上的消耗记作常量 1。归并算法中比较耗时的是归并操作,也就是把两个子数组合并为大数组。从图中我们可以看出,每一层归并操作消耗的时间总和是一样的,跟要排序的数据规模有关。我们把每一层归并操作消耗的时间记作 n。

现在,我们只需要知道这棵树的高度 h,用高度 h 乘以每一层的时间消耗 n,就可以得到总的时间复杂度 O(n∗h)。

从归并排序的原理和递归树,可以看出来,归并排序递归树是一棵满二叉树。我们前两节中讲到,满二叉树的高度大约是 log2n,所以,归并排序递归实现的时间复杂度就是 O(nlogn)。我这里的时间复杂度都是估算的,对树的高度的计算也没有那么精确,但是这并不影响复杂度的计算结果。

利用递归树的时间复杂度分析方法并不难理解,关键还是在实战,所以,接下来我会通过三个实际的递归算法,带你实战一下递归的复杂度分析。学完这节课之后,你应该能真正掌握递归代码的复杂度分析。

实战一:分析快速排序的时间复杂度

在用递归树推导之前,我们先来回忆一下用递推公式的分析方法。你可以回想一下,当时,我们为什么说用递推公式来求解平均时间复杂度非常复杂?

快速排序在最好情况下,每次分区都能一分为二,这个时候用递推公式 T(n)=2T(2n)+n,很容易就能推导出时间复杂度是 O(nlogn)。但是,我们并不可能每次分区都这么幸运,正好一分为二。

我们假设平均情况下,每次分区之后,两个分区的大小比例为 1:k。当 k=9 时,如果用递推公式的方法来求解时间复杂度的话,递推公式就写成

这个公式可以推导出时间复杂度,但是推导过程非常复杂。那我们来看看,用递归树来分析快速排序的平均情况时间复杂度,是不是比较简单呢?

我们还是取 k 等于 9,也就是说,每次分区都很不平均,一个分区是另一个分区的 9 倍。如果我们把递归分解的过程画成递归树,就是下面这个样子:

快速排序的过程中,每次分区都要遍历待分区区间的所有数据,所以,每一层分区操作所遍历的数据的个数之和就是 n。我们现在只要求出递归树的高度 h,这个快排过程遍历的数据个数就是 h∗n ,也就是说,时间复杂度就是 O(h∗n)。

因为每次分区并不是均匀地一分为二,所以递归树并不是满二叉树。这样一个递归树的高度是多少呢?

也就是说,对于 k 等于 9,99,甚至是 999,9999……,只要 k 的值不随 n 变化,是一个事先确定的常量,那快排的时间复杂度就是 O(nlogn)。所以,从概率论的角度来说,快排的平均时间复杂度就是 O(nlogn)。

实战二:分析斐波那契数列的时间复杂度

在递归那一节中,我们举了一个跨台阶的例子,你还记得吗?那个例子实际上就是一个斐波那契数列。为了方便你回忆,我把它的代码实现贴在这里。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  return f(n-1) + f(n-2);
}

这样一段代码的时间复杂度是多少呢?你可以先试着分析一下,然后再来看,我是怎么利用递归树来分析的。

我们先把上面的递归代码画成递归树,就是下面这个样子:

实战三:分析全排列的时间复杂度

前面两个复杂度分析都比较简单,我们再来看个稍微复杂的。

我们在高中的时候都学过排列组合。“如何把 n 个数据的所有排列都找出来”,这就是全排列的问题。

我来举个例子。比如,1,2,3 这样 3 个数据,有下面这几种不同的排列:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1, 2, 3
1, 3, 2
2, 1, 3
2, 3, 1
3, 1, 2
3, 2, 1

如何编程打印一组数据的所有排列呢?这里就可以用递归来实现。

如果我们确定了最后一位数据,那就变成了求解剩下 n−1 个数据的排列问题。而最后一位数据可以是 n 个数据中的任意一个,因此它的取值就有 n 种情况。所以,“n 个数据的排列”问题,就可以分解成 n 个“n−1 个数据的排列”的子问题。

如果我们把它写成递推公式,就是下面这个样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
假设数组中存储的是123...n。
        
f(1,2,...n) = {最后一位是1, f(n-1)} + {最后一位是2, f(n-1)} +...+{最后一位是n, f(n-1)}

如果我们把递推公式改写成代码,就是下面这个样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 调用方式:
// int[]a = a={1, 2, 3, 4}; printPermutations(a, 4, 4);
// k表示要处理的子数组的数据个数
public void printPermutations(int[] data, int n, int k) {
  if (k == 1) {
    for (int i = 0; i < n; ++i) {
      System.out.print(data[i] + " ");
    }
    System.out.println();
  }

  for (int i = 0; i < k; ++i) {
    int tmp = data[i];
    data[i] = data[k-1];
    data[k-1] = tmp;

    printPermutations(data, n, k - 1);

    tmp = data[i];
    data[i] = data[k-1];
    data[k-1] = tmp;
  }
}

如果不用我前面讲的递归树分析方法,这个递归代码的时间复杂度会比较难分析。现在,我们来看下,如何借助递归树,轻松分析出这个代码的时间复杂度。

首先,我们还是画出递归树。不过,现在的递归树已经不是标准的二叉树了。

这里我稍微说下,掌握分析的方法很重要,思路是重点,不要纠结于精确的时间复杂度到底是多少。

内容小结

今天,我们用递归树分析了递归代码的时间复杂度。加上我们在排序那一节讲到的递推公式的时间复杂度分析方法,我们现在已经学习了两种递归代码的时间复杂度分析方法了。

有些代码比较适合用递推公式来分析,比如归并排序的时间复杂度、快速排序的最好情况时间复杂度;有些比较适合采用递归树来分析,比如快速排序的平均时间复杂度。而有些可能两个都不怎么适合使用,比如二叉树的递归前中后序遍历。

时间复杂度分析的理论知识并不多,也不复杂,掌握起来也不难,但是,在我们平时的工作、学习中,面对的代码千差万别,能够灵活应用学到的复杂度分析方法,来分析现有的代码,并不是件简单的事情,所以,你平时要多实战、多分析,只有这样,面对任何代码的时间复杂度分析,你才能做到游刃有余、毫不畏惧。

课后思考

1 个细胞的生命周期是 3 小时,1 小时分裂一次。求 n 小时后,容器内有多少细胞?请你用已经学过的递归时间复杂度的分析方法,分析一下这个递归问题的时间复杂度。

参考

27 | 递归树:如何借助树来求解递归算法的时间复杂度? https://time.geekbang.org/column/article/69388

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-05-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
超全递归技巧整理,这次一起拿下递归
大家好,我是多选参数的程序锅,一个正在 neng 操作系统、学数据结构和算法以及 Java 的硬核菜鸡。本篇将主要介绍递归相关的内容,下面是本篇的内容提纲。
syy
2020/08/10
1.4K0
超全递归技巧整理,这次一起拿下递归
重学数据结构和算法(二)之二叉树、红黑树、递归树、堆排序
最近学习了极客时间的《数据结构与算法之美]》很有收获,记录总结一下。 欢迎学习老师的专栏:数据结构与算法之美 代码地址:https://github.com/peiniwan/Arithmetic
六月的雨
2021/03/04
4540
数据结构与算法学习笔记之 适合大规模的数据排序 数据结构与算法学习笔记之如何分析一个排序算法?
在数据排序的算法中,不同数据规模应当使用合适的排序算法才能达到最好的效果,如小规模的数据排序,可以使用冒泡排序、插入排序,选择排序,他们的时间复杂度都为O(n2),大规模的数据排序就可以使用归并排序和快速排序,时间复杂度为O(nlogn)。今天我们就来看一下归并排序和快速排序。
Dawnzhang
2018/12/06
3880
看动画轻松理解时间复杂度(二)
上篇文章讲述了与复杂度有关的大 O 表示法和常见的时间复杂度量级,这篇文章来讲讲另外几种复杂度: 递归算法的时间复杂度(recursive algorithm time complexity),最好情况时间复杂度(best case time complexity)、最坏情况时间复杂度(worst case time complexity)、平均时间复杂度(average case time complexity)和均摊时间复杂度(amortized time complexity)。
五分钟学算法
2018/12/25
6470
【算法复习2】时间复杂度 O(nlogn)快速排序 归并排序分析
归并排序 稳定 主要看 子数组 排序后 merge 合并的函数如何执行 可以按先后顺序 合并 merge 函数 保证算法的稳定性
韩旭051
2021/04/14
1K0
【算法复习2】时间复杂度 O(nlogn)快速排序 归并排序分析
递归算法时间复杂度分析[通俗易懂]
一般情况下,算法中基本操作重复的次数就是问题规模n的某个函数f(n),进而分析f(n)随n的变化情况并确定T(n)的数量级。这里用‘o’来表示数量级,给出算法时间复杂度。 T(n)=o(f(n)); 它表示随问题规模n的增大,算法的执行时间增长率和f(n)增长率成正比,这称作算法的渐进时间复杂度。而我们一般情况下讨论的最坏的时间复杂度。 空间复杂度: 算法的空间复杂度并不是实际占用的空间,而是计算整个算法空间辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度S(n)定义为该算法所耗费空间的数量级。 S(n)=o(f(n)) 若算法执行所需要的辅助空间相对于输入数据n而言是一个常数,则称这个算法空间复杂度辅助空间为o(1); 递归算法空间复杂度:递归深度n*每次递归所要的辅助空间,如果每次递归所需要的辅助空间为常数,则递归空间复杂度o(n)。
全栈程序员站长
2022/08/27
2.9K0
递归算法时间复杂度分析[通俗易懂]
【愚公系列】2023年12月 十一大排序算法(十一)-二叉树排序
排序算法是一种将一组数据按照特定的规则进行排列的方法。排序算法通常用于对数据的处理,使得数据能够更容易地被查找、比较和分析。
愚公搬代码
2023/12/11
2830
七大经典、常用排序算法的原理、Java 实现以及算法分析
大家好,我是多选参数的程序锅,一个正在 neng 操作系统、学数据结构和算法以及 Java 的硬核菜鸡。数据结构和算法是我准备新开的坑,主要是因为自己在这块确实很弱,需要大补(残废了一般)。这个坑以排序为开端,介绍了 7 种最经典、最常用的排序算法,分别是:冒泡排序、插入排序、选择排序、归并排序、快速排序、桶排序、计数排序、基数排序。对应的时间复杂度如下所示:
syy
2020/06/23
7600
算法渣-递归算法
之前的排序算法 《快速排序》 与 《归并排序》 都使用了递归手法,如果不能理解递归,那分治思想类算法实现就难以理解
码农戏码
2021/03/23
7810
JavaScript 数据结构与算法之美 - 递归
现实例子:周末你带着女朋友去电影院看电影,女朋友问你,咱们现在坐在第几排啊 ?电影院里面太黑了,看不清,没法数,现在你怎么办 ?
夜尽天明
2019/07/10
5270
JavaScript 数据结构与算法之美 - 递归
浅谈常见数据结构和算法的应用系列(一)
当然不止。我觉得刷题是一件有意思的事,就像小猫小狗咬自己尾巴,玩弄的不亦乐乎。比喻可能不太恰当,是有种沉迷小游戏的感觉。可是在艰难打野的过程中,我们不要忘了,最重要的是:了解每种技能包的特点,适合解决的问题和场景。在特定实战场景下能够使用特定的技能包,自创技能包。这才是武功的至高境界。
Monica2333
2020/07/23
1.8K0
浅谈常见数据结构和算法的应用系列(一)
排序算法-下(Java语言实现)
的排序算法,归并排序和快速排序。这两种排序算法适合大规模的数据排序,比上一节讲的那三种排序算法要更常用。
acc8226
2022/05/17
4770
排序算法-下(Java语言实现)
Python-排序-归并排序中如何哨兵来追求极致的性能?
每当我在编写递归程序的时候,我都能感受到分治算法的强大威力。分治思想,也就是分而治之,将一个复杂的大问题可以分解成若干个子问题,子问题解决后,经过合并,最终得到大问题的解。想想生活中不也到处充满着分治思想吗?
somenzz
2020/12/10
9010
数据结构与算法 --- 排序算法(三)
上一篇数据结构与算法 --- 排序算法(二)中,介绍了分治算法思想及借助分治算法思想实现的归并排序。
Niuery Diary
2023/10/22
2730
数据结构与算法 --- 排序算法(三)
二叉树的基础---四种遍历方式的 Java 实现
大家好,我是多选参数的程序锅,一个正在“研究”操作系统、学数据结构和算法以及 Java 的硬核菜鸡。本篇将带来的是二叉树的相关知识,知识提纲如图所示。
syy
2020/07/27
2K0
告别递归,从零开始一文学会递归解题
递归是算法中一种非常重要的思想,应用也很广,小到阶乘,再在工作中用到的比如统计文件夹大小,大到 Google 的 PageRank 算法都能看到,也是面试官很喜欢的考点
五分钟学算法
2019/12/05
6500
重学数据结构和算法(五)之归并排序、快速排序
冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是 O(n2),比较高,适合小规模数据的排序。归并排序和快速排序的时间复杂度为 O(nlogn) 。这两种排序算法适合大规模的数据排序
六月的雨
2021/09/26
1.3K0
重学数据结构和算法(五)之归并排序、快速排序
数据结构与算法 --- 排序算法(二)
上一篇数据结构与算法 --- 排序算法(一)中,学习了冒泡排序,插入排序,选择排序这三种时间复杂度为
Niuery Diary
2023/10/22
3220
数据结构与算法 --- 排序算法(二)
快排查找数组中的第K个最大元素
两种时间复杂度为O(nlogn)的排序算法,归并排序和快速排序。这两种排序算法适合大规模数据排序,更常用。
JavaEdge
2021/12/07
4.3K0
快排查找数组中的第K个最大元素
归并快排算法比较及求第K大元素
核心思想:将数组从中间分成前后两部分,然后对前后两部分分别进行排序,再将排序好的两个部分有序合并在一起,这样整个数组有序。全文图示来源于王争的《数据结构和算法之美》
evenleo
2020/04/01
9430
归并快排算法比较及求第K大元素
推荐阅读
相关推荐
超全递归技巧整理,这次一起拿下递归
更多 >
LV.1
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档