5 一般程式推導

5.2 關於執行效率

有點意外地,本書直到現在才較正式地談執行效率。 在本書中,我們假設數字四則運算、邏輯運算元等等都可在常數時間內完成。 資料結構方面,使用資料建構元或對其做樣式配對都只需要常數時間。 例如,將 x 與 y 做成 (x,y) 是常數時間內可完成的動作;一個型別是序對的值如果已經是弱首範式,將其配對成 (x,y) 以取出其中的 x 和 y 也只需要常數時間。 如果有資料結構定義 data T = A | B, 給一個已經是弱首範式的 x :: T, 只需常數時間便可判斷它究竟是 A 還是 B.

回顧 List 的定義:

data List a = [] | a : List a 

我們可看出這是一個偏一邊的表示法。 對於串列的左邊的操作都可以在常數時間內完成。 因此,我們可在常數時間內判斷一個串列 xs 究竟是 [] 還是可分解成頭和尾; 產生 [] 只需要常數時間;在 xs 的左邊添加一個元素,傳回 x:xs,也是常數時間內可完成的動作。 但如果我們要拿出某串列最右邊的元素,或著在串列的右邊加東西呢?回顧 (++) 的定義:

(++) :: List a -> List a -> List a
[]      ++ ys  = ys
(x:xs)  ++ ys  = x : (xs ++ ys) 

若我們試著看看 [1,2,3] ++ [4,5] 是怎麼被算出來的

   (1 : 2 : 3 : []) ++ (4 : 5 : [])
=  1 : ((2 : 3 : []) ++ (4 : 5 : []))
=  1 : 2 : ((3 : []) ++ (4 : 5 : []))
=  1 : 2 : 3 : ([] ++ (4 : 5 : []))
=  1 : 2 : 3 : 4 : 5 : [] 

可發現 (++) 需把第一個參數從頭到尾走過一遍。 因此,若第一個參數的長度是 n,(++) 是一個時間複雜度為 $O(n)$ 的函數。 函數 last 的情況也類似。

練習 5.6

回顧 last 的定義,試著展開 last [1,2,3,4] 並確認 last xs 需要的時間是否為 $O(|length xs|)$。

諸如 sum, length, maximum 之類的函數將串列從頭到尾走過一次。 當輸入串列長度為 n, 它們均需時 $O(n)$。如果函數 f 需時 $O(t)$,map f 需時 $O(t\times n)$. 函數 filter p, takeWhile p 等等在最壞情況下需將串列走完,因此它們也是需要線性時間的函數。 函數 zip 需要的時間與兩個串列中較短者成正比。

在習題 2.5 中,我們曾請讀者定義一個函數 reverse :: List a -> List a,將輸入的串列反轉,例如 reverse [1,2,3,4,5] = [5,4,3,2,1]. 一個可能的答案如下:

reverse []      = []
reverse (x:xs)  = reverse xs ++ [x] 

這個程式的效率如何呢?我們看看 reverse [1,2,3,4] 如何被歸約:

   reverse [1,2,3,4]
=  reverse [2,3,4] ++ [1]
=  (reverse [3,4] ++ [2]) ++ [1]
=  ((reverse [4] ++ [3]) ++ [2]) ++ [1]
=  (((reverse [] ++ [4]) ++ [3]) ++ [2]) ++ [1]
=  ((([] ++ [4]) ++ [3]) ++ [2]) ++ [1] 

為了把 [1] 接在左邊,(++ [1]) 需要走過一個長度為 3 的串列。 而在那之前,(++ [2]) 需要走過一個長度為 2 的串列。 推廣說來,要反轉一個長度為 n 的串列,(++) 會被使用 $O(n)$ 次。每個 (++) 左邊的串列長度也是 $O(n)$,因此 reverse 是一個需時 $O(n^2)$ 的演算法! 「反轉串列」這個看來很基本的操作竟需要 $O(n^2)$ 的時間,似乎令人難以接受。 是否有更快的做法呢?我們將在第 5.6 節討論到。

練習 5.7

回顧第1.10 與2.8 節中提及的 外標籤二元樹:

data ETree a  = Tip a | Bin (ETree a) (ETree a) 

以下函數傳回樹中所有的標籤:

tips :: ETree a -> List a
tips (Tip x)    = [x]
tips (Bin t u)  = tips t ++ tips u 

函數 tips 最壞情況的時間複雜度為何?請做出一個含有 n 個標籤的樹 t,使得 tips t 僅需要 $O(n)$ 的時間算完;也請做出一個含有 n 個標籤的樹 u,使得 tips u 需要 $O(n^2)$ 的時間。

答案

令 t 為一個向右傾斜的樹:

t = Bin (Tip 1) (Bin (Tip 2) .. (Bin (Tip (n-1)) (Tip n))) 

tip t 展開成為 [1] ++ ([2] .. ([n-1] ++ [n])), 可在 $O(n)$ 時間內歸約成範式。 這是最好的情況。 令 u 為一個向左傾斜的樹:

u =  Bin (Bin (... (Bin (Bin (Tip 1) (Tip 2)) (Tip 3)))...
       (Tip (n-1))) (Tip n) 

tip u 展開成為 ((..(([1] ++ [2])++[3])..) .. ++ [n-1]) ++ [n], 需要 $O(n^2)$ 的時間,也是 tips 最壞情況的時間複雜度.