用递归树求解递归算法时间复杂度

文章内容、图片均来自极客时间。
递归代码复杂度分析起来比较麻烦。一般来说有两种分析方法:递推公式和递归树。

1 递推公式法

归并排序的递推公式是:

merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
终止条件:
p >= r 不用再继续分解

我们假设对n个元素排序的时间是T(n),那分解成两个子数组排序的时间是 T ( n 2 ) T(\dfrac{n}{2}) T(2n)。merge函数合并两个子数组的时间复杂度是O(n)。所以归并排序时间复杂度计算公式就是:
  T ( n ) = 2 ∗ T ( n 2 ) + n , n > 2 T(n)=2*T(\dfrac{n}{2})+n,n>2 T(n)=2T(2n)+n,n>2
 T(1)=c;
 
 继续计算T(n)

T(n)=2*T(n/2)+n
     =2*(2*T(n/4)+n/2)+n=4*T(n/4)+2n
     =4*(2*T(n/8)+n/4)+2n=8*T(n/8)+3n
     =8*(2*T(n/16)+n/8)+3n=16*T(n/16)+4n
     ...
     =2^k*T(n/2^k)+k*n

n / 2 k = 1 n/2^k=1 n/2k=1的时候, k = l o g 2 n k=log_2n k=log2n,代入上面的式子: T ( n ) = c ∗ n + n l o g 2 n T(n)=c*n+nlog_2n T(n)=cn+nlog2n,用大O表示法,T(n)=O(nlogn)。

2 递归树法

递归的思想就是将大问题分解为小问题来求解。然后再将小问题分解成小小问题。这样一层层分解直到问题不能再分解。如果我们把这一层层的分解过程画成图,其实就是一棵树。我们把它叫做递归树。
参看下图,括号中的数字表示问题的规模。

在这里插入图片描述

归并排序比较耗时的操作是合并,也就是将两个小数组合并成一个大数组。其他操作的代价很低,可以记为常数L。
从图中看出每一层的耗时是相同的,都为n。现在我们只要知道这棵树的高度h,就可以得到总的时间复杂度O(h*n)。
从图中能看到这是一颗满二叉树。满二叉树的高度大约是 l o g 2 n log_2n log2n。所以归并排序的时间复杂度就是O(nlogn)。

3 递归树分析法实战

3.1 快排时间复杂度

快排的递推公式:quick_sort(p…q) = quick_sort(p,i-1)+ quick_sort(i+1,q)
退出条件:p>=q
快排最好的情况是每次都能将数组一分为二。这时候用递推公式T(n)=2T(n/2)+n,就能得到时间复杂度O(nlogn)。
但是不可能每次都是理想情况。我们假设平均情况下每次分区,两个分区的比例是1:k。当k=9的时候,公式变为这样:
T ( n ) = T ( 9 n 10 ) + T ( n 10 ) + n T(n)=T(\dfrac{9n}{10})+T(\dfrac{n}{10})+n T(n)=T(109n)+T(10n)+n
这个公式的值不太好求。用递归树是不是更简单呢?
在这里插入图片描述

我们看到每一层因为有选择交换操作,且最多n次。所以每一层的操作代价是相同的:n。
递归树的路径长度却是不一样的。快排结束的条件是待排序的区间大小为1,也就是说叶子节点的数据规模是1。从节点n到叶子节点1,递归树中的最长路径是每次乘以 1 10 \dfrac{1}{10} 101,最短路径是每次乘以 9 10 \dfrac{9}{10} 109。通过计算,我们可以得到,从根节点到叶子节点的最短路径是 l o g 10 n log_{10}n log10n,最长路径是 l o g ( 10 9 ) n log_(\dfrac{10}{9})n log(910)n。所以总操作代价在 n ∗ l o g 10 n n*log_{10}n nlog10n n ∗ l o g ( 10 9 ) n n*log_(\dfrac{10}{9})n nlog(910)n之间。大O表示法不关注对数的底数,所以平均时间复杂度O(n*logn)。

将k=99,999…替换之后结论相同。

3.2 斐波那契数列

上面的两个例子每层的操作数相同,都是n。这次需要具体分析每层的操作数。
菲波那切数列的实现代码:

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

把递归代码画成递归树如下。
在这里插入图片描述
f(n)分解为f(n-1)和f(n-2),每次-1,或者-2。叶子节点的数据规模是1或者2。那最长路径大概就是n,最短路径大概是 n 2 \dfrac{n}{2} 2n

每次分解之后的合并操作只是一次加法,算一个时间1。第一层是f(n-1)+f(n-2),1次运算;第二层是f(n-2)+f(n-3),f(n-3)+f(n-4)是两次运算…依次类推,第k层的总耗时是 2 k − 1 2^{k-1} 2k1。算法总耗时是每层耗时相加。

如果路径长度都为n,那么总耗时是: 2 n − 1 2^n-1 2n1
1 + 2 + 2 2 + . . . + 2 n − 1 = 2 n − 1 1+2+2^2+...+2^{n-1}=2^n-1 1+2+22+...+2n1=2n1

