`
blueyanghualong
  • 浏览: 225287 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

如何提升JavaScript的递归效率

阅读更多
Nicholas为您讲解如何提升JavaScript的递归效率!

影响JavaScript性能的另外一个杀手就是递归,在上一节中提到采用 memoization技术可以优化计算数值的递归函数,但memoization不是万能的,不是所有的递归函数都可以用memoization技术优 化,本文介绍了这些情况,并介绍了解决办法,就是将递归转换为迭代,同时需要注意,本文末尾介绍的方案不是最终的方案,还需要和上一节优化循环的方案综合 起来才能达到最佳效果。

【原文】Speed up your JavaScript, Part 3

【作者】Nicholas C. Zakas

【译者】明达

以下是对原文的翻译:

递归是拖慢脚本运行速度的大敌之一。太多的递归会让浏览器变得越来越慢直到死掉或者莫名其妙的突然自动退出,所以我们一定要解决在 JavaScript中出现的这一系列性能问题。在这个系列文章的第二篇中, 我曾经简短的介绍了如何通过memoization技术来替代函数中太多的递归调用。memoization是一种可以缓存之前运算结果的技术,这样我们 就不需要重新计算那些已经计算过的结果。对于通过递归来进行计算的函数,memoization简直是太有用了。我现在使用的memoizer是由 Crockford写的,主要应用在那些返回整数的递归运算中。当然并不是所有的递归函数都返回整数,所以我们需要一个更加通用的memoizer()函 数来处理更多类型的递归函数。



function memoizer(fundamental, cache) {
  cache = cache || {};
  var shell = function(arg) {
      if (! (arg in cache)) {
          cache[arg] = fundamental(shell, arg);
      }
      return cache[arg];
  };
  return shell;
}

这 个版本的函数和Crockford写的版本有一点点不同。首先,参数的顺序被颠倒了,原有函数被设置为第一个参数,第二个参数是缓存对象,为可选参数,因 为并不是所有的递归函数都包含初始信息。在函数内部,我将缓存对象的类型从数组转换为对象,这样这个版本就可以适应那些不是返回整数的递归函数。在 shell函数里,我使用了in操作符来判断参数是否已经包含在缓存里。这种写法比测试类型不是undefined更加安全,因为undefined是一 个有效的返回值。我们还是用之前提到的斐波纳契数列来做说明:



var fibonacci = memoizer(function(recur, n) {
  return recur(n - 1) + recur(n - 2);
}, { "0": 0, "1": 1} );

同 样的,执行fibonacci(40)这个函数,只会对原有的函数调用40次,而不是夸张的331,160,280次。memoization对于那些有 着严格定义的结果集的递归算法来说,简直是棒极了。然而,确实还有很多递归算法不适合使用memoization方法来进行优化。



我在学 校时的一位教授一直坚持认为,任何使用递归的情况,如果有需要,都可以使用迭代来代替。实际上,递归和迭代经常会被作为互相弥补的方法,尤其是在另外一种 出问题的情况下。将递归算法转换为迭代算法的技术,也是和开发语言无关的。这对JavaScript来说是很重要的,因为很多东西在执行环境中是受到限制 的(the importance in JavaScript is greater, though, because the resources of the execution environment are so restrictive.)。让我们回顾一个典型的递归算法,比如说归并排序,在JavaScript中实现这个算法需要下面的代码:



function merge(left, right) {
  var result = [];
  while (left.length > 0 && right.length > 0) {
      if (left[0] < right[0]) {
          result.push(left.shift());
      } else {
          result.push(right.shift());
      }
  }
  return result.concat(left).concat(right);
}

//采用递归实现的归并排序算法
function mergeSort(items) {
  if (items.length == 1) {
      return items;
  }
  var middle = Math.floor(items.length / 2),
  left = items.slice(0, middle),
  right = items.slice(middle);
  return merge(mergeSort(left), mergeSort(right));
}

调 用mergeSort()函数处理一个数组,就可以返回经过排序的数组。注意每次调用mergeSort()函数,都会有两次递归调用。这个算法不可以使 用memoization来进行优化,因为每个结果都只计算并使用一次,就算缓冲了结果也没有什么用。如果你使用mergeSort()函数来处理一个包 含100个元素的数组,总共会有199次调用。1000个元素的数组将会执行1999次调用。在这种情况下,我们的解决方案是将递归算法转换为迭代算法, 也就是说要引入一些循环(关于算法,可以参考这篇《List Processing: Sort Again, Naturally》):



// 采用迭代实现的归并排序算法
function mergeSort(items) {
  if (items.length == 1) {
      return items;
  }
  var work = [];
  for (var i = 0,
  len = items.length; i < len; i++) {
      work.push([items[i]]);
  }
  work.push([]); //in case of odd number of items
  for (var lim = len; lim > 1; lim = (lim + 1) / 2) {
      for (var j = 0,
      k = 0; k < lim; j++, k += 2) {
          work[j] = merge(work[k], work[k + 1]);
      }
      work[j] = []; //in case of odd number of items
  }
  return work[0];
}

这 个归并排序算法实现使用了一系列循环来代替递归进行排序。由于归并排序首先要将数组拆分成若干只有一个元素的数组,这个方法更加明确的执行了这个操作,而 不是通过递归函数隐晦的完成。work数组被初始化为包含一堆只有一个元素数组的数组。在循环中每次会合并两个数组,并将合并后的结果放回 work数组中。当函数执行完成后,排序的结果会通过work数组中的第一个元素返回。在这个归并排序的实现中,没有使用任何递归,同样也实现了这个算 法。然而,这样做却引入了大量的循环,循环的次数基于要排序的数组中元素的个数,所以我们可能需要使用在上篇讨论过的技术来进行修订,处理这些额外开销。



总结一下基本原则,不管是什么时候使用递归的时候都应该小心谨慎。memoization和迭代是代替递归的两种解决方案,最直接的结果当然就是避免那个提示脚本失控的对话框。
分享到:
评论

相关推荐

    javascript中递归函数用法注意点

    2. **效率问题**:递归函数的效率较低,因为每次函数调用都会在内存中创建新的堆栈帧。如果递归深度过大,可能导致栈溢出。 3. **缓存结果**:对于重复计算的情况,可以使用记忆化(memoization)技术来存储之前...

    json字符串递归解析

    JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,被广泛用于Web应用程序之间传递数据。它基于JavaScript的一个...在实际编程中,理解并熟练掌握这一技巧,对于提升开发效率和代码质量有着重要的作用。

    JavaScript递归函数定义与用法实例分析

    JavaScript中的递归函数是一种强大的编程技术,它允许一个函数在其定义内部调用自身,从而解决复杂问题。在本文中,我们将深入探讨递归函数的定义、工作原理以及如何避免潜在问题。 递归函数通常用于处理那些可以...

    javascript-leetcode面试题解递归与回溯问题之第59题递归矩阵-题解.zip

    在JavaScript编程领域,LeetCode是一个非常重要的在线平台,它提供了大量的编程题目,帮助开发者提升算法能力和准备求职面试。本题解主要关注的是LeetCode中的第59题,这是一个关于递归与回溯策略的问题,特别是在...

    JavaScript递归操作实例浅析

    JavaScript中的递归是一种强大的编程技巧,它允许函数在其定义内部调用自身,通常用于解决具有重复子问题的问题。...学习和掌握递归是提升JavaScript编程技能的重要一环,对于理解和解决复杂问题非常有帮助。

    递归程序

    2. **效率**:递归通常不如迭代算法效率高,因为每次递归调用都会增加内存开销。在性能敏感的应用中,可能需要考虑使用循环或其他非递归方法。 3. **可读性**:尽管递归可以使代码更简洁,但也可能导致逻辑更难以...

    javascript中递归的两种写法

    在JavaScript中,递归是一种强大的编程技术,它允许函数调用自身来解决问题。递归通常用于处理层次结构数据、树形结构或者解决那些可以分解...理解这两种方法的优缺点以及何时选择它们是提升JavaScript编程技能的关键。

    JsRecursiveTree:Javascript递归树绘制

    JavaScript是一种广泛应用于网页和网络应用开发的脚本...总的来说,JavaScript递归树绘制是一个结合了数据结构、算法、DOM操作和事件处理的综合实践,对于提升JavaScript开发者在前端数据可视化方面的能力大有裨益。

    javascript-leetcode面试题解递归与回溯问题之第1219题黄金矿工-题解.zip

    本题解聚焦于JavaScript解决LeetCode中的第1219题,即“黄金矿工”问题,它涉及到递归与回溯这两种重要的算法。 递归是编程中的一种基本概念,指的是函数在其定义中调用自身的过程。在解决复杂问题时,递归可以使...

    javascript-leetcode面试题解递归与回溯问题之第1079题活字印刷-题解.zip

    此外,为了防止重复计算和提高效率,可以使用哈希表或集合来记录已经使用过的字符,避免在后续的递归调用中重复尝试。在返回所有可能的组合时,可以使用数组或生成器来收集和返回结果。 在实际解题过程中,我们需要...

    javascript-leetcode面试题解递归与回溯问题之第77题组合-题解.zip

    在JavaScript中,回溯往往与递归结合使用,通过在递归过程中剪枝(剔除无效的解决方案)来提高效率。 第77题“组合”要求找出所有可能的组合,使得给定数量的元素可以从给定的无重复元素集合中选取。这可以通过回溯...

    如何提升JavaScript的运行速度(循环篇).doc

    JavaScript 优化是一个重要的主题,尤其是对于提升网页应用的性能至关重要。循环是JavaScript代码中常见的结构,但是不恰当的循环设计可能会严重拖慢程序运行速度,甚至导致浏览器出现“脚本失控”的警告。以下是对...

    javascript-leetcode面试题解递归与回溯问题之第401题二进制手表-题解.zip

    在实际编写代码时,需要注意边界条件的处理,避免无限递归,以及优化回溯过程以提高效率。递归与回溯的结合可以有效地解决这类组合问题,同时它也是许多面试题中的常见题型,如排列组合、迷宫问题、子集问题等。 总...

    JavaScript采用递归算法计算阶乘实例

    因此,在实际应用中,我们可能需要考虑使用尾递归优化、循环或其他非递归方法来计算阶乘,以提高效率并避免潜在的问题。 在JavaScript中,由于引擎没有内置的尾递归优化,即使使用尾递归(即递归调用是函数体的最后...

    循环代替递归

    在编程领域,循环和递归是两种常见的控制流结构,它们都能用来解决一系列问题,但实现方式和效率有所不同。本文将深入探讨“循环代替递归”这一...在实际编程中,了解如何灵活运用循环替代递归,能有效提升程序性能。

    优雅的使用javascript递归画一棵结构树示例代码

    但是作为一个合格的程序员,我们也因该知道,递归算法相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。在递归调用的过程当中系统...

    javascript函数式编程函数柯里化,惰性函数,递归,纯函数.docx

    JavaScript 函数式编程之函数柯里化、惰性函数、递归、纯函数 函数柯里化是 JavaScript 函数式编程中的一种重要概念。它允许我们将函数的参数分批传递,从而实现函数的预加载和缓存。柯里化的优点在于可以提高函数...

Global site tag (gtag.js) - Google Analytics