文章目录

  • 前言
  • 一、何为'递归'
  • 二、调用栈
    • 1.观察调用栈运作
    • 2.调用栈的限制
    • 3.迭代算法和递归算法的比较
  • 总结

前言

第一次认识递归这个概念在两年前, 一个递归函数(不是深拷贝)看了好久才看懂, 在函数内部再次调用自己, 返回一个新的值, 再此期间还涉及多次对自己的调用, 这对于当时的我还是太过超前了.

不过直到最近, 我对’递归’的认知也几乎只是这些, 我还是希望能有更深刻的理解.


一、何为’递归’

递归并不像我理解的那样只是’自己调自己’这种, 正相反, '对自身的调用’这一行为包含在递归的范畴内.
在第三版《学习JavaScript数据结构与算法》里对递归的定义是:

递归是一种解决问题的办法, 它从解决问题的各个小部分开始, 直到解决最初的大问题. 递归通常涉及函数调用自身.

但是对于递归函数, 就是特指能够直接或者间接调用自身的函数了.
比如:

function recursiveFunction1 (someParam) {recursiveFunction2(someParam);
}

但是很明显不能在开发中使用上面这个函数——它会一直执行下去, 无休无止.
因此一个完整的递归函数应当具备基线条件, 即一个不再递归调用的条件, 就像while那样.

如果换成这样的话:

function understandRecursion (doIunderstandRecursion) {const recursionAnswer = confirm('understand');if (recursionAnswer === true) return; // 基线条件understandRecursion();
}

是一个合格的JavaScript递归函数了.


二、调用栈

每当一个函数被一个算法调用, 该函数会进入调用栈的顶部, 而递归函数对自身的调用也将导致更多的自己被压入调用栈, 因为每一次调用都可能依赖上一次调用的结果.

1.观察调用栈运作

调用栈的情况可以通过浏览器探查, 用一个递归阶乘函数作为例子:

function factorial (n) {if (n === 1 || n === 0) { // 基线条件return 1;}return n * factorial(n - 1);
}
console.log(factorial(5));

在基线打断点, 暂停执行, 然后刷新页面, 让调用栈回到初始状态:

factorial(3)调用.

factorial(3): 等待factorial(2).
factorial(3)调用.

factorial(2): 等待factorial(1),
factorial(3): 等待factorial(2).
factorial(3)调用.


2.调用栈的限制

我相信我们都或多或少的遇到过这个错误:

js maximum call stack size exceeded

递归函数在浏览器中并不能无限制的执行下去, 每个浏览器都有自己的调用栈上限, 也许你的递归可以确定并不是无限执行的, 但是浏览器调用栈盛放不了这么多函数, 那么就会抛出错误, 也就是所谓栈溢出(stack overflow error);
根据操作系统和浏览器的不同, 调用栈的承受能力也不同:

let i = 0;
function recursiveFn () {i++;recresiveFn();
}try {recursiveFn();
} catch (ex) {console.log(`i = ${i} error: ${ex}`);
}

比如如上递归函数在Chorme v65中执行了15662次塞满了调用栈, 而在Firefox v59中该函数执行了18661次才填满调用栈.

ECMAScript2015出现了尾调用优化的概念, 即如果函数内的最后一个操作是调用函数, 那么会通过跳转指令(jump)而非子程序调用来控制, 即在ECMAScript2015中递归函数的多余消耗甚至能被消除, 递归函数可以一直执行下去.


3.迭代算法和递归算法的比较

以求斐波那契数列为目标, 分别用迭代和递归实现, 检查执行速度.

迭代方案:

function fibonacci_Iterative (n) {if (n < 1) return 0;if (n <= 2) return 1;fet fibNMinus2 = 0;let fibNMinus2 = 1;let fibN = n;for (let i = 2; i <= n; i++) {fibN = fibNMinus1 + fibNMinus2;fibNMinus2 = fibNMinus1;fibNMinus1 = fibN;}return fibN;
}

递归方案:

function fibonacci (n) {if (n < 1) return 0;if (n < 2) return 1;return fibonacci(n - 1) + fibonacci(n - 2);
}

明显可见的是递归方案的代码少且易懂.

在递归深拷贝函数内部处理对象循环引用时常用的一种方法是将已经有的值存到weakMap或者Map里, 再次遇到需要使用该值的时候直接使用get从map结构中获取来节省计算量, 这种方法有个专用的名字记忆化.

记忆化是一种保存前一个结果的值的优化技术, 类似于缓存.

如果用记忆化法来求斐波那契数列:

function fibonacciMemoization (n) {const memo = [0, 1];const fibonacci = (n) => {if (memo[n] !== null) return memo[n];return memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);}return fibonacci;
}

使用记忆化WeakMap来处理深拷贝中循环引用:

