0
点赞
收藏
分享

微信扫一扫

递归保姆级教程(1)

如果这篇文章有帮助到您还请 ‘点赞加关注’,给予因即将开学而万分失落的我最大的鼓励。

在我刷力扣的时候,发现应对一些双递归之类的题时略显笨拙,特别是双递归之间穿插一些赋值、条件判断等,有时甚至心生怯意不敢去碰难题。思来想去总结了这一篇递归文章,希望能够帮助到与我有过同等困惑的小白。

一、基本递归

先不去扯那些乱七八糟的原理,我们先把最基础的代码看懂:

案例1:累加

这里为了方便,不考虑输入非正整数报错的自我抬杠情况,只计算正整数范围,下同。

def foo(n):
if n == 1:
return 1
return n + foo(n - 1)

累加毫无疑问就是1+2+3+4+... ,假设n=3这里代码可以看作是3+2+1,注意,只有最后到了1,也就是经过多次n-1之后,我们才能得到这个函数唯一的返回值 return 1 ,这时这个递归函数的最内层便在这一句return 1结束掉,回到了上一层 return 2+foo(2-1) 即return 2+1,此时得到结果return 3结束返回最开始那层 return 3 + 3 ,最终返回结果为6.

总结:从这里可以看出,这个return 1 尤为重要,这也就是那些视频教程一开始便说的要有一个终止条件。假如我们不写这个return 1 ,必然会报错,它会告诉你递归超过最大深度。

初学者不要觉得这里简单,我们再分析一下这个错误代码,加深一下理解:

def foo1(n):
return n + foo(n - 1)

这虽然一眼看上去就是错的,但我们也要思考一下错在哪里,显然是因为无限次的foo(n - 1)超过了递归深度限制,我们可以看到这个过程中return n + ?这里一直得不到返回值。

即:我们需要一个return来 1、终止递归;2、得到最基本的返回值

当然,我们还需要一个叫做递推式的东西,为加深印象,我们案例2再做介绍。

练习1:累乘

与累加思路相同,无非是把加号换为了乘号,建议痛苦小白根据上面的理解手敲一遍,而不是根据大概印象完成这一简单操作。

def foo(n):
if n == 1:
return 1
return n * foo(n - 1)

案例2:斐波那契数列

下图来自百度百科

我们可以看到,这一组数字1、1、2、3、5、8、13...(当然有的是从0开始的,这都无所谓)

在这里,它给出了递归的另一关键因素:递推式 

F(n)= F(n-1)+F(n-2) \, \, \, \: \: \: \: \: \: \: \: \: \: \: \: (n>=2,n\epsilon N^{*})

注:回想我们前面的累加,除了return 1 之外,还有一步return n + foo(n - 1),这便是我们累加的递推式,数学写法:F(n)=n+F(n-1) 当然要求n>=1,取到0也无所谓。它负责把我们要计算的东西由大到小,由多到少,由宏观到微观,直至细分到我们的最小元素/终止条件,得到返回值后,开始由小到大,由少到多,由微观到宏观,加(减乘除)到我们最后想要的结果。

参考累加,代码如下:

def foo1(n):
if n <= 1:
return 1
return foo1(n - 1) + foo1(n - 2)

当然如果考虑什么从0开始或者取第0项而不是第一项,可以稍加改造,要学会灵活变通:

def foo2(n):
if n == 0:
return 0
if n == 1:
return 1
return foo2(n - 1) + foo2(n - 2)

运行结果如下:

for i in range(10):
print(foo3(i), end=',')
print('')
for i in range(10):
print(foo4(i), end=',')

# 1,1,2,3,5,8,13,21,34,55,
# 0,1,1,2,3,5,8,13,21,34,

练习2:杨辉三角

力扣上面有几道杨辉三角的题,不过我当时没有用递归的知识去做。递归往往都是最浪费时间和空间的办法,它往往涉及的一些子问题的重复经计算,因此用一些栈,动态规划等的知识去解决往往是最好的,且不同的要求有不同的最优解。虽然如此,我们依旧要掌握好基础。

...有点扯远了,这里我们先联系学会基本递归。

我们不妨这样定义但他的递归式子:

对于它的i行j列(i+j>=2,即i、j都是从1开始的),有:

F(i,j) = F(i-1,j-1)+F(i-1,j)

通过观察发现,当j=1 or i 时,此时结果1.

则可以直接写出递推式用来求解第i行j列的值:

def foo(i, j):
if j == 1 or j == i:
return 1
return foo(i - 1, j - 1) + foo(i - 1, j)

运行如下(这里为了直观直接打印出前6行):

for i in range(1, 7):
res = ''
for j in range(1, i + 1):
res += str(foo(i, j)) + ' '
print(res.center(20, '-'))

'''
---------1 ---------
--------1 1 --------
-------1 2 1 -------
------1 3 3 1 ------
-----1 4 6 4 1 -----
---1 5 10 10 5 1 ---
'''

总结:

到这为止都是一些单递归的简单案例,要注意的是:

  • 我们需要一个return来

        1、终止递归;2、得到最基本的返回值

  • 我们需要构造出一个递推式

        它负责把我们要计算的东西由大到小,由多到少,由宏观到微观,直至细分到我们的最小元素/终止条件,得到返回值后,开始由小到大,由少到多,由微观到宏观,加(减乘除)到我们最后想要的结果。

二、双递归

注:基本递归双递归什么的名字都是我自己起的,别当真。

复习:我们先随便写个递归,并无多大意义,你能看出它最终输出什么吗?

