好的,让我来试试能不能用尽量容易理解的方式讲一讲递归。
我会举几个例子,可以自己挑着看,如果你觉得哪一个例子,更容易理解,可以评论里跟我说一声。
我们分下面几部分:基础知识
递归的写法
f(n) = f(n-1) +1递归函数举例
倒序输出正整数
二叉树的递归查找举例
递归习题
习题部分有问题的可以在评论区评论。
基础知识
要理解递归需要的基础知识只有一点:在你调用了一个函数之后,函数会从内存中开拓出一个新的地方,来保存当前函数的参数变量,以及你在这个函数里面声明的变量。即使是自己调用自己。
看下面的例子:
def func(depth):
if depth == 2:
print("当前深度为2,即将返回到 func(1) ")
return
print("当前深度为:%d" % depth)
func(2)
print("从 func(2) 中返回到 func(1),当前深度为%d" %depth)
调用 func(1) 输出为:
当前深度为: 1
当前深度为2,即将返回到 func(1)
从 func(2) 中返回,当前深度为 1
这段代码做的事情很简单,我们调用 func(1) 然后 func(1) 又自己调用了 func(2) 。我们可以在看到在 func(1) 中 depth 值为1,虽然在进入 func(2) 后,depth 为 2,但是从 func(2) 中返回后,depth 值仍为1。也就是说,func(1) 中的 depth 变量跟 func(2) 中的 depth 变量是不同的两个变量。
我们用调用图表示是这样子的:
函数内的局部变量也是一样道理的,func(1)中的局部变量是属于func(1)的,与func(2)的无关。这些都是函数部分的基础知识。
递归的写法
递归函数,其实就是自己调用自己的函数。这是递归函数的第一要素。
即然递归函数是自己调用自己的函数,那样子不就死循环了吗?为了防止造成死循环,我们就需要有一终止递归的条件,我们需要知道在什么情况下,递归函数不再自己调用自己,这个终止递归的条件,就是递归函数的第二要素。
所以,其实写递归函数,只需要确定以下两个要素就可以了:终止递归的条件
非终止递归条件下,自己调用自己
一般情况下,我们写递归条件,直接两步走就行了:先判断是否处于终止递归,是则返回
做一些处理,然后自己调用自己
其实,我们上面基础知识中的代码,就已经用到了递归。我们就拿这部分代码来举个例子:
def func(depth):
# 这是第一部分,判断终止递归的条件(深度为2时,终止递归,不再自己调用自己)
if depth == 2:
print("当前深度为2,即将返回到 func(1) ")
return # 直接返回,跳出当前的函数
# 这是第二部分,做一些处理(这里是打印depth的值),然后自己调用自己
print("当前深度为:%d" % depth)
func(2) # 自己调用自己
print("从 func(2) 中返回到 func(1),当前深度为%d" %depth)
f(n) = f(n-1) +1 递归函数举例
这种函数,我们经常在数学中看到:
f(n) = f(n-1) + 1
f(1) = 1
其实数学中的这种函数就是递归函数:
f(1) = 1 # 这是递归的终止条件
f(n) = f(n-1) + 1 # 这是第二部分,自己调用自己的部分
所以我们写成代码是这样子写的:先判断递归的终止条件:
def f(n):
if n == 1:
return 1 # 我们知道 f(1) 的值是1,不需要通过调用 f(0) + 1 来确定 f(1) 的值,我们直接返回1就行了
2. 现在,写第二部分:
def f(n):
# 这是第一部分
if n == 1:
return 1 # 我们知道 f(1) 的值是1,不需要通过调用 f(0) + 1 来确定 f(1) 的值,我们直接返回1就行了
# 这是第二部分
return f(n - 1) + 1 # 调用 f(n-1)取得 f(n-1) 的值,加1计算得到 (fn) 的值并返回
调用 f(3) 输出如下:
>>> f(3)
3
整个过程的调用如下:
正序与倒序输出正整数
假设我们有一个正整数 2019 我们要求正序与倒序输出其中各位的数,如:
# 正序输出,每行一个
2
0
1
9
# 倒序输出,每行一个
9
1
0
2
我们想到的算法是,我们先用求余10的方法,取得它的个位数:例如2019 % 10 = 9
使用除以10的方法,取得剩下还没有输出的的整数:2019 / 10 = 201
整个过程应该是:
# 2019
2019 % 10 = 9 => 输出 9
2019 / 10 = 201
# 201
201 % 10 = 1 => 输出 1
201 / 10 = 20
# 20
20 % 10 = 0 => 输出 0
20 / 10 = 2
# 2
2 % 10 = 2 => 输出 2
2 / 10 = 0
# 0
0 => 直接返回,终止。
我们可以看到,这是一个倒序输出的算法。
我们的递归函数终止条件是什么:如果当前值等于0,那么就终止返回
而我们的递归函数的第二部分是什么?我们看上面的算法可以看到,我们一直重复做的除以10,求余10其实就是我们的递归函数的第二部分:
def reverse_order(n):
# 第一部分,递归终止条件判断
if n == 0:
return
# 第二部分
print(n % 10) # 打印 n % 10 的值,也就是它的个位数
reverse_order(int(n / 10)) # 调用自己打印剩下的还没有输出的整数
运行结果示例:
>>> reverse_order(2019)
9
1
0
2
调用图如下:
那么怎么进行正序打印呢?
其实很简单,相对于倒序打印来说我们进行的操作是:
先打印出 2019 % 10 = 9
调用 reverse_order(201) 打印剩下的正整数 201
我们只要调换一下这两个操作的顺序就行了:
先调用 reverse_order(201) 来打印 201
201打印过了,现在我打印出 2019 % 10 = 9
代码如下:
def order(n):
# 第一部分,递归终止条件判断
if n == 0:
return
# 第二部分
order(int(n / 10)) # 先打印 n / 10 部分
print(n % 10) # 再打印 n % 10 的值,也就是它的个位数
结果示例如下:
>>> order(2019)
2
0
1
9
调用图如下:
所以现在大家应该可以理解调用自己后,再处理(打印),与先处理(打印)后再调用自己所产生的区别了吗?
二叉树的递归查找举例
接下来我们举一个与数据结构结合的例子:
假设,我们有一个结点:我是一个小结点
我们可以给这个结点添加一个左结点,但是左结点必须小于这个结点:在我左边的是比我小的
我们也可以给这个结点添加一个右结点,但是右结点必须大于这个结点:在我右边的是比我大的
这种拓扑结构就是树结构。在这里,我们做了“左小右大”的规则限定。
假设当前结点为结点12:根结点为12的树
我们面临的问题是:当前结点不是我们目标结点(结点75),请在当前结点的左子树或右子树中,查找值为75的结点。
根据左小右大,我们可以知道因为目标结点值75大于当前结点的12,所以我们应该去 当前结点.右子树 中去找目标结点75:
def 查找结点(当前结点, 目标结点值): # 当前结点为 结点12, 目标结点值为75
if 目标结点值 > 当前结点值: # 75 > 12,去右边找
查找结点(当前结点.右子树, 目标结点值) # 即 查找结点(结点12.右子树, 75),查到后,把结果返回
走到根结点12的右边来
当我们在 350 结点处时,我们面临着与在根结点12时,一样的问题:当前结点不是我们目标结点,请在当前结点的左子树或右子树中,查找值为75的结点。
因为75小于350,所以我们要去 当前结点.左子树 去找结点75:
def 查找结点(当前结点, 目标结点值): # 当前结点为 结点350, 目标结点值为75
if 目标结点值 < 当前结点值: # 75 < 350 要去左子树查找
return 查找结点(当前结点.左子树, 目标结点值) # 即查找结点(结点350.左子树,目标结点值75),查到后,把结果返回走到结点50处
现在,我们又面临着跟之前一样的问题:当前结点不是我们目标结点,请在当前结点的左子树或右子树中,查找值为75的结点。
因为75大于50,所以要去 当前结点.右子树 中找:
def 查找结点(当前结点, 目标结点值): # 当前结点为结点50, 目标结点值为75
if 目标结点值 > 当前结点值: # 75 > 50,去右边找
return 查找结点(当前结点.右子树, 目标结点值) # 即 查找结点(结点50.右子树, 75),查到后,把结果返回找到啦!
当我们走到结点50的右子树中去时,发现当前结点的值就是75,就是我们要查找的值,我们就可以返回这个结点了,不用再往下去了。
def 查找结点(当前结点, 目标结点值): # 当前结点为结点75, 目标结点值为75
if 目标结点值 == 当前结点值: # 5= == 50,找到啦!
return 当前结点
现在我们可以知道,这就是第一步递归终止条件:如果当前结点是我们要找的结点,直接返回
在这里我们不考虑不存在的条件,如果考虑的话,递归终止条件再加一个就是了:如果当前结点不是我们要找的结点,可是它已经没有子树了,直接返回 None,表示没有找到我们要的结点
如果当前结点就是我们要找的结点,直接返回当前结点
而之前一直面临的同一个问题,其实就是递归函数第二部分的代码。我们把之前的代码合并在一起就完成了这个递归函数的代码(还是按两步走写):
def 查找结点(当前结点, 目标结点值):
# 这是第一部分,递归函数的终止条件判断
if 目标结点值 == 当前结点值:
return 当前结点 # 如果找到了直接返回
# 这是第二部分,根据情况调用自己
if 目标结点值 > 当前结点值:
return 查找结点(当前结点.右子树, 结点值)
elif 目标结点值 < 当前结点值:
return 查找结点(当前结点.左子树, 结点值)
(不知道为啥,举了这么个例子,就直接上伪代码吧,大家看得懂就行 ==)
递归习题
最经典的递归例题,斐波那契数列:
f(n) = f(n-1) + f(n-2)
f(1) = 1
f(2) = 1
写一个递归函数 f(n) 打印出斐波那契数列的第 n 位。
(斐波那契数列从第1位起为:1、1、2、3、5、8、13……)
汉诺塔问题:
(不想打了,我直接拉百科的描述过来了==)
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如下图)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
写一个递归函数,f(n) 输出应该怎么操作才可以把 n 个盘子从 A杆转移到 C 杆上。
欧几里德算法求最大公约数
假如需要求 1997 和 615 两个正整数的最大公约数,用欧几里德算法,是这样进行的:
1997 / 615 = 3 (余 152)
615 / 152 = 4(余7)
152 / 7 = 21(余5)
7 / 5 = 1 (余2)
5 / 2 = 2 (余1)
2 / 1 = 2 (余0)
至此,最大公约数为1
以除数和余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数,所以就得出了 1997 和 615 的最大公约数 1。
写一个递归函数,f(n, m) 求得 n, m 的最大公约数。
想不到了,大家有什么好的例题可以评论里讲一下。