本文介绍: 本节课程思维导图:今天我们来讲这种数据结构的一种特殊应用递归树。我们知道递归代码时间复杂度分析起来很麻烦。除了用递推公式这种比较复杂分析方法,有没有简单的方法呢?今天我们就来学习另外一种方法,借助递归树来分析递归算法时间复杂度。

前言

在这里插入图片描述
本节课程思维导图:
在这里插入图片描述
今天,我们来讲这种数据结构的一种特殊应用,递归树。
我们都知道,递归代码时间复杂分析起来很麻烦。除了用递推公式这种比较复杂分析方法,有没有简单的方法呢?今天,我们就来学习另外一种方法,借助递归树来分析递归算法时间复杂度。

递归树与时间复杂度分析

递归的思想就是,将大问题分解为小问题求解然后再将小问题分解为小小问题。这样一层一层分解,直到问题的数据规模被分解得足够小,不用继续递归分解为止。
如果我们把这个一层一层的分解过程画成图,它其实就是一棵树。我们给这棵树起一个名字,叫作递归树。我这里画了一棵斐波那契数列的递归树,你可以看看节点里的数字表示数据的规模,一个节点求解可以分解为左右子节点两个问题的求解
在这里插入图片描述
在,我们就来看,如何用递归树来求解时间复杂度。现在我们就借助归并排序看看,如何用递归树,来分析递归代码的时间复杂度。
在这里插入图片描述
因为每次分解都是一分为二,我们把时间上的消耗记作常量 1。归并算法比较耗时的是归并操作,也就是把两个子数组合并为大数组。从图中我们可以看出,每一层归并操作消耗的时间总和是一样的,跟要排序数据规模有关。我们把每一层归并操作消耗的时间记作 n

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

归并排序原理和递归树,可以看出来,归并排序递归树是一棵二叉树。我们前两节中讲到,满二叉树的高度大约是 log2​n,所以,归并排序递归实现的时间复杂度就是 O(nlogn)。

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

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

快速排序在最好情况下,每次分区都能一分为二,这个时候用递推公式 T(n)=2T(2n​)+n,很容易就能推导出时间复杂度是 O(nlogn)。
我们假设平均情况下,每次分区之后,两个分区大小比例为 1:k。当 k=9 时,如果用递推公式的方法来求解时间复杂度的话,递推公式就写成 :
在这里插入图片描述
那我们来看看,用递归树来分析快速排序平均情况时间复杂度,是不是比较简单呢?
在这里插入图片描述
快速排序的过程中,每次分区都要遍历分区区间的所有数据,所以,每一层分区操作所遍历的数据个数之和就是 n。我们现在只要求出递归树的高度 h,这个快排过程遍历的数据个数就是 h∗n ,也就是说,时间复杂度就是 O(h∗n)。
因为每次分区并不是均匀地一分为二,所以递归树并不是满二叉树。这样一个递归树的高度是多少呢?
我们知道快速排序结束条件就是待排序的小区间大小为 1,也就是说叶子节点里的数据规模是 1。从根节点 n 到叶子节点 1,递归树中最短的一个路径每次都乘以 1/10,最长的一个路径每次都乘以 9/10。
通过计算,我们可以得到,从根节点到叶子节点的最短路径

log

10

n

log_{10} n

log10n,最长的路径

log

10

/

9

n

log_{10/9} n

log10/9n
在这里插入图片描述
在这里插入图片描述

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

int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  return f(n-1) + f(n-2);
}

我们先把上面的递归代码画成递归树,就是下面这个样子:
在这里插入图片描述
这棵递归树的高度是多少呢?
在这里插入图片描述
如果路径长度都为 n,那这个总和就是

2

n

1

2^n−1

2n1
如果路径长度都是 n​ /2,那整个算法的总的时间消耗就是

2

n

/

2

1

2^{n/2}−1

2n/21

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

“如何把 n 个数据的所有排列找出来”,这就是全排列的问题。我来举个例子比如,1,2,3 这样 3 个数据,有下面这几种不同排列

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 个数据的排列”的子问题。
递推公式

假设数组存储的是123...n。
        
f(1,2,...n) = {最后一位是1, f(n-1)} + {最后一位是2, f(n-1)} +...+{最后一位是n, f(n-1)}

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

// 调用方式
// 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;
  }
}

现在,我们来看下,如何借助递归树,轻松分析出这个代码的时间复杂度。首先,我们还是画出递归树。不过,现在的递归树已经不是标准二叉树了。
在这里插入图片描述
第一层分解有 n 次交换操作,第二层有 n 个节点,每个节点分解需要 n−1 次交换,所以第二层总的交换次数是 n∗(n−1)。第三层有 n∗(n−1) 个节点,每个节点分解需要 n−2 次交换,所以第三层总的交换次数是 n∗(n−1)∗(n−2)。以此类推,第 k 层总的交换次数就是 n∗(n−1)∗(n−2)∗…∗(n−k+1)。最后一层的交换次数就是 n∗(n−1)∗(n−2)∗…∗2∗1。每一层的交换次数之和就是总的交换次数

n + n*(n-1) + n*(n-1)*(n-2) +... + n*(n-1)*(n-2)*...*2*1

这个公式的求和比较复杂,我们看最后一个数,n∗(n−1)∗(n−2)∗…∗2∗1 等于 n!,而前面的 n−1 个数都小于最后一个数,所以,总和肯定小于 n∗n!,也就是说,全排列的递归算法的时间复杂度大于 O(n!),小于 O(n∗n!),虽然我们没法知道非常精确的时间复杂度,但是这样一个范围已经让我们知道,全排列的时间复杂度是非常高的。

内容小结

今天,我们用递归树分析了递归代码的时间复杂度。加上之前的递推公式的时间复杂度分析方法,我们现在已经学习了两种递归代码的时间复杂度分析方法了。
有些代码比较适合用递推公式来分析,比如归并排序的时间复杂度、快速排序的最好情况时间复杂度;有些比较适合采用递归树来分析,比如快速排序的平均时间复杂度。而有些可能两个都不怎么适合使用比如二叉树的递归前中后序遍历。

原文地址:https://blog.csdn.net/weixin_43597208/article/details/134675437

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_40250.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注