递归Recursion总结_python

发布时间:2023-01-21 19:30

初步感受

我们先来初步实现一下阶乘,也就是 n! = n*(n-1)*(n-2)…*1。0的阶乘为1。我们能想到一般的做法,就是用一个for循环,从第一个数乘到最后一个:

#big-o(n)
def f2(n):
    if n == 0:
        return 1
    sum = 1
    for i in range(1,n+1):
        sum *= i
    return sum

返回结果:

f2(3)
6

现在我们这样考虑,如果我们n的阶乘n!,要求(n+1)!,只要乘上n+1就可以了。也就是说:f(n+1)与f(n)始终存在着某种联系。这和我们高中学习的数学归纳法非常相似。
把关系一步一步列出来:

f(n)    =   n       *     f(n-1)
f(n-1) =   (n-1)    *     f(n) 
.............
f(1)    =   1       *     f(0)
f(0)    =   1  (Base)

这个时候需要停止,如果不停止就会进入死循环。所以,我们应当有一个base,这个base决定了程序在哪里停止。这个问题里,我们就要写一个遇到f(0)时返回。这就是递归的意思了:一写base二写递归, 程序如下

#big-o(n)
def f(n):
    if n == 0:
        return 1
    return n * f(n-1)  #big-o(1) all n 

那递归和for循环相比有什么异同点,对于for循环,我们记一次计算为o(1),for就是把所有数遍历一遍,做乘法运算,时间复杂度为big-o(n);对于递归,每次return n*f(n-1) 这是一次乘法计算,递归n次,时间复杂度也为big-o(n)。

f(3000) 
RecursionError: maximum recursion depth exceeded in comparison

如果我们用递归的方式计算3000的阶乘,会出现这样的问题,显示超出递归深度的错误。
这是因为,每次递归调用的时候都会在栈里面创建一个新的地址空间,而最大递归深度一般是1000左右。
这样一看,好像并没有什么优势,但在很多地方,递归有着他独到的地方,我们一一来看。

斐波拉契数列

斐波拉契数列:1 1 2 3 5 8 13 21…每个数都是前俩个数的和。
我们能立刻找到递归关系: f(n) = f(n-1) + f(n-2)
但前提是n>= 3, f(3) = f(2) + f(1),再往前走就出现错误了。
我们在这就要写base了:如果n = 1或n = 2,就返回1,其余n就直接递归,程序就出来了:

def fibonacci1(n):
	assert(n>=0)
	if(n <= 2):
		return 1
	return fibonacci1(n-1) + fibonacci1(n-2) 

运行结果:

fibonacci1(6)
8

我们来运行一下第40个数:

time fibonacci1(40)
Wall time: 21.8 s

一共花掉了21.8s,这肯定是不能够被允许的。我们考虑这种递归方式的复杂度:
递归Recursion总结_python_第1张图片
每次递归都用到了前两次的递归结果,就像一棵树一样每根枝向下伸出两根新枝,直到最后一根枝f(1),最下面一行的f(1)出现了2^ n次。 这样来看,时间复杂度为2^n。

如果我们用for循环来做,把每俩个看成一组,1 1 2 3 5 8 13 21…
(1,1)(1,2)(2,3)(3,5)(5,8)(8,13)(13,21)…
我们记每一组的俩个数分别为a,b。后一组的b是前一组的a+b,a是前一组的b。程序输入我们想要的第n个数,就要做n-1次循环,程序如下:

def fibonacci2(n):
	a = 1
	b = 1
	for i in range(n-1):
		a, b = b, a + b
	return a 

简化一下程序,按照这个规律往前推一位:(0,1) (1,1) (1,2) (2,3)…
求第n位就是进行n次循环

def fibonacci3(n):
	a = 0
	b = 1
	for i in range(n):
		a, b = b, a + b
	return  a


仍然选择用递归来做,在解答①当中一次递归调用了前两次结果。所以可以尝试每次递归只调用一次结果,这里用到②的思想,我们去返回一个组。
思路:把 (1,1)(1,2)(2,3)(3,5)(5,8)(8,13)这样每一个组看作递归的结果,每次调用一个组。

def fibonacci4(n):
	if n == 1:
		return (1,1)
	a, b = fibonacci4(n-1)
	return (b, a + b)

返回结果

fibonacci4(4)
(3, 5)
fibonacci5(5)
(5, 8)

这样我们对结果取出第一个数就可以了。

打印尺子

1

1 2 1

1 2 1 3 1 2 1

1 2 1 3 1 4 1 3 1 2 1

我们观察f(n)和f(n+1)的关系。可以发现 f(n+1) 是这样摆置的: f(n) n f(n)。
思路就有了:一先写base,当n值降到1时,就return 1 。二写递归

def ruler_bad(n):
	if n == 1:
		return '1'
	return ruler_bad(n-1) + ' ' + str(n) + ' ' + ruler_bad(n-1)
ruler_bad(3)
'1 2 1 3 1 2 1'

结果正确,但机智的人就反映过来了,这里每次递归调用了两次ruler_bad(n-1)呀,和上一题fibonacci1出现了同样的问题。再仔细看一眼,还是有点不一样,这里两次调用的都是同一个东西! 那我们就可以做一个小correct:

def ruler_bad(n):
	if n == 1:
		return '1'
	t = ruler_bad(n-1)
	return t + ' ' + str(n) + ' ' + t

把前一次调用赋值给t,相当于只调用了一次,时间复杂度降为o(n), 完美!

下面我们开始画尺子:

def draw_line2(n, mark=''):
    if mark:
        print('-'* n + mark)
    else:
        print('-' * n)
    
def draw_interval2(n):
    if (n == 0):
        return
    draw_interval(n-1)
    draw_line(n)
    draw_interval(n-1)
    
