递归可以说是函数式编程里面最强大的思路之一,咱们从一个一个小例子逐步体会递归思维之美妙,其实哪怕是不搞编程的外行,了解递归的思路也大有裨益的。
从一个小例子说起
所谓递归,就是假想一个问题已经解决了,然后去构思问题的解法,有种薅自己头发上天的感觉。咱们先看一个最简单的例子,从1到n求和,比如1+2+3+4...+100。这个简单问题当然有很多很多的解法,今天咱们为了说明递归的思想,就用递归的方式。
咱们先不谈Haskell,先从数学的角度思考这个问题,我们可以把1到n求和的结果,看做n的函数f(n),因为只要n这个值确定了,1到n求和的结果就唯一确定了,完全符合数学对于函数的定义。那么我们一步一步的分析:
- n=1:当然就是1了。所以f(1)=1.
- n=2:当然就是在前一个的基础上,增加2,也就是f(2)=f(1)+2=1+2=3
- n=3:当然就是在前一个的基础上,增加3,也就是f(3)=f(2)+3=3+3=6
- 。。。
很显然,从n=2开始,我们就看出规律了,什么规律呢?就是如果我们知晓了f(n-1),我们就知晓了f(n),用数学语言来说就是,我们发现了一个函数,它是从n,f(n-1)这两个数,映射到了f(n),也就是:
f(n) = f(n-1) + n
显然,这正是“累加”之含义所在。走到n就加到n嘛!
从数学的角度来说,只要咱们定义了起点,和一步一步推算下一个步骤的规则,理论上整个计算序列就定了,也就是说,咱们就提供了足够的信息,可以计算出来第n步骤的结果了。那么问题来了,如果我们把这些信息“告知”Haskell,是不是它就可以帮我们算出结果,而不需要我们具体告诉他如何把这N个步骤组合起来?
答案是,行。实际上,Haskell代码写出来,跟数学形态几乎别无二致,道友请看:
f 1 = 1
f n = f (n-1) + n
f 100
-- 5050
我想,这个例子虽然简单,却足以展现Haskell递归函数之美妙,你看这个代码,咱们其实并没有描述如何从1加到n,咱们只是把数学上的递归函数定义给原封不动的按照Haskell语法写下来罢了,而且这个Haskell语法,和原本数学上的写法,也只是些许差异而已。
嗯。其实就一个差异,Haskell的函数调用没有括号,哈哈。
递归程序设计方法
当然了,现实中从1到n累加这种小问题是没啥意义的。但是在思考现实问题前,咱们先归纳一下思路,就是到底如何设计一个递归程序?
我们先考虑一类递归问题:可以分解成n个步骤的问题,简单的说就是,这个问题的求解,可以“一步一步”来。累加问题显然就是属于这类问题。咱们再看一个经典问题:求解斐波那契数列。
道友请看定义:
斐波那契数列是一个整数序列,其中每个数字是前两个数字的和。数列通常以 0 和 1 开始,即:
F(0)=0
F(1)=1
对于 n≥2,每个后续的数字 F(n) 由以下递归关系定义:
F(n)=F(n−1)+F(n−2)
简单地说,它的模式是,第一个步骤的结果,是前两个步骤的和。这个定义,咱们可以直接改写成Haskell语法:
fib 1 = 1
fib 2 = 1
fib n = fib (n-1) + fib (n-2)
然后,bling bling的斐波那契函数就被咱写出来了。奥利给。
总结一下,咱们要做一个递归程序,主要是两件事:
- 定义起点,开头的1步或者少数步骤
- 定义递推规则,即从n-1,n-2..的步骤,计算第n步骤的结果的方法
然后,按照Haskell语法写下来,打完收工。
你就说这代码,它骚不骚?
快速排序
下面咱们要上强度了。排序算法里面有一个钻石经典的快速排序。其思想大致是这样的。设想我们有一个数组,里面是一坨数字(其实可以是任何可排序对象),咱想排序,怎么搞呢?咱把数组的头取出来,也就是数组第一个数字,以它为准,剩余数字里面,跟它相等或者比它更小的,分为一组,都在它前面,然后排序。比它更大的,分另一组,在它后面,然后排序。我们用伪代码可以表达为:
sort(list) := sort(smaller) + list_head + sort(bigger)
当然还要补充一个特殊情况,那就是,对于空数组,那显然是不需要排序的,或者说排序后还是空。即:
sort(empty_list) := empty_list
现在,咱们看看怎么写成Haskell代码。对于Haskell不熟悉的道友,这里要先说明几个事情。
- (first:rest) = someList 在Haskell里面,表示把someList拆成头(first)和剩余的(rest)
- [ y | y<- xs, y <= x ] 表示xs这个数组里面,所有<=x的东西,新组成一个数组。
- xs ++ ys 表示把xs和ys两个数组连接成一个新数组
OK。这就差不多了。上代码:
qsort [] = []
qsort (x : xs) = smaller ++ [x] ++ bigger
where
smaller = qsort [y | y <- xs, y <= x]
bigger = qsort [y | y <- xs, y > x]
qsort [3, 2, 5, 1, 4]
-- [1,2,3,4,5]
看这个代码,是不是几乎就跟快速排序的定义一模一样。
递归的思想
其实就是:大事化小小事化了。
当我们求解一个复杂问题的时候,我们假设在更小的尺度上,这个问题已经解决了,在这个假设下,考虑基于简单问题的结果,如何解决这个复杂问题。当问题小到一定境界,则答案足够简单,直接看得出了。
如此,我们就把一个复杂问题,转变成思考复杂问题的一步,从而实现问题的化简。
三由二生,二由一生,一从虚无中来,虚无中有大道。
祝各位道友体悟算法之妙,领悟大道之简。