递归循环一个无限极数组_理解递归、尾调用优化和蹦床函数优化
想要理解递归,您必须先理解递归。开个玩笑罢了, 递归 是一种编程技巧,它可以让函数在不使用 for 或 while 的情况下,使用一个调用自身的函数来实现循环。
例子 1:整数总和
例如,假设我们想要求从 1 到 i 的整数的和,目标是得到以下结果:
sumIntegers(1); // 1sumIntegers(3); // 1 + 2 + 3 = 6sumIntegers(5); // 1 + 2 + 3 + 4 + 5 = 15复制代码
这是不用递归来实现的代码:
// 循环const sumIntegers = i => { let sum = 0; // 初始化 do { // 重复 sum += i; // 操作 i --; // 下一步 } while(i > 0); // 循环停止的条件 return sum;}复制代码
用递归来实现的代码如下:
// 循环const sumIntegers = (i, sum = 0) => { // 初始化 if (i === 0) { // return sum; // 结果 } return sumIntegers( // 重复 i - 1, // 下一步 sum + i // 操作 );}// 甚至实现得更简单const sumIntegers = i => { if (i === 0) { return i; } return i + sumIntegers(i - 1);}复制代码
这就是递归的基础。
注意,递归版本中是没有 中间变量 的。它不使用 for 或者 do...while 。由此可见,它是 声明式 的。
我还可以告诉您的是,事实上递归版本比循环版本 慢 —— 至少在 JavaScript 中是这样。但是递归解决的不是性能问题,而是可表达性的问题。
例子 2:数组元素之和
让我们尝试一个稍微复杂一点的例子,一个将数组中的所有数字相加的函数。
sumArrayItems([]); // 0sumArrayItems([1, 1, 1]); // 1 + 1 + 1 = 3sumArrayItems([3, 6, 1]); // 3 + 6 + 1 = 10// 循环const sumArrayItems = list => { let result = 0; for (var i = 0; i++; i <= list.length) { result += list[i]; } return result;}复制代码
正如您所看到的,循环版本是命令性的:您需要确切地告诉程序要 做什么 才能得到所有数字的和。下面是递归的版本:
// 递归const sumArrayItems = list => { switch(list.length) { case 0: return 0; // 空数组的和为 0 case 1: return list[0]; // 一个元素的数组之和,就是这个唯一的元素。#显而易见 default: return list[0] + sumArrayItems(list.slice(1)); // 否则,数组的和就是数组的第一个元素 + 其余元素的和。 }}复制代码
递归版本中,我们并没有告诉程序要 做什么 ,而是引入了简单的规则来 定义 数组中所有数字的和是多少。这可比循环版本有意思多了。
如果您是函数式编程的爱好者,您可能更喜欢 Array.reduce() 版本:
// reduce 版本const sumArrayItems = list => list.reduce((sum, item) => sum + item, 0);复制代码
这种写法更短,而且更直观。但这是另一篇文章的主题了。
例子 3:快速排序
现在,我们来看另一个例子。这次的更复杂一点: 快速排序 。快速排序是对数组排序最快的算法之一。
快速排序的排序过程:获取数组的第一个元素,然后将其余的元素分成比第一个元素小的数组和比第一个元素大的数组。然后,再将获取的第一个元素放置在这两个数组之间,并且对每一个分隔的数组重复这个操作。
要用递归实现它,我们只需要遵循这个定义:
const quickSort = array => { if (array.length <= 1) { return array; // 一个或更少元素的数组是已经排好序的 } const [first, ...rest] = array; // 然后把所有比第一个元素大和比第一个元素小的元素分开 const smaller = [], bigger = []; for (var i = 0; i < rest.length; i++) { const value = rest[i]; if (value < first) { // 小的 smaller.push(value); } else { // 大的 bigger.push(value); } } // 排序后的数组为 return [ ...quickSort(smaller), // 所有小于等于第一个的元素的排序数组 first, // 第一个元素 ...quickSort(bigger), // 所有大于第一个的元素的排序数组 ];};复制代码
简单,优雅和声明式,通过阅读代码,我们可以读懂快速排序的定义。
现在想象一下用循环来实现它。我先让您想一想,您可以在本文的最后找到答案。
例子 4:取得一棵树的叶节点
当我们需要处理 递归数据结构 (如树)时,递归真的很有用。树是具有某些值和 孩子 属性的对象;孩子们又包含着其他的树或叶子(叶子指的是没有孩子的对象)。例如:
const tree = { name: 'root', children: [ { name: 'subtree1', children: [ { name: 'child1' }, { name: 'child2' }, ], }, { name: 'child3' }, { name: 'subtree2', children: [ { name: 'child1', children: [ { name: 'child4' }, { name: 'child5' }, ], }, { name: 'child6' } ] } ]};复制代码
假设我需要一个函数,该函数接受一棵树,返回一个叶子(没有孩子节点的对象)数组。预期结果是:
getLeaves(tree);/*[ { name: 'child1' }, { name: 'child2' }, { name: 'child3' }, { name: 'child4' }, { name: 'child5' }, { name: 'child6' },]*/复制代码
我们先用老方法试试,不用递归。
// 对于没有嵌套的树来说,这是小菜一碟const getChildren = tree => tree.children;// 对于一层的递归来说,它会变成:const getChildren = tree => { const { children } = tree; let result = []; for (var i = 0; i++; i < children.length - 1) { const child = children[i]; if (child.children) { for (var j = 0; j++; j < child.children.length - 1) { const grandChild = child.children[j]; result.push(grandChild); } } else { result.push(child); } } return result;}// 对于两层:const getChildren = tree => { const { children } = tree; let result = []; for (var i = 0; i++; i < children.length - 1) { const child = children[i]; if (child.children) { for (var j = 0; j++; j < child.children.length - 1) { const grandChild = child.children[j]; if (grandChild.children) { for (var k = 0; k++; j < grandChild.children.length - 1) { const grandGrandChild = grandChild.children[j]; result.push(grandGrandChild); } } else { result.push(grandChild); } } } else { result.push(child); } } return result;}复制代码
呃,这已经很令人头疼了,而且这只是两层递归。您想想看如果递归到第三层、第四层、第十层会有多糟糕。
而且这仅仅是求一些叶子;如果您想要将树转换为一个数组并返回,又该怎么办?更麻烦的是,如果您想使用这个循环版本,您必须确定您想要支持的最大深度。
现在看看递归版本:
const getLeaves = tree => { if (!tree.children) { // 如果一棵树没有孩子,它的叶子就是树本身。 return tree; } return tree.children // 否则它的叶子就是所有子节点的叶子。 .map(getLeaves) // 在这一步,我们可以嵌套数组 ([child1, [grandChild1, grandChild2], ...]) .reduce((acc, item) => acc.concat(item), []); // 所以我们用 concat 来连接铺平数组 [1,2,3].concat(4) => [1,2,3,4] 以及 [1,2,3].concat([4]) => [1,2,3,4]}复制代码
仅此而已,而且它适用于任何层级的递归。
JavaScript 中递归的缺点
遗憾的是,递归函数有一个很大的缺点:该死的越界错误。
Uncaught RangeError: Maximum call stack size exceeded复制代码
与许多语言一样,JavaScript 会跟踪 堆栈 中的所有函数调用。这个堆栈大小有一个最大值,一旦超过这个最大值,就会导致 RangeError 。在循环嵌套调用中,一旦根函数完成,堆栈就会被清除。但是在使用递归时,在所有其他的调用都被解析之前,第一个函数的调用不会结束。所以如果我们调用太多,就会得到这个错误。
为了解决堆栈大小问题,您可以尝试确保计算不会接近堆栈大小限制。这个限制取决于平台,这个值似乎都在 10,000 左右。所以,我们仍然可以在 JavaScript 中使用递归,只是需要小心谨慎。
如果您不能限制递归的大小,这里有两个解决方案:尾调用优化和蹦床函数优化。
尾调用优化
所有严重依赖递归的语言都会使用这种优化,比如 Haskell。JavaScript 的尾调用优化的支持是在 Node.js v6 中实现的。
尾调用 是指一个函数的最后一条语句是对另一个函数的调用。优化是在于让尾部调用函数替换堆栈中的父函数。这样的话,递归函数就不会增加堆栈。注意,要使其工作,递归调用必须是递归函数的 最后一条语句 。所以 return loop(..); 是一次有效的尾调用优化,但是 return loop() + v; 不是。
让我们把求和的例子用尾调用优化一下:
const sum = (array, result = 0) => { if (!array.length) { return result; } const [first, ...rest] = array; return sum(rest, first + result);}复制代码
这使运行时引擎可以避免调用堆栈错误。但是不幸的是,它在 Node.js 中已经不再有效,因为 在 Node 8 中已经删除了对尾调用优化的支持 。也许将来它会支持,但到目前为止,是不存在的。
蹦床函数优化
另一种解决方法叫做 蹦床函数 。其思想是使用延迟计算稍后执行递归调用,每次执行一个递归。我们来看一个例子:
const sum = (array) => { const loop = (array, result = 0) => () => { // 代码不是立即执行的,而是返回一个稍后执行的函数:它是惰性的 if (!array.length) { return result; } const [first, ...rest] = array; return loop(rest, first + result); }; // 当我们执行这个循环时,我们得到的只是一个执行第一步的函数,所以没有递归。 let recursion = loop(array); // 只要我们得到另一个函数,递归过程中就还有其他步骤 while (typeof recursion === 'function') { recursion = recursion(); // 我们执行现在这一步,然后重新得到下一个 } // 一旦执行完毕,返回最后一个递归的结果 return recursion;}复制代码
这是可行的,但是这种方法也有一个很大的缺点:它很 慢 。在每次递归时,都会创建一个新函数,在大型递归时,就会产生大量的函数。这就很令人心烦。的确,我们不会得到一个错误,但这会减慢(甚至冻结)函数运行。
从递归到迭代
如果最终出现性能或者最大调用堆栈大小超出的问题,您仍然可以将递归版本转换为迭代版本。但不幸的是,正如您将看到的,迭代版本通常更复杂。
让我们以 getLeaves 的实现为例,并将递归逻辑转换为迭代。我知道结果,我以前试过,很糟糕。现在我们再试一次,但这次是递归的。
// 递归版本const getLeaves = tree => { if (!tree.children) { // 如果一棵树没有孩子,它的叶子就是树本身。 return tree; } return tree.children // 否则它的叶子就是所有子节点的叶子。 .map(getLeaves) // 在这一步,我们可以嵌套数组 ([child1, [grandChild1, grandChild2], ...]) .reduce((acc, item) => acc.concat(item), []); // 所以我们用 concat 来连接铺平数组 [1,2,3].concat(4) => [1,2,3,4] 以及 [1,2,3].concat([4]) => [1,2,3,4]}复制代码
首先,我们需要重构递归函数以获取累加器参数,该参数将用于构造结果。它写起来甚至会更短:
const getLeaves = (tree, result = []) => { if (!tree.children) { return [...result, tree]; } return tree.children .reduce((acc, subTree) => getLeaves(subTree, acc), result);}复制代码
然后,这里技巧就是将递归调用展开到剩余计算的堆栈中。 在递归外部 初始化结果累加器,并将进入递归函数的参数推入堆栈。最后,将堆叠的运算解堆叠,得到最后的结果:
const getLeaves = tree => { const stack = [tree]; // 将初始树添加到堆栈中 const result = []; // 初始化结果累加器 while (stack.length) { // 只要堆栈中有一个项 const currentTree = stack.pop(); // 得到堆栈中的第一项 if (!currentTree.children) { // 如果一棵树没有孩子,它的叶子就是树本身。 result.unshift(currentTree); // 所以把它加到结果里 continue; } stack.push(...currentTree.children);// 否则,将所有子元素添加到堆栈中,以便在下一次迭代中处理 } return result;}复制代码
这好像有点难,所以让我们用 quickSort 再次做一次。这是递归版本:
const quickSort = array => { if (array.length <= 1) { return array; // 一个或更少元素的数组是已经排好序的 } const [first, ...rest] = array; // 然后把所有比第一个元素大和比第一个元素小的元素分开 const smaller = [], bigger = []; for (var i = 0; i < rest.length; i++) { const value = rest[i]; if (value < first) { // 小的 smaller.push(value); } else { // 大的 bigger.push(value); } } // 排序后的数组为 return [ ...quickSort(smaller), // 所有小于等于第一个的元素的排序数组 first, // 第一个元素 ...quickSort(bigger), // 所有大于第一个的元素的排序数组 ];};复制代码
const quickSort = (array, result = []) => { if (array.length <= 1) { return result.concat(array); // 一个或更少元素的数组是已经排好序的 } const [first, ...rest] = array; // 然后把所有比第一个元素大和比第一个元素小的元素分开 const smaller = [], bigger = []; for (var i = 0; i < rest.length; i++) { const value = rest[i]; if (value < first) { // 小的 smaller.push(value); } else { // 大的 bigger.push(value); } } // 排序后的数组为 return [ ...quickSort(smaller, result), // 所有小于等于第一个的元素的排序数组 first, // 第一个元素 ...quickSort(bigger, result), // 所有大于第一个的元素的排序数组 ];};复制代码
然后使用堆栈来存储数组进行排序,在每个循环中应用前面的递归逻辑将其解堆栈。
const quickSort = (array) => { const stack = [array]; // 我们创建一个数组堆栈进行排序 const sorted = []; //我们遍历堆栈直到它被清空 while (stack.length) { const currentArray = stack.pop(); // 我们取堆栈中的最后一个数组 if (currentArray.length == 1) { // 如果只有一个元素,那么我们把它加到排序中 sorted.push(currentArray[0]); continue; } const [first, ...rest] = currentArray; // 否则我们取数组中的第一个元素 //然后把所有比第一个元素大和比第一个元素小的元素分开 const smaller = [], bigger = []; for (var i = 0; i < rest.length; i++) { const value = rest[i]; if (value < first) { // 小的 smaller.push(value); } else { // 大的 bigger.push(value); } } if (bigger.length) { stack.push(bigger); // 我们先向堆栈中添加更大的元素来排序 } stack.push([first]); // 我们在堆栈中添加 first 元素,当它被解堆时,更大的元素就已经被排序了 if (smaller.length) { stack.push(smaller); // 最后,我们将更小的元素添加到堆栈中来排序 } } return sorted;}复制代码
瞧!我们就这样有了快速排序的迭代版本。但是记住,这只是一个优化,
不成熟的优化是万恶之源 —— 唐纳德·高德纳
因此,仅在您需要时再这样做。
结论
我喜欢递归。它比迭代版本更具声明式,并且通常情况下代码也更短。递归可以轻松地实现复杂的逻辑。尽管存在堆栈溢出问题,但在不滥用的前提下,在 JavaScript 中使用它是没问题的。并且如果有需要,可以将递归函数重构为迭代版本。
递归循环一个无限极数组_理解递归、尾调用优化和蹦床函数优化相关推荐
- php无限极递归概念,php无限极分类递归与普通
1. 递归 public function getInfo(){ $data=$this->select(); $arr=$this->noLimit($data,$f_id=0,$lev ...
- PHP:打造一个无限极评论模块
我的毕设项目的评论模块原来是采用多说插件完成的,但是我现在希望能够自己管理评论内容,所以自己开始写评论模块.具体准备采用与简书下评论类似的结构,即一级评论直接显示在文章下方,而二三级评论显示在一级评论 ...
- php 递归实现无限极分类和排序_php 无限极分类以及使用递归实现的排序方法
至于添加删除之类的功能我就不多写了!仔细看看就知道这么用了. 难的是显示方面 希望高手扩展一下! 这是类 代码如下:<?php /*=============================== ...
- python如何初始化一个二维数组_使用Python实现一个简单的商品期货布林指标突破策略...
布林指标突破策略,思路非常简单.使用Python语言编写该策略,也非常容易实现,加上回测配置信息,有70行代码,实际可以更加精简,鉴于教学策略,没有使用难懂的Python语法,使用的是比较基础的语句. ...
- java如何定义一个变长数组_如何自定义一个长度可变数组
摘要:本文主要写了如何自定义一个长度可变数组 数组是在程序设计中,为了处理方便,把具有相同类型的若干元素按无序的形式组织起来的一种形式 在定义之初,数组的长度就被定义 新建数组有很多方式 下面两个都可 ...
- c++定义一个动态对象数组_如何在Python中自定义一个可被调用的对象实例?
前言 在关于Python描述符函数的详解三篇中,我们有提到如何基于类创建一个"描述符函数",之所以能够基于类创建这样一个概念,是因为用到了类中的__call__属性,从前述文章中可 ...
- php递归权限,PHP递归菜单/权限目录(无限极数组)
输出结果: Array ( [1] => Array ( [id] => 1 [title] => 操作员列表 [pid] => 0 [list] => Array ( ...
- new 一个结构体数组_每天一个IDA小技巧(四):结构体识别
之前提到IDA可以将一长串的数组数据声明变成一行数组声明,简化反汇编代码,对于结构体,IDA也同样支持通过各种设置工具来改善结构体代码的可读性. 这篇文章的目标是将[edx+10h]之类的结构体元素访 ...
- python循环生成二维数组_嵌套循环二维数组的计算与构造 - python
我正在尝试使用Python进行计算.我想产生一个带有嵌套循环的20 * 20数组.我不知道我的方向是否正确,但这是我的代码: w = 1.5 m = 0.556 E = np.linspace(15. ...
最新文章
- 服务器架设笔记——使用Apache插件解析简单请求
- Linux文件合并去重
- 浅析Asp.net MVC 中Ajax的使用
- anaconda安装keras_关于yolo模型的试安装及关于现阶段安排的一点想法
- python函数返回布尔值_python-3.x - 函数不返回正确的布尔值 - SO中文参考 - www.soinside.com...
- mongo(删除操作)
- Centos7 Kubernetes(k8s) 开发服务器(单服务器)部署 grafana 度量分析和可视化
- 学生信息管理系统(C语言)
- gbq6什么软件能打开_GBQ5是啥文件,用哪个软件打开
- golang http长连接
- 实验室设备选择UPS电源方法
- 计算机应用应届求职简历,计算机应用应届生个人简历模板
- HTML+CSS学习笔记(篇幅较大)
- 【Python爬虫】:模拟登录QQ空间
- 【算法】并查集的运用
- axTOCControl.HitTest方法
- 神操作!树莓派上运行滴滴开源的Logi-KafkaManager,你见过没?
- 【CISSP备考】考前情报收集
- 记事本写HTML中文出现乱码的问题
- vue使用异形swiper
热门文章
- matlab播放 视频帧,如何把连续视频帧转为视频的matlab代码 | 学步园
- python的总结与心得词云设计理念_Python编程语言:使用词云来表示学习和工作报告的主题...
- GeneXus笔记本—城市级联下拉
- 用C++实现二分查找
- linux nohup命令
- (转载)Linux编程获取本机IP地址的三种方法
- 查看当前机器.net 版本的方法
- 【转】浅析动态代理类实现过程
- 最小二乘法C#实现,简单代码
- OpenCV中的cv::String和CString互相转换