递归
递归概念
直接或间接地调用自身的算法称为递归算法,用函数自身给出定义的函数称为递归函数。
- 为了描述问题的某一状态,必须用到它的上一状态;而描述上一状态,又必须用到它的上一状态…这种用自已来定义自己的方法,称为递归定义
递归的两个要素
-
递归边界条件。也就是所描述问题的最简单情况,它本身不再使用递归的定义。
-
递归定义:使问题向边界条件转化的规则。递归定义必须能使问题越来越简单。
如:f(n)由f(n-1)定义,越来越靠近f(0),也即边界条件。
例子
下面将通过几个简单的例子来简单的体会下递归
1、阶乘函数
阶乘函数可递归地定义为:
n
!
=
{
0
n=0,
边界条件
n
(
n
−
1
)
n>0,
递归方程
n!= \begin{cases} 0& \text{n=0,} & \text{边界条件}\\ n(n-1)& \text{n>0,} & \text{递归方程} \end{cases}
n!={0n(n−1)n=0,n>0,边界条件递归方程
边界条件与递归方程是递归函数的两个要素,递归函数只有具备了这两个要素,才能在有限次计算后得出结果。
2、斐波那切数列
无穷数列1,1,2,3,5,8,13,21,34,55,…,被称为Fibonacci数列。它可以递归地定义为
F
(
n
)
=
{
1
n=0,1
边界条件
F
(
n
−
1
)
+
F
(
n
−
2
)
n>1,
递归方程
F(n)= \begin{cases} 1& \text{n=0,1} & \text{边界条件}\\ F(n-1) + F(n-2)& \text{n>1,} & \text{递归方程} \end{cases}
F(n)={1F(n−1)+F(n−2)n=0,1n>1,边界条件递归方程
第n个Fibonacci数可递归地计算如下:
def fibonacci(n):
if n <= 1: return 1
return fibonacci(n-1)+fibonacci(n-2)
上面的例子本身的递归关系太明显了,一眼就能看出来,下面来个不太容易看出来的~
3、整数划分问题
将正整数n表示成一系列正整数之和:n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1。正整数n的这种表示称为正整数n的划分。求正整数n的不同划分个数。
例如:
3有3种不同的划分;
3;
2+1;
1+1+1。
4有5种不同的划分;
4;
3+1;
2+2,2+1+1;
1+1+1+1。
6有11种不同的划分:
6;
5+1;
4+2,4+1+1;
3+3,3+2+1,3+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
1+1+1+1+1+1。
如果设p(n)为正整数n的划分数,比较难以找到递归关系。。
因此考虑增加一个自变量:将最大加数n1不大于m的划分个数记作q(n,m)。可以建立q(n,m)的如下递归关系。
这句话又什么加数,什么划分个数比较难理解,需要慢慢体会,大体描述一下就是:
q(n,m) 中的n就是要划分的数,m就是划分的加数中最大的数,比方说上面6中间有一种划分方式:3+1+1+1,这里m就等于3.
好的,理解了新增自变量m之后开始分情况讨论边界条件及递归方程:
-
n=1,q(1,m) 表示对1来划分,结果当然就是1了
-
m=1,q(n,1) 表示划分的这个数只能用1来划分,那不就是1+1+…+1,当然还是一种了。
-
n=m,q(n,n) 表示划分没有限制,这里还是要分两种情况:
1)划分的数包含自己本身,比方说3,划分成3自己,只有一种方式
2)不包含自己本身,也就是划分出来的数都小于自己,如3划分出来的数2及2以下都可以
那就是1+q(n,m-1) -
n<m ,这时候划分出来的最大数比自己还大。。那不就是自己本身,所以也就是q(n,n)
-
n>m,q(n,m) 最重要的一个,对n划分出来的最大数要小于m,还是上面的例子:
1) 比方说以q(6,4),对6划分出来的数最大是4为例:也就是q(6,4),对6划分出来的数包含4本身,(9种情况)
4+2,4+1+1;
-----------------------------------不包含:
3+3,3+2+1,3+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
1+1+1+1+1+1。
下方不包含4本身的递归方程为q(n,4-1)
所以,这个例子中q(n,m) = q(6-4, 4)+ q(6,4-1) =9
2) 在以q(6,5),对6划分出来的数最大是5为例:q(6,5),对6划分出来的数包含5本身:
5+1;
--------------------------------不包含:
4+2,4+1+1;
3+3,3+2+1,3+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
1+1+1+1+1+1。
同样的下方不包含5本身的递归方程为q(n,5-1)
所以,这个例子中q(n,m) = q(6-5, 4)+ q(6,5-1) =10仔细体会下,递归方程可以写成q(n,m) = q(n-m, m)+ q(n,m-1)
所以根据以上分析:
q
(
n
,
m
)
=
{
1
n=1,m=1
q
(
n
,
n
)
n<m,
1
+
q
(
n
,
n
−
1
)
n=m,
q
(
n
,
m
−
1
)
+
q
(
n
−
m
,
m
)
n>m>1
q(n,m)= \begin{cases} 1& \text{n=1,m=1}\\ q(n,n)& \text{n<m,}\\ 1 + q(n,n-1)& \text{n=m,}\\ q(n,m-1) + q(n-m,m)& \text{n>m>1}\\ \end{cases}
q(n,m)=⎩⎪⎪⎪⎨⎪⎪⎪⎧1q(n,n)1+q(n,n−1)q(n,m−1)+q(n−m,m)n=1,m=1n<m,n=m,n>m>1
def q(n,m):
if n==1 or m==1: return 1
elif n<m: return q(n,n)
elif n==m: return 1+q(n,n-1)
else: return q(n-m,m)+q(n,m-1)
q(6,6)
# 11
4、Hanoi塔(汉诺塔)
汉诺塔问题算是非常经典的递归例题了,可以说搞懂了这个,也就懂了大半部分的递归~
设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆 盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠 圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘 时应遵守以下移动规则:
规则1:每次只能移动1个圆盘;
规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘 之上;
规则3:在满足移动规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。
1.首先理解题目,
输入参数: A、B、C三个塔外加加盘子个数n
输出:总移动次数,(当然移动过程也可以输出)
2.开始找规律,将问题变成数学问题
1)n=1时:只有1个盘子,A–>B ,总共1次
2)n=2时:2个盘子,总共3次
第一次 1号盘 A-->C
第二次 2号盘 A-->B
第三次 1号盘 C-->B
3)n=3时:3个盘子,总共7次
第1次 1号盘 A---->B
第2次 2号盘 A---->C
第3次 1号盘 B---->C
第4次 3号盘 A---->B
第5次 1号盘 C---->A
第6次 2号盘 C---->B
第7次 1号盘 A---->B
不难发现规律,移动总次数为:2n - 1
移动规律为:
- 把n-1个盘子由A 移到 C
- 把第n个盘子由 A移到 B
- 把n-1个盘子由C 移到 B
cnt = 0
def move(n, x, y):
global cnt
cnt+=1
print(f'第{str(cnt)}次, move{str(n)}号盘:{x}--->{y}')
def hanoi(n, A, B, C):
if n == 1: move(1, A, B)
else:
hanoi(n - 1, A, C, B) # 将n-1个盘子由A经过B移动到C
move(n, A, B) # 最大盘子A移动到B
hanoi(n - 1, C, B, A) # 剩下的n-1盘子,由C经过A移动到B
hanoi(3, 'A', 'B', 'C')
print(f'共计{str(cnt)}次')
"""
第1次, move1号盘:A--->B
第2次, move2号盘:A--->C
第3次, move1号盘:B--->C
第4次, move3号盘:A--->B
第5次, move1号盘:C--->A
第6次, move2号盘:C--->B
第7次, move1号盘:A--->B
共计7次
"""
小结
优点:
结构清晰,可读性强,而且容易用数学归 纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。
缺点:
递归算法的运行效率较低,无论是耗费的 计算时间还是占用的存储空间都比非递归算法要多。