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的成本。
解答