def draw_rule2(n,length):
    for i in range(length):
        draw_line(n,str(i))
        draw_interval(n)
    draw_line(n,str(length))

结果:

draw_rule2(3, 2)
---0
-
--
-
---
-
--
-
---1
-
--
-
---
-
--
-
---2

数字表达式

给定a<=b ,a只能做乘2或加1的操作,找到最小操作顺序使得a=b
23 = ((5 * 2+1) * 2+1)
解决这个问题需要逆向思维,不是考虑a怎么变到b,而是考虑b怎么降到a更容易。
我们首先想到的就是,把b和2倍的a相比,如果>2a就除2,如果小于2a,那么b只要一直减去1就行了。
值得注意的是,因为可能要除2,所以在与2a相比较之前,把b变成一个偶数。
如果b本来就是偶数,不变。如果b本来是奇数,对b-1,最后结果再加一就可以了。

写程序之前我们举个例子:a = 5,b = 23
23为奇数-> 23 - 1 = 22
22 > 2*5 -> 22 / 2 = 11
11为奇数-> 11 - 1 = 10
10/2 = 5 =a 返回a
第一步,写base,这道题的base就是b降到和a一样大的时候,返回a。第二步按先奇偶再比较的顺序写递归:

def intseq(a, b):
    if a == b:
        return str(a)
    if b % 2 == 1:
        return '(' + intseq(a, b-1) + '+1)'
    
    if b >= 2 * a:
        return  intseq(a, b//2) + '*2'
    if b < 2 * a:
        return '(' + intseq(a, b-1) + '+1)'

结果:

a = 11
b = 23
print(str(b) + '=' + intseq(a,b))
23=(11*2+1)

套圈——汉诺塔问题

递归Recursion总结_python_第2张图片
一共三个杆子,左中右分别记为start,by,end。一开始第一个杆子上套了n个圈,只能从小到大排。现在要把这些圈从小到大放到最右边的杆子上,每次只能移动一个杆子,求最小路径。

我们以3个圈为例,红->end, 绿->by, 红->by,蓝->end, 红->start, 绿->end, 红->end
一共花费了7次 。可以感受到,我们是把红绿这俩个摆到中间,蓝色摆到末位,最后把红绿摆过去,这就是递归的意味了。

假设现在有n+1个圈子,那我们就可以先把上面的n个圈子,摆到中间,第n+1个摆到末位,最后再把前n个摆过来,就可以了。

同样的道理,先写base再写递归。这里的base,就是n降为1的时候
我们定义函数hanoi(n, start, by, end):

def hanoi(n, start, end, by):
    if n == 1:
        print("start from " + start + " to " + end)
    else:
        hanoi(n-1, start, by, end)
        hanoi(1, start, end, by)
        hanoi(n-1, by, end, start)

结果

hanoi(3, "start", "end", "by")
start from start to end
start from start to by
start from end to by
start from start to end
start from by to start
start from by to end
start from start to end

打印子集Subset

举个例子:求出{1,2,3}的所有子集.
一一列出:[] [1] [2] [3] [1,2] [1,3] [2,3] [1,2,3]

递归Recursion总结_python_第3张图片
思路1:首先定义一个空数组,复制并我们append原集的第一个数,得到[]和[1]。第二步,复制上一步的[]和[1]并append原集的第二个数,以此类推。

def Subset(nums):
	result = [[]]
	for num in nums:
		for element in result[:]:
			x = element[:]
			x.append(num)
			result.append(x)
	return result

这里要注意程序编写时,第一处result要copy一份result[:],否则会陷入死循环,这是因为我们在第二个for循环里就是对result做增添元素操作,增添之后又会遍历到这个元素。比如第一步加入[1],element只等于[],result增添元素之后变成了[[],[1]],此时应当循环结束了。但是,程序返回来又去遍历result,element就变成了新加进去的[1],如此循环没有尽头。
第二步,result里的元素element也要copy,如果没有copy,就会出现这样的结果:

[[1, 2, 2, 3, 3, 3, 3],
 [1, 2, 2, 3, 3, 3, 3],
 [1, 2, 2, 3, 3, 3, 3],
 [1, 2, 2, 3, 3, 3, 3],
 [1, 2, 2, 3, 3, 3, 3],
 [1, 2, 2, 3, 3, 3, 3],
 [1, 2, 2, 3, 3, 3, 3],
 [1, 2, 2, 3, 3, 3, 3]]

每个返回的元素都是一样的,这是因为上一次append后的element会继承到第二次循环里。比如result里现在是[]和[1],element第一次取[],加入2,element变成[2],result里也加入[2],第二次遍历element=[1]时,再加入2,本应该是[1,2],这个时候却变成了[1,2,2],上一次循环留下来的2没有清除掉,就出现了这样的错误,把握细节很重要。

思路2:

递归Recursion总结_python_第4张图片
从第一个数a开始向下走,保留或继续选第二个数,首先得到a,ab,ac。对于ab,往下走得到ab或abc,ac往下走还是ac,abc往下走也还是abc,不能再往下走后原路返回,搜寻这个路径下的其他可能情况,一直到第一个数b开始。

def subsets_recursive(nums):
    lst = []
    result = []
    subsets_recursive_helper(result, lst, nums, 0);
    return result 
  
def subsets_recursive_helper(result, lst, nums, pos):
    result.append(lst[:])
    for i in range(pos, len(nums)):
        lst.append(nums[i])
        subsets_recursive_helper(result, lst, nums, i+1)
        lst.pop()
nums = ['a','b','c']
subsets_recursive(nums)
[[], ['a'], ['a', 'b'], ['a', 'b', 'c'], ['a', 'c'], ['b'], ['b', 'c'], ['c']]

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号