还不懂递归?读完这篇文章保证你会懂


Posted in Javascript onJuly 29, 2018

前言

这篇文章一个多月前以英文发表在我的个人博客,现在抽空翻译成中文,并补充一些没来得及写的内容。

昨天我发表的《如何在 JS 代码中消灭 for 循环》引起很多争议。为了避免没营养的讨论,我先声明一下。递归性能差是没争议的事实,如果你觉得 for 循环更好,没必要学递归,那看到这里你可以不用看了。这篇文章要展示的大部分代码,仅仅是学习目的,我不推荐在生产环境中用。但是如果你对函数式编程感兴趣,想深入理解一些核心概念,你应该读下去。

今年年初我开始学 Haskell 的时候,我被函数式代码的优雅和简洁俘获了。代码居然还能这样写!用指令式代码要写一堆的程序,用递归几行就解决了。这篇文章里,我会把我在 Haskell 里面看到的递归函数翻译成 JS 和 Python,并尽量每一步解释。最后我会尝试解决递归爆栈(Stack Overflow)的问题。

递归基础

我从 Python 代码开始,然后展示 JS 实现。

很多解释递归的教程是从解释斐波那契数列开始的,我觉得这样做是在用一个已经复杂的概念去解释另一个复杂的概念,没有必要。我们还是从简单的代码开始吧。

运行这段 Python 代码:

def foo():
 foo()

foo()

当然会报错。? foo 函数会一直调用自己。因为我没有告诉它何时停,它会一直执行下去,直到爆栈。那我们稍作修改再运行一下:

def foo(n):
 if n <= 1:
 return
 foo(n-1)

foo(10)

这段代码基本什么都没做,但是这次它不会报错了。我在 foo 函数定义初始就告诉它什么时候该停,然后我每次调用的时候都把参数改一下,直到参数满足判断条件,函数停止执行。

如果你理解了上面两段代码,你已经理解递归了。

从上面的代码我总结一下递归的核心构成:

  • 递归函数必须接受参数。
  • 在递归函数的定义初始,应该有一个判断条件,当参数满足这个条件的时候,函数停止执行,并返回值。
  • 每次递归函数执行自己的时候,都需要把当前参数做某种修改,然后传入下一次递归。当参数被累积修改到符合初始判断条件了,递归就停止了。

现在我们来用 Python 写个 max 函数,找出列表中的最大值。是的,我知道 Python 原生有 max 函数,我重新发明个轮子只是为了学习和好玩。

# 不要用这个函数,还是用原生的 max 吧。
def max2(list):
 if len(list) == 1:
  return list[0]
 head, tail = list[0], list[1:]
 return head if head > max2(tail) else max2(tail)

print max2([3,98,345])
# 345

max2函数接受一个列表作为参数,如果列表长度为 1,函数停止执行并把列表第一个元素返回出去。注意,当递归停止时,它必须返回值。(但是如果你想用递归去执行副作用,而不是纯计算的话,可以不返回值。)如果初始判断条件不满足,把列表的头和尾取出来。接着,我们比较头部元素和尾部列表中最大值的大小(我们先不管尾部列表中最大值是哪个),并把比较结果中更大的那个值返回出去。那我们怎样知道尾部列表中的最大值?答案是我们不用知道。我们已经在 max2 函数中定义了比较两个值,并把大的值返回出去这个行为了。我们只需要把这同一个行为作用于尾部列表,程序会帮我们找到。

下面是 JS 的实现:

const max = xs => {
 if (xs.length === 1) return xs[0];
 const [head, ...tail] = xs;
 return head > max(tail) ? head : max(tail);
};

更多递归的例子

接下来我展示几个我从 Haskell 翻译过来的递归函数。刚刚已经用很大篇幅解释递归了,这些函数就不解释了。

reverse

Python 版:

# Python 内置有 reverse 函数
def reverse2(list):
 if len(list) == 1:
 return list
 head, tail = list[0], list[1:]
 return reverse2(tail) + [x]

print reverse2([1,2,3,4,5,6])
# [6,5,4,3,2,1]

JS 版:

const reverse = xs => {
 if (xs.length === 1) return xs;
 const [head, ...tail] = xs;
 return reverse(tail).concat(head);
};

map

Python 版:

# Python 内置有 map 函数
def map2(f, list):
 if len(list) == 0:
 return []
 head, tail = list[0], list[1:]
 return [f(head)] + map2(f, tail)

print map2(lambda x : x + 1, [2,2,2,2])
# [3,3,3,3]

JS 版:

const map = f => xs => {
 if (xs.length === 0) return [];
 const [head, ...tail] = xs;
 return [f(head), ...map(f)(tail)];
};

zipWith

zipWith 接受一个回调函数和两个列表为参数。他会并行遍历两个列表,并把单遍历到的元素一一对应,传进回调函数,把每一步遍历的计算结果存在新的列表里,最终返回这个心列表。

Python 版:

