前言
本文探討兩個和串列走訪相關的議題,一個是迭代器 (iterator),一個是和串列走訪相關的高階函式 (higher-order function)。這兩個議題不是典型資料結構教材會提到的主題,如果覺得目前用不到先略過也無妨。
迭代器 (Iterator)
串列的迭代器的抽象資料結構
迭代器 (iterator) 是在不暴露資料結構內部的前提下走訪該資料結構的公開方法。對資料結構實作者而言,藉由實作迭代器可以隱藏實作細節;對資料結構使用者來說,不用知道資料結構的內部實作也可以走訪該結構;對雙方來說,是雙贏的局面。
迭代器的抽象資料結構有顯性和隱性兩種。以下是顯性的迭代器:
L is a list.
I is an iterator.
sub Start(L): I
sub Next(I): I
sub End(I): bool
sub Value(I): data
顯性的迭代器會回傳代表迭代器的變數 I
。使用範例如下:
for I <- Start(L); not End(I); I <- Next(I) do
data <- value(I)
Do something on data
end for
隱性迭代器的抽象資料結構如下:
L is a list.
sub Start(L): bool
sub Next(L): bool
sub End(L): bool
sub Value(L): data
和顯性迭代器不同,這時候不會有額外的變數。使用範例如下:
if Start(L) then
while not End(L) do
data <- value(L)
Do something on data
Next(L)
end while
end if
實作迭代器
要實作外部迭代器可參考以下虛擬碼:
I belongs to Node type.
sub Start(L):
assert L != null
I <- L->head
return I
sub Next(I):
if I == null then
return null
end if
I <- I->next
return I
sub End(I):
check whether I == null
sub Value(I):
assert I != null
return I->data
在隱性迭代器中,迭代器 I
會變成串列 L
的內部屬性,實作方式則大同小異,此處不列出。以下是等效的 C 語言外部迭代器程式碼:
Node * list_start(const List *self)
{
assert(self);
// Init an iterator.
Node *iter = self->head;
return iter;
}
Node * list_next(Node *self)
{
if (!self) {
return NULL;
}
self = self->next;
return self;
}
bool list_end(Node *self)
{
return self == NULL;
}
和串列走訪相關的高階函式 (Higher-Order Function)
串列相關高階函式的抽象資料結構
L, K are lists.
// User-defined function objects.
cmp <- fn(a, b): bool
filter <- fn(a): bool
mapper <- fn(a): result
reducer <- fn(a, b): result
// ADT w/o side effects.
sub Any(L, filter): bool
sub All(L, filter): bool
sub Find(L, filter): (result, bool)
sub Select(L, filter): K
sub Sort(L, cmp): K
sub Map(L, mapper): K
sub Reduce(L, reducer): result
// Alternative ADT w/ side effects.
sub SelectMut(L, filter): void
sub SortMut(L, cmp): void
sub MapMut(L, mapper): void
Any(L, filter)
All(L, filter)
Find(L, filter)
Select(L, filter)
SelectMut(L, filter)
Sort(L, cmp)
SortMut(L, cmp)
Map(L, mapper)
MapMut(L, mapper)
Reduce(L, reducer)
基本上,帶有高階函式風格的串列走訪函式都可寫成 traverse(list, action)
的形式,我們只要預先寫好 traverse
的部分,使用者可隨需求自行填入 action
來搭配 traverse
以操作 list
。
在這類函式中,有兩種風格,一種是無副作用 (side effect) 的,一種是有副作用的。前者不會改變原有的串列,而會回傳新的串列,後者則會直接修改原有的串列。兩種風格各有優劣,讀者可自行選擇喜好的方式。
有些進階的程式設計教材會考慮多執行緒的情境,這種實作較有用,但會讓實作更複雜,本文暫不考慮這種情境。
Any
:確認串列中至少有一個元素符合條件
Any
的虛擬碼如下:
sub Any(L, filter):
assert L != null
curr <- L->head
while curr != null do
if filter(curr->data) then
return true
end if
curr <- curr->next
end while
return false
只要串列 L
中有符合 filter
的元素,就回傳 true
中止函式,若所有元素皆不符合則回傳 false
。其 C 語言實作如下:
typedef bool (*filterFn) (int);
bool list_any(const List *self, filterFn filter)
{
assert(self);
Node *curr = self->head;
if (!curr) {
return false;
}
while (curr) {
if (filter(curr->data)) {
return true;
}
curr = curr->next;
}
return false;
}
All
:確認串列中所有元素皆符合條件
All
的虛擬碼如下:
sub All(L, filter):
assert L != null
curr <- L->head
while curr != null do
if not filter(curr->data) then
return false
end if
curr <- curr->next
end while
return true
只要串列 L
有不符合 filter
的元素,就回傳 false
中止函式,若所有元素皆符合則回傳 true
。其 C 語言實作如下:
typedef bool (*filterFn) (int);
bool list_all(const List *self, filterFn filter)
{
assert(self);
Node *curr = self->head;
if (!curr) {
return false;
}
while (curr) {
if (!filter(curr->data)) {
return false;
}
curr = curr->next;
}
return true;
}
Find
:從串列中根據特定條件找出元素
Find
的虛擬碼如下:
sub Find(L, filter):
assert L != null
curr <- L->head
i <- 0
while curr != null do
if filter(curr->data) then
return (i, true)
end if
curr <- curr->next
i <- i + 1
end while
return (0, false)
在我們這個演算法中,若有符合條件 filter
的元素,會直接回傳 data
並中止函式,若所有元素皆不符合條件,則回傳空值。如果想要得到所有符合條件的元素,請參考下一節的 Select
函式。
以下是等效的 C 語言實作:
typedef bool (*filterFn) (int);
bool list_find(const List *self, filterFn filter, size_t *out)
{
assert(!list_is_empty(self));
Node *curr = self->head;
size_t i = 0;
while (curr) {
if (filter(curr->data)) {
*out = i;
return true;
}
curr = curr->next;
i++;
}
*out = 0;
return false;
}
Select
:從串列中取得所有符合條件的元素
以下是 Select
的虛擬碼:
sub Select(L, filter):
assert L != null
K <- a new empty list.
curr <- L->head
while curr != null do
if filter(curr->data) then
Push(K, curr->data)
end
curr <- curr->next
end while
return K
由於此函式會回傳多個元素,我們要設計回傳的方式。一般來說,有兩種方法;一種是做一個新串列,將符合條件的元素放入該串列後回傳;另一種是將函式做成迭代器;前者會簡單一些,這裡展示第一種做法。
以下是等效的 C 程式實作:
typedef bool (*filterFn) (int);
bool list_select_mut(List **self, filterFn filter)
{
assert(*self);
List *out = list_new();
if (!out) {
return false;
}
Node *curr = (*self)->head;
while (curr) {
if (filter(curr->data)) {
if (!list_push(out, curr->data)) {
list_free(out);
return false;
}
}
curr = curr->next;
}
list_free(*self);
*self = out;
return true;
}
Sort
:根據特定條件對串列排序
本節採用插入排序法 (insertion);但因內部為串列,故採新增串列的方式來實作,在空間開銷上較大。以下是 Sort
的虛擬碼:
sub Sort(L, filter):
K <- a new empty list
if L->head == null
return K
end if
curr <- L->head
Push(K, curr->data)
curr <- curr->next
while curr != null do
InsertBy(K, curr->data, filter)
curr <- curr->next
end while
return K
這個程式的關鍵在 InsertBy(L, value, predicate)
函式,下文會說明。以下是等效的 C 語言程式碼:
typedef bool (*predicateFn) (int, int);
List * list_sort(const List *self, predicateFn filter)
{
assert(self);
if (list_is_empty(self)) {
return NULL;
}
List *out = list_new();
if (!out) {
return out;
}
Node *curr = self->head;
list_push(out, curr->data);
curr = curr->next;
while (curr) {
list_insert_by(out, curr->data, filter);
curr = curr->next;
}
return out;
}
InsertBy(L, value, predicate)
本身就是有序串列的插入元素的過程,但我們將節點關係拉到外部,變成參數。以下是其程式碼:
sub InsertBy(L, value, predicate): void
node <- Node(value)
if IsEmpty(L) then
L.head <- node
L.tail <- node
L.size += 1
return
end if
if L.head == L.tail then
if predicate(value, L.head.data) then
node.next <- L.head
L.head.prev <- node
L.head <- node
else
L.tail.next <- node
node.prev <- L.tail
L.tail <- node
end if
L.size += 1
return
end if
p <- null
q <- L.head
while q.next != null do
if predicate(value, q.data) then
if p == null then
node.next <- L.head
L.head.prev <- node
L.head <- node
else
p.next <- node
node.prev <- p
q.prev <- node
node.next <- q
end if
break
end if
p <- q
q <- q.next
end while
if q == L.tail then
L.tail.next <- node
node.prev <- L.tail
L.tail <- node
end if
L.size += 1
當串列為空時,直接放入元素即可。
當串列僅存有單一元素時,會依情境放入串列的頭端或尾端,由於會影響頭 (或尾) 端指標所指向的元素,需分開處理。
走訪串列的方式為使用兩個指標來走訪,一個指標指向目標節點,另一個指標指向前一個節點。
當串列有多個元素時,需再細分如下:
- 放入串列頭端
- 放入串列尾端
- 放入串列其他位置
當放入串列的頭 (或尾) 端時,會影響串列頭 (或尾) 端指標指向的節點,故需獨立出來處理。以下是等效的 C 語言程式碼:
typedef bool (*predicateFn) (int, int);
bool list_insert_by(List *self, int value, predicateFn predicate)
{
assert(self);
Node *node = node_new(value);
if (!node) {
return false;
}
if (!(self->head)) {
self->head = node;
self->tail = node;
self->size++;
return true;
}
if (self->head == self->tail) {
if (predicate(value, self->head->data)) {
node->next = self->head;
self->head->prev = node;
self->head = node;
}
else {
self->tail->next = node;
node->prev = self->tail;
self->tail = node;
}
self->size++;
return true;
}
Node *p = NULL;
Node *q = self->head;
while (q->next) {
if (predicate(value, q->data)) {
if (!p) {
node->next = self->head;
self->head->prev = node;
self->head = node;
} else {
p->next = node;
node->prev = p;
q->prev = node;
node->next = q;
}
break;
}
p = q;
q = q->next;
}
if (q == self->tail) {
self->tail->next = node;
node->prev = self->tail;
self->tail = node;
}
self->size++;
return true;
}
Map
:走訪串列並根據特定條件修改元素
以下是 Map
的虛擬碼:
sub Map(L, mapper):
assert L != null
K <- a new empty list
curr <- L->head
while curr != null do
Push(K, mapper(curr->data))
curr <- curr->next
end while
return K
在本節中,我們另生成新串列,若要改成直接修改現有串列也可以。以下是等效的 C 語言程式碼:
typedef int (*mapFn) (int);
bool list_map(List **self, mapFn mapper)
{
assert(*self);
List *result = list_new();
if (!result) {
return false;
}
Node *p = (*self)->head;
while (p) {
if (!list_push(result, mapper(p->data))) {
list_free(result);
result = NULL;
return false;
}
p = p->next;
}
list_free(*self);
*self = result;
return true;
}
Reduce
:將串列所有元素根據特定條件合併
以下是 Reduce
的虛擬碼:
sub Reduce(L, reducer):
assert not IsEmpty(L)
curr <- L->head
a <- curr->data
curr <- curr->next // Iterate one step.
while curr != null do
a <- reducer(a, curr->data)
curr <- curr->next
end while
return a
若串列只有一個元素時,while
迴圈不會運轉,函式仍是相容的。以下是等效的 C 語言程式碼:
typedef int (*reduceFn) (int, int);
int list_reduce(const List *self, reduceFn reducer)
{
assert(!list_is_empty(self));
Node *curr = self->head;
int a = curr->data;
curr = curr->next;
while (curr) {
a = reducer(a, curr->data);
curr = curr->next;
}
return a;
}
Function Chaining:簡潔而緊湊的函數式程式
由於我們實作的語言是 C,無法寫得很簡潔,像下例使用 Ruby 做 function chaining 的語法就無法達成:
# Ruby excerpt.
# Chaining higher-order functions.
(1..10)
.select { |n| n % 2 != 0 }
.map { |n| n * n }
.reduce { |a, b| a + b } \
== (1 * 1 + 3 * 3 + 5 * 5 + 7 * 7 + 9 * 9) \
or raise "Wrong result"
對等的 C 程式碼會 verbose 得多,因為兩者設計的理念不同,無法混為一談。
在演算法上的效率
以下是各個高階函式的效率:
Any(L, filter)
:O(n)
All(L, filter)
:O(n)
Find(L, filter)
:O(n)
Select(L, filter)
:O(n)
Sort(L, cmp)
:O(n^2)
Map(L, mapper)
:O(n)
Reduce(L, reducer)
:O(n)
由於高階函式皆需走訪整個串列,效率至少為 O(n)
。高階函式的功能在於其功能性而非程式效率。