JS技术

JavaScript之递归模式

字号+ 作者: 来源: 2014-11-16 22:20 我要评论( )

JavaScript之递归模式

      当你陷入调用栈尺寸限制时,第一步应该定位在代码中的递归实例上。为此,有两个递归模式值得注意。首先是直接递归模式为代表的前面提到的factorial()函数,即一个函数调用自身。其一般模式如下:
      function recurse(){
            recurse();
      }
      recurse();
      当发生错误时,这种模式比较容易定位。另外一种模式称为精巧模式,它包含两个函数:
      function first(){
            second();
      }

      function second(){
            first();
      }
      first();
      在这种递归模式中,两个函数互相调用对方,形成一个无限循环。这是一个令人不安的模式,在大型代码库中定位错误很困难。
      大多数调用栈错误与这两种模式之一有关。常见的栈溢出原因是一个不正确的终止条件,所以定位模式错误的第一步是验证终止条件。如果终止条件是正确的,那么算法包含了太多层递归,为了能够安全地在浏览器中运行,应当改用迭代制表,或两者兼而有之。
      memoization,没错,就是这么个单词,译为“制表”,不是memorization!memoization,又称tabulation,源自键盘上的tab键,就是用一个数组栈记录每次递归的结果,如果某值曾经计算过,那么直接从数组栈中以查表法获得结果,而不必重复计算。
      一、迭代
      任何可以用递归实现的算法都可以用迭代实现。迭代算法通常包括几个不同的循环,分别对应算法过程的不同方面,也会导致自己的性能为题。但是,使用优化的循环替代长时间运行的递归函数可以提高性能,因为运行一个循环比反复调用一个函数的开销要低。例如,合并排序算法是最常用的以递归实现的算法。一个简单的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()函数被调用非常频繁。一个具有n个项的数组总共调用mergeSort()达2 * n – 1 次,也就是说,对一个超过1500 个项的数组操作,就可能在Firefox上导致栈溢出。
      程序陷入栈溢出错误并不一定要修改整个算法;它只是意味着递归不是最好的实现方法。合并排序算法还可以用迭代实现,如下:
      //uses the same mergeSort() function from previous example
      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];
      }
      此mergeSort()实现与前面的函数实现同样功能而没有使用递归。虽然迭代版本的合并排序可能比递归版本的慢一些,但它不会像递归版本那样影响调用栈。将递归算法切换为迭代只是避免栈溢出错误的方法之一。
      二、制表
      减少工作量就是最好的性能优化技术。代码所做的事情越少,它的运行速度就越快。根据这些原则,避免重复工作也很有意义。多次执行相同的任务也在浪费时间。制表,通过缓存先前计算结果为后续计算所重复使用,避免了重复工作。这使得制表成为递归算法中有用的技术。当递归函数多次被调用时,重复工作很多。在factorial()函数中(在前面介绍过的阶乘函数),是一个递归函数重复多次的典型例子。考虑下面的代码:
      var fact6 = factorial(6);
      var fact5 = factorial(5);
      var fact4 = factorial(4);
      此代码生成三个阶乘结果,factorial()函数总共被调用了18次。此代码中最糟糕的部分是,所有必要的计算已经在第一行代码中执行过了。因为6的阶乘等于6乘以5的阶乘,所以5的阶乘被计算了两次。更糟糕的是,4的阶乘被计算了三次。更为明智的方法是保存并重利用它们的计算结果,而不是每次都重新计算整个函数。你可以使用制表技术来重写factorial()函数,如下:
      function memfactorial(n){
            if (!memfactorial.cache){
                  memfactorial.cache = {
                        "0": 1,
                        "1": 1
                  };
            }
            if (!memfactorial.cache.hasOwnProperty(n)){
                  memfactorial.cache[n] = n * memfactorial (n-1);
            }
            return memfactorial.cache[n];
      }
      这个使用制表技术的阶乘函数的关键是建立一个缓存对象。此对象位于函数内部,并预置了两个最简单的阶乘:0 和1。在计算阶乘之前,首先检查缓存中是否已经存在相应的计算结果。没有对应的缓冲值说明这是第一次进行此数值的计算,计算完成之后结果被存入缓存之中,以备今后使用。此函数与原始版本的factorial()函数用法相同。
      var fact6 = memfactorial(6);
      var fact5 = memfactorial(5);
      var fact4 = memfactorial(4);
      此代码返回三个不同的阶乘值,但总共只调用memfactorial()函数八次。既然所有必要的计算都在第一行代码中完成了,那么后两行代码不会产生递归运算,因为直接返回缓存中的数值。制表过程因每种递归函数而略有不同,但总体上具有相同的模式。为了使一个函数的制表过程更加容易,你可以定义一个memoize()函数封装基本功能。例如:
      function memoize(fundamental, cache){
            cache = cache || {};
            var shell = function(arg){
                  if (!cache.hasOwnProperty(arg)){
                        cache[arg] = fundamental(arg);
                  }
                  return cache[arg];
            };
            return shell;
      }
      此memoize()函数接收两个参数:一个用来制表的函数和一个可选的缓存对象。如果你打算预设一些值,那么就传入一个预定义的缓存对象;否则它将创建一个新的缓存对象。然后创建一个外壳函数,将原始函数(fundamential)包装起来,确保只有当一个此前从未被计算过的值传入时才真正进行计算。计算结果由此外壳函数返回,你可以直接调用它,例如:
      //memoize the factorial function
      var memfactorial = memoize(factorial, { "0": 1, "1": 1 });
      //call the new function
      var fact6 = memfactorial(6);
      var fact5 = memfactorial(5);
      var fact4 = memfactorial(4);
      这种通用制表函数与人工更新算法相比优化较少,因为memoize()函数缓存特定参数的函数调用结果。当代码以同一个参数多次调用外壳函数时才能节约时间(如果外壳函数内部还存在递归,那么内部的递归就不能享用这些中间运算结果了)。因此,当一个通用制表函数存在显著性能问题时,最好在这些函数中人工实现制表法。

 

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

相关文章
  • WEB前端教程-JavaScript里的类和继承

    WEB前端教程-JavaScript里的类和继承

    2016-01-21 15:28

  • 高性能动画“box-shadow”属性 - FedFun - 博客频道 - CSDN.NET FedFun 爱前端,乐分

    高性能动画“box-shadow”属性 - FedFun - 博客频道 - CSDN.NET FedF

    2015-12-14 16:15

  • JS开发者调查 - FedFun - 博客频道 - CSDN.NET FedFun 爱前端,乐分享,前端痴王海庆的博客!

    JS开发者调查 - FedFun - 博客频道 - CSDN.NET FedFun 爱前端,乐分

    2015-12-13 11:08

  • Jquery下编写流行的前端的应用源码_Javascript教程

    Jquery下编写流行的前端的应用源码_Javascript教程

    2015-10-01 09:24

网友点评