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


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 相关文章推荐
js利用事件的阻止冒泡实现点击空白模态框的隐藏
Jan 24 Javascript
JavaScript中的style.cssText使用教程
Nov 06 Javascript
javascript实现的淘宝旅行通用日历组件用法实例
Aug 03 Javascript
使用jQuery.form.js/springmvc框架实现文件上传功能
May 12 Javascript
js添加绑定事件的方法
May 15 Javascript
AngularJs表单验证实例详解
May 30 Javascript
AngularJS之依赖注入模拟实现
Aug 19 Javascript
浅析JS中的 map, filter, some, every, forEach, for in, for of 用法总结
Mar 29 Javascript
bootstrap modal+gridview实现弹出框效果
Aug 15 Javascript
Vue实现active点击切换方法
Mar 16 Javascript
解决jquery的ajax调取后端数据成功却渲染失败的问题
Aug 08 jQuery
vue中rem的配置的方法示例
Aug 30 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
phpmyadmin的#1251问题
2006/11/25 PHP
PHP详解ASCII码对照表与字符转换
2011/12/05 PHP
超小PHP小马小结(方便查找后门的朋友)
2012/05/05 PHP
PHP echo,print,printf,sprintf函数之间的区别与用法详解
2013/11/27 PHP
php版本的cron定时任务执行器使用实例
2014/08/19 PHP
PHP使用strrev翻转中文乱码问题的解决方法
2017/01/13 PHP
javascript flash下fromCharCode和charCodeAt方法使用说明
2008/01/12 Javascript
jQuery Autocomplete自动完成插件
2010/07/17 Javascript
鼠标拖拽移动子窗体的JS实现
2014/02/25 Javascript
javascript检测两个数组是否相似
2015/05/19 Javascript
js改变style样式和css样式的简单实例
2016/06/28 Javascript
vue实现添加标签demo示例代码
2017/01/21 Javascript
Vue.js 插件开发详解
2017/03/29 Javascript
JavaScript判断变量名是否存在数组中的实例
2017/12/28 Javascript
echarts设置图例颜色和地图底色的方法实例
2018/08/01 Javascript
实例详解ztree在vue项目中使用并且带有搜索功能
2018/08/24 Javascript
JavaScript实现的前端AES加密解密功能【基于CryptoJS】
2018/08/28 Javascript
javascript将非数值转换为数值
2018/09/13 Javascript
微信小程序wx.request拦截器使用详解
2019/07/09 Javascript
javascript设计模式 ? 组合模式原理与应用实例分析
2020/04/14 Javascript
vue中touch和click共存的解决方式
2020/07/28 Javascript
原生JS实现拖拽效果
2020/12/04 Javascript
python2 与 python3 实现共存的方法
2018/07/12 Python
Python re 模块findall() 函数返回值展现方式解析
2019/08/09 Python
解决paramiko执行命令超时的问题
2020/04/16 Python
如何对python的字典进行排序
2020/06/19 Python
Python爬虫实现自动登录、签到功能的代码
2020/08/20 Python
html5调用摄像头功能的实现代码
2018/05/07 HTML / CSS
HTML5 Canvas概述
2009/08/26 HTML / CSS
大四学年自我鉴定
2013/11/13 职场文书
学习党的群众路线教育实践活动心得体会
2014/03/01 职场文书
承诺书格式
2014/06/03 职场文书
大学生心理活动总结
2014/07/04 职场文书
父亲节寄语大全
2015/02/27 职场文书
2015年助理政工师工作总结
2015/05/26 职场文书
关于python3 opencv 图像二值化的问题(cv2.adaptiveThreshold函数)
2022/04/04 Python