def zipWith(f, listA, listB):
 if len(listA) == 0 or len(listB) == 0:
 return []
 headA, tailA = listA[0], listA[1:]
 headB, tailB = listB[0], listB[1:]
 return [f(headA, headB)] + zipWith(f, tailA, tailB)

print zipWith(lambda x, y : x + y, [2,2,2,2], [3,3,3,3,3])
# [5,5,5,5]
# 结果列表长度由参数中两个列表更短的那个决定

JS 版:

const zipWith = f => xs => ys => {
 if (xs.length === 0 || ys.length === 0) return [];
 const [headX, ...tailX] = xs;
 const [headY, ...tailY] = ys;
 return [f(headX)(headY), ...zipWith(f)(tailX)(tailY)];
};

replicate

Python 版:

def replicate(n,x):
 if n <= 0:
 return []
 return [x] + replicate(n-1,x)

print replicate(4, 'hello')
# ['hello', 'hello', 'hello', 'hello']

JS 版:

const replicate = (n, x) => {
 if (n <= 0) return [];
 return [x, ...replicate(n - 1, x)];
};

reduce

Python 不鼓励用 reduce,我就不写了。

JS 版:

const reduce = (f, acc, arr) => {
 if (arr.length === 0) return acc;
 const [head, ...tail] = arr;
 return reduce(f, f(head, acc), tail);
};

quickSort

用递归来实现排序算法肯定不是最优的,但是如果处理数据量小的话,也不是不能用。

Python 版:

def quickSort(xs):
 if len(xs) <= 1:
  return xs
 pivot, rest = xs[0], xs[1:]
 smaller, bigger = [], []
 for x in rest:
 smaller.append(x) if x < pivot else bigger.append(x)
 return quickSort(smaller) + [pivot] + quickSort(bigger)

print quickSort([44,14,65,34])
# [14, 34, 44, 65]

JS 版:

const quickSort = list => {
 if (list.length === 0) return list;
 const [pivot, ...rest] = list;
 const smaller = [];
 const bigger = [];
 rest.forEach(x =>
 x < pivot ? smaller.push(x) : bigger.push(x);
 );

 return [...quickSort(smaller), pivot, ...quickSort(bigger)]
};

解决递归爆栈问题

由于我对 Python 还不是特别熟,这个问题只讲 JS 场景了,抱歉。

每次递归时,JS 引擎都会生成新的 frame 分配给当前执行函数,当递归层次太深时,可能会栈不够用,导致爆栈。ES6引入了尾部优化(TCO),即当递归处于尾部调用时,JS 引擎会把每次递归的函数放在同一个 frame 里面,不新增 frame,这样就解决了爆栈问题。

然而,V8 引擎在短暂支持 TCO 之后,放弃支持了。那为了避免爆栈,我们只能在程序层面解决问题了。 为了解决这个问题,大神们发明出了 trampoline 这个函数。来看代码:

const trampoline = fn => (...args) => {
 let result = fn(...args);
 while (typeof result === "function") {
 result = result();
 }
 return result;
};

给trampoline传个递归函数,它会把递归函数的每次递归计算结果保存下来,然后只要递归没结束,它就不停执行每次递归返回的函数。这样做相当于把多次的函数调用放到一次函数调用里了,不会新增 frame,当然也不会爆栈。

先别高兴太早。仔细看 trampoline 函数的话,你会发现它也要求传入的递归函数符合尾部调用的情况。那不符合尾部调用的递归函数怎么办呢?( 比如我刚刚写的 JS 版 quickSort,最后 return 的结果里,把两个递归调用放在了一个结果里。这种情况叫 binary recursion,暂译二元递归,翻译错了勿怪 )

这个问题我也纠结了很久了,然后直接去 Stack Overflow 问了,然后真有大神回答了。要解决把二元递归转换成尾部调用,需要用到一种叫 Continuous Passing Style (CPS) 的技巧。来看怎么把 quickSort 转成尾部调用:

const identity = x => x;

const quickSort = (list, cont = identity) => {
 if (list.length === 0) return cont(list);

 const [pivot, ...rest] = list;
 const smaller = [];
 const bigger = [];
 rest.forEach(x => (x < pivot ? smaller.push(x) : bigger.push(x)));

 return quickSort(smaller, a =>
 quickSort(bigger, b => cont([...a, pivot, ...b])),
 );
};

tramploline(quickSort)([5, 1, 4, 3, 2]) // -> [1, 2, 3, 4, 5]

如果上面的写法难以理解,推荐去看 Kyle Simpson 的这章内容。我不能保证比他讲的更清楚,就不讲了。

屠龙之技

虽然我将要讲的这个概念在 JS 中根本都用不到,但是我觉得很好玩,就加进来了。有些编程语言是不支持递归的(我本科不是学的计算机,不知道是哪些语言),那这时候如果我知道用递归可以解决某个问题,该怎么办?用 Y-combinator.

JS 实现:

