接着上一次的内容来讲递归,这次将写到几个基本的递归功能函数!
在上一次的内容中,我们已经知道了一个递归程序最大的特点,但这是远远不够的,因为目前的递归就是无限的调用自己,我们并不能从中体会到什么可改善编程的算法,那么让我们离递归程序再近一部吧!
1.求0~n的和(这个当然可以用等差数列前N项和公式一下得出答案!)
sum(n)=1+2+3+……+n
sum(n)=sum(n-1)+n
sum(1)=1
前面的公式是至关重要的,任何一个可以递归求解的问题都可以拆分为规模更小的子问题,而最小的子问题可以用做跳出递归的标志,假设递归求和运算到了sum(2) = sum(1)+2,下一步我们便可以知道sum(1)=1,这个时候就可以将问题逐层解决了。
def nsum(n):
if n == 1:
return 1
else:
return nsum(n-1)+n
print(nsum(100))
如果是非递归的写法如下:
def getsum(n):
SUM = 0
for i in range(n + 1):
SUM += i
return SUM
print(getsum(100))
这里貌似还是看不出什么优越性,只是单纯将代码写得更玄乎了,其实也没见得代码变得有多玄乎!还是接着看吧。
做了加法,再来做下乘法,1~n连乘就是n!,下面看递归实现的阶乘。
def fac(n):
if n == 1:
return 1
else:
return n * fac(n - 1)
print(fac(10))
非递归的写法:
def getfac(n):
FAC = 1
for i in range(1, n + 1):
FAC *= i
return FAC
好吧!我承认还是看不到什么优越性,除了少用哪么一个变量外……
下面是重头戏了,请注意观察前面的实例,每个递归对应的非递归中都是用for循环来实现一系列重复的操作,而且for循环的个数是给定的也就是说for循环多少次都是已知的,哪么会不会遇到不知道要for循环多少次的时候呢?我告诉你没答案结对是的肯定的,有很多时候,我们并不知道要for多少次才能解决问题,但是我们知道结束的标志(成功的标志),这个时候我想就是非递归不可的了。
递归中每次都讲到的汉诺塔问题。(这个问题的困难之处就在于移动的次数本身就是我们需要探讨的问题,我根本不知道如何用for循环来解决它,因为在这个问题中,for多少次就成了这个问题的核心,虽然最后可以用数学求解出要移动的次数,但是还是不好写for循环中的具体移动步骤,我是暂时不知道怎么用for循环来改写汉诺塔问题的递归代码)
最简单的原型是:按照大圆盘不能覆盖小圆盘的规则,最少几步可以实现将三个圆盘从A位置移动到C位置?
(图片来自维基百科)
下面还有4个的:
在移动的过程中,我们总能发现同样的规律,每当我们需要将n个圆盘从A移动到C处时,总要将n-1个移动到B处,这样最下面那个大的才可以挪动到C处,这时候再将n-1中的n-2个移动到A处,这时第n-1个(第二大)的圆盘才又能移动到C处,只要一直重复这个操作,最后就会遇到移动最后三个的情况:(最后的操作过程总是下面这样,每次代表将对应堆上面的一个圆盘移动到对应的位置上)
A——>C
A——>B
C——>B
A——>C
B——>A
B——>C
A——>C
移动三个圆盘并不是最简单的操作,还有移动两个的,移动一个的。
如果是移动两个的,就是:
A——>B
A——>C
B——>C
移动一个就是直接A——>C,在这里可以看出一个简单的规律,移动n个汉诺塔时需要的最少操作就是2^n-1次,如何通过数学证明这个次数是正确的呢?
要想证明这个,首先得把这个问题的递推公式写出来,递推公式的意义在于明确相邻数量的圆盘在移动次数上的关系。
把移动N个圆盘的最少操作记为M(n),移动N-1个的最少操作记为M(n-1),我们从前面的探讨过程发现,要最快移动N个,就得先最快的将N-1个移动到中间的柱子B上,最后再将最大的一块移动到C柱子上,这个时候还有N-1个圆盘在中间柱子B上,这个时候在将这N-1快移动到C上,我们已经定义了其最少操作次数为M(n-1),要知道从A移动N-1个到C上,和从B移动N-1个到C上最少次数是一样的,不同在于使用的中间过度柱子不同而已,所以我们接着把N-1个圆盘移动到C上又需要M(N-1)次,所以整个过程中的总次数为
M(n)=M(n-1)+1+M(n-1)=2*M(n-1)+1
M(n)=2*M(n-1)+1就是这个问题的递推公式,那么怎么根据这个公式来求移动N个圆盘的总次数通项公式呢?
下面我给出不完全归纳法的证明:
现在已知递推式为:M(n)=2*M(n-1)+1
根据:1个的情况移动1次,2个的情况移动3次,三个的情况移动7次……
我猜测移动n个的情况需要移动2^n-1次。
显然前面的情况n=1,2,3时都成立,假设n=i时也成立,则有M(i) = 2^i-1;
当n=i+1时,则有M(i+1) = 2 * M(i)+1 ,带入假设中的M(i) = 2^i-1得M(n+1) = 2 * (2^i - 1) + 1 = 2^(i+1) - 1;
满足n+1带入猜想的式子,M(n+1) = 2^(n+1) - 1,所以得证移动n个圆盘的最少次数为2^n - 1次。
如何用代码来实现这个过程呢?
我们需要从递归函数中提取出重复的操作,现在我们做如下分析:
要移动第n个 从A到C,需要先移动n-1个从A到B,再将第n 个从A移动到C
要移动第n-1个从B到C,需要先移动n-2个从B到A,再将第n-1个从B移动到C
要移动第n-2个从A到C,需要先移动n-3个从A到B,再将第n-2个从A移动到C
要移动第n-3个从B到C,需要先移动n-4个从B到A,再将第n-3个从B移动到C
……(虽然这样想可能不对,这就反复在找迷宫的同路时从出口逆向来退入口)
注意看前面的描述,从最大的一个依次被移动到C柱子上,最后就能将所有的圆盘移动到C上,而其中重复的操作只有三步,而这三步就是需要在递归代码中实现的部分,而这个递归代码的实现网上有很多,对于我而言代码实现的过程及其难以理解,超过现在看到的很多算法,但是这些写出这样代码的人大概是因为长期看到的都是这个版本的代码,所以他们到觉得很好理解了。下面我给出Python代码的实现:
# 汉诺塔的移动步骤实现
count = 0
def move(from_where, to_where):
print(from_where, "——>", to_where)
def hanoi(A, B, C, n):
global count
count += 1 # 每次移动做一次计数
if n == 1:
move(A, C)
else:
hanoi(A, C, B, n - 1) # 将n-1 个圆盘从A 位置移动到C位置
move(A, C)
hanoi(B, A, C, n - 1) # 将剩下的圆盘从B 移动到C位置
hanoi('A', 'B', 'C', 5)
print("共移动了%d次" % count)
注意代码中的注释只分析了眼前最接近胜利(就是移动最下面一块)的情况,用心体会这个代码的神奇之处,我如果不看到别人的版本,定然是写不出这个的代码的,move()函数只是为了方便看我加进来用作打印的,如果直接写成print(A, '-->', C)的话,就更加不好理解了,感觉整个函数明明什么也没有做,就是换了下参数的位置,还有就是n值递减,怎么就实现了汉诺塔问题的求解了呢?
怎么理解这个代码呢?
这个我明天写了,一定没有你想的那么简单,除非是我智商真的跟不上了,就算我智商跟不上了,我明天也得写,因为这个是记录嘛!我觉定不再活在空白之中,所以才会来这里留下些记录!