def foo(n):
if n > 0:
print(n)
return foo(n - 1)

print(foo(3))

实际上这个代码在输出3 2 1之后便会报错,因为我们缺少了终止条件,print不是return。终止条件要和return配合在一起使用保证递归能够终止。

这里我们修改一下,

def foo(n):
if n > 0:
print(n)
return foo(n - 1)


print(foo(3))

'''
3
2
1
None
'''

双递归:

注意:这里开始难度可能提高一些了,请认真思考。

先看代码思考一下三种情况有什么不同,想想结果再看答案。

情况1、2、3:

def foo(n):
if n > 0:
print(n)
foo(n - 1)
foo(n - 1)

def foo(n):
if n > 0:
foo(n - 1)
print(n)
foo(n - 1)

def foo(n):
if n > 0:
foo(n - 1)
foo(n - 1)
print(n)

首先说明一点,这里我们并没有进行加减乘除等的计算,因此没有return这类的东西,为方便起见,我们只研究双递归时,函数内部都发生了什么。

如果刚刚n=2时你带进去简单算了一下,不妨通过这一次尝试猜测一下规律,为了让自己易于接受,哪怕称不上规律的东西我们也要把它叫做规律当成自己发现的东西。

可能有人急了,别急。

对于情况1:

(忽略None 下同)

我们可以看到,作用于双递归之前,最大的数都靠前,由于我们递归是在1终止,因此就有211,比2大的数是3,我们就有3211+211->3211211;比3大的数是4,我们就有43211211+3211211->432112113211211...

 对于情况2:

我们可以看到,作用于双递归之间,最大的数都在中间,由于我们递归是在1终止,因此就有121,比2大的数是3,我们就有121 3 121,4的时候就有 1213121 4 1213121...

对于情况3:

我们可以看到,作用于双递归之后,最大的数都在最后,由于我们递归是在1终止,因此就有112,比2大的数是3,我们就有112112 3,4的时候就有 11211231121123 4...

此时,你觉得你好像懂了,又好像没懂。没关系。

我们分析了微观(即一个个数带进去跟着程序走),刚刚又分析了宏观,您再思考一下您懂没懂。

其实我们可以看到,无论前中后打印,它所打印出来的结果都是由上一次打印出的结果组成的,即最后由终止条件为最小单元,大部分的常规递归会重复执行子问题很多次无论是单次递归还是多次递归,这便是微观。而若把上一次的结果当作一个整体,递归仿佛又只执行了一轮或者说是某几轮,这便是宏观。宏观离不开微观,微观也会自动组成宏观,难点是我们如何灵运用。

微观:

我们把函数体内的两个函数命名为递归式1和递归式2,以其中一个为例:

def foo(n):
if n > 0:
foo(n - 1) # 递归式1
print(n)
foo(n - 1) # 递归式2

该函数每次进入函数体,先判断 n是否大于0,是的话向下执行递归式1到最大深度(即触发递归终止条件),此时从最后一层向外释放,最后一层的递归式1结束任务,执行倒数第二层的print()函数,打印出数字1,之后倒数第二层的递归式2深入最后一层,发现还是不符合n>0,此时最后一层结束、倒数第二层结束、倒数第三层的递归式1结束,打印出1之前的数字2... 由此有了1,有了121,有了1213121......
 

宏观:

我们这里一直用的都是print(),其实这个print完全可以换成一些其他的东西,如给一个列表赋值、打印出数字变化顺序...,我们可以根据具体要求,结合上面三种情况的结果特征,直接拿来用。

这对于初学者可能很难,难于把握住熟练运用的精髓。下面是一些实例,供大家参考。

案例1:老生常谈,汉诺塔问题:

python解决汉诺塔问题_suic009的博客-CSDN博客_汉诺塔问题python

案例2:快速排序

import random

#b,e是列表的范围,left,right是‘指针’每次移动到的位置 left=right时停止循环进入下一轮递归
def quick_sort(li, b, e):
if b < e: #结束递归的条件
left = b
right = e
key = li[b]
while left < right: # 这里不能写等于 要靠left=right结束
while key <= li[right] and left < right: #and 前等于号加于不加 都会分开处理(建议加上,少执行一步交换/赋值)
right -= 1
if left < right:
li[left] = li[right]
left += 1 #减少以后循环次数

while key >= li[left] and left < right:
left += 1
if left < right:
li[right] = li[left]
right -= 1 #减少以后循环次数

li[left] = key #大while执行完毕 此时left = right,中间的位置知道了,是left。
quick_sort(li, b, left - 1)
quick_sort(li, left + 1, e)


li = [random.randint(1, 9) for i in range(10)]
print(li)
quick_sort(li, 0, len(li) - 1)
print(li)

其它排序算法请见:

十大排序算法_suic009的博客-CSDN博客

案例3:二叉树的前中后序遍历(只看看递归遍历即可):

class BST:
# 略

# 前序遍历
def pre_order(self, root):
if root:
print(root.data, end=' ')
self.pre_order(root.lchild)
self.pre_order(root.rchild)

# 中序遍历
def in_order(self, root):
if root:
self.in_order(root.lchild)
print(root.data, end=' ')
self.in_order(root.rchild)


# 后序遍历
def post_order(self, root):
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=' ')

# 略

二叉树代码详情见下方链接,点击文章目录里的二叉树即可。

初学数据结构(偏向python用户)_suic009的博客-CSDN博客

举报

相关推荐

0 条评论