算法图解-递归

349 阅读2分钟

递归一种优雅的问题解决方法,很多算法都在使用

案例理解递归

假设有一个神秘手提箱,要是很可能再里面的盒子里。但是打开箱子发现,里面不只有一个盒子,
有可能是盒子套盒子....

1646379051.png

现在如何查找盒子里面的钥匙呢?

方案一

  1. 创建一个要查找的盒子堆
  2. 从盒子堆取出一个盒子,在里面找
  3. 如果找到的是盒子,就将其加入盒子堆中以便以后再查找
  4. 如果找到钥匙,则大功告成
  5. 回到第二步
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!"

方案二

  1. 检查盒子里面每样东西
  2. 如果是盒子回到第一步
  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 "found the key!"

看哪一种比较好理解,第一种方案使用while循环,只要盒子不空就从里面取出一个盒子仔细查找。属于深度优先遍历, 第二种方案是递归,函数自己调用自己。 两种方案都在解决同一个问题,但递归我感觉可能更好理解一点

递归带来的问题

  1. 递归自己调用自己,容易书写出错导致无限循环
  2. 递归调用函数被压入栈中不被释放,会占用大量内存(当然可以使用尾递归优化)

解释问题一

假设需要编写一个倒计时函数输出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 出栈

此处借助原文理解一下 企业微信截图_16463818194288.png

调用栈

计算机在内部使用被称为调用栈的栈,及函数运行的时候相当于一个压栈出栈的过程。

const fn = (i)=>{
    console.log(i)
    debugger
    if(i<=0){ // 基线条件
        return
    } else {  // 递归条件
        fn(i-1)
    }
}
fn(3)

fn(3)执行第一个函数被压入栈中,执行里面代码

企业微信截图_16463822816504.png 继续执行递归条件生效 fn(3-1) => fn(2) 执行,这里需要注意的是第一个函数还没有执行完;因为fn(2)属于第一个函数的一部分

企业微信截图_16463824869428.png

继续执行....

企业微信截图_16463825463672.png

当第四个函数执行时i是零,走基线提交,fn(0)这个函数执行完成,f(0)退栈,因为f(0)属于f(1)的一部分,f(0)退栈完成后,f(1)函数里面代码执行完退栈 [这里没有其他代码],f(1)退栈=> f(2)退栈=> f(3)退栈。 整个代码执行完毕。

递归规模比较大时,压入栈中函数越多,占用内存越多。。。。