详解递归算法执行的栈结构,简要介绍尾递归。
堆栈(英语:stack)又称为栈或堆叠,是计算机科学中一种特殊的串列形式的抽象数据类型,其特殊之处在于只能允许在链表或数组的一端(称为堆栈顶端指针,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。另外栈也可以用一维数组或链表的形式来完成。堆栈的另外一个相对的操作方式称为队列。
由于堆栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
维基百科:https://zh.wikipedia.org/wiki/%E5%A0%86%E6%A0%88
简单说,递归就是调用自己,如下代码:
def countdown(i):
print i
countdown( i- 1)
上面的代码打印 i 之后继续调用自己,但这个调用是无限循环的,无法停止。所以递归函数还有一个要素,基线条件,也就是调用边界,什么时候不调用自己,返回。如:
def countdown(i):
print(i)
# base case
if i <= 0:
return
# recursive case
else:
countdown(i-1)
这样就设置了一个基线条件,避免无线循环。
计算机在内部使用被称为调用栈的栈。
看一段 python 代码
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("maggie")
出于简化考虑,赞不将 print 视为函数。调用 greet("maggie") 函数时,计算机将为函数调用分配一块内存,变量被设置为 maggie 并保存在内存中。两步图示如下:
分配内存
使用内存
每当你调用函数时,计算机都像这样将函数调用涉及的所有变量的值存储到内存中。接下来,你打印hello,maggie!,再调用greet2("maggie")。同样,计算机也为这个函数调用分配一块内存。
计算机使用一个栈来表示这些内存块,其中第二个内存块位于第一个内存块上面。你打印howareyou,maggie?,然后从函数调用返回。此时,栈顶的内存块被弹出。
现在栈顶的内存是函数 greet 的这意味着返回到了函数 greet 。当调用 greet2 时,greet 函数只执行了一部分。
调用另一个函数时,当前函数暂停并处于未完成状态。
该函数所有的变量的值都还在内存中。
调用 greet2 之后,打印 getting ready to say bye...,然后调用 bye(),此时 函数 bye 来到了栈顶:
打印 ok bye! 之后,函数返回。回到 greet,函数已经没有其他事情可做,直接返回。
这个栈用于存储多个函数的变量,被称为调用栈。
递归函数也是函数,所以使用的也是调用栈。下面是一段计算阶乘的递归函数:
def fact(x):
if x == 1:
return 1
else:
return x * fact(x-1)
其调用栈:
每个fact调用都有自己的x变量。在一个函数调用中不能访问另一个的x变量。
使用栈虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。在这种情况下,你有两种选择。
- 重新编写代码,转而使用循环。
- 使用尾递归。这是一个高级递归主题,不在本书的讨论范围内。另外,并非所有的语言都支持尾递归。
在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。这种情形下称该调用位置为尾位置。若这个函数在尾位置调用本身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归,是递归的一种特殊情形。尾调用不一定是递归调用,但是尾递归特别有用,也比较容易实现。
尾调用的重要性在于它可以不在调用栈上面添加一个新的堆栈帧——而是更新它,如同迭代一般。尾递归因而具有两个特征:
- 调用自身函数(Self-called);
- 计算仅占用常量栈空间(Stack Space)。
而形式上只要是最后一个return语句返回的是一个完整函数,它就是尾递归。
维基百科:https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8
只看定义可能还是比较模糊,来看两段代码
def recsum(x):
if x == 1:
return x
else:
return x + recsum(x - 1)
def tailrecsum(x, running_total=0):
if x == 0:
return running_total
else:
return tailrecsum(x - 1, running_total + x)
两段代码不同的地方在于,尾递归函数传递了返回值,因此在每次 return 时原来的函数已经结束,栈退出,返回的是一个新的栈。两种递归方式的栈空间如下:
recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
分别为普通递归和尾递归,使用尾递归不会出现栈溢出的现象。
以上!
算法示例代码:https://github.com/egonschiele/grokking_algorithms
引用内容来自:巴尔加瓦(AdityaBhargava).算法图解(图灵程序设计丛书)(Kindle位置194-196).人民邮电出版社.Kindle版本.