圖 23.6 “鏈表”所示的鏈表即單鏈表(Single Linked List),本節我們學習如何創建和操作這種鏈表。每個鏈表有一個頭指針,通過頭指針可以找到第一個節點,每個節點都可以通過指針域找到它的後繼,最後一個節點的指針域為NULL
,表示沒有後繼。數組在內存中是連續存放的,而鏈表在內存中的佈局是不規則的,我們知道訪問某個數組元素b[n]
時可以通過基地址+n×每個元素的位元組數
得到它地址,或者說數組支持隨機訪問,而鏈表是不支持隨機訪問的,只能通過前一個元素的指針域得知後一個元素的地址,因此只能從頭指針開始順序訪問各節點。以下代碼實現了單鏈表的基本操作。
例 26.1. 單鏈表
/* linkedlist.h */ #ifndef LINKEDLIST_H #define LINKEDLIST_H typedef struct node *link; struct node { unsigned char item; link next; }; link make_node(unsigned char item); void free_node(link p); link search(unsigned char key); void insert(link p); void delete(link p); void traverse(void (*visit)(link)); void destroy(void); void push(link p); link pop(void); #endif
/* linkedlist.c */ #include <stdlib.h> #include "linkedlist.h" static link head = NULL; link make_node(unsigned char item) { link p = malloc(sizeof *p); p->item = item; p->next = NULL; return p; } void free_node(link p) { free(p); } link search(unsigned char key) { link p; for (p = head; p; p = p->next) if (p->item == key) return p; return NULL; } void insert(link p) { p->next = head; head = p; } void delete(link p) { link pre; if (p == head) { head = p->next; return; } for (pre = head; pre; pre = pre->next) if (pre->next == p) { pre->next = p->next; return; } } void traverse(void (*visit)(link)) { link p; for (p = head; p; p = p->next) visit(p); } void destroy(void) { link q, p = head; head = NULL; while (p) { q = p; p = p->next; free_node(q); } } void push(link p) { insert(p); } link pop(void) { if (head == NULL) return NULL; else { link p = head; head = head->next; return p; } }
/* main.c */ #include <stdio.h> #include "linkedlist.h" void print_item(link p) { printf("%d\n", p->item); } int main(void) { link p = make_node(10); insert(p); p = make_node(5); insert(p); p = make_node(90); insert(p); p = search(5); delete(p); free_node(p); traverse(print_item); destroy(); p = make_node(100); push(p); p = make_node(200); push(p); p = make_node(250); push(p); while (p = pop()) { print_item(p); free_node(p); } return 0; }
在初始化時把頭指針head
初始化為NULL
,表示空鏈表。然後main
函數調用make_node
創建幾個節點,分別調用insert
插入到鏈表中。
void insert(link p) { p->next = head; head = p; }
正如上圖所示,insert
函數雖然簡單,其中也隱含了一種特殊情況(Special Case)的處理,當head
為NULL
時,執行insert
操作插入第一個節點之後,head
指向第一個節點,而第一個節點的next
指針域成為NULL
,這很合理,因為它也是最後一個節點。所以空鏈表雖然是一種特殊情況,卻不需要特殊的代碼來處理,和一般情況用同樣的代碼處理即可,這樣寫出來的代碼更簡潔,但是在讀代碼時要想到可能存在的特殊情況。當然,insert
函數傳進來的參數p
也可能有特殊情況,傳進來的p
可能是NULL
,甚至是野指針,本章的函數代碼都假定調用者的傳進來的參數是合法的,不對參數做特別檢查。事實上,對指針參數做檢查是不現實的,如果傳進來的是NULL
還可以檢查一下,如果傳進來的是野指針,根本無法檢查它指向的內存單元是不是合法的,C標準庫的函數通常也不做這種檢查,例如strcpy(p, NULL)
就會引起段錯誤。
接下來main
函數調用search
在鏈表中查找某個節點,如果找到就返回指向該節點的指針,找不到就返回NULL
。
link search(unsigned char key) { link p; for (p = head; p; p = p->next) if (p->item == key) return p; return NULL; }
search
函數其實也隱含了對於空鏈表這種特殊情況的處理,如果是空鏈表則循環體一次都不執行,直接返回NULL
。
然後main
函數調用delete
從鏈表中摘除用search
找到的節點,最後調用free_node
釋放它的存儲空間。
void delete(link p) { link pre; if (p == head) { head = p->next; return; } for (pre = head; pre; pre = pre->next) if (pre->next == p) { pre->next = p->next; return; } }
從上圖可以看出,要摘除一個節點需要首先找到它的前趨然後才能做摘除操作,而在單鏈表中通過某個節點只能找到它的後繼而不能找到它的前趨,所以刪除操作要麻煩一些,需要從第一個節點開始依次查找要摘除的節點的前趨。delete
操作也要處理一種特殊情況,如果要摘除的節點是鏈表的第一個節點,它是沒有前趨的,這種情況要用特殊的代碼處理,而不能和一般情況用同樣的代碼處理。這樣很不爽,能不能把這種特殊情況轉化為一般情況呢?可以把delete
函數改成這樣:
void delete(link p) { link *pnext; for (pnext = &head; *pnext; pnext = &(*pnext)->next) if (*pnext == p) { *pnext = p->next; return; } }
定義一個指向指針的指針pnext
,在for
循環中pnext
遍歷的是指向鏈表中各節點的指針域,這樣就把head
指針和各節點的next
指針統一起來了,可以在一個循環中處理。
然後main
函數調用traverse
函數遍歷整個鏈表,調用destroy
函數銷毀整個鏈表。請讀者自己閲讀這兩個函數的代碼。
如果限定每次只在鏈表的頭部插入和刪除元素,就形成一個LIFO的訪問序列,所以在鏈表頭部插入和刪除元素的操作實現了堆棧的push
和pop
操作,main
函數的最後幾步把鏈表當成堆棧來操作,從打印的結果可以看到出棧的順序和入棧是相反的。想一想,用鏈表實現的堆棧和第 2 節 “堆棧”中用數組實現的堆棧相比有什麼優點和缺點?
1、修改insert
函數實現插入排序的功能,鏈表中的數據按從小到大排列,每次插入數據都要在鏈表中找到合適的位置再插入。在第 6 節 “折半查找”中我們看到,如果數組中的元素是有序排列的,可以用折半查找算法更快地找到某個元素,想一想如果鏈表中的節點是有序排列的,是否適用折半查找算法?為什麼?
2、基于單鏈表實現隊列的enqueue
和dequeue
操作。在鏈表的末尾再維護一個指針tail
,在tail
處enqueue
,在head
處dequeue
。想一想能不能反過來,在head
處enqueue
而在tail
處dequeue
?
3、實現函數void reverse(void);
將單鏈表反轉。如下圖所示。
鏈表的delete
操作需要首先找到要摘除的節點的前趨,而在單鏈表中找某個節點的前趨需要從表頭開始依次查找,對於n個節點的鏈表,刪除操作的時間複雜度為O(n)。可以想像得到,如果每個節點再維護一個指向前趨的指針,刪除操作就像插入操作一樣容易了,時間複雜度為O(1),這稱為雙向鏈表(Doubly Linked List)。要實現雙向鏈表只需在上一節代碼的基礎上改動兩個地方。
在linkedlist.h
中修改鏈表節點的結構體定義:
struct node { unsigned char item; link prev, next; };
在linkedlist.c
中修改insert
和delete
函數:
void insert(link p) { p->next = head; if (head) head->prev = p; head = p; p->prev = NULL; } void delete(link p) { if (p->prev) p->prev->next = p->next; else head = p->next; if (p->next) p->next->prev = p->prev; }
由於引入了prev
指針,insert
和delete
函數中都有一些特殊情況需要用特殊的代碼處理,不能和一般情況用同樣的代碼處理,這非常不爽,如果在表頭和表尾各添加一個Sentinel節點(這兩個節點只用於界定表頭和表尾,不保存數據),就可以把這些特殊情況都轉化為一般情況了。
例 26.2. 帶Sentinel的雙向鏈表
/* doublylinkedlist.h */ #ifndef DOUBLYLINKEDLIST_H #define DOUBLYLINKEDLIST_H typedef struct node *link; struct node { unsigned char item; link prev, next; }; link make_node(unsigned char item); void free_node(link p); link search(unsigned char key); void insert(link p); void delete(link p); void traverse(void (*visit)(link)); void destroy(void); void enqueue(link p); link dequeue(void); #endif
/* doublylinkedlist.c */ #include <stdlib.h> #include "doublylinkedlist.h" struct node tailsentinel; struct node headsentinel = {0, NULL, &tailsentinel}; struct node tailsentinel = {0, &headsentinel, NULL}; static link head = &headsentinel; static link tail = &tailsentinel; link make_node(unsigned char item) { link p = malloc(sizeof *p); p->item = item; p->prev = p->next = NULL; return p; } void free_node(link p) { free(p); } link search(unsigned char key) { link p; for (p = head->next; p != tail; p = p->next) if (p->item == key) return p; return NULL; } void insert(link p) { p->next = head->next; head->next->prev = p; head->next = p; p->prev = head; } void delete(link p) { p->prev->next = p->next; p->next->prev = p->prev; } void traverse(void (*visit)(link)) { link p; for (p = head->next; p != tail; p = p->next) visit(p); } void destroy(void) { link q, p = head->next; head->next = tail; tail->prev = head; while (p != tail) { q = p; p = p->next; free_node(q); } } void enqueue(link p) { insert(p); } link dequeue(void) { if (tail->prev == head) return NULL; else { link p = tail->prev; delete(p); return p; } }
/* main.c */ #include <stdio.h> #include "doublylinkedlist.h" void print_item(link p) { printf("%d\n", p->item); } int main(void) { link p = make_node(10); insert(p); p = make_node(5); insert(p); p = make_node(90); insert(p); p = search(5); delete(p); free_node(p); traverse(print_item); destroy(); p = make_node(100); enqueue(p); p = make_node(200); enqueue(p); p = make_node(250); enqueue(p); while (p = dequeue()) { print_item(p); free_node(p); } return 0; }
這個例子也實現了隊列的enqueue
和dequeue
操作,現在每個節點有了prev
指針,可以反過來在head
處enqueue
而在tail
處dequeue
了。
現在結合第 5 節 “環形隊列”想一想,其實用鏈表實現環形隊列是最自然的,以前基于數組實現環形隊列,我們還需要“假想”它是首尾相接的,而如果基于鏈表實現環形隊列,我們本來就可以用指針串成首尾相接的。把上面的程序改成環形鏈表(Circular Linked List)也非常簡單,只需要把doublylinkedlist.c
中的
struct node tailsentinel; struct node headsentinel = {0, NULL, &tailsentinel}; struct node tailsentinel = {0, &headsentinel, NULL}; static link head = &headsentinel; static link tail = &tailsentinel;
改成:
struct node sentinel = {0, &sentinel, &sentinel}; static link head = &sentinel;
再把doublylinkedlist.c
中所有的tail
替換成head
即可,相當於把head
和tail
合二為一了。
回想一下我們在例 12.4 “用廣度優先搜索解迷宮問題”中使用的資料結構,我把圖重新畫在下面。
這是一個靜態分配的數組,每個數組元素都有row
、col
和predecessor
三個成員,predecessor
成員保存一個數組下標,指向數組中的另一個元素,這其實也是鏈表的一種形式,稱為靜態鏈表,例如上圖中的第6、4、2、1、0個元素串成一條鏈表。
1、Josephus是公元1世紀的著名歷史學家,相傳在一次戰役中他和另外幾個人被圍困在山洞裡,他們寧死不屈,決定站成一圈,每次數到三個人就殺一個,直到全部死光為止。Josephus和他的一個朋友不想死,於是串通好了站在適當的位置上,最後只剩下他們倆的時候這個遊戲就停止了。如果一開始的人數為N
,每次數到M
個人就殺一個,那麼要想不死應該站在什麼位置呢?這個問題比較複雜,[具體數學]的1.3節研究了Josephus問題的解,有興趣的讀者可以參考。現在我們做個比較簡單的練習,用鏈表模擬Josephus他們玩的這個遊戲,N
和M
作為命令行參數傳入,每個人的編號依次是1~N,打印每次被殺者的編號,打印最後一個倖存者的編號。
2、在第 2.11 節 “本節綜合練習”的習題1中規定了一種日誌檔案的格式,每行是一條記錄,由行號、日期、時間三個欄位組成,由於記錄是按時間先後順序寫入的,可以看作所有記錄是按日期排序的,對於日期相同的記錄再按時間排序。現在要求從這樣的一個日誌檔案中讀出所有記錄組成一個鏈表,在鏈表中首先按時間排序,對於時間相同的記錄再按日期排序,最後寫回檔案中。比如原檔案的內容是:
1 2009-7-30 15:16:42 2 2009-7-30 15:16:43 3 2009-7-31 15:16:41 4 2009-7-31 15:16:42 5 2009-7-31 15:16:43 6 2009-7-31 15:16:44
重新排序輸出的檔案內容是:
1 2009-7-31 15:16:41 2 2009-7-30 15:16:42 3 2009-7-31 15:16:42 4 2009-7-30 15:16:43 5 2009-7-31 15:16:43 6 2009-7-31 15:16:44