在上一篇中介紹了一門程序設計語言必須具備的一些特性,以及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) (...))