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 相关文章推荐
Javascript实现的分页函数
Feb 07 Javascript
javascript HTMLEncode HTMLDecode的完整实例(兼容ie和火狐)
Jun 02 Javascript
学习从实践开始之jQuery插件开发 菜单插件开发
May 03 Javascript
ie与ff下的event事件使用介绍
Nov 25 Javascript
javascript四舍五入函数代码分享(保留后几位)
Dec 10 Javascript
jQuery照片伸缩效果不影响其他元素的布局
May 09 Javascript
Grunt入门教程(自动任务运行器)
Aug 06 Javascript
jQuery筛选数组之grep、each、inArray、map的用法及遍历json对象
Jun 20 Javascript
Vue.js实战之Vuex的入门教程
Apr 01 Javascript
微信小程序表单弹窗实例
Jul 19 Javascript
解决vue项目nginx部署到非根目录下刷新空白的问题
Sep 27 Javascript
Vue源码中要const _toStr = Object.prototype.toString的原因分析
Dec 09 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访问MYSQL数据库封装类(附函数说明)
2010/12/04 PHP
php阿拉伯数字转中文人民币大写
2015/12/21 PHP
php session实现多级目录存放实现代码
2016/02/03 PHP
PHP正则表达式匹配替换与分割功能实例浅析
2017/02/04 PHP
php根据命令行参数生成配置文件详解
2019/03/15 PHP
解决jquery1.9不支持browser对象的问题
2013/11/13 Javascript
JavaScript DOM 学习总结(五)
2015/11/24 Javascript
极力推荐一款小巧玲珑的可视化编辑器bootstrap-wysiwyg
2016/05/27 Javascript
浅谈JS读取DOM对象(标签)的自定义属性
2016/11/21 Javascript
BootStrap Table后台分页时前台删除最后一页所有数据refresh刷新后无数据问题
2016/12/28 Javascript
vue.js 微信支付前端代码分享
2018/02/10 Javascript
Vue前后端不同端口的实现方法
2018/09/19 Javascript
前端Electron新手入门教程详解
2019/06/21 Javascript
jQuery--遍历操作实例小结【后代、同胞及过滤】
2020/05/22 jQuery
详解Python中的元组与逻辑运算符
2015/10/13 Python
13个最常用的Python深度学习库介绍
2017/10/28 Python
python交互式图形编程实例(三)
2017/11/17 Python
python如何拆分含有多种分隔符的字符串
2018/03/20 Python
Python3 批量扫描端口的例子
2019/07/25 Python
Python爬虫实现“盗取”微信好友信息的方法分析
2019/09/16 Python
python 判断txt每行内容中是否包含子串并重新写入保存的实例
2020/03/12 Python
matplotlib 对坐标的控制,加图例注释的操作
2020/04/17 Python
DNA基因检测和分析:23andMe
2019/05/01 全球购物
Footshop乌克兰:运动鞋的最大选择
2019/12/01 全球购物
夜不归宿检讨书
2014/02/25 职场文书
中级会计职业生涯规划书
2014/03/01 职场文书
春节联欢会主持词
2014/03/24 职场文书
个人简历自荐信
2014/06/26 职场文书
有子女的离婚协议书怎么写(范本)
2014/09/29 职场文书
学习型家庭事迹材料
2014/12/20 职场文书
作文评语怎么写
2014/12/25 职场文书
2015年党风廉政建设目标责任书
2015/05/08 职场文书
2016春季田径运动会广播稿
2015/12/21 职场文书
小学英语听课心得体会
2016/01/14 职场文书
MySql存储过程之逻辑判断和条件控制
2021/05/26 MySQL
POST提交数据常见的四种方式
2022/01/18 HTML / CSS