第三章 递归
3.1 递归
引例:
假设你发现了一个上锁的箱子和一个盒子。钥匙可能在盒子里面,这个盒子里还有盒子,而盒子里的盒子又有盒子…(钥匙就在某个盒子中)为了找到钥匙,你将使用什么算法?
第1种方法:
1.创建一个要查找的盒子堆。
2.从盒子堆取出一个盒子,在里面找。
3.如果找到的是盒子,就会将其加入盒子堆中,以便以后再查找。
4.如果找到钥匙,则大功告成。
5.回到第2步。
伪代码:
def look_for_key(main_box):
pile=main_box.make_a_pile_to_look_through()
while pile is not empty:#盒子堆
box=pile.grab_a_box()#取一个盒子
for item in box:#遍历盒子
if item.is_a_box():#是盒子就添加在盒子堆中
pile.append(item)
elif item.is_a_key():#找到钥匙结束
print:"find key!"
第2种方法:
1.检查盒子中的每样东西。
2.如果是盒子就回到第1步。
3.如果是钥匙就大功告成。
伪代码:
def look_for_key(box):
for item in box:
if item.is_a_box():
look_for_key(item)#递归函数调用自己
elif item.is_a_key():
print: "find key!"
在你看来哪一种方法更容易呢?
第1种方法是使用while循环,只要盒子堆不空就从中取出一个盒子,并在其中仔细查找,如下:
第2种方法是使用递归函数调用自己这种方法的伪代码,如下:
总结:这两种方法的作用相同。递归只是让解决方案更清晰,并没有性能上的优势,实际上在有些情况下使用循环的性能更好。使用循环程序的性能可能更高,使用递归程序可能更容易理解。
PS:很多算法都使用了递归,因此理解这种概念很重要。
3.2 基线条件和递归条件
编写递归函数时必须告诉它何时停止递归。正因如此,每个递归函数都有两部分:基线条件和递归条件
递归条件是指函数调用自己。
基线条件则指函数不再调用自己,从而避免形成无限循环。
3.3 栈
引例:
假设你去野外烧烤,并为此创建了一个待办事项清单——一叠便条(在讨论数组和链表时,也有一个待办事项清单,你可将待办事项添加到清单的任何地方,还可以删除任何一个待办事项)
一叠便条要简单的多——插入的待办事项放在清单的最前面;读取待办事项时,你只读取最上面那个,并将其删除。
因此这个待办事项清单只有两种操作:压入(插入)和弹出(删除并读取)
这种数据结构称为栈(请结合上述例子具象理解)。栈是一种简单的数据结构,刚才我们一直在使用它,却没有意识到。
栈的定义:
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
3.4 调用栈
引例:
计算机在内部使用被称为调用栈的栈,为了理解调用栈,来看一个函数:
def greet(name):
print:"hello," + name
greet2(name)
print:"getting ready to say bye..."
bye()
def greet2(name):
print:"how are you ?"+name
def bye():
print:"ok bye!"
下面详细介绍调用函数时发生的情况:
假如你调用greet(“Marry”)计算机将首先为该函数调用分配一块内存。
然后,我们来使用这些内存变量name被设置为Marry,这需要储存到内存中。
每当你调用函数时,计算机都像这样将函数调用涉及的所有变量的值储存到内存中。接下来你打印:hello Marry!
再调用greet2(“Marry”) 同样,计算机也为这个函数调用分配一块内存。
计算机使用一个栈来表示这些内存块,其中第2个内存块(greet2)位于第1个内存块(greet1)上面,你打印:how are you,Marry?**
然后从函数调用返回。此时站顶的内存块被弹出,现在占点的内存块是函数greet的,这意味着你返回到了函数greet。当你调用函数greet2时 ,函数greet指只执行了的一部分。
这是一个重要的概念!调用另一个函数时,当前函数暂停并处于未完成状态,该函数所有变量的值都还在内存中。
执行完函数greet2后,你回到函数greet并从离开的地方接着往下执行,首先打印:getting ready to say bye…
再调用函数bye(),在栈顶添加了函数bye()的内存块,然后你打印:OK,bye!并从这个函数返回。
现在你又回到了函数greet,由于没有别的事情要做,你就从函数greet返回这个栈。
因此,用于储存多个函数的变量成为调用栈。
3.5 递归调用栈
递归函数也使用调用栈。一起来看看递归函数f的调用栈。
注:f(5)写作5!(其实就是5的阶乘)下面是计算阶乘的递归函数:
def f(x):
if x==1:
return 1
else:
return x*f(x-1)
下面来详细分析调用f(3)时调用栈是如何变化的(别忘了栈顶的方框指出了当前执行到了什么地方):
注意:每一个f调用都有自己的x变量。在一个函数调用中不能访问另一个x变量——只能访问当前处在栈顶位置的变量。
总结:栈在递归中扮演着重要的角色,在本章开头的事例中,有两种寻找钥匙的办法。
使用第1种方法时,你创建一个带查找的盒子堆,因此你始终知道还有哪些盒子是需要查找的。
使用第2种方法及递归的方法时,没有盒子堆。既然没有盒子堆,那么算法怎么知道还有哪些盒子需要查找呢?原来盒子堆储存在了栈中,这个栈包含未完成的函数调用,每个函数调用都包括还未检查完的盒子。
使用栈很方便,因为你无需跟踪盒子堆栈替你这样做了。
最后,使用栈虽然很方便,但是也要付出代价,储存详尽的信息,可能占用大量的内存,每个函数调用都要占用一定的内存,如果占很高就意味着计算机储存了大量函数调用的信息。
在这种情况下你有两种选择:
一、重新编写代码,转而使用循环。
二、使用尾递归,这是一个高级的递归主题,不在我们讨论的范围内。