递归函数的调用次数必须是有限的!!!!
递归函数的调用次数必须是有限的!!!!
递归函数的调用次数必须是有限的!!!!
重要的事情说三遍。编译器在编译程序的时候,对递归的调用都是有一定次数限制的。我们知道,对递归的调用其实就是一个一直压栈的过程,等递归到了递归出口的时候,再出栈回溯。对于整个递归的过程,编译器会分配一定的栈空间,如果栈空间满了而递归仍然在进行,程序就会终止,当我们去调试程序的时候,就会出现我们常说的“爆栈”。因此,递归函数必须有一个递归出口,递归函数的调用次数必须是有限的,否则该递归函数就是一个错误的函数。
所有的递归都可以等价的转换为用栈来解决问题。
递归定义
递归的基本要素
递归 = 递归体 + 递归出口
递归体便是每一次递归函数的调用所要执行的内容,递归出口是递归函数终止的条件。递归出口是一个特定的情况,可以有多个,但是必须有一点:如果将所有的递归出口看做一个集合A,只要向递归函数传递的参数是合法的,那么无论参数是什么,它的递归出口一定在集合A里,否则该递归函数将无限次数的进行递归调用,是一个错误的递归函数。
何时使用递归
- 定义是递归的
比如斐波那契数、形式系统的原子、公式等一些数学上的递归的定义, - 数据结构是递归的
在数据结构中,我们有一种非常明显的递归定义的数据结构——树。无论是完全二叉树还是二叉查找树亦或者平衡二叉树,都是一种递归的定义。而我们在解决对树进行前、中、后序遍历,求树的深度等问题时,因为树本身就是一个递归的定义,因此我们可以用递归来解决问题。这一点在力扣、洛谷等刷题网站中树相关的问题中得到证实。 - 问题求解方法是递归的
有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法?
比如,
每次走1级台阶,共走10步,这是其中一种走法,可简写成1,1,1,1,1,1,1,1,1,1
再比如,
每次走2级台阶,共走5步,这是另一种走法,可简写成2,2,2,2,2
再比如列举全排列的所有情况等问题时,都可以将其分解抽象成一个递归的问题,在这里先不作解答,后文会对其进行讨论。
递归算法的执行过程
- 第一步是分解过程,将大问题分解成小问题
- 第二步是求值过程,已知小问题计算大问题
递归算法
递归算法的特点是:代码简单,过程复杂。 过程有多复杂呢?可能复杂到你只知道这样用可以,但是你根本不知道他到底为啥这样用,只知其然不知其所以然。
void
PreorderTraversal_recursion(STreeNodePtr pTreeNode)
{
if(pTreeNode == NULL)
return ;
visit(pTreeNode);
PreorderTraversal_recursion(pTreeNode->lchild);
PreorderTraversal_recursion(pTreeNode->rchild);
}
void
InorderTraversal_recursion(STreeNodePtr pTreeNode)
{
if(pTreeNode == NULL)
return ;
InorderTraversal_recursion(pTreeNode->lchild);
visit(pTreeNode);
InorderTraversal_recursion(pTreeNode->rchild);
}
void
PostorderTraversal_recursion(STreeNodePtr pTreeNode)
{
if(pTreeNode == NULL)
return ;
PostorderTraversal_recursion(pTreeNode->lchild);
PostorderTraversal_recursion(pTreeNode->rchild);
visit(pTreeNode);
}
/*
* 算法:
* 1、从根节点开始,沿着左子结点依次入栈,知道左子结点为空
* 2、栈顶元素出栈。
* 3、如果出栈元素有右子结点,将右子节点视为根执行步骤1,
* 如果出栈元素无右子结点,执行步骤2
* 4、如果栈为空,表示遍历完成
*/
void
PreorderTraversal(STreeNodePtr pTreeNode)
{
if(pTreeNode == NULL)
return ;
SStackPtr pStack = InitStack();
StackData pTemp = pTreeNode;
while((pTemp != NULL) || (pStack->top != -1))
{
if(pTemp != NULL) // 左子结点入栈
{
visit(pTemp);
PushStack(pStack, &pTemp);
pTemp = pTemp->lchild;
}
else
{
PopStack(pStack, &pTemp); // 出栈
pTemp = pTemp->rchild; // 下一循环将判断右子结点是否存在
}
}
}
void
InorderTraversal(STreeNodePtr pTreeNode)
{
if(pTreeNode == NULL)
return ;
SStackPtr pStack = InitStack();
StackData pTemp = pTreeNode;
while(pTemp != NULL || pStack->top != -1)
{
if(pTemp != NULL)
{
PushStack(pStack, &pTemp);
pTemp = pTemp->lchild;
}
else
{
PopStack(pStack, &pTemp);
visit(pTemp);
pTemp = pTemp->rchild;
}
}
}
void
PostorderTraversal(STreeNodePtr pTreeNode)
{
if(pTreeNode == NULL)
return ;
SStackPtr pStack = InitStack();
StackData pTemp = pTreeNode;
StackData pHelp = NULL;
while(pTemp != NULL || pStack->top != -1)
{
if(pTemp != NULL)
{
PushStack(pStack, &pTemp);
pTemp = pTemp->lchild;
}
else
{
pTemp = pStack->data[pStack->top];
if(pTemp->rchild != NULL && pTemp->rchild != pHelp)
{
pTemp = pTemp->rchild;
PushStack(pStack, &pTemp);
pTemp = pTemp->lchild;
}
else
{
PopStack(pStack, &pTemp);
visit(pTemp);
pHelp = pTemp;
pTemp = NULL;
}
}
}
}
void
visit(STreeNodePtr pTreeNode)
{
if(pTreeNode == NULL)
return ;
printf("%d ", pTreeNode->data);
}
这里给出了二叉树的前中后序遍历的递归和非递归两种形式的实现,可见递归具有代码简单的特点。
再次强调:递归调用的次数必须是有限的。
用递归方法求解问题的基本步骤
- 对原问题进行分析,抽象出合理的小问题
- 假设小问题是可解的,再次基础上确定原问题的解,即给出大问题与小问题之间的关系
- 确定一个特定情况作为递归出口
之后会有例子。
递归算法的时间复杂度分析方法
-
第一步,建立递推方程
T ( n ) = T ( n 1 ) + T ( n 2 ) + . . . + T ( n k ) + g ( n ) T(n)=T(n_1) + T(n_2) + ... + T(n_k) + g(n) T(n)=T(n1)+T(n2)+...+T(nk)+g(n)
T ( n ) 表 示 求 解 问 题 规 模 n 的 时 间 复 杂 度 T(n)表示求解问题规模n的时间复杂度 T(n)表示求解问题规模n的时间复杂度
n 1 , n 2 通 常 要 比 n 小 n_1, n_2通常要比n小 n1,n2通常要比n小
g ( n ) 表 示 什 么 呢 g(n)表示什么呢 g(n)表示什么呢
我们先看一个例子用递归求解阶乘问题n! = n * (n - 1)! (n - 1) * n 需要消耗O(1)的时间 因此在时间复杂度上我们可以表示为T(n) = T(n - 1) + O(1) 注意,递归函数必须有函数出口 T(0) = 1 T(1) = 1
递归方程的一般形式
T
(
n
)
=
∑
i
=
1
k
C
i
T
(
n
i
)
+
g
(
n
)
T(n)=\sum_{i=1}^{k}C_iT(n_i) + g(n)
T(n)=∑i=1kCiT(ni)+g(n)
-
n i n_i ni为第i个子问题的规模
-
T( n i n_i ni)为第i个子问题的时间复杂度
-
g(n)为把大问题划分成若干个小问题以及把各个小问题的解整合成整个问题的解所需要的工作量之和
-
C i C_i Ci为常数
-
n i < n , 1 ≤ i ≤ k n_i < n, 1 \le i \le k ni<n,1≤i≤k
-
第二步,求解递归方程
求解递归方程,我们一般使用迭代法、递归树和主定理这三种方法。
迭代法
迭代法和我们高中时学习的等差数列、等比数列和各种各样的数列的解法所用的方法很像。
例1
我们的递归有如下的时间复杂度关系
T(1) = 1
T(m) = T(n - 1) + 1
这就是上面用阶乘求递归的时间复杂度
由 递 推 关 系 式 , 我 们 可 以 得 到 T ( n ) = 1 + 1 + . . . + 1 ( 一 共 n 个 1 ) 即 T ( n ) = n T ( n ) = O ( n ) 由递推关系式,我们可以得到\\ T(n) = 1 + 1 + ... + 1(一共n个1)\\ 即T(n) = n\\ T(n) = O(n) 由递推关系式,我们可以得到T(n)=1+1+...+1(一共n个1)即T(n)=nT(n)=O(n)
例2
我们的递归有如下的时间复杂度关系
T(1) = 1
T(n) = T(n - 1) + n
由 递 推 关 系 式 , 我 们 可 以 得 到 T ( n ) = 1 + 2 + . . . + n 即 T ( n ) = n ( n + 1 ) 2 T ( n ) = O ( n 2 ) 由递推关系式,我们可以得到\\ T(n) = 1 + 2 + ... + n\\ 即T(n) = \frac{n(n + 1)}{2}\\ T(n) = O(n^2) 由递推关系式,我们可以得到T(n)=1+2+...+n即T(n)=2n(n+1)T(n)=O(n2)
常用的方法还有等比数列的求和公式,还有错位相减法等,都是高中学过的知识,同时高数中级数相关的方法也可以使用,递归就好比一个级数,分析时间复杂度时就是在求极限。
主定理和递归树
设
a
≥
1
,
b
>
1
为
常
数
,
f
(
n
)
为
函
数
,
T
(
n
)
为
非
负
整
数
T
(
n
)
=
a
T
(
n
b
)
+
f
(
n
)
1.
f
(
n
)
=
O
(
n
l
o
g
b
a
−
ε
)
,
ε
>
0
,
那
么
T
(
n
)
=
θ
(
n
l
o
g
b
a
)
2.
f
(
n
)
=
θ
(
n
l
o
g
b
a
,
那
么
T
(
n
)
=
θ
(
n
l
o
g
b
a
l
o
g
n
)
3.
f
(
n
)
=
Ω
(
n
l
o
g
b
a
+
ε
)
,
ε
>
0
,
且
对
于
常
数
c
<
1
和
所
有
充
分
大
的
n
有
a
f
(
n
b
)
≤
c
f
(
n
)
,
那
么
T
(
n
)
=
θ
(
f
(
n
)
)
设a≥1,b>1为常数,f(n)为函数,T(n)为非负整数\\ T(n)=aT(\frac{n}{b})+f(n)\\ 1.f(n)=O(n^{log_b{a−ε}}), ε>0,那么T(n)=\theta(n^{log_ba})\\ 2.f(n)=\theta(n^{log_ba}, 那么T(n)=\theta(n^{log_ba}logn)\\ 3.f(n)=\Omega(n^{log_b{a+ε}}),ε>0,且对于常数c<1 和所有充分大的n有af(\frac{n}{b})≤cf(n),那么 T(n)=\theta(f(n))
设a≥1,b>1为常数,f(n)为函数,T(n)为非负整数T(n)=aT(bn)+f(n)1.f(n)=O(nlogba−ε),ε>0,那么T(n)=θ(nlogba)2.f(n)=θ(nlogba,那么T(n)=θ(nlogbalogn)3.f(n)=Ω(nlogba+ε),ε>0,且对于常数c<1和所有充分大的n有af(bn)≤cf(n),那么T(n)=θ(f(n))
例题
例1 斐波那契数列变形
有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者两级,问一共有多少张走法?
比如,每一次只走一级台阶,一共走10步,这是一种走法
再比如,每一次走两级台阶,一共走5步,这也是一种走法
很明显,这道题就是一道排列组合的题。根据题目,我们去找递推关系式。
首先,当我们走到第10级台阶时,我们的前一步可以是从第9级台阶向上走1级台阶走到第10级,也可以从第8级向上走2级台阶走到第10级,一共是两个走法。我们可以得到下面的关系式
F
(
10
)
=
F
(
9
)
+
F
(
8
)
同
理
,
F
(
9
)
=
F
(
8
)
+
F
(
7
)
F
(
8
)
=
F
(
7
)
+
F
(
6
)
.
.
.
.
.
.
最
后
是
递
归
出
口
F
(
2
)
=
1
F
(
1
)
=
1
F(10) = F(9) + F(8)\\ 同理,\\ F(9) = F(8) + F(7)\\ F(8) = F(7) + F(6)\\ ......\\ 最后是递归出口\\ F(2) = 1\\ F(1) = 1
F(10)=F(9)+F(8)同理,F(9)=F(8)+F(7)F(8)=F(7)+F(6)......最后是递归出口F(2)=1F(1)=1
可以得出,这一个问题本质上就是斐波那契数列的问题。就不上代码了。从这个问题也可以看出,如果能把一个具体的问题抽象出它的本质,那么问题也就很好解决了。
例2 排列问题
设
R
=
{
r
1
,
r
2
,
.
.
.
,
r
n
}
R = \{r_1, r_2, ... , r_n\}
R={r1,r2,...,rn}是要进行排列的n个元素,
R
i
=
R
−
{
r
i
}
。
R_i = R - \{r_i\}。
Ri=R−{ri}。集合X中元素的全排列记为Perm(X)。
(
r
i
)
P
e
r
m
(
X
)
(r_i)Perm(X)
(ri)Perm(X)表示在全排列Perm(X)的每个排列前加上前缀
r
i
r_i
ri得到的排列。R的全排列可归纳定义如下:
- 当n = 1时, Perm( R) = ( r),其中r是集合R中唯一的元素
- 当n > 1时,Perm ( R)由
(
r
1
)
P
e
r
m
(
R
1
)
,
(
r
2
)
(
R
2
)
.
.
.
,
(
r
n
)
P
e
r
m
(
r
n
)
构
成
(r_1)Perm(R_1), (r_2)(R_2) ... ,(r_n)Perm(r_n)构成
(r1)Perm(R1),(r2)(R2)...,(rn)Perm(rn)构成
依据此定义,可以设计Perm®的递归算法
#include<iostream>
#include<aglorithm>
#include<vector>
#include<iterator>
using namespace std;
template<class Type>
void
Perm(vector<Type> &vec, int i, int j)
{
if(i == j)
{
for(const auto &elem : vec)
cout << elem << " ";
cout << endl;
}
else
{
for(int k = i; k < j; ++l)
{
swap(vec[i], vec[k]);
Perm(vec, i + 1, j);
swap(vec[i], vec[k]);
}
}
}
int main()
{
istream_iterator<int> it_in(cin), eof;
vector<int> ivec(it_in, eof);
Perm(ivec, 0, ivec.size() - 1);
}