Javascript 高性能之递归,迭代,查表法详解及实例


Posted in Javascript onJanuary 08, 2017

Javascript 高性能之递归,迭代,查表法详解

递归

概念:函数通过直接调用自身,或者两个函数之间的互相调用,来达到一定的目的,比如排序,阶乘等

简单的递归

阶乘

function factorial(n) {
  if (n == 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

递归实现排序

/*
  排序且合并数组
 */
function myMerge(left, right) {
  // 保存最后结果的数组
  var res = [];

  // 有一个数组结束了就结束排序,一般情况下,两个数组长度应该保持一样
  while (left.length > 0 && right.length > 0) {
    if ( left[0] < right[0] ) {
      // 把left第一个成员删除,存储到新数组res中
      res.push(left.shift());
    } else {
      res.push(right.shift());
    }
  }

  // 如果还有剩余直接合并到新数组
  return res.concat(left).concat(right);
}

/*
  递归方式
 */
function recursion(items) {
  if (items.length == 1) {
    return items;
  }

  var middle = Math.floor(items.length / 2),
    left = items.slice(0, middle), // 取数组前半部分
    right = items.slice(middle);  // 取数组后半部分

  // 递归排序
  return myMerge(recursion(left), recursion(right));
}

迭代

每个浏览器对递归都有调用栈上限的问题,且如果不小心使用也很容易造成死循环导致程序崩溃。如果考虑到性能问题,可以使用迭代来代替递归,因为运行循环总比反复调用函数的开销少很多

/*
  迭代方式,不使用递归,可以避免出现栈溢出问题
 */

function iteration(items) {
  if (items.length == 1) {
    return items;
  }

  var work = [];

  // 将items成员全部转化成数组,保存到数组中
  for (var i = 0, len = items.length; i < len; i++) {
    work.push([items[i]]);
  }
  work.push([]); // 数组长度为奇数时

  // 迭代
  for (var lim = len; lim > 1; lim = (lim + 1) / 2) {
    for (var j = 0, k = 0; k < lim; j++, k+=2) {
      work[j] = myMerge(work[k], work[k + 1]);
    }
    work[j] = [];  // 数组长度为奇数时
  }

  return work[0];
}

/* 迭代过程分析
  假设: var test = [1, 3, 9, 7, 4, 8, 6, 5, 0, 2]; // len == 10
  work = [[1], [3], [9], [7], [4], [8], [6], [5], [0], [2], []]; // len == 11;

  // 迭代(二分法)
  a) lim: 11 > 1
    1) k = 0, work[0] = myMerge([1], [3]) ==> work[0] = [1, 3]
    2) k = 2, work[1] = myMerge([9], [7]) ==> work[1] = [7, 9]
    3) k = 4, work[2] = myMerge([4], [8]) ==> work[2] = [4, 8]
    4) k = 6, work[3] = myMerge([6], [5]) ==> work[3] = [5, 6]
    5) k = 8, work[4] = myMerge([0], [2]) ==> work[4] = [0, 2]
    > 在后面添加个空数组是为了数组长度为奇数时的情况,能有个对象做比较,否则会出现越界错误
  b) lim: 6 > 1, work = [[1,3], [7,9], [4,8], [5,6], [0,2], []];
    1) k = 0, work[0] = myMerge([1,3], [7,9]) ==> work[0] = [1, 3, 7, 9]
    2) k = 2, work[1] = myMerge([4,8], [5,6]) ==> work[1] = [4, 5, 6, 8]
    3) k = 4, work[2] = myMerge([0,2], [])  ==> work[2] = [0,2]
    > 最后一个[]会被myMerge函数给合并,所以不用担心添加的空数组对后续产生影响
  c) lim: 3 > 1, work = [[1, 3, 7, 9], [4, 5, 6, 8], [0, 2], []];
    1) k = 0, work[0] = myMerge([1, 3, 7, 9], [4, 5, 6, 8]) ==> work[0] = [1,3,4,5,6,7,8,9]
    2) k = 2, work[1] = myMerge([0, 2], []) ==> work[1] = [0, 2]
  d) lim: 2, work = [[1,3,4,5,6,7,8,9], [0,2], []];
    1) k = 0, work[0] = myMerge([1,3,4,5,6,7,8,9], [0,2]) ==> work[0] = [0,1,2,3,4,5,6,7,8,9]
    > 至此排序整个过程全部完成

  // 关键点
  a) 将数组中的每个元素数组化,以便后续存放已经排好序的元素
  b) k的取值,k+=2, 每次取两个数组进行比较,形成一个新的数组
  c) 一定要在比较完之后附加空数组,否则会在数组个数为奇数个的时候出现访问越界错误
  d) 最外层循环的lim的取值, lim = (lim + 1) / 2即原数组长度的一半,作为内循环终止的条件


*/

递归优化,查表法-Memoization(记忆): 函数可以用对象去记住先前操纵的成果,从而能避免无谓的运算

避免重复工作,将执行过的运算或操作,缓存起来,如果后续有相同的操作可直接从缓存中查找,没有则进行递归,可大大减少递归的工作量,提高性能。

var count = 0;

function factorial(n) {

  console.log("factorial count = " + count++);

  if (n == 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

// var f1 = factorial(6);
// var f2 = factorial(5);
// var f3 = factorial(4);
// >>>>> 结果是函数被调用了:18次

/*
  递归优化:查表法,通过缓存
 */
function memFactorial(n) {

  console.log("memFactorial count = " + count++);

  // JS中函数即可视为一个对象,所以可以直接通过函数名+点语法给此对象添加属性
  if (!memFactorial.cache) { 
    memFactorial.cache = {
      "0": 1,
      "1": 1
    };
  }

  // hasOwnProperty(n)可以判断对象中是否存在该属性,不会查找原型(但是"in"会先查实例再原型)
  if (!memFactorial.cache.hasOwnProperty(n)) {
    // 缓存中不存在的情况下再进行递归,并且将新数组缓存起来
    memFactorial.cache[n] = n * memFactorial(n - 1);
  }

  // 最终数据都可以在缓存中找到
  return memFactorial.cache[n];
}


var f1 = memFactorial(6);
var f2 = memFactorial(5);
var f3 = memFactorial(4);

// >>>>> 结果是函数被调用了:只有8次,大大的减少了函数被调用的次数

Memoization通用版,但不建议使用,建议自己手动在普通版中实现函数内容

通用版需要缓存特定参数的函数调用结果,即,传入的参数不同,调用函数的时候,其结果需要缓存起来

/*
  递归优化通用版,性能不如普通版,需要缓存每次调用结果, 即内部函数  
 */ 
function memoize(func, cache) {
  // 缓存池
  cache = cache || {};  // 没有则新建

  var result = function(arg) {
    // console.log("memoize count = " + count++);
    if (!cache.hasOwnProperty(arg)) {
      cache[arg] = func(arg);
    }
  };

  return result;
}  

// 使用
// 将阶乘函数缓存起来
// 只是优化了cache的通用性,但损失了一部分性能
var memOpfactorial = memoize(factorial, {"0": 1, "1": 1});

var f1 = memOpfactorial(6);
var f2 = memOpfactorial(5);
var f3 = memOpfactorial(4);

// 结果次数依旧是:18次。因此不推荐使用

总结

  1. 谨慎使用递归,以防造成死循环,程序崩溃,或者调用栈溢出;
  2. 学会使用迭代来替代递归,可以避免递归调用栈或死循环问题出现;
  3. 查表法,递归的优化版:Memoization减少重复工作,提升性能。但不推荐使用通用版,相比普通版会损失部分性能。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

Javascript 相关文章推荐
可简单避免的三个JS发布错误的详细介绍
Aug 02 Javascript
兼容Firefox的Javascript XSLT 处理XML文件
Dec 31 Javascript
JQuery插件Quicksand实现超炫的动画洗牌效果
May 03 Javascript
jquery实现鼠标拖拽滑动效果来选择数字的方法
May 04 Javascript
javascript实现带下拉子菜单的导航菜单效果
May 14 Javascript
javascript运动框架用法实例分析(实现放大与缩小效果)
Jan 08 Javascript
JavaScript关联数组用法分析【概念、定义、遍历】
Mar 15 Javascript
vue 注册组件的使用详解
May 05 Javascript
浅谈vue方法内的方法使用this的问题
Sep 15 Javascript
JavaScript实现学生在线做题计时器功能
Dec 05 Javascript
封装微信小程序http拦截器过程解析
Aug 13 Javascript
解决vue 给window添加和移除resize事件遇到的坑
Jul 21 Javascript
jQuery实现页面滚动时智能浮动定位
Jan 08 #Javascript
jQuery实现滚动条滚动到子元素位置(方便定位)
Jan 08 #Javascript
jquery实现文字单行横移或翻转(上下、左右跳转)
Jan 08 #Javascript
jQuery实现文字自动横移
Jan 08 #Javascript
div实现自适应高度的textarea实现angular双向绑定
Jan 08 #Javascript
JavaScript中日常收集常见的10种错误(推荐)
Jan 08 #Javascript
详解js中==与===的区别
Jan 08 #Javascript
You might like
php addslashes 利用递归实现使用反斜线引用字符串
2013/08/05 PHP
Win7 64位系统下PHP连接Oracle数据库
2014/08/20 PHP
php判断数组中是否存在指定键(key)的方法
2015/03/17 PHP
php实现的http请求封装示例
2016/11/08 PHP
Laravel5框架添加自定义辅助函数的方法
2018/08/01 PHP
Laravel 解决composer相关操作提示php相关异常的问题
2019/10/23 PHP
laravel框架使用FormRequest进行表单验证,验证异常返回JSON操作示例
2020/02/18 PHP
js 创建书签小工具之理论
2011/02/25 Javascript
google jQuery 引用文件,jQuery 引用地址集合(jquery 1.2.6至jquery1.5.2)
2011/04/24 Javascript
通过JavaScript使Div居中并随网页大小改变而改变
2013/06/24 Javascript
js 控制页面跳转的5种方法
2013/09/09 Javascript
javascript在网页中实现读取剪贴板粘贴截图功能
2014/06/07 Javascript
javascript基于HTML5 canvas制作画箭头组件
2014/06/25 Javascript
理解JavaScript事件对象
2016/01/25 Javascript
JS实现的仿QQ空间图片弹出效果代码
2016/02/23 Javascript
深入浅出ES6之let和const命令
2016/08/25 Javascript
JQuery实现图片轮播效果
2017/05/08 jQuery
js replace 全局替换的操作方法
2018/06/12 Javascript
Node.js assert断言原理与用法分析
2019/01/04 Javascript
Vue2.0实现简单分页及跳转效果
2019/07/29 Javascript
vue+element tabs选项卡分页效果
2020/06/29 Javascript
ant-design-vue中的select选择器,对输入值的进行筛选操作
2020/10/24 Javascript
Python实现新浪博客备份的方法
2016/04/27 Python
详解python字节码
2018/02/07 Python
python三大神器之fabric使用教程
2019/06/10 Python
Django中自定义模型管理器(Manager)及方法
2019/09/23 Python
python多线程semaphore实现线程数控制的示例
2020/08/10 Python
乌克兰网上服装店:Bolf.ua
2018/10/30 全球购物
波兰多品牌运动商店:StreetStyle24.pl
2020/09/22 全球购物
AJax面试题
2014/11/25 面试题
会计主管岗位职责
2014/01/03 职场文书
会计出纳员的自我评价
2014/01/15 职场文书
职业生涯规划书怎么写?
2014/09/14 职场文书
汽车销售助理岗位职责
2015/04/14 职场文书
社区宣传标语口号
2015/12/26 职场文书
2016教师学习党章心得体会
2016/01/15 职场文书