递归一种优雅的问题解决方法,很多算法都在使用
案例理解递归
假设有一个神秘手提箱,要是很可能再里面的盒子里。但是打开箱子发现,里面不只有一个盒子,
有可能是盒子套盒子....
现在如何查找盒子里面的钥匙呢?
方案一
- 创建一个要查找的盒子堆
- 从盒子堆取出一个盒子,在里面找
- 如果找到的是盒子,就将其加入盒子堆中以便以后再查找
- 如果找到钥匙,则大功告成
- 回到第二步
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 "found the key!"
方案二
- 检查盒子里面每样东西
- 如果是盒子回到第一步
- 如果是钥匙,大功告成
def look_for_key(box):
for item in box:
if item.is_a_box():
look_for_key(item)
elif item.is_a_key():
print "found the key!"
看哪一种比较好理解,第一种方案使用while循环,只要盒子不空就从里面取出一个盒子仔细查找。属于深度优先遍历, 第二种方案是递归,函数自己调用自己。 两种方案都在解决同一个问题,但递归我感觉可能更好理解一点
递归带来的问题
- 递归自己调用自己,容易书写出错导致无限循环
- 递归调用函数被压入栈中不被释放,会占用大量内存(当然可以使用尾递归优化)
解释问题一
假设需要编写一个倒计时函数输出3、2、1、0
const fn = (i)=>{
console.log(i)
fn(i-1)
}
fn(3)
但是实际上这是一个死循环 会按照3、2、1、0、-1、-2、-3....
所以我们编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分,基线条件,和递归条件。基线条件是函数不再调用自己,递归条件是指函数调用自己。 现在给上述函数增加基线条件
const fn = (i)=>{
console.log(i)
if(i<=0){ // 基线条件
return
} else { // 递归条件
fn(i-1)
}
}
fn(3)
解释问题二
栈
如果要理解上述问题,及必须要清楚重要的编程概念栈及函数的调用栈(call stack)。 先入后出,后入先出的数据结构称为栈
const stack = [];
stack.push(1); // 1入栈 [1]
stack.pop(); // 1 出栈 []
stack.push(2); // [2]
stack.push(3); // [2,3]
stack.push(4); // [2,3,4]
// 按照栈数据结构规范 只能取最后一个
stack.pop() // 4 出栈
此处借助原文理解一下
调用栈
计算机在内部使用被称为调用栈的栈,及函数运行的时候相当于一个压栈出栈的过程。
const fn = (i)=>{
console.log(i)
debugger
if(i<=0){ // 基线条件
return
} else { // 递归条件
fn(i-1)
}
}
fn(3)
fn(3)执行第一个函数被压入栈中,执行里面代码
继续执行递归条件生效 fn(3-1) => fn(2) 执行,这里需要注意的是第一个函数还没有执行完;因为fn(2)属于第一个函数的一部分
继续执行....
当第四个函数执行时i是零,走基线提交,fn(0)这个函数执行完成,f(0)退栈,因为f(0)属于f(1)的一部分,f(0)退栈完成后,f(1)函数里面代码执行完退栈 [这里没有其他代码],f(1)退栈=> f(2)退栈=> f(3)退栈。 整个代码执行完毕。
递归规模比较大时,压入栈中函数越多,占用内存越多。。。。