function y(le) {
 return (function(f) {
 return f(f);
 })(function(f) {
 return le(function(x) {
 return f(f)(x);
 });
 });
}

const factorial = y(function(fac) {
 return function(n) {
 return n <= 2 ? n : n * fac(n - 1);
 };
});

factorial(5); // 120

factorial函数不用递归实现了递归。

展示这段代码不是为了炫技。这根本不是我写的代码,是我从 Douglas Crockford (《JS 语言精髓》的作者)的课程里学到的。看到这段代码时我感到很激动,惊叹计算机科学的精妙和优雅。很多人把程序员职业当做是搬砖的,但是我不这么看。我在学习 CS 的过程中感受更多的是人类智力的不可思议和计算机科学中体现的普遍认识论规律。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
jQuery图片前后对比插件beforeAfter用法示例【附demo源码下载】
Sep 20 Javascript
AngularJS中的按需加载ocLazyLoad示例
Jan 11 Javascript
浅谈js for循环输出i为同一值的问题
Mar 01 Javascript
vuejs 单文件组件.vue 文件的使用
Jul 28 Javascript
Vue项目中跨域问题解决方案
Jun 05 Javascript
Angular6笔记之封装http的示例代码
Jul 27 Javascript
vue 自定义指令自动获取文本框焦点的方法
Aug 25 Javascript
小程序两种滚动公告栏的实现方法
Sep 17 Javascript
基于vue实现图片验证码倒计时60s功能
Dec 10 Javascript
Vue computed 计算属性代码实例
Apr 22 Javascript
原生JS实现汇率转换功能代码实例
May 13 Javascript
Vue+Element自定义纵向表格表头教程
Oct 26 Javascript
如何在js代码中消灭for循环实例详解
Jul 29 #Javascript
Vue-cli3项目配置Vue.config.js实战记录
Jul 29 #Javascript
vue权限路由实现的方法示例总结
Jul 29 #Javascript
JS高级技巧(简洁版)
Jul 29 #Javascript
js运算符的一些特殊用法
Jul 29 #Javascript
不得不知的ES6小技巧
Jul 28 #Javascript
JS 中可以提升幸福度的小技巧(可以识别更多另类写法)
Jul 28 #Javascript
You might like
PHP调用三种数据库的方法(1)
2006/10/09 PHP
PHP编程与应用
2006/10/09 PHP
PHP学习笔记之数组篇
2011/06/28 PHP
php addslashes及其他清除空格的方法是不安全的
2012/01/25 PHP
php类常量的使用详解
2013/06/08 PHP
CodeIgniter框架过滤HTML危险代码
2014/06/12 PHP
php基于双向循环队列实现历史记录的前进后退等功能
2015/08/08 PHP
JavaScript中使用replace结合正则实现replaceAll的效果
2010/06/04 Javascript
jQuery的实现原理的模拟代码 -1 核心部分
2010/08/01 Javascript
JavaScript中的isXX系列是否继续使用的分析
2011/04/16 Javascript
利用javascript实现全部删或清空所选的操作
2014/05/27 Javascript
影响jQuery使用的14个方面
2014/09/01 Javascript
新手快速学习JavaScript免费教程资源汇总
2015/06/25 Javascript
JavaScript语言精粹经典实例(整理篇)
2016/06/07 Javascript
Ajax+FormData+javascript实现无刷新表单信息提交
2016/10/24 Javascript
javascript 利用arguments实现可变长参数
2016/11/21 Javascript
实例解析angularjs的filter过滤器
2016/12/14 Javascript
JS动态生成年份和月份实例代码
2017/02/04 Javascript
详解mpvue开发微信小程序基础知识
2019/09/23 Javascript
layui form.render('select', 'test2') 更新渲染的方法
2019/09/27 Javascript
微信小程序服务器日期格式化问题
2020/01/07 Javascript
微信小程序连续签到7天积分获得功能的示例代码
2020/08/20 Javascript
js+canvas实现图片格式webp/png/jpeg在线转换
2020/08/22 Javascript
[04:53]DOTA2英雄基础教程 祈求者
2014/01/03 DOTA
Python 中迭代器与生成器实例详解
2017/03/29 Python
Python数据拟合与广义线性回归算法学习
2017/12/22 Python
Django实现web端tailf日志文件功能及实例详解
2019/07/28 Python
python实现七段数码管和倒计时效果
2019/11/23 Python
解决Keras 中加入lambda层无法正常载入模型问题
2020/06/16 Python
小学生国旗下演讲稿
2014/04/25 职场文书
化工工艺设计求职信
2014/06/25 职场文书
安全在我心中演讲稿
2014/09/01 职场文书
金秋助学感谢信
2015/01/21 职场文书
2015年中学总务处工作总结
2015/07/22 职场文书
教师节主题班会教案
2015/08/17 职场文书
深入理解margin塌陷和margin合并的解决方案
2021/06/26 HTML / CSS