掌握递归调用栈思想 由浅入深研究递归🎉

2,054 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。


递归是超级强大的一种解决问题的思路

作为一只面向找工作刷题的前端菜狗 刚开始刷题的时候我经常在暴力迭代A掉一道力扣之后抱着试试看的想法去尝试“递归解法”

然后自闭掉

流下了不学无术的泪水.jpg

经过几个月的刷题 我慢慢了解到——

  • 递归函数们都被暂存在栈中 等触发边界条件之后开始弹栈并执行栈中的函数

  • 从最里面那层开始研究 return 的内容更好想

...

研究递归的题目倒蛮像抽丝剥茧的🤔

之前看过不少讲递归的文章 但是总感觉 要不就是大神段位比我高太多 内容有点不好理解 要不就是例子太少 大量理论往我脸上一怼 瞬间懵逼😂

所以本篇文章想写一篇就算是小白也能很舒服地理解的“递归” 同时结合着例子 大家可以先尝试着自己写一下递归解法 再画个图 如果不明白再来看看解法🙌

另外 本篇文章打算对不同题型的递归做法进行研究(随着我碰到不同题型中的递归方法 我会更新到本文中 当作是日后想不通递归法的一个小手册吧!)—— 数组 链表 二叉树...中的递归

读过本文 你会收获——

  • 利用栈的思想 肉脑轻松想递归(当然是简单一点的递归啦🤣)

  • 使用递归解决数组循环遍历问题 —— 最基础 用来入门递归思想再合适不过了😄

之后我还会用本文的思想去解决更多的问题

  • 使用递归解决有序链表合并问题 —— 使用递归跳过复杂的链表遍历 轻松获得答案😎

  • 使用递归解决二叉树问题 —— 二叉树的题目基本就离不开递归了XD 所以 想做出来二叉树?先把递归研究明白~🤔

  • 使用递归解决排序问题

1.理解递归函数所处的调用栈

前段时间学习到 JavaScript的 “执行上下文栈” 发现其可以完美解释递归函数的执行

首先我们说 一点点 理论的东西(之前学习过的文章里看到的解释 感觉很有道理~)

递归就像是“查字典” 我们看到一句话 发现其中有不懂的 于是开始查字典(入栈递归函数)

查完以后发现字典的解释里还有不懂的 于是继续查(再入栈一个递归函数)

...省略n个层层查找(也就是不断的递归函数入栈)

终于 我们弄懂了一个(抵达了base case——用于告诉递归函数何时结束) ✨

之后利用这个词再去理解上层的那个(不断地进行递归函数出栈并执行递归函数 返回值传给调用栈中的函数)

直到我们理解这句话的意思(所有调用函数全部出栈 返回最终结果)递归函数完成了它的使命~

总结:递归分两步——

  • 递:“查字典” 不断将递归函数入栈

  • 归:“通过字典获取内容 来理解句子”:将递归函数出栈并一个个地返回结果供后续函数使用

口说无凭 举个例子 上个例子 画个图——

利用调用栈的思想秒杀递归

来看道题

foo(1);
function foo(i) {
    if (i == 4) {
        return;
    }
    console.log('foo() begin:' + i);
    foo(i + 1);
    console.log('foo() end:' + i);
}
// 输出 ? 

借助调用栈很好想!(别嫌我图丑哈😢)

image.png

很明显最终结果是1 2 3 3 2 1

其实这里如果有动图就更清楚了 但是我懒我相信大家都能脑补出来的!😘

根据黑色箭头的方向 递归函数先入栈再出栈~

2.利用递归代替循环解决数组问题

这里利用三个很简单的数组操作 来帮助大家加深对递归的印象

灵感来自 freecodecamp 使用递归代替循环 使用递归创建一个倒计时 使用递归来创建一个数字序列 (顺便给前端学习者们安利一波这个333k star数的宝藏学习平台❤️)

利用递归计算数组前n个元素的和

这题用循环的方式大家肯定都能秒杀

正因为它的简单 所以我们可以利用其入门/练习 递归思想

// 题目
写一个递归函数,`sum(arr, n)`,返回递归调用数组 `arr` 的前 `n` 个元素和。

递归秒杀

function sum(arr, n){
    if(n <= 0){
        return 0;
    }
    else{
        return sum(arr, n - 1) + arr[n - 1];
    }
}
sum([6, 6, 6, 6], 3);

画一下递归调用栈

依旧是不要嫌弃我的图丑😂

image.png

使用递归创建一个倒计时

上面那题太简单?

再来一道(好吧还是很简单😂)

// 题目
已经定义了一个函数 `countdown`,函数有一个参数(`n`)。 
函数应该基于参数 `n` 递归调用返回 `n` 到 `1` 的连续数字的数组。 
如果函数以小于 1 的参数调用,函数应该返回空数组。 

比如,用 `n = 5` 调用函数应该返回数组 `[5, 4, 3, 2, 1]`。 
函数必需使用递归函数调用自身,不能使用任何形式的循环。

递归秒杀

循环?才不要用 (其实是因为题目不让用😐)

递归直接秒杀!

常规方法

// 01 先将函数入栈(递 的过程) n = 0 时 countdown就是 []
// 出栈的时候将值插入数组中(归 的过程)
function countdown(n){
    if(n <= 0){
        return [];
    }
    else{
        const countArray = countdown(n - 1);
        countArray.unshift(n);
        // 需要头插 因为是倒序的数组嘛~
        // countArray.splice(0, 0, n);
        // 这样也可以达到头插的效果
        return countArray;
    }
}
countdown(5);//[5,4,3,2,1]

三元表达式一行秒杀

// 02 更为简洁的一行代码秒杀法
// n = 0时 返回值为 [5,...[]] 之后随着递归函数出栈 返回值数组逐渐插入数字
function countdown(n){
	return n <= 0 ? [] : [n].concat(countdown(n - 1));
    // return n <= 0? [] : [n, ...countdown(n - 1)];// 更加简洁的展开运算符~
}

画一下递归调用栈

image.png

使用递归来创建一个数字序列

最后来练一道题

到了这里 建议大家点开上面☝️的链接自己做一下试试!

一边画调用栈 一边写代码 很快啊!就做出来了~😎

递归秒杀

常规解法

function rangeOfNumbers(startNum, endNum) {
  if(startNum === endNum){
    return [startNum];
  }
  else{
    const countArray = rangeOfNumbers(startNum + 1, endNum);
    countArray.unshift(startNum)
    return countArray;
  }
};

三元表达式一行秒杀

function rangeOfNumbers(startNum, endNum) {
    return startNum === endNum 
      ? [startNum] 
      :rangeOfNumbers(startNum, endNum - 1).concat(endNum);
      // : [...rangeOfNumbers(startNum, endNum - 1), endNum];  
};

rangeOfNumbers(1,3)

为了美观拆分了一下~

画一下递归调用栈

前两个图都是常规方法的 这个来画一个三元表达式的

害!其实都一样

image.png

3.小结 & 可以使用递归解决的问题

本文涉及的递归都是 最最最基础的递归 但是工欲善其事必先利其器!

我们要先把基础的递归思想理解得透透 再去实践更加复杂的题型

学习了递归法之后可以尝试解决的题型——

(持续更新中)

easy

推荐看这个 动图题解 看完不明白 我自己吃了它 满意了吧🍉

var reverseList = function(head) {
    // “递”的过程
    if(head === null || head.next === null){
        return head;
    }
    let newHead = reverseList(head.next);// 把head.next这个子问题传进去 
    // 在弹出最顶部的function(链表末尾处节点)之后 返回的节点赋给了newHead 
    // “归”的过程
    head.next.next = head;
    head.next = null;
    return newHead;
};

medium

总之 这上面的题目都是最基础最基础的递归题目

掌握了上面的递归调用栈的思路 理解它们的用法很简单 但是——

真的拿到一道题目 你能想到用递归去做嘛 能想明白内层的操作是哪样的嘛 ? 根据本文的知识点 还是不能的...

所以说 还是得多做题 多去思考呐!