function deepCopy(value, weakMap = new WeakMap()) {if (is.Function(value)) return value;if (is.Date(value)) return new Date(value.valueOf());if (is.Symbol(value)) return Symbol(value.description);if (is.Set(value)) {const newSet = new Set();for (const item of value) newSet.add(deepCopy(item), weakMap);return newSet;}if (is.Map(value)) {const newMap = new Map();for (const item of value) newMap.set(deepCopy(item[0], weakMap), deepCopy(item[1], weakMap));return newMap;}if (weakMap.has(value)) return weakMap.get(value);if (!is.Object(value) && !is.Array(value)) return value;const newObj = is.Array(value) ? [] : addProto(value, {});weakMap.set(value, newObj);for (const key in value) {newObj[key] = deepCopy(value[key], weakMap);}return newObj
}

总结

迭代递归执行起来要快很多, 递归相较于迭代需要的代码更少且更易理解, 另外, 有了尾调用优化, 递归的多余消耗甚至可能被消除.

JavaScript 递归算法相关推荐

  1. JavaScript递归算法统计(将整数n分成任意两份不能相同的k份)分法的种数

    JavaScript递归算法实现将整数n分成k份,任意两份不能相同 题目: 将整数n分成k份,且每份不能为空,任意两种分法不能相同,求有多少种分法. 例如:n=7,k=3,下面三种分法被认为是相同的: ...

  2. 递归算法,JavaScript实现

    我们先来看一下定义.递归算法,是将问题转化为规模缩小的同类问题的子问题,每一个子问题都用一个同样的算法去解决.一般来说,一个递归算法就是函数调用自身去解决它的子问题. 递归算法的特点: 在函数过程中调 ...

  3. python百鸡百钱递归_百钱百鸡,一百块钱买一百只鸡的递归算法 javascript实现

    // 求百钱百鸡 function buy(ind, indexs, start) { start++; if (start > 2) { return; } if (!indexs[start ...

  4. 一个带CheckBox的树形目录的递归算法(javascript)

    唉,很久以前写的代码,晒一晒,估计自己看都看不懂了,:( var head="display:''" img_close=new Image() img_close.src=&qu ...

  5. javascript递归函数定义和常见递归算法

    递归的概念 就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己. 从概念上出发,给出以下的例子: function foo(){console.log("函数 foo 是递归函 ...

  6. Javascript函数之深入浅出递归思想,附案例与代码!

    作者 | 浮世万千吾爱有三 责编 | Carol 来源 | CSDN 博客 递归函数的理解 1.生活中的递归 "递归"在生活中的一个典例就是"问路".如图小哥哥 ...

  7. JavaScript初学者编程题(7)

    JavaScript初学者编程题(7) 题目:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总对数为多少? 一个经典题目,使用 ...

  8. 《JavaScript 闯关记》之函数

    函数是一段代码,它只定义一次,但可以被执行或调用任意次.在 JavaScript 里,函数即对象,程序可以随意操控它们.比如,可以把函数赋值给变量,或者作为参数传递给其他函数,也可以给它们设置属性,甚 ...

  9. 如何在javascript中使用多个分隔符分割字符串?

    如何在JavaScript中使用多个分隔符拆分字符串? 我正在尝试在逗号和空格上进行拆分,但是AFAIK,JS的拆分功能仅支持一个分隔符. #1楼 对于那些想要在拆分功能中进行更多自定义的人,我编写了 ...

最新文章

  1. 18不使用委托实现能自动侦测车距的智能汽车
  2. 数据结构Java实现03----单向链表的插入和删除b
  3. FindStringExact
  4. PAT A1063——set的常见用法详解
  5. 基于区块链的健康链系统设计与实现(3)系统设计
  6. [leetcode]111.二叉树的最小深度
  7. 关于TCP的粘包问题
  8. 使用15年,竟未得授权?真功夫遭李小龙女儿起诉索赔,回应:我们也很疑惑...
  9. quick-cocos2d-x 游戏开发——StateMachine 状态机
  10. hdu-1521 排列组合 指数型母函数
  11. 三线表里加小短线_三线表的格式
  12. SPSS问卷数据处理步骤
  13. iPhone 各屏幕尺寸整理
  14. 网络工程师成长日记140-机遇都是碰出来的
  15. NLP自然语言处理系列-音乐推荐系统实战 -计算相似度得到推荐结果
  16. java 导出复杂格式的 Excel 留着自己备用
  17. 机器学习模型上线及优化流程
  18. 元数据管理系统解决方案及产品调研-数仓系列(一)
  19. 思维精进01:罗辑思维2019跨年演讲--小趋势
  20. 侧滑、、、mark一下、、、

热门文章

  1. ijk的那些事--知其所以然
  2. 知乎大V点赞一个多少费用,知乎大V推广问答发布操作
  3. 【C语言】判断一个浮点数是否等于零
  4. 企业管理软件从勤哲excel服务器迁移到奥多odoo的感触
  5. Java社招最全面试题,成功收获美团,小米offer
  6. 愿你一生欢喜,不为世俗裹挟 | 笔记摘要
  7. 去掉python的花括号
  8. Python:字符串中的方法--strip()
  9. matlab三维图 魔方,matlab制作魔方图片
  10. 第4章 软件的设计——总体设计与详细设计