算法图解part3:递归&栈
1.什么是递归
百度百科
程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
文中是通过在盒子堆中找钥匙的示例,来说明递归这一概念的,其流程图如下:
2.基线条件和递归条件
每个递归函数都有两部分:基线条件(base case) 和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。
Python代码
//递归打印*
def countdown(i):
print(i)
if i <= 0: #基线条件
return
else: #递归条件
countdown(i-1)
countdown(3)
运行结果:
3
2
1
0
Python代码:递归阶乘 5!
//递归阶乘
def cheng(n):
if n == 1:
return 1
else:
return n*cheng(n-1)
plus = cheng(5)
plus
运行结果:(返回0,说明 1 在 list 的 0 号索引)
120
3.递归与循环
在找钥匙的案例中,使用循环和递归的方式都能解决问题,递归是让解决方案更清晰,而使用循环的性能更好些。引用大佬在Stack Overflow上的一句话 :
Leigh Caldwell:
如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。
使用尾递归可以提升线性递归的性能,如裴波那契数列的尾递归实现:(日后填坑)
def fibonacci(int n, int a):
{
#尾递归计算fibonacci
if n < 0 :
return 0
elif n == 0 :
return 1
elif n == 1 :
return a
else :
return fibonacci(n - 1, n * a)
}
运行结果:
0
4.栈(Stack)
- 栈是一种数据结构,它的主要操作方式是后进先出。
- 就如堆盒子,你第一个放下的盒子一定是在底部(在栈中的就叫push(压入)),最后一个盒子在顶部,当你想找盒子里钥匙的时候,一定是从顶部拿起(在栈中就叫做pop(弹出))
- Python里面实现栈,是把list包装成一个类,再添加一些方法作为栈的基本操作
4.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')
运行结果:
hello, maggie!
how are you, maggie?
getting ready to say bye…
ok bye!
调用栈过程如下:
- 调用函数greet(‘maggie’),计算机将首先为该函数调用分配一块内存。(不考虑print函数)
- 然后变量name被设置为maggie,存储到内存中。
- 每当调用函数时,计算机都像这样将函数调用涉及的所有变量的值存储到内存中。
接下来,打印 hello, maggie! ,再调用 greet2(“maggie”) 。同样,计算机也为这个函数调用分配一块内存。
PS:⭐这里不是共用一个name变量 - 计算机使用一个栈来表示这些内存块,其中第二个内存块位于第一个内存块上面。你打印how are you, maggie? ,然后从函数调用返回。此时,栈顶的内存块被弹出。
- 现在,栈顶的内存块是函数 greet 的,这意味着你返回到了函数 greet 。当你调用函数 greet2时,函数 greet 只执行了一部分。这是本节的一个重要概念: 调用另一个函数时,当前函数暂停并处于未完成状态。 该函数的所有变量的值都还在内存中。执行完函数 greet2 后,你回到函数greet ,并从离开的地方开始接着往下执行:首先打印 getting ready to say bye… ,再调用函数 bye 。
- 在栈顶添加了函数 bye 的内存块。然后,你打印 ok bye! ,栈顶的内存块函数 bye 被弹出。
并从这个函数返回。
- 现在程序又回到了函数 greet 。由于没有别的事情要做,就从函数 greet 返回。这个栈用于存储多个函数的变量,被称为调用栈。
4.2递归调用栈
通过一个Python阶乘代码来理解递归中的调用栈:
def fact(x):
if x == 1:
return 1
else:
return x * fact(x - 1)
fact(3)
运行结果:
6
图解如下:
- 输入为3,第一次调用fact(),首先进行if判断,执行else语句,进行递归调用,此时调用栈中,栈底为主函数fact(),栈顶为第二次调用fact()。两块内存分别存了变量X(不是同一个x)。
- 在第二次fact()调用中,此时X = 2,进行if判断后执行else语句,进行第三次调用
PS:不同的调用中不能访问各自的x变量
- 现在有三个fact()调用,栈顶执行完语句后为1,第三次调用返回第二次调用,同时弹出并删除内存块
- 在第二次调用中,x为2,此时第三次调用返回为1,执行语句后,第二次调用返回第一次调用,将第二次调用从栈顶弹出并删除。
- 回到第一次调用,x为2,第二次调用返回的值为2,则执行完语句后,最终结果为6,与代码执行结果相同。所有函数调用都进入调用栈
PPS:⭐注意,每个 fact 调用都有自己的 x 变量。在一个函数调用中不能访问另一个的 x 变量。
5.总结
- 递归指的是调用自己的函数。
- 每个递归函数都有两个条件:基线条件和递归条件。
- 栈有两种操作:压入和弹出。
- 所有函数调用都进入调用栈。
- 调用栈可能很长,这将占用大量的内存。
6.参考资料
《算法图解》第三章
此部分学习算法内容已上传github:https://github.com/ShuaiWang-Code/Algorithm/tree/master/Chapter3