算法:递归知识及题目整理-python

递归

递归就是重复调用,直至达到base的情况。想法和数学归纳法很像。值得注意的是不同写法会有不一样的时间复杂读。比如计算斐波那契数列,如果按照公式 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n1)+f(n2)直接这么写,时间代价就很大。

万门课程第一部分例题

斐波那契数列

最基本的递归

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)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值