List 是課程第一次把 recursion 變成資料結構本身的語言,而不只是程式技巧。 List 不是單純用括號畫出來的 array;它有一個契約:要麼是 empty list,要麼 由一個 head element 和一個本身仍然是 list 的 tail 組成。
這個遞歸定義解釋了為甚麼 list 程式常常有同一個形狀:先處理 empty list, 再處理 head,然後把剩下的工作交給 tail。
List 契約
定義
List ADT
List 是有次序的元素序列。在 head-tail 觀點下,每個非空 list 都可以寫成
CreateList(head, tail),其中 head 是一個元素,而 tail 是另一個 list。
Lecture 先強調三個操作:
CreateList(head, tail):由一個元素和一個 list 建立新 list;ListHead(list):取第一個元素;ListTail(list):取去掉第一個元素後剩下的 list。
Head 和 tail 的類型不同。Head 是元素;tail 是 list。例如 時,,但 。
例題
由 empty list 建立 list
List [3, 4, 5] 可以寫成:
CreateList(3, CreateList(4, CreateList(5, EmptyList())))
由內到外讀:先建立 [5],再把 4 放到前面,最後把 3 放到前面。
為甚麼 recursion 自然適合 list
List 的遞歸結構直接給出很多 function 的寫法。
int ListLength(listADT list) {
if (ListIsEmpty(list)) {
return 0;
}
return 1 + ListLength(ListTail(list));
}
End case 是 empty list。Recursive call 是對 tail 做,而 tail 比原本 list 短。如果沒有 end case,或者 recursive call 沒有令問題變小,遞歸就不安全。
定理
List recursion 的終止理由
對有限 list,不斷把 list 換成它的 tail,最後一定會到 empty list。因此只
對 ListTail(list) 遞歸、並處理 EmptyList() 的 function 會終止。
同樣,NthElement 的思路是:第 0 個元素是 head;第 n 個元素是 tail 的第
個元素。
Iteration 和 recursion 只是保存狀態的方法不同
Iterative ListLength 會用 cursor 逐步走過 successive tails。
int ListLength(listADT list) {
int count = 0;
for (listADT L = list; !ListIsEmpty(L); L = ListTail(L)) {
count++;
}
return count;
}
Recursive 版本把未完成工作放在 call stack;iterative 版本把它放在 local variables。兩者正確,都是因為每一步都從 current list 走向 tail。
Representation 會改變成本
Array-based list 可以實作同一個 ADT,但 CreateList 可能要把 tail 的所有
元素複製到新 array。Linked representation 則儲存一個 head 和一個指向
tail 的 pointer,因此 CreateList 可以只 allocate 一個新 cell,再指向原本
的 tail。
ADT 告訴我們操作的意思;representation 決定操作的成本。
定義
Linked-list representation
Linked-list cell 儲存一個元素和一個指向剩餘 list 的 pointer。這個 pointer field 就是 representation 層面的 tail。
常見錯誤
- 把
ListTail(L)當成元素,而不是 list。 - 未處理 empty list 就呼叫
ListHead。 - 假設 array representation 和 linked representation 成本一樣。
- Recursive call 仍然用原本 list,沒有把問題縮小。
快速檢查
若 L 是 [7, 2, 9],ListHead(L) 和 ListTail(L) 是甚麼?
用 head-tail 契約回答。
解答
答案
快速檢查
為甚麼 recursive ListLength 會在有限 list 上終止?
留意 recursive call 的 argument。
解答
答案
練習
- 說明
DisplayList的 recursive idea:print head,再 display tail。 - 解釋為甚麼
ListConcat(list1, list2)在list1empty 時應該 returnlist2。 - 比較 array representation 和 linked representation 下
CreateList的成本。
解答