0. 简介
首先来看维基百科里面的一个语言例子:
从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?‘从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……’”
递归的正式定义:在数学和计算机科学中,递归指一种(或)多种简单的基本情况定义的一类对象或方法,并规定其他所有情况都能被还原为其基本情况。
算法与数据结构中的递归主要体现在函数自己调用自己上,函数在自己调用自己的过程中有个结束点,称之为递归出口。此外,函数自己调用自己的目的是为了缩小范围,不断向递归出口推进。因此必须找到一种合理的递推关系式。因此,使用递归的两个基本点是:递归出口、递推关系式。一般情况下,寻找递归出口较简单,寻找递推关系式较难。首先来从一个
C
{\rm C}
C++程序例子说起:
void f(int i) {
cout << "第" << i << "层调用..." << endl;
if (i < 5) {
f(i + 1);
}
cout << "第" << i << "层返回..." << endl;
}
f
(
1
)
{\rm f(1)}
f(1)调用上面函数的输出结果为:
根据上面的叙述,首先来看递归出口:在程序中满足 i < 5 {\rm i<5} i<5时才调用自身,所以递归出口为 i = 5 {\rm i=5} i=5;递推关系式:在满足 i < 5 {\rm i<5} i<5时执行 f ( i + 1 ) {\rm f(i+1)} f(i+1),所以将 i {\rm i} i值加 1 1 1后再去调用 f {\rm f} f函数就是函数的递推关系式。递归程序在执行过程中存在两个过程,即调用和返回。以上述程序为例说明:
首先说明一点,函数在调用过程会使用栈来保存函数的调用过程和返回值,而递归由于是函数调用自身,所以往往需要栈存储中间过程。
调用过程, i = 1 {\rm i=1} i=1时调用 f ( 1 ) {\rm f(1)} f(1),打印"第1层调用";满足 i < 5 {\rm i<5} i<5条件,调用 f ( 2 ) {\rm f(2)} f(2),打印"第2层调用";满足 i < 5 {\rm i<5} i<5条件,调用 f ( 3 ) {\rm f(3)} f(3),打印"第3层调用",依次打印剩下内容。当 i = 5 {\rm i=5} i=5时,已经不满足 i f {\rm if} if条件,执行后续语句,打印"第5层返回",下面开始进入返回过程:
返回过程,函数的返回为调用本身的函数,调用 f ( 5 ) {\rm f(5)} f(5)的为 f ( 4 ) {\rm f(4)} f(4),此时 i = 4 {\rm i=4} i=4,打印"第4层返回"; f ( 4 ) {\rm f(4)} f(4)返回至调用本身的函数 f ( 3 ) {\rm f(3)} f(3)处,此时 i = 3 {\rm i=3} i=3,打印打印"第3层返回",依次打印后续内容。这个过程用一个栈来存储各函数,入栈过程是递归调用前执行,出栈过程是递归调用后执行。
至此,对递归做一个小结:
(1)在到达递归出口前调用,在达到递归出口后返回(
i
=
5
{\rm i=5}
i=5为递归出口);
(2)每一次函数调用都有一次返回,用栈来记录返回的地址和参数值;
(3)在递归调用前后语句执行顺序相反(如上述程序中针对不同变量的打印语句);
(4)函数在每一层的调用和返回中均有自己的私有变量(如上述程序中变化的
i
{\rm i}
i值);
(5)递归函数中必须要有合适的递归出口(如上述程序中
i
=
5
{\rm i=5}
i=5的条件)。
1. 递归的分类
递归函数有许多种类,下面列举几种常见的类别:
(一)线性递归
线性递归在函数执行期间只调用一次自身,且在每一层调用上构成一个线性关系。求阶乘是一种典型的线性递归。
C
{\rm C}
C++代码:
int f(int n) {
if (n == 1) {
return 1;
}
else
{
return n * f(n - 1);
}
}
在线性递归中每一次进行函数调用时均会执行 r e t u r n {\rm return} return语句,线性递归的调用方式更方便理解,但是往往会产生许多冗余的操作。如求阶乘的递归函数中,函数调用情况为:
执行 f ( 5 ) {\rm f(5)} f(5)语句,函数运行过程是: 5 ∗ f ( 4 ) → 5 ∗ 4 ∗ f ( 3 ) → 5 ∗ 4 ∗ 3 ∗ f ( 2 ) → 5 ∗ 4 ∗ 3 ∗ 2 ∗ f ( 1 ) {\rm 5*f(4)→5*4*f(3)→5*4*3*f(2)→5*4*3*2*f(1)} 5∗f(4)→5∗4∗f(3)→5∗4∗3∗f(2)→5∗4∗3∗2∗f(1),由于 f ( 1 ) = 1 {\rm f(1)=1} f(1)=1,所以 f ( 5 ) {\rm f(5)} f(5)的值为 120 120 120。
(二)尾递归
尾递归也是一种线性递归,顾名思义,在尾递归中函数的最后一步操作是递归。也即在进行递归之前,将全部操作执行完后将参数传给递归部分。基于尾递归的形式,其常常能转化为迭代形式实现。将上述求阶乘的程序改尾递归的方式,
C
{\rm C}
C++代码:
int f(int n, int res) {
if (n == 1) {
return res;
}
else
{
return f(n - 1, n*res);
}
}
执行 f ( 5 , 1 ) {\rm f(5,1)} f(5,1)语句,函数运行过程是: f ( 4 , 5 ∗ f ( 3 ) ) → f ( 3 , 20 ∗ f ( 2 ) ) → f ( 2 , 120 ∗ f ( 1 ) ) {\rm f(4,5*f(3))→f(3,20*f(2))→f(2,120*f(1))} f(4,5∗f(3))→f(3,20∗f(2))→f(2,120∗f(1)),由于 f ( 1 ) = 1 {\rm f(1)=1} f(1)=1,所以 f ( 5 ) {\rm f(5)} f(5)的值为 120 120 120。在上述尾递归中 n {\rm n} n用于控制递归的出口, r e s {\rm res} res用于保存上一次调用的参数。
转化为迭代形式:
int f(int n, int res) {
while (n >= 1)
{
res *= n;
n--;
}
return res;
}
线性递归和尾递归的特点(以上述求阶乘为例):线性递归的书写方式更简单、易于理解,但在有些复杂的问题中需要进行,尾递归则相反;在线性递归中,因为在到达递归出口 f ( n ) = 1 {\rm f(n)=1} f(n)=1时需要逐步返回,所以需要保存 n {\rm n} n的值,而在尾递归中结果存放在 r e s {\rm res} res中,每次调用已经计算了前面项的值 n ∗ r e s {\rm n*res} n∗res,函数不断向后调用直到达到递归出口 f ( n ) = 1 {\rm f(n)=1} f(n)=1。
(三)二分递归
二分递归中,函数会调用自身多于一次,也称“多路递归”。求斐波拉契数列是一种二分递归,斐波拉契数列的形式为:
F
(
x
)
=
{
1
x
=
1
o
r
x
=
2
F
(
x
−
1
)
+
F
(
x
−
2
)
o
t
h
e
r
w
i
s
e
(1.1)
F(x)=\left\{ \begin{aligned} &1 &x=1\ or\ x=2\\ &F(x-1)+F(x-2) & otherwise \end{aligned} \right.\tag{1.1}
F(x)={1F(x−1)+F(x−2)x=1 or x=2otherwise(1.1)
对应的数列为: 1 1 2 3 5 8 13... {\rm 1\ 1\ 2\ 3\ 5\ 8\ 13... } 1 1 2 3 5 8 13..., C {\rm C} C++代码:
int F(int i) {
if (i == 1 || i == 2) {
return 1;
}
else
{
return F(i - 1) + F(i - 2);
}
}
执行 F ( 5 ) {\rm F(5)} F(5)语句,函数运行过程是: F ( 4 ) + F ( 3 ) → F ( 3 ) + F ( 2 ) + F ( 3 ) → F ( 2 ) + F ( 1 ) + 1 + F ( 3 ) {\rm F(4)+F(3)→F(3)+F(2)+F(3) →F(2)+F(1)+1+F(3)} F(4)+F(3)→F(3)+F(2)+F(3)→F(2)+F(1)+1+F(3) → 3 + F ( 3 ) → 3 + F ( 2 ) + F ( 1 ) → 5 {\rm→3+F(3)→3+F(2)+F(1)→5} →3+F(3)→3+F(2)+F(1)→5。可以看到,在执行过程中, F ( 3 ) {\rm F(3)} F(3)的值计算了两次。函数的调用顺序为 F ( 5 ) F ( 4 ) F ( 3 ) F ( 2 ) F ( 1 ) F ( 2 ) F ( 3 ) F ( 2 ) F ( 1 ) {\rm F(5)\ F(4)\ F(3)\ F(2)\ F(1)\ F(2)\ F(3)\ F(2)\ F(1)} F(5) F(4) F(3) F(2) F(1) F(2) F(3) F(2) F(1)。
小结:在二分递归中,可以将多次递归调用分开对待,如上面求斐波拉契数列的例子,加号前面是一次递归,加号后面是一次递归,最后将结果值相加。由于将两次递归看做是独立的,所以避免会计算重复的项(如上述提到的 f ( 3 ) {\rm f(3)} f(3)的值)。独立看待后可以改为:
int f(int n) {
if (n == 1 || n == 2) {
return 1;
}
else
{
int l = f(n - 1);
int r = f(n - 2);
return l + r;
}
}
(四)指数递归
指数递归多用于绘制函数的所有调用形式。例如求一个序列的全排列是一种指数递归,全排列的目的就是得到数列的所有排列形式,这符合指数递归的定义。递归实现全排列的思路是,1)保持首元素不变,对剩下元素全排列;2)继续保持首元素不变,对剩下元素全排列,直到只剩下一个元素得到第一个序列为顺序序列;3)返回,直到得到以首元素为首的全部全排列;4)循环依次将后面元素放到首位置充当首元素直到完成整个全排列过程。 注意,为了避免产生重复的序列,在每次与首元素交换前我们都将数组重置为原始状态,具体做法为在递归调用完后调用
s
w
a
p
{\rm swap}
swap函数恢复。
C
{\rm C}
C++代码:
void permutations(vector<int> a, int m, int n) {
// 只有一个元素,打印序列
if (m == n) {
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
}
else
{
// 依次与首元素交换
for (int i = m; i < n; i++) {
// 交换
swap(a[i], a[m]); // #1
// 递归调用,不断往后推
permutations(a, m + 1, n);
// 交换恢复数组
swap(a[i], a[m]); // #2
}
}
}
假设输入是 p e r m u t a t i o n s ( [ 0 , 1 , 2 , 3 ] , 0 , 4 ) {\rm permutations([0,1,2,3],0,4)} permutations([0,1,2,3],0,4)递归函数执行流程如下:
1)第一次执行交换语句 1 1 1均是自身交换,无作用、直到 m {\rm m} m的值为 2 2 2后,调用 p e r m u t a t i o n s ( a , 3 , 3 ) {\rm permutations(a,3,3)} permutations(a,3,3),进入 i f {\rm if} if语句直接打印第一个序列 0 1 2 3 0\ 1\ 2 \ 3 0 1 2 3
2)根据简介部分中的例子,现在要逐级返回,首先返回 i = 3 m = 3 {\rm i=3\ m=3} i=3 m=3执行交换语句 2 2 2也是自身交换
3)执行 f o r {\rm for} for循环 i {\rm i} i++,不满足循环条件,继续返回 i = 2 m = 2 {\rm i=2\ m=2} i=2 m=2
4)执行 f o r {\rm for} for循环 i {\rm i} i++,满足循环条件,此时 i = 3 m = 2 {\rm i=3\ m=2} i=3 m=2,执行交换语句 1 1 1,此时 a [ 3 ] = 3 {\rm a[3]=3} a[3]=3和 a [ 2 ] = 2 {\rm a[2]=2} a[2]=2值交换,调用 p e r m u t a t i o n s ( a , 3 , 3 ) {\rm permutations(a,3,3)} permutations(a,3,3)进入 i f {\rm if} if语句直接打印第二个序列 0 1 3 2 0\ 1\ 3 \ 2 0 1 3 2
5)逐级返回,首先返回 i = 3 m = 3 {\rm i=3\ m=3} i=3 m=3,不满足循环条件,继续返回 i = 3 m = 2 {\rm i=3\ m=2} i=3 m=2(注意和递归调用前是对称的),执行交换语句 2 2 2,这时数列恢复原序
6)执行 f o r {\rm for} for循环 i {\rm i} i++,不满足循环条件,这时返回的是 i = 1 m = 1 {\rm i=1\ m=1} i=1 m=1,因为上述 i = 2 m = 2 {\rm i=2\ m=2} i=2 m=2已经返回过了(系统栈里面已经没有该元素了),满足循环条件,执行交换语句 1 1 1,此时 a [ 2 ] = 2 {\rm a[2]=2} a[2]=2和 a [ 1 ] = 1 {\rm a[1]=1} a[1]=1交换
7)调用 p e r m u t a t i o n s ( a , 2 , 3 ) {\rm permutations(a,2,3)} permutations(a,2,3),重复上述步骤,直到上去执行 i f {\rm if} if语句直接打印第三个序列 0 2 1 3 0\ 2\ 1 \ 3 0 2 1 3
8)…
最终全排列的结果为:
图2:全排列
小结:注意每次输出序列后必有一个将数组恢复为原序的过程,因为递归前后的语句调用时对称的,前面语句 1 1 1是怎么交换的,后面语句 2 2 2也必然以同等方式交换。全排列的核心思想是固定首元素,依次对剩余元素全排列,排列方式是固定首元素,如此递归下去。
(五)嵌套递归
在嵌套递归中,递归函数的某个参数为函数本身。嵌套递归在实际中运用得不多,这里只举一个求阿克曼函数值的例子。阿克曼函数的形式:
A
(
m
,
n
)
=
{
n
+
1
m
=
0
A
(
m
−
1
,
1
)
m
>
0
a
n
d
n
=
0
A
(
m
−
1
,
A
(
m
,
n
−
1
)
)
o
t
h
e
r
w
i
s
e
(1.2)
A(m,n)=\left\{ \begin{aligned} &n+1 &m=0\\ &A(m-1,1)&\ m>0\ and\ n=0\\ &A(m-1,A(m,n-1))&otherwise \end{aligned} \right.\tag{1.2}
A(m,n)=⎩⎪⎨⎪⎧n+1A(m−1,1)A(m−1,A(m,n−1))m=0 m>0 and n=0otherwise(1.2)
C {\rm C} C++代码:
int ackerman(int m, int n) {
if (m == 0) {
return n + 1;
}
else if (n == 0) {
return ackerman(m - 1, 1);
}
else
{
return ackerman(m - 1, ackerman(m, n - 1));
}
}
根据上面的式子可以很容易地写出阿克曼函数的递归形式,递归调用形式同上述递归调用形式类似,只是这里在返回的时候不仅需要返回参数值而且还需要返回函数值。值得一提的是,阿克曼函数的增长极为迅速,尤其是函数的第一个参数的细微增长可能会带来函数值爆炸式的增加。以下是来自维基百科的一张阿克曼函数值表:
m\n | 0 | 1 | 2 | 3 | 4 | … | n |
---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | … | n+1 |
1 | 2 | 3 | 4 | 5 | 6 | … | n+2 |
2 | 3 | 5 | 7 | 9 | 11 | … | 2(n+3)-3 |
3 | 5 | 13 | 29 | 61 | 125 | … | 2 n + 3 2^{n+3} 2n+3-3 |
4 | 13 | 65533 | 2 65536 − 3 2^{65536}-3 265536−3 | A(3, 2 65536 − 3 2^{65536}-3 265536−3) | A(3,A(4,3)) | … | 2 2 2 2 . . . 2^{2^{2^{2^{...}}}} 2222...- 3 3 3共 ( n (n (n+ 3 ) 3) 3)个 2 2 2 |
(六)相互递归
相互递归中,递归函数不会调用本身。而是存在多个函数,函数之间相互调用。例如函数
A
{\rm A}
A调用函数
B
{\rm B}
B,而函数
B
{\rm B}
B反过来调用函数
A
{\rm A}
A。一个相互递归的经典例子是判断一个数的奇偶性。我们知道,
0
0
0为偶数,则将其视为递归出口。当给定数为偶数时,输出
1
1
1;当给定数为奇数时,输出
0
0
0。
C
{\rm C}
C++代码:
int is_even(int m) {
if (m == 0) {
return 1;
}
else
{
return is_odd(m - 1);
}
}
int is_odd(int n) {
return !is_even(n);
}
上面函数的递归即是 ! ! !的递归使用,例如语句 i s _ e v e n ( 5 ) {\rm is\_even(5)} is_even(5):
1)进入 e l s e {\rm else} else,调用 i s _ o d d ( 4 ) {\rm is\_odd(4)} is_odd(4),返回 ! i s _ e v e n ( 4 ) {\rm !is\_even(4)} !is_even(4)
2)进入 e l s e {\rm else} else,调用 i s _ o d d ( 3 ) {\rm is\_odd(3)} is_odd(3),返回 i s _ e v e n ( 3 ) {\rm is\_even(3)} is_even(3)
3)。。。
上述程序只是为了直观展示相互递归中函数的调用过程,判断奇偶性直接使用取余操作即可。
递归分类总结:
(1)线性递归是所有递归分类中最好理解的,代码也较为简单;
(2)尾递归可以通过转化为迭代方式加强理解;
(3)二分递归通常可以拆分成多个更小的递归过程;
(4)指数递归易于展现递归过程中变量的变化情况;
(5)嵌套递归往往会产生数据量的陡增;
(6)相互递归使用不多,一般在函数间的相互调用时在一个函数中注明递归出口即可。
2. 经典例子
2.1 二叉树
由于二叉树结构的特殊性,通常使用递归解决二叉树的遍历、深度、打印路径等问题较为方便(二叉树的相关操作参考这里)。这是因为一棵二叉树通常由多个子二叉树构成,这种父子结构正好满足递归的性质。
(一)二叉树的遍历
首先访问二叉树的根节点,然后不断访问当前节点的左孩子,直到访问到叶子节点。然后返回,访问当前节点的右孩子。不断重复上述过程即完成二叉树的先序遍历过程。
C
{\rm C}
C++代码:
void preVisitBiTree(TreeNode* &T) {
if (T) {
Visit(T);
preVisitBiTree(T->left);
preVisitBiTree(T->right);
}
}
(二)二叉树的深度
二叉树的深度定义为从根节点到叶子节点会形成一条路径,在所有路径中最长的那条路径所包含的节点数定义为二叉树的深度。递归出口:如果为空树,则返回深度为1。递推关系式否则,分别求当前节点左子树和右子树的深度,返回最大值+
1
1
1(包含当前节点),如此递归执行下去。
C
{\rm C}
C++代码:
int getDepthBiTree(TreeNode* &T) {
int LD, RD;
if (T == NULL) {
return 0;
}
else
{
LD = getDepthBiTree(T->left);
RD = getDepthBiTree(T->right);
return (LD > RD ? LD : RD) + 1;
}
}
2.2 汉诺塔
如图,汉诺塔问题的描述是: A {\rm A} A杆为起始杆, B {\rm B} B杆为辅助杆, C {\rm C} C杆为目标杆。现在 A {\rm A} A杆上有一些圆盘,其中大的圆盘只能在小的圆盘下方,而不能颠倒。现在的目标是通过辅助杆 B {\rm B} B将 A {\rm A} A杆上的圆盘移动到 C {\rm C} C杆,而在移动过程中同样遵循大圆盘在小圆盘下方的规则。首先寻找递归出口:如果 A {\rm A} A上只有一个圆盘,则直接将其移动到 C {\rm C} C上即可。递推关系式:否则,首先将 C {\rm C} C当做辅助杆,将全部 n − 1 {\rm n-1} n−1个圆盘从 A {\rm A} A移动到 B {\rm B} B。这时 A {\rm A} A上只剩一下最大那个圆盘,直接移动到 C {\rm C} C即可。然后将 A {\rm A} A当做辅助杆,将全部 n − 1 {\rm n-1} n−1个圆盘从 B {\rm B} B移动到 C {\rm C} C。这时就完成了全部圆盘的移动。 C {\rm C} C++代码:
void Han(int a, int b, int c, int n) {
// 只有一个圆盘的情况
if (n == 1) {
move(a, c);
}
else
{
// 首先将n-1个圆盘经由c从a到b
Han(a, c, b, n - 1);
// 将最后一个圆盘从a到c
move(a, c);
// 最后将n-1个圆盘经由a从b到c
Han(b, a, c, n - 1);
}
}
3. 时间复杂度
(一)迭代法
迭代法计算递归函数的时间复杂度时,将规模缩小迭代下去即可。下面以汉诺塔问题理解迭代法,假设时间复杂度为
f
(
n
)
f(n)
f(n)。由汉诺塔递归函数的
e
l
s
e
{\rm else}
else部分可知,每次通过将
n
n
n值减
1
1
1而使问题规模缩小。则可以得到:
f
(
n
)
=
2
⋅
f
(
n
−
1
)
+
O
(
1
)
f(n)=2·f(n-1)+O(1)
f(n)=2⋅f(n−1)+O(1)
其中 O ( 1 ) O(1) O(1)为常数阶操作,如 m o v e {\rm move} move函数、条件语句判断等。继续迭代: f ( n ) = 2 ⋅ ( 2 ⋅ f ( n − 2 ) + O ( 1 ) ) + O ( 1 ) = 2 2 ⋅ f ( n − 2 ) + O ( 1 ) f(n)=2·(2·f(n-2)+O(1))+O(1)=2^2·f(n-2)+O(1) f(n)=2⋅(2⋅f(n−2)+O(1))+O(1)=22⋅f(n−2)+O(1)
如此迭代下去,得到: f ( n ) = 2 k ⋅ f ( n − k ) + O ( 1 ) f(n)=2^k·f(n-k)+O(1) f(n)=2k⋅f(n−k)+O(1)
则可以得到,汉诺塔问题的时间复杂度为
O
(
2
n
)
O(2^n)
O(2n)。
迭代法在计算递归函数的时间复杂度时应用范围有限,通常只有在递归函数有明确的迭代格式时才能通过迭代法求时间复杂度。下面介绍一种计算递归函数最方便的方法。
(二)公式法
当递归函数的执行时间满足如下关系式时,可以使用公式法:
f
(
n
)
=
a
⋅
f
(
n
b
)
+
t
(
n
)
(3.1)
f(n)=a·f(\frac{n}{b})+t(n)\tag{3.1}
f(n)=a⋅f(bn)+t(n)(3.1)
其中
t
(
n
)
t(n)
t(n)指每次递归完毕后,额外的计算执行时间,如交换、打印、排序等。当参数
a
a
a和
b
b
b均确定时,这是只看递归部分的时间复杂度为
O
(
n
l
o
g
b
a
)
O(n^{log_ba})
O(nlogba)。现在分以下三种情况:
(1)当递归部分时间复杂度大于
t
(
n
)
t(n)
t(n)时,最终的时间复杂度为
O
(
n
l
o
g
b
a
)
O(n^{log_ba})
O(nlogba);
(2)当递归部分时间复杂度小于
t
(
n
)
t(n)
t(n)时,最终的时间复杂度为
t
(
n
)
t(n)
t(n);
(3)当递归部分时间复杂度等于
t
(
n
)
t(n)
t(n)时,最终的时间复杂度为
O
(
n
l
o
g
b
a
)
l
o
g
n
O(n^{log_ba})logn
O(nlogba)logn。
现在举例说明,在归并排序时,执行时间可以写作(参考这里):
f
(
n
)
=
2
⋅
f
(
n
2
)
+
n
f(n)=2·f({\frac{n}{2})+n}
f(n)=2⋅f(2n)+n
则可以得到 a = 2 , b = 2 , t ( n ) = n a=2,b=2,t(n)=n a=2,b=2,t(n)=n,递归部分时间复杂度为 O ( n l o g 2 2 ) = O ( n ) O(n^{log_22})=O(n) O(nlog22)=O(n),符合第(3)种情况。所以最终的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。又假如一个递归函数的执行时间可以写作: f ( n ) = 2 ⋅ f ( n 4 ) + 1 f(n)=2·f({\frac{n}{4})+1} f(n)=2⋅f(4n)+1
则可以得到
a
=
2
,
b
=
4
,
t
(
n
)
=
1
a=2,b=4,t(n)=1
a=2,b=4,t(n)=1,递归部分时间复杂度为
O
(
n
l
o
g
4
2
)
=
O
(
n
)
O(n^{log_42})=O(\sqrt{n})
O(nlog42)=O(n),符合第(1)种情况。所以最终的的时间复杂度为
O
(
n
)
O(\sqrt{n})
O(n)。
总结:求解递归函数的时间复杂度时,如果使用迭代法则必须要求递归函数具有明显的迭代格式,通常通过在不断缩小规模的过程中迭代计算时间复杂度。而公式法是一种更为简便的计算时间复杂度的方法,只要将递归函数的执行时间以公式(3.1)的形式表示出来,就可以通过公式法快速准确地求出递归函数的时间复杂度。
总结
借用引用
[
6
]
[6]
[6]和自己的思考对递归做一波总结:
(1)在找到题目描述有明显的递归性质,即函数调用朝着参数范围缩小的方向的进行,并可以达到函数出口,考虑使用递归;
(2)思考两个方面的事情,递归出口和递推关系式。其中递推关系式的寻找可以分为两部分,寻找返回值和考虑第一次函数调用执行的功能(因为以后每一次函数调用都执行此功能,只是参数不同);
(3)在理解递归函数的精髓后,可以写出递归代码。但是由上面的例子可以看到,递归过程中存在很多重复执行的过程,如斐波拉契数列、阶乘等。如上述尾递归就是一种优化。
参考
- https://zh.wikipedia.org/wiki/递归.
- https://blog.youkuaiyun.com/ysuncn/article/details/1793896.
- https://www.cnblogs.com/youxin/p/3284090.html.
- https://www.jianshu.com/p/50a27d7d2972.
- https://zh.wikipedia.org/zh-hans/阿克曼函数.
- https://www.cnblogs.com/king-lps/p/10748535.html.