函数式编程的一个强大之处在于递归,使用递归可以简化算法设计的思路。
尾递归是递归的一种特殊形式,它的特点是可以不创建新的堆栈帧而是改变当前堆栈帧来实现,这样做的好处是不会浪费运行时空间,递归层次加深也不会发生栈溢出,如同执行迭代一样。
如何将非尾递归函数改写成尾递归函数是函数式编程的一项重要的基本功,而最简单的题目就是“n的阶乘”。有许多blog讲解如何将“n的阶乘”改写成尾递归的形式,而这里假设读者已经具备写出尾递归版本的“n的阶乘”的能力。
今天我们来看另外一个经典的题目: 链表插入元素。
题目的描述是这样的:给定一个有序链表L和一个元素v,将v插入到L中并保持新链表仍然有序。例如 L = List(1, 2, 4, 5), v = 3,函数应该返回List(1, 2, 3, 4, 5)。
递归的思路:如果当前链表为空,直接返回只有一个元素v的链表。否则需要比较链表头部x和v的大小,如果x < v,那么将链表尾部作为L继续调用当前函数,然后将x和结果连接起来;如果 x >= v,那么将v和L连接起来返回。
这样我们就很容易可以写出简单递归第一个版本:
def insert1(l: List[Int], v: Int): List[Int] = l match {
case List() => List(v)
case x :: xs =>
if (x < v) x :: insert1(xs, v)
else v :: l
}
那么如何将函数改成尾递归版本呢?了解“n的阶乘”的实现的话很容易可以猜到使用helper function和accumulator(帮助函数和累加器)。但是对于链表插入这个问题还稍微有点复杂:因为accumulator一定是一个链表,而链表只能在头部进行操作,所以如果accumulator的排序和原顺序一样的话,每次处理一个元素就需要将这个元素插入到链表尾部,这个是不现实的。所以结论是accumulator需要逆序排列,一旦找到插入位置再将其逆序,这样我们就得出了使用数据累加器的第二个版本的实现:
def insert2(l: List[Int], v: Int): List[Int] = {
def concatReversePrefix(l: List[Int], prefix: List[Int]): List[Int] = prefix match {
case List() => l
case x :: xs => concatReversePrefix(x :: l, xs)
}
def insertRec(l: List[Int], prefix: List[Int]): List[Int] = l match {
case List() => concatReversePrefix(List(v), prefix)
case x :: xs =>
if (x < v) insertRec(xs, x :: prefix)
else concatReversePrefix(v :: l, prefix)
}
insertRec(l, List())
}
def insert3(l: List[Int], v: Int): List[Int] = {
def insertRec(l: List[Int], f: (List[Int]) => List[Int]): List[Int] = l match {
case List() => f(List(v))
case x :: xs =>
if (x < v) insertRec(xs, {l => f(x :: l)})
else f(v :: l)
}
insertRec(l, {l => l})
}
总结一下,本文对一个经典的问题“链表插入元素”进行剖析,解释了尾递归的含义以及作用。并介绍了三种递归函数的实现:简单递归,数据累加器,函数累加器以及分析问题的思路。理解并掌握尾递归的实现方法和设计思路是函数式编程中必不可少的学习环节,希望这篇文章可以帮助更多的人了解尾递归的技术。
PS:文章中使用Int作为List的元素类型,源代码中附有更通用的版本,有兴趣的读者可以作为参考。