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


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实现层随着内容大小动态渐变改变 推荐
Dec 19 Javascript
轻轻松松学JS调试(不下载任何工具)
Apr 14 Javascript
JS关键字球状旋转效果的实例代码
Nov 29 Javascript
js实现键盘控制DIV移动的方法
Jan 10 Javascript
Redux 和 Mobx的选择问题:让你不再困惑!
Sep 18 Javascript
简单实现jQuery弹窗效果
Oct 30 jQuery
vue的状态管理模式vuex
Nov 30 Javascript
js合并两个数组生成合并后的key:value数组
May 09 Javascript
vue中使用gojs/jointjs的示例代码
Aug 24 Javascript
js图数据结构处理 迪杰斯特拉算法代码实例
Sep 11 Javascript
jQuery操作元素的内容和样式完整实例分析
Jan 10 jQuery
vue实现列表滚动的过渡动画
Jun 29 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实现基本留言板功能原理与步骤详解
2020/03/26 PHP
javascript中的有名函数和无名函数
2007/10/17 Javascript
javascript sudoku 数独智力游戏生成代码
2010/03/27 Javascript
input输入框鼠标焦点提示信息
2015/03/17 Javascript
JS根据生日算年龄的方法
2015/05/05 Javascript
JQuery替换DOM节点的方法
2015/06/11 Javascript
jQuery实现div随意拖动的实例代码(通用代码)
2016/01/28 Javascript
JavaScript模拟数组合并concat
2016/03/06 Javascript
Bootstrap零基础入门教程(三)
2016/07/18 Javascript
AngularJS入门教程之AngularJS 模板
2016/08/18 Javascript
详解如何使用webpack打包Vue工程
2017/05/27 Javascript
微信小程序动态显示项目倒计时效果
2017/06/13 Javascript
Vue2路由动画效果的实现代码
2017/07/10 Javascript
Vue实现类似Spring官网图片滑动效果方法
2019/03/01 Javascript
通过GASP让vue实现动态效果实例代码详解
2019/11/24 Javascript
JavaScript 接口原理与用法实例详解
2020/05/12 Javascript
Python Mysql数据库操作 Perl操作Mysql数据库
2009/01/12 Python
Python多维/嵌套字典数据无限遍历的实现
2016/11/04 Python
Python如何生成树形图案
2018/01/03 Python
Python模拟脉冲星伪信号频率实例代码
2018/01/03 Python
python+Splinter实现12306抢票功能
2018/09/25 Python
详解使用postMessage解决iframe跨域通信问题
2019/11/01 HTML / CSS
如何打印出当前源文件的文件名以及源文件的当前行号
2015/04/05 面试题
SQL Server面试题
2013/04/04 面试题
What is view? why do we have view?
2012/06/22 面试题
为什么UNION ALL比UNION快
2016/03/17 面试题
sealed修饰符是干什么的
2012/10/23 面试题
学生会宣传部部长竞选演讲稿
2014/04/25 职场文书
销售活动策划方案
2014/08/26 职场文书
银行员工犯错检讨书
2014/09/16 职场文书
学校师德师风自我剖析材料
2014/09/29 职场文书
初二数学教学反思
2016/02/17 职场文书
解决numpy数组互换两行及赋值的问题
2021/04/17 Python
MyBatis配置文件解析与MyBatis实例演示
2022/04/07 Java/Android
Windows Server 2012配置DNS服务器的方法
2022/04/29 Servers
Win11如何默认打开软件界面最大化?Win11默认打开软件界面最大化的方法
2022/07/15 数码科技