如何提升JavaScript的运行速度(递归篇)
作者:Nicholas C. Zakas 来源:nczonline.net 发布时间:2010-05-17 13:30:00
影响 JavaScript性能的另外一个杀手就是递归,在上一节中提到采用memoization技术可以优化计算数值的递归函数,但memoization 不是万能的,不是所有的递归函数都可以用memoization技术优化,本文介绍了这些情况,并介绍了解决办法,就是将递归转换为迭代,同时需要注意,本文末尾介绍的方案不是最终的方案,还需要和上一节优化循环的方案综合起来才能达到最佳效果。
以下是对原文的翻译:
递归是拖慢脚本运行速度的大敌之一。太多的递归会让浏览器变得越来越慢直到死掉或者莫名其妙的突然自动退出,所以我们一定要解决在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和迭代是代替递归的两种解决方案,最直接的结果当然就是避免那个提示脚本失控的对话框。 (翻译:明达)
猜你喜欢
- Python连接Oracle本地测试依赖安装准备Python、链接Oracle需要Python依赖和本地Oracle客户端,测试环境Orac
- Ajax类  
- 1、使用字符串函数replace>>> a = 'hello world'>>> a.r
- 前言由于一直用Linux系统,对于词典的支持特别不好,对于我这英语渣渣的人来说,当看英文文档就一直卡壳,之前用惯了有道词典,感觉很不错,虽然
- 发现这个也是偶然,在测试的时候发现的,因此问题还发现一个bug。蛮有意思~ 假如输入http://www.aspxhome.com的话,在
- 在数据库应用,我们经常要用到唯一编号,以标识记录。在MySQL中可通过数据列的AUTO_INCREMENT属性来自动生成。MySQL支持多种
- MySQL 8.0.28引入新功能MySQL 8.0.28开始,新增一个特性,支持监控统计并限制各个连接(会话)的内存消耗,避免大量用户连接
- 在CSS中,实现分栏布局有两种方法。第一种方法是使用四种CSS定位选项(absolute 、static、relative和fixed)中的
- 问题:生产环境的操作系统和数据库可能是英文版的,而我们的母语是中文,如果英语能力差点,可能有时对英语环境下的数据库脚本报错的英文提示看不懂,
- 1. 首先看要设置登陆的界面 book/view.py@user_util.my_login #相当于 select_all=my_logi
- 引言在review 一些代码中,发现经常某个类型定义的方法,其接收者既有值类型,又有指针类型,然后 Goland 就有提示: Struct
- 如何在线更改密码?<%id = Request("id")newpassword =
- 本文实例讲述了Python实现二维数组按照某行或列排序的方法。分享给大家供大家参考,具体如下:lexsort支持对数组按指定行或列的顺序排序
- Mysql 远程连接配置实现的两种方法大家在公司工作中,经常会遇到mysql数据库存储于某个人的电脑上,大家要想连接mysql服务,装有my
- 进程、线程和协程之间的关系和区别也困扰我一阵子了,最近有一些心得,写一下。进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调
- 姓名的翻译: 英语是名(First name)在前,姓(Last name)在后。中文地址的翻译:如果你英语水平不高,填表时只要国家名用英语
- 前言MySQL日志记录了MySQL数据库日常操作和错误信息。MySQL有不同类型的日志文件(各自存储了不同类型的日志),从日志当中可以查询到
- 排序排序是指以特定格式排列数据。排序算法指定以特定顺序排列数据的方式。最常见的顺序是数字或字典顺序。在 Numpy 中,我们可以使用库中提供
- 这一篇笔记介绍 Django 系统 model 的外键处理,ForeignKey 以及相应的处理方法。这是一种一对多的字段类型,表示两张表之
- 在Spring Boot JPA连接Mysql的过程中,经过 8小时后会发现断连的情况。application.properties配置如下