在上一篇中介绍了一门程序设计语言必须具备的一些特性,以及Scheme语言的基本语法。这一篇用上一篇提到的平方根的问题来看看一个问题是如何被逐步分解并解决的。我们首先看下平方根的数学定义:
上面的数学公式描述了一个数的平方根所具备的性质,但并没有告诉我们应该如何去求一个数的平方根。这是数学公式和计算机程序的不同之处。对于同一个问题,数学公式关心的是该问题的解所具备的性质 (what is);而计算机程序则关心应该如何去得到问题的解 (how to)。那么到底求一个数的平方根呢?我们这里使用牛顿提出的无穷逼近法,这个算法思路很简单,先假定我们要求数y的平方根x:
- 为x设定一个初始值
- 检查x^2是否等于y,若是,则返回x, 否则将x赋值为(x+(x/y))/2
- 重复执行第2步直到获得解。
简书的markdown对数学公式支持的不好,请见谅。有了上面的描述,我们就可以很快写出代码了,下面我就试试使用Scheme语言来实现。这里还是要扯点题外话,我看SCIP这本书并不是为了学习Scheme语言,而是学习书中分析问题和将抽象问题的方法,学习了这些之后,你会恍然发现现在大多数语言或者工具的一些特性在这本书中都讲到了,比如python语言、guava库、Java8主打的lambda表达式,stream等。编程语言的发展有两条截然不同的路,一条是为了适应计算机底层硬件或者说计算机体系结构发展起来的,最具代表性的就是C语言;另一条路则是关心计算的本质(比较抽象,这里仅是个人观点),主要的代表就是Lisp语言,而我们这里的谈到的Scheme就是Lisp语言的一种变体。但随着技术的发展,这两种不同类型的语言有点融合的趋势。
在写代码前,我们首先分析下这个问题,在上一篇中我们说过,要解决一个问题时,我们应该将问题进行分解,得到多个子问题,当子问题解决后,我们将子问题的解组合就得到了原问题的解。通过算法的描述我们可以将原有的问题分解成一些子问题:判断数x是不是问题的解;使用算法描述中的方法将x加以改进,每次对x的改进都能够更加接近问题的解,这就是无穷逼近。我们用Scheme语言翻译下就是这样:
(define (sqrt-iter x y)
(if (good-enough? x y)
x
(sqrt-iter (imporve x y) y)))
这里引入了函数sqrt-iter,它接收两个参数x和y,x表示对y的平方根的猜想,通过递归调用(在Scheme语言里十分重要)得到解。在sqrt-iter里又引入了两个函数:good-enough?和imporve,对应着我们分析的两个子问题。而good-enough?应该如何定义的呢?它接收两个参数x和y,判断x^2是否等于y。这个问题是一个比较经典的面试题:判断两个浮点数是否相等。
(define (good-enough? x y)
(< (abs (- (square x) y)) 0.001))
(define (square x) (* x x))
(define (abs x)
(if (> x 0) x (- x)))
improve函数我们可以根据算法描述得到:
(define (improve x y)
(average x (/ x y)))
(define (average x y)
(/ (+ x y) 2))
这些子问题解决后,原问题就迎刃而解了:
(define (sqrt x)
(sqrt-iter 1.0 x))
这里我们假定任何数的平方根的初始值为1.0。现在我们再来看下sqrt函数,我们可以得到下面这张图:
我们在处理原问题(sqrt)的时候,我们只需关心抽象的各个子问题,而每个子问题又可以分解为更多的子问题,各个子问题可以看做是一个个的黑匣子,我们无需关心起内部实现的细节,我们关心其提供的功能就够了。其实任何的程序设计都可以通过这种手段去将问题逐步分解,这里的分解还需要注意一个问题,就是每个被分解后的子问题应该遵循单一职责的原理,只有这样,解决该子问题的方法才有可能被其他的模块进行复用。就像搭积木的例子里,我们应该去组合一些通用形状的积木。
好了,这一篇就简单介绍了分析问题的方法。有疑问?请留言。另外课后的作业有一道题是之前搜狗公司的面试题,读者可以思考下:
有inc函数和dec函数,inc函数的作用是将输入的参数加1后返回,dec函数的作用是将输入的参数减1后返回,利用inc函数和dec函数定义加法函数。
(define (+ a b) (...))