一、问题描述
对于递归问题的时间复杂度计算问题,在王道书上是这样描述的:
这个看起来简单,但是实际操作的时候,却很困难,很容易被公式绕晕。因此,在查阅一些资料之后,总结出了递归树法,用于计算递归问题的时间复杂度。
二、递归树法
递归树法的基本思路是将递归过程展开成一棵树,然后计算这棵树的所有结点的时间复杂度并求和,就可以得到递归程序的总时间复杂度。
具体的计算步骤如下:
- 将递归程序展开成一棵递归树,树的结点表示递归函数调用的次数。
- 计算每个结点的时间复杂度,包括每个递归调用的时间复杂度和每个非递归语句的时间复杂度。
- 对每层的结点时间复杂度求和,得到每层时间复杂度的上界。
- 对所有层的时间复杂度上界求和,得到递归程序的总时间复杂度。
需要注意的是,递归树法只能用于计算递归程序的时间复杂度,对于非递归程序,需要使用其他的计算方法。
注:时间复杂度的上界指的是算法执行的最坏情况下的时间复杂度的一个上限估计
三、案例
3.1 案例1
针对上面的方法,我们用斐波那契数列来验证一下,题目来源是王道课后习题。
斐波那契数列递归实现的代码如下:
int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
我们假设输入参数 n
为正整数,现在使用递归树法来计算它的时间复杂度。
- 画出递归树
我们可以使用递归树来描述斐波那契数列递归算法的执行过程。根据递归算法的定义,斐波那契数列的第 n 项可以通过计算第 n-1 和第 n-2 项的和得出。因此,我们可以画出如下的递归树,其中每个节点都表示一次计算。
fib(n)
/ \
fib(n-1) fib(n-2)
/ \ / \
fib(n-2) fib(n-3) fib(n-3) fib(n-4)
......
递归树的深度为 n,因为计算每个斐波那契数的过程需要递归计算前面两个数,直到递归到第一项和第零项为止。
- 计算每个节点的执行时间
斐波那契递归算法每次计算斐波那契数时需要进行一次加法运算,因此每个节点的执行时间为 O(1)
。
- 计算每层节点的执行时间
递归树的第 i 层,共有 2^(i-1)
个节点。因此,第 i 层节点的总执行时间为 O(2^(i-1))
。例如,第一层有 1 个节点,第二层有 2 个节点,第三层有 4 个节点,以此类推。
- 计算时间复杂度
我们可以将每层节点的执行时间加起来得到算法的总时间复杂度,即:
T(n) = O(2^0) + O(2^1) + O(2^2) + ... + O(2^(n-1))
= 1 + 2 + 4 + ... + 2^(n-1)
= 2^n - 1
因此,斐波那契递归算法的时间复杂度为 O(2^n)
。
补充:fib(5)完整的递归调用过程如下所示:
fib(5)
├─ fib(4) # 调用 fib(4)
│ ├─ fib(3) # 调用 fib(3)
│ │ ├─ fib(2) # 调用 fib(2)
│ │ │ ├─ fib(1) # 调用 fib(1)
│ │ │ └─ fib(0) # 调用 fib(0)
│ │ └─ fib(1) # 调用 fib(1)
│ └─ fib(2) # 调用 fib(2)
│ ├─ fib(1) # 调用 fib(1)
│ └─ fib(0) # 调用 fib(0)
└─ fib(3) # 调用 fib(3)
├─ fib(2) # 调用 fib(2)
│ ├─ fib(1) # 调用 fib(1)
│ └─ fib(0) # 调用 fib(0)
└─ fib(1) # 调用 fib(1)
3.2 案例2
同样来自于王道书
- 画出递归树
首先,我们可以画出递归树,以描述算法的执行过程。根据算法的定义,每次递归调用都会将输入规模除以 2。因此,我们可以画出如下的递归树,其中每个节点都表示一次算法的执行过程,并且输入规模在每次递归调用时减半。
T(n)
/ \
T(n/2) T(n/2)
/ \ / \
T(n/4) T(n/4) T(n/4) T(n/4)
......
递归树的深度为 log2 n
,因为每次递归调用都会将输入规模减半,直到达到输入规模为 1。
- 计算每层节点的数目和节点的执行时间
我们发现,递归树的根节点 T(n) 的执行时间是 O(n)
,每个子问题的规模是 n/2
,因此,递归树的第一层上有两个节点,且每个节点的执行时间都是 O(n/2)
,因此第一层的总执行时间是 O(n)
。
同理,递归树的第二层上有 4 个节点,每个节点的执行时间是 O(n/4),因此第二层的总执行时间是 O(n)
。递归树的第三层上有 8 个节点,每个节点的执行时间是 O(n/8)
,因此第三层的总执行时间是 O(n)
。以此类推,对于递归树的第 i 层,有 2^i
个节点,每个节点的执行时间是 O(n/2^i)
,因此第 i 层的总执行时间是 O(n)
。
因此,每层的执行时间都是 O(n)
。
- 计算时间复杂度
我们可以将递归树中每层节点的执行时间加起来得到算法的总时间复杂度,即:
T(n) = O(n) + O(n) + O(n) + ... + O(n) (共 logn 项)
= logn * O(n)
= O(nlogn)
因此,T(n) = 2T(n/2) + n 的时间复杂度为 O(n log n)。