递归
递归就是重复调用,直至达到base的情况。想法和数学归纳法很像。值得注意的是不同写法会有不一样的时间复杂读。比如计算斐波那契数列,如果按照公式 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n−1)+f(n−2)直接这么写,时间代价就很大。
万门课程第一部分例题
斐波那契数列
最基本的递归
def f(n):
assert(n>=0)
result = [0, 1]
for i in range(2, n+1):
result.append(result[-2] + result[-1])
return result
数学表达式
23 = ((5 * 2 + 1) * 2 + 1)
113 = ((((11 + 1) + 1) + 1) * 2 * 2 * 2 + 1)
学习:base与return
def draweq(a,b):
if a == b:
return str(a)
if b % 2 == 1:
return '(' + draweq(a,b-1) + ' + 1' +')'
if b < 2*a:
return '(' + draweq(a,b-1) + ' + 1' + ')'
return draweq(a,b/2) + ' * 2'
这个关键在于,想清楚base是什么,b>=a,所以base是b = a
其次,我要返回什么,因为是打印表达式,而且base的情况是个字符串,实际每次就是要返回一个表达式,缺的部分就是括号,还有+1 与*2。要做的就是根据不同情况,输出相应的表达式。这就是一个嵌套的过程。
打印尺子
学习:一点点工程的感觉
打印尺子:需要画刻度,画刻度需要画线。因此,需要不同的函数。
def draw_line(length,label =''):
line = '-'*length
if label:
line += ' '+ label
print(line)
def draw_intervals(length):
if length > 0:
draw_intervals(length-1)
draw_line(length)
draw_intervals(length-1)
def draw_ruler(inches,length):
draw_line(length,'0')
for i in range(1,inches+1):
draw_intervals(length-1)
draw_line(length,str(i))
递归的运用在画刻度上
汉诺塔
数学归纳法的感觉。n=1,只要从start移动到end。假设n=k时,我知道做法。n=k+1时,将 k部分移动到by,再讲剩下的1移动到end,最后将k从by移动到end。
对应:base就是n=1的情况,其他就是我要递归的部分。
def hano(n,start,by,end):
if n == 1:
print('move from',start,'to',end)
else:
hano(n-1,start,end,by)
hano(1,start,by,end)
hano(n-1,by,start,end)
万门课程第二部分例题
1.集合的子集
遍历的做法
def subsets(nums):
result = [[]]
for num in nums:
for element in list(result):
x = list(element)
x.append(num)
result.append(x)
print(result)
return result
def subsets_2(nums):
res = [[]]
for num in nums:
res += [ i + [num] for i in res]
print(res)
return res
这边有一点要注意,for循环直接用result行不行?
打印结果看看
---- i = 1
result = [[]]
---- i = 2
result = [[], [1]]
---- i = 3
result = [[], [1], [2], [1, 2]]
final :[[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
直接用result不行,这边list(result) or result[:] 都是一种列表拷贝。
第一个for循环,每次拷贝一份result,对其中的每个元素添加一个新元素。这里element也要拷贝(若直接x = element,其实x\element指向的是同一个数组,就会改变原来已经定好的集合。
递归写法
这边用到了回溯的思想。
回溯中比较关键的两点:
- 哪些访问过?
- 要维护什么状态?
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]
1.result = [ [] ],注意这里是拷贝。
2.开始循环0_0,对于 i = 0
3.lst->[a] -> 进入第一次递归
4.result = [ [] , [a] ]
5.进入循环1_0,start1 = 1, lst -> [a,b] -> 进入第二次递归
6.result = [ [] , [a] , [a,b] ]
7. 进入循环 2_0,start2 = 2 ,lst -> [a,b,c] -> 进入第三次递归
8. result = [ [] , [a] , [a,b] , [a,b,c] ]
9. 【回溯】没东西循环了,对于第三次递归,lst.pop() 弹出 c ,->lst = [a,b],结束第三次递归。从[a,b,c]回到了 原来状态[a,b]
10. 10.【回溯】. 回到第二次递归,lst.pop()弹出b,循环2结束, 第二次 递归结束,lst = [a]。此时,[a,b]回到了 原来状态[a]
11. 进入循环1_1, start1 + 1 = 2, lst -> [a,c] -> 进入第四次递归
12. result = [ [] , [a] , [a,b] , [a,b,c] ,[a,c]]
13. 【回溯】没东西循环了,lst.pop()弹出c,第四次递归结束, lst = [a]。此时从[a,c]回到了原来状态[a]
14. 【回溯】对循环1_1,lst.pop()弹出a, lst = [],循环1结束,第一次递归结束。此时从[a]回到了原来状态[]
15. 截止,完成了循环0的第一个循环,总共做了四次递归。
16. 进入循环0_1,对于 i =1, lst-> [b],开始第五次递归。。。。后续同
2.排列
排列中有几个子问题:
- 不重复元素全排列
- 重复元素全排列
- 元素为k的子集全排列
利用回溯法全部解决:(模仿子集的做法)
def perm(nums,k):
result = []
lst = []
n = len(nums)
nums.sort()
perm_helper(result,nums,lst,k)
return result
def perm_helper(result,nums,lst,k):
for i in range(len(nums)):
if i > 0 and nums[i] == nums[i-1]:
continue
lst.append(nums[i])
print(lst)
perm_helper(result,nums[0:i]+nums[i+1:],lst,k)
lst.pop()
if len(lst) == k:
result.append(lst[:])
print('res =', result)
和子集差在哪?
区别在于我什么时候更新维护的结果数组(result)。子集中,我每次都加入新的,然后缩短。在排列中,我是只要lst达到指定长度,才添加;以及,怎么样减小原数组。子集中:则利用Pos,不断前移,缩小原数组;而在排列中,则是去除已经添加过的元素。
剪枝写法要注意的是排序后剪枝才有意义。
以[1,2,3]为例,将lst打印出来
[1]
[1, 2] 、[1, 2, 3]
[1, 3]、[1, 3, 2]
[2]
[2, 1]、[2, 1, 3]
[2, 3]、[2, 3, 1]
[3]
[3, 1]、[3, 1, 2]
[3, 2]、[3, 2, 1]
这样就很清看出晰回溯发生在什么地方
基于交换的思想(个人做法)
(可以剪枝解决重复的情况,K子集排列还没想好)
思想:什么是排列,排列是不断交换位置。比如【1,2,3,4】,理解为【1,2,3,4 】、【2,1,3,4】、【3, 2,1,4】、【4,2,3,1】,这是第一个位置与其他3个位置交换。然后,再看【2,3,4】,同样也可以进行【3,2,4】、【4,3,2】。。。以此类推,就可以实现全排列。
代码如下:
def perm1(nums):
nums.sort()
num = [nums]
for i in range(len(nums)):
num = swap(num,i)
return num
def swap(num,k):
for lis in num[:]:
n = len(lis)
for i in range(k+1,n):
x = lis[:]
if x[i-1]==x[i] :
continue
x[k],x[i] = x[i],x[k]
num.append(x)
return num
swap函数是用于将k位置的元素与后面元素进行交换。然后排列就是将k从0到n-1进行遍历,即可得到全排列。
剪枝则是对于前后两个一样的元素,就不需要与k都进行交换,不然会造成重复。
参考解法:
再来看看万门课件中给的解法
def permUnique(result, nums):
nums.sort()
if (len(nums)==0):
print(result)
for i in range(len(nums)):
if (i != 0 and nums[i] == nums[i-1]):
continue;
permUnique(result+str(nums[i]), nums[0:i]+nums[i+1:])
这边,想想为什么len(nums)==0后,循环还能进行。注意到递归中的nums是nums[0:i]+nums[i+1:],这个切片实际是一种复制(浅拷贝),实际最初的nums长度是不变的。
3.和为K的所有子集
还是用子集的模板,只是何时append?达到目标和append。
有两种要求:
- a.允许同一元素出现多次
- b.同一个元素只能使用一次
先看b吧
def comb(nums,target):
result = []
lst = []
nums.sort()
helper(result,lst,nums,target)
return result
def helper(result,lst,nums,remain):
if remains < 0: return
if remain == 0 :
result.append(lst[:])
for i in range(len(nums)):
if i > 0 and nums[i] == nums[i-1]:
continue
lst.append(nums[i])
helper(result,lst,nums[i+1:],remain-nums[i])
lst.pop()
多维护了一个状态,remain。当remain为0时,加入到result中。remain < 0直接return 开始回溯
同样注意重复元素的剪枝。
再看a:怎么样才能让同一元素使用多次,又不会造成重复。
首先要排序,其次确保使用过的不再使用。课程解答对于第二点是再用一个start来保证,其实只要稍作改动就好。
def comb1(nums,target):
result = []
lst = []
nums.sort()
helper(result,lst,nums,target)
return result
def helper(result,lst,nums,remain):
if remains < 0: return
if remain == 0 :
result.append(lst[:])
for i in range(len(nums)):
if i > 0 and nums[i] == nums[i-1]:
continue
lst.append(nums[i])
helper(result,lst,nums[i:],remain-nums[i])
lst.pop()
只需将循环中的nums[i+1:]变更为nums[i:]也就是每次还是从自身开始循环,直到remain<=0为止。
4.括号的正确组合
所谓正确的组合就是 ) ( 这样是不行的。
按照子集模板的做法
def kuohao(n):
result = []
lst = []
nums = [0 if i<=n else 1 for i in range(1,2*n+1) ] #编码
helper_k(result,lst,nums,n)
return result
def helper_k(result,lst,nums,n):
if len(lst) == 2*n:
result.append(trans(lst[:],n)) #解码
for i in range(len(nums)):
if count(nums,n) or( i>0 and nums[i-1]==nums[i]): #剪枝
continue
lst.append(nums[i])
helper_k(result,lst,nums[:i]+nums[i+1:],n)
lst.pop()
def count(nums,n): #剪枝约束函数
l = len([i for i in nums if i == 0])
r = len([i for i in nums if i == 1])
return True if l > r else False
def trans(lst,n): #解码函数
for i,num in enumerate(lst[:]):
lst[i] = '(' if num == 0 else ')'
return ''.join(lst)
想法比较简单,括号无非 ( 与),看成0与1。即有n个0与n个1.我要做的无非是找到0与1的排列。有两个约束:没有重复的排列,对于每个排列,从左往右,0的个数要大于等于1的个数。
做法:编码—>正常全排列写法—>剪枝—>解码。
课程做法:
def generateParenthesis(n):
def generate(prefix, left, right, parens=[]):
if right == 0: parens.append(prefix)
if left > 0: generate(prefix + '(', left-1, right)
if right > left: generate(prefix + ')', left, right-1)
return parens
return generate('', n, n)