如果路径长度都为 n 2 \dfrac{n}{2} 2n,那么总耗时是 2 n 2 − 1 2^{\dfrac{n}{2}}-1 22n1
1 + 2 + 2 2 + . . . + 2 n 2 − 1 = 2 n 2 − 1 1+2+2^2+...+2^{\dfrac{n}{2}-1}=2^{\dfrac{n}{2}}-1 1+2+22+...+22n1=22n1

所以算法的时间复杂度在 2 n 2 − 1 2^{\dfrac{n}{2}}-1 22n1 2 n − 1 2^n-1 2n1之间。这样的计算不够精确,只是一个范围。但是我们知道了该算法的时间复杂度是指数级的。复杂度非常高。

心得:时间复杂度是可以估算的,不用非常准确。只要数量级保证正确即可。

3.3 全排列的时间复杂度

1 递归公式是

假设数组里的内容是1,2,3.....n
f(n)={最后一位是1,f(n-1)}+{最后一位是2,f(n-1)}+...+{最后一位是n,f(n-1)}

接着画出递归树。
在这里插入图片描述

2 对于问题f(n),在得到子问题f(n-1)的结果之后需要把结果相加,有n次相加,所以第一层的操作数是n。
对于问题f(n-1),首先会有n个f(n-1)的问题需要求解。每个f(n-1),在得到子问题f(n-2)的结果之后需要把结果相加,有n-1次相加操作,所以第二层的操作数是n*(n-1)。
以此类推,到最后一层的子问题是f(1),会有n*(n-1)(n-2)2个f(1)。f(1)可以直接返回,操作数是1。所以最后一层的操作数是: n ∗ ( n − 1 ) ∗ ( n − 2 ) ∗ . . . ∗ 2 ∗ 1 = n ! n*(n-1)*(n-2)*...*2*1=n! n(n1)(n2)...21=n!
3 因为叶子问题为f(1),问题每次-1分解,所以有n层。
4 前面每一层的操作数都小于n!,所以时间复杂度在 n ∗ n ! n*n! nn! n ! n! n!之间。
5 算法时间复杂度大于O(n!),小于O(n
n!)。复杂度非常高。

4 递归树法分析得步骤

1 找到递归公式画递归树
2 计算每一层在得到子问题的解以后,还要进行哪些操作才能得到本问题的答案,计算这些操作的操作数。
3 考虑树的最长路径和最短路径。
4 分别按照最长路径和最短路径计算所有层的操作数的和。
5 得出时间复杂度范围,推测时间复杂度。

  • 45
    点赞
  • 225
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: 时间复杂度求解取决于实际的算法,一般可以分析算法的执行步骤,统计每个步骤所用的时间,从而求得时间复杂度。对于递归式算法,可以通过分析递归函数的执行次数,以及每次调用递归函数所消耗的时间,来求解时间复杂度。 ### 回答2: 要求解递归式的时间复杂度,我们可以按照以下步骤进行: 1. 首先,确定递归式的形式。递推式通常具有递归的特点,即问题的规模需要通过不断缩小来递归求解。例如,递归式可能包含递归调用,或者具有递归的结构。 2. 其次,推导递归式的递归深度。递归时间复杂度通常与递归的深度相关,即需要确定递归式的递归深度。 3. 然后,分析递归函数的时间代价。将递归式的执行过程分解为不同的子问题,确定每个子问题的时间代价。这可能涉及到递归子问题的规模和计算时间。 4. 最后,通过递归的时间代价和递归式的递归深度来确定递归式的时间复杂度。 需要注意的是,递归式的时间复杂度可能与递归的规模有关,也可能与递归的深度有关,具体取决于具体的情况和问题的性质。同时,递归式的时间复杂度也可能需要通过数学推导或递归等方法进行求解。 总的来说,求解递归式的时间复杂度需要通过对递归的分析、递归深度的确定以及递归函数的时间代价的分析来进行。 ### 回答3: 求解递归式的时间复杂度需要以下步骤: 1. 确定递归式的形式:首先,我们需要确定递归式的形式和递归方程,即描述递归的基本操作和递归关系的数学等式。这通常需要根据问题的特点和递归的实现进行分析。 2. 求解递归方程:接下来,我们需要求解递归方程,即找到递归式的解析解。这可以通过代入法、特征根法或母函数法等数学方法来实现。在这一步骤中,我们可以得到递归式的通项公式,并进一步进行化简。 3. 分析递归时间复杂度:一旦我们得到递归式的通项公式,我们可以通过分析公式的增长率来确定递归时间复杂度。具体来说,我们可以评估递归式中的递归调用次数和每次递归操作的时间复杂度,然后将它们相乘得到最终的时间复杂度。 4. 解决递归的边界条件:最后,我们需要解决递归的边界条件,即递归的终止条件。这是因为递归式只有在满足终止条件时才能收敛,否则递归会无限进行下去。在分析时间复杂度时,我们需要考虑递归的基本操作在边界条件下的执行次数和时间复杂度。 需要注意的是,求解递归式的时间复杂度可能涉及到数学推理和推导,需要运用到数学分析的方法。具体的求解过程会根据不同的递归式和问题而有所不同。同时,我们也可以借助工具和数值计算对递归式进行近似求解,以便更好地估计时间复杂度的上界和下界。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值