算法图解第三章笔记与习题(递归)
3.1 递归
def factorial(x): # 用递归计算阶乘,可读性更强,但实际性能与循环相同,甚至更差。
if x == 0 or x == 1:
return 1
else:
return x * factorial(x-1)
递归只是让解决方案更加清晰,但实际上,并没有性能上的优势。
3.2 基线条件和递归条件
编写递归函数时,必须告诉它何时停止递归。
正因为如此,每个递归函数都有两部分:
- 基线条件(base case):函数不再调用自己,从而避免无限循环。
- 递归条件(recursive case):函数继续调用自己。
如此,递归函数就会按照我们的预期的那样运行。
def countdown(i):
print(i)
if i <= 0: # 基线条件
return
else: # 递归条件
countdown(i-1)
3.3 栈
**栈(stack)**是一种简单的数据结构,只有压入和弹出两种操作(先进后出)。
3.3.1调用栈
def greet2(name):
print("how are you, " + name + "?")
def bye():
print("ok bye!")
def greet(name):
print("hello, " + name + "!")
greet2(name)
print("getting ready to say bye...")
bye()
上述即是一个栈的体现,其整体调用流程如下:
假设调用greet("Mars")
,计算机将首先为该函数调用分配一块内存。
在该内存中,变量name
被设置为Mars
,并被存储到内存中。
接下来,打印出hello, Mars
,再调用greet2("Mars")
。同样,计算机也为这个函数调用分配一块内存。
计算机使用一个栈来表示这些内存块,其中第二个内存块位于第一个内存块上面。你打印 how are you, Mars?
,然后从函数调用返回。此时,栈顶的内存块被弹出。
现在,栈顶的内存块是函数greet
的,这意味着返回到了函数greet
。当在调用函数greet2
时,函数greet
只执行了一部分。即:**调用另一个函数时,当前函数暂停并处于未完成状态。**该函数的所有变量的值都还在内存中。执行完函数greet2
后,回到函数greet
,并从离开的地方开始接着往下执行:首先打印getting ready to say bye...
,再调用函数bye
。
在栈顶添加了函数bye
的内存块。然后,打印ok bye!
,并从这个函数返回。
现在又回到了函数greet
。由于没有别的事情要做,就从函数greet
返回。这个栈用于存储多个函数的变量,被称为调用栈。
3.3.2 递归调用栈
递归函数运行时,正需要调用栈。栈在递归中扮演着重要的角色。
以开头的factorial(x)
为例,当输入参数x=3
时,其递归调用栈如下。
在递归函数运行时,递归函数本身将被一次又一次地调用,不断地先前的前递归函数压入栈中,直到参数达到基线条件并得出返回值后被弹出栈,则先前的一系列的递归函数也将获得返回值并被弹出栈,运行其余被压入栈的递归函数。
使用栈虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。在这种情况下,你有两种选择。
- 重新编写代码,转而使用循环。
- 使用尾递归。
3.4 小结
- 递归指的是调用自己的函数。
- 每个递归函数都有两个条件:基线条件和递归条件。
- 栈有两种操作:压入和弹出。
- 所有函数调用都进入调用栈。
- 调用栈可能很长,这将占用大量的内存。
练习
习题3.1
- 根据下面的调用栈,你可获得哪些信息?(函数在3.3节处)
调用了函数greet
,并将参数name
的值指定为maggie
。而greet
函数调用了greet2
,并将greet2
中的name
参数也指定为maggie
。此时greet
处于未完成状态,等待greet2
函数运行完成后,greet
函数将继续运行。
习题3.2
- 假设你编写了一个递归函数,但不小心导致它没完没了地运行。正如你看到的,对于每次函数调用,计算机都将为其在栈中分配内存。递归函数没完没了地运行时,将给栈带来什么影响?
栈将会没完没了地拓展,直到内存地址不足为止(栈溢出)。