内容简介:递归和迭代是一枚硬币的两面,在不可变的条件下,递归提供了一种更具表现力、强大且优秀的迭代替代方法递归函数由以下两个主要部分组成:递归主要的核心思想是将问题分解为较小的问题,逐个解决后再组合,构建出整个问题的答案。
递归和迭代是一枚硬币的两面,在不可变的条件下,递归提供了一种更具表现力、强大且优秀的迭代替代方法
递归函数由以下两个主要部分组成:
- 基准
- 递归条件
递归主要的核心思想是将问题分解为较小的问题,逐个解决后再组合,构建出整个问题的答案。
具体概念不详述,可谷歌百度自行搜索。递归适合解决类似XML解析、语法树构建,深度遍历等问题。
而在Haskell这种纯函数编程语言里,原本是没有循环结构的,递归是天然代替循环的,比如求和函数(当然,Haskell有原生的sum方法支持)实现,如下所示:
_sum [] = 0 _sum (x:xs) = x + _sum xs 复制代码
你会发现两行基本表达了上述所说的递归两个主要部分。很优雅诶~
递归适当时候可以优雅的解决迭代不适合处理的问题。掌握递归思考的方式是一个长期训练的过程。
下文将带大家学习几个递归的姿势,由于篇幅有限,不详述原理。
求和的几种姿势
考虑给一个数组求和:
const nums = [1, 2, 3, 4, 5]; 复制代码
命令式
命令式的开发思维,会很自然写出以下代码:
let total = 0; for(let i = 0; i < nums.length; i++) { total += nums[i]; } console.log(total); // 15 复制代码
声明式
更进一步,学了点函数式编程的同学会写出以下代码:
const add = (x, y) => x + y; const sum = (...nums) => nums.reduce(add, 0); console.log(sum(...nums)); // 15 复制代码
递归
了解递归的同学,写出来以下代码:
function getTotal(sum, num, ...nums) { if (nums.length === 0) { return sum + num; } else { return sum + getTotal(num, ...nums); } } console.log(getTotal(...nums)); // 15 复制代码
但是,目之所及,递归还是很少用的,不仅仅常见的缺乏递归思维问题,也是有性能问题的考虑,大家会发现写递归存在栈溢出的问题:
于是我写了个函数,测试一下Chrome浏览器支持递归的深度是多少?
function getMaximumCallStack(getTotal) { const f = n => getTotal(...'1'.repeat(n).split('').map(Number)); let i = 1; while(true) { try { const res = f(i); console.log(`Stack size: ${i}, f(${i})=${res}`); i++; } catch(e) { console.info(`Maximum call stack size: ${i}`); break; } } } getMaximumCallStack(getTotal); 复制代码
测试了上述写的getTotal递归,
Chrome宝宝竟然只是到了484层栈就跪了,实在不敢相信!
------------浏览器三八分割线------------
Safari宝宝表现如何呢?
貌似比Chrome好一丢丢,不过也没什么很大的区别...
那这样让我们如何愉快的使用递归呀?
递归的几种优化方式
如上文所述,递归虽然优雅,但是常常会遇到栈溢出的情况,那么这种问题怎么优化呢?以下三种优化方式:
PTC(Proper Tail Calls)
function getTotal_PTC(sum, num, ...nums) { sum += num; if (nums.length === 0) { return sum; } else { return getTotal_PTC(sum, ...nums); } } console.log(getTotal_PTC(...nums)); // 15 复制代码
PTC版的递归其实和上文写的递归只有些微写法上的区别:
// 正常递归 return sum + getTotal(num, ...nums); // PTC版的递归 return getTotal_PTC(sum, ...nums); 复制代码
改成PTC写法之后,支持支持PTC优化的浏览器,可以不断重复利用原有的栈,从而避免了栈溢出的问题。(原理大致上是由于浏览器不用保留记住每一次递归中的值,在这个函数里特指 sum + getTotal(num, ...nums) 中的sum变量值,从而新栈替换旧栈。
支持PTC优化的浏览器不多,目前可能只有Safari支持,仍然为了眼见为实,在Chrome和Safari两个浏览器进行了测试。
运行上述 工具 方法测试: getMaximumCallStack(getTotal_PTC)
Chrome宝宝很可惜的偷懒了,木有支持~(残念),见下图:
Safari宝宝果然优秀,对其有所支持!跑了一段时间,未见溢出,见下图:
CPS(Continuation Passing Style)
const getTotal_CPS = (function() { return function(...nums) { return recur(nums, v => v); }; function recur([sum, ...nums], identity) { if (nums.length === 0) { return identity(sum); } else { return recur(nums, v => identity(sum + v)); } } })(); console.log(getTotal_CPS(...nums)); // 15 复制代码
这种优化技巧通过创建额外的包裹函数:
- 将值的计算延迟
- 避免调用栈的堆积
但是不可避免的消耗了更多的内存用来存放这些多余的包裹函数。 (关于具体原理比较复杂,有空单独写篇文章论述)
Chrome浏览器测试如下图:
木有栈溢出...只是运算越来越慢。
Trampoline
function getTotal_f(sum, num, ...nums) { sum += num; if (nums.length === 0) { return sum; } else { return () => getTotal_f(sum, ...nums); } } function trampoline(f) { return function trampolined(...args) { let result = f(...args); while (typeof result == "function") { result = result(); } return result; }; } const getTotal_trampoline = trampoline(getTotal_f); console.log(getTotal_trampoline(...nums)); // 15 复制代码
这种思维技巧将递归巧妙的转换为了迭代! 写法保持了递归的思维,但是经过trampoline工具函数的处理,实际上交给浏览器执行的时候变成了迭代。
Chrome测试如下:
速度飞快!丝滑流畅~
考虑到内存堆栈问题,trampoline还是蛮适合作为折中的方案的。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 开发函数计算的正确姿势——OCR 服务
- 开发函数计算的正确姿势——tensorflow serving 原 荐
- 开发函数计算的正确姿势 —— Fun validate 语法校验排错指南 原 荐
- 强大的姿势感知模型用于姿势不变的人脸识别
- 从姿势到图像——基于人体姿势引导的时尚图像生成算法
- 行人重识别告别辅助姿势信息,港中文、商汤等提出姿势无关的特征提取GAN
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。