前言
最近在读一本叫做《算法图解》的书,很有意思,将很多算法解析的非常简洁明了,比算法导论不知道高到哪里去了(不是)
这里简单记录一下看书过程中的相关思考。
关于递归
关于递归,有一个很好的名词可以解释,那就是 “套娃”,例如,当我们在一个大箱子中找钥匙时,箱子中还存在其他的小箱子,而小箱子里面还可能有箱子,这时,我们仅需要按照以下三步执行:
- 检查当前箱子中的每样物品
- 如果该物品是箱子,则回到第一步
- 如果是钥匙,则大功告成!
需要注意的是,递归并不能提高算法性能,但是很大程度上能够提高可读性(前提是要能理解。。)
基线条件与递归条件
所谓递归函数,就是自己会调用自己,因此如果没有停止的条件,那就会调用个没完了!
例如,当我们想利用递归实现一个倒计时的函数,如果我们这样写:
def countdown(i):
print(i)
countdown(i-1)
我们会发现程序会无限循环下去,直到机器崩溃。。。
因此,当我们想去实现一个递归方法时,必须要告诉递归何时停止才行。因此,所有的递归函数都存在两个部分:
- 基线条件(base case)——指函数不再调用自己
- 递归条件(recursive case)——指函数调用自己
回到上面的例子:
def countdown(i):
print(i)
# 基线条件:
if i <= 1:
return
# 递归条件:
else:
countdown(i-1)
栈
虽然上面介绍了递归的逻辑设计,但是总感觉还是云里雾里,好像明白了,但是又感觉各种调用和返回有些跳跃。
这里,就必须要介绍递归实现的底层原理——栈。
栈这种数据结构自然不必多说,先进后出。
我们要说的是计算机内部使用的被称为 “调用栈” 的结构。
这里,我们以下面这段代码举例:
def greet(name):
print("hello, ", name)
greet2(name)
print("time to say goodbye")
bye()
def greet2(name):
print("nice to meet you, ", name)
def bye():
print("ok, bye")
当我们调用greet("maggie")
这个方法时,会发生以下事情:
- 首先,计算机会将函数greet、变量name以及值maggie以栈的形式,整块存储到内存中,逐步执行,直到碰到
greet2
方法,此时,会先打印出hello, maggie
- 在遇到第二个greet2方法后,计算机同样会将相关信息压入栈中,此时第二个内存块在顶部,因此会先执行
greet2
中的相关内容,也即会打印:nice to meet you, maggie
- 这里有一个重要概念:当我们调用
greet2
时,实际上greet
方法只执行了一部分,也即:调用另一个函数时,当前函数暂停并出于未完成的状态。因此,当greet2
执行完毕后,会再次回到函数greet
中,并接着打印:time to say good bye
,接着调用bye
方法,打印,结束。
回到递归的例子中来,还是以上面的检查盒子为例,此时如果我们有盒子A,打开后发现有盒子B和C,这时我们检查盒子B,发现其中还有盒子D,当我们再检查盒子D时,发现已经是空的了,那么,此时调用栈类似于:
最右侧代表着待检查的盒子,我们可以看到,这个栈中包含了未完成的函数调用(例如上图中的盒子C还未检查),这就是递归中“自动记录”的特性。
对于任何函数递归调用而言,都可以通过这种函数栈的分析模式,总结起来,就是如下步骤: