掌握递归调用栈思想 由浅入深研究递归🎉
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
递归是超级强大的一种解决问题的思路
作为一只面向找工作刷题的前端菜狗 刚开始刷题的时候我经常在暴力迭代A掉一道力扣之后抱着试试看的想法去尝试“递归解法”
然后自闭掉
经过几个月的刷题 我慢慢了解到——
-
递归函数们都被暂存在栈中 等触发边界条件之后开始弹栈并执行栈中的函数
-
从最里面那层开始研究 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);
}
// 输出 ?
复制代码
借助调用栈很好想!(别嫌我图丑哈😢)
很明显最终结果是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);
复制代码
画一下递归调用栈
依旧是不要嫌弃我的图丑😂
使用递归创建一个倒计时
上面那题太简单?
再来一道(好吧还是很简单😂)
// 题目
已经定义了一个函数 `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)];// 更加简洁的展开运算符~
}
复制代码
画一下递归调用栈
使用递归来创建一个数字序列
最后来练一道题
到了这里 建议大家点开上面☝️的链接自己做一下试试!
一边画调用栈 一边写代码 很快啊!就做出来了~😎
递归秒杀
常规解法
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)
复制代码
为了美观拆分了一下~
画一下递归调用栈
前两个图都是常规方法的 这个来画一个三元表达式的
害!其实都一样
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;
};
复制代码
-
剑指 Offer 10- II. 青蛙跳台阶问题 本题用递归做有点牵强(强行靠到斐波那契数列那里)其实这是个考察动态规划的题XD
medium
总之 这上面的题目都是最基础最基础的递归题目
掌握了上面的递归调用栈的思路 理解它们的用法很简单 但是——
真的拿到一道题目 你能想到用递归去做嘛 能想明白内层的操作是哪样的嘛 ? 根据本文的知识点 还是不能的...
所以说 还是得多做题 多去思考呐!