iThome 網站首載:資料結構與演算法的七個思考術
一般文件或書籍在探討演算法與資料結構時,主要著重在時間與空間複雜度的探討,在以某語言示範實作時,為了集中探討問題,經常採用語言的最小子集來實現。程式語言的語法形式,多半會限制程式設計者對演算法與資料結構重要觀念的思考,因而不少文件或書籍亦會使用虛擬碼(pseudocode)來作高層次描述。就初次學習資料結構與演算法而言,這是必要的過程,去除語言的語法雜訊,著重心力在時間與空間的成本探討,是為了培養程式的效率觀點。若對資料結構與演算法已有一定基礎,可嘗試應用語言特性,從語言的主要典範(Paradiam)來實作資料結構與演算法,從中獲得各種角度的啟發,甚至培養應用程式的架構觀點。
- C - 分解函式讓邏輯更清晰
C是一門古老但歷久彌堅的語言,從過去到現在,許多資料結構與演算法的書籍或文件,都以C當為實作時的示範語言。以現在眼光來說,C語言語法元素不多,用來描述特定問題時,可讓觀摩者集中心思在資料結構與演算法的主要概念。多半的範例程式碼是在一個函式(Function)中描述完實作方式,不過這對一個較複雜的資料結構與演算法,容易令人費神去追蹤與揣摩出每個步驟的狀態與目的為何。
觀察C語言對資料結構與演算法的實作程式碼時,可試著分解當中的輸入輸出,並導入函式來代替邏輯泥塊(Logic clump)。一般文件或書籍為了讓示範程式碼精簡,會直接在函式中夾帶輸入輸出,將輸入輸出抽出可讓函式進行純綷計算,培養讓函式通用化的想法。逐步導入函式取代泥塊,可讓邏輯更為清晰,曝露不必要的流程、計算或條件判斷,進而改進程式的可讀性與效率問題。
- Java - 將相關職責封裝為物件
Java雖不能說是純綷的物件導向語言,但由於其普及程度,稱其為物件導向觀念的重要推手並不為過。由於Java開發者眾多,不少資料結構與演算法書籍是以Java作為實作。有些書籍文件,只是將C語言的實作版本翻譯為Java語法,作為首次接觸資料結構與演算法,並想看看如何以Java語言實作是可以,但日後可試著將程式碼整理組織為物件,從中培養基本的物件導向觀念。
被分解夠細的方法(Method)是組織物件的必要基礎,下一步是將相關方法組織為物件。如果演算較複雜,甚至可組織出兩個或多個物件,並思考物件間的互動與避免依賴。Java無需花費太多心思在回收不再使用的物件,因而可試著讓資料結構與演算法的實作更具動態性。在考慮通用化時,適當地實作標準介面或自訂介面,或者嘗試使用泛型(Generics)讓資料結構與演算法實作能更適用於各種場合,可藉以培養抽象化的思考。
- Python - 拘謹而簡明的動態語言
相較於編譯時期執行型態檢查的靜態語言來說,動態語言可以更直接、簡潔且快速地以程式碼描述出解題方式。由於Python簡明的語法,將虛擬碼翻譯為Python程式碼往往是非常自然的過程。將C或是Java的程式碼翻譯為Python,往往更能清楚看出計算的邏輯性。不用在乎變數型態,能使得開發者更著重在物件應有的行為,而不僅僅是實作某個介面型態,但犯下不遵守行為規範的錯誤。
Python支援程序式(Procedural)、物件導向等多重典範設計。儘管Python本身聲明,函數式(Functional)設計不會是Python追求的目標,但令人意外地,Python具有多種支援函數式設計的元素,使之成為最適合銜接函數式設計的媒介。試著用Python將命令式(Imperative)實現的資料結構與演算法改為函數式實現,經常比直接使用函數式為主要典範的語言來得容易。
- Scala - 融合物件導向與函數式
相較於C、Java,使用Scala介紹資料結構與演算法的資料不多,試著自行實作會是種矛盾、衝突與改進的過程。單看程式碼外觀,你可以將它當作瘦身版的Java,只不過在將Java的資料結構與演算法程式碼翻譯為Scala時,容易感受到它對Java的不滿與理念上的堅持。這會令你反省原本Java程式碼是否設計不當,結果往往是如此。你會感受到Java原來應有的樣貌,進一步體會到物件導向該是怎樣的設計。
作為更好的Java不是Scala唯一目標,它提供了更多函數式元素。如果要同時融合物件導向與函數式,Python會是一種選擇,不過Scala似乎更好。如果你覺得同時融合物件導向與函數式有點難,先試著用Python,如果熟練了,再用Scala實現一次。
- Ruby - 有趣的市集語言
因為Rails而在世界知名度上躍昇的Ruby頗為有趣,就像個熱鬧的市集,你要的東西它幾乎都有,只不過可能換了名字,或者是改造為它特有的語法。多半情況下,把Python程式碼的資料結構與演算法實現拿來修改會很快,不過總要想一下,Python的這個語法在Ruby中到底是什麼呢?你可以用程序式、物件導向、函數式,把C、Java、Python、Scala的程式碼重新用Ruby實現,沒有哪個實現方式會是錯的,重點是始終保持一致風格,或許這是用Ruby實現時,最該努力思考的方向。
- JavaScript - 有就湊合點用,沒就自己打造
身為一門鹹魚翻身的語言,JavaScript的運氣也真是好得過頭。有時你直覺認為它該有的特性結果沒有,有時直覺它不會有的特性偏偏就讓你有撿到寶的感覺。JavaScript是物件導向語言,不過是基於原型的(Prototype-based);語法像Java,不過是動態語言,但弱型別讓人傷腦筋;它擁有一級(First-class)函式這個好東西,不過閉包(Closure)以及可怕的this,好幾次都差點讓你砸了電腦。使用JavaScript實現資料結構與演算法時,重點在於有得用的東西就湊合點用,沒得用的特性就自己打造。好處大概就是,你比較不會有重新打造輪子的感覺。
- Haskell - 嘗試純綷的函數式
儘管你已經試過函數式概念實現資料結構與演算法,但往往相同問題改用Haskell實現時,才發現先前的實現不是那麼純綷的函數式。使用像Scala這種比較寬容且摻雜物件導向概念的語言來瞭解函數式,畢竟還是蒙上了一層面紗。純函數式語言有許多令初接觸者匪夷所思的限制,像是Haskell其實沒有變數(儘管不少場合還是用了變數這個名字讓人較易理解),光這點就讓許多人腦筋轉不太過來。
嘗試以純函數式來實現資料結構與演算法,對熟悉命令式的開發者是種挑戰,也正如Haskell設計者Simon Peyton Jones所言:「純函數式領域中學到的觀點和想法,可能會給主流領域帶來資訊、帶來啟發。」現在許多主流語言都是多重典範,其中不少元素都源自純函數式領域。嘗試使用純函數式的Haskell,往往令人反省得知Python、Scala、Ruby、JavaScript中的某些元素,本來該是何種樣貌,你會有種想回去修改程式碼的衝動。
其實不只是使用Haskell時會獲得啟發,當你嘗試用另一門語言來實作同樣的資料結構與演算法時,總是會因為語言元素不同而導致不同的思考角度,這類啟發是一連串彼此回饋的迭代過程,從而在實務上加深對各種語言、程式庫等的認識,進一步為更高層的架構設計奠定重要基礎。