第3章 順序表的鏈式儲存
一、鏈式儲存
- 解決問題:對於線性結構,使用順序儲存,需要足夠大的連續儲存區域
- 鏈式儲存:結點除了存放資訊,並且附設指標,用指標體現結點之間的邏輯關係
- 注:\(c\)語言的動態分配函式\(malloc()\)和\(free()\)分別實現記憶體空間的動態分配和回收,所以不必知道某個結點的具體地址
- 注:鏈式儲存中,必須有一個指標指向第一個結點的儲存位置,一般為\(head\)標示
- 順序儲存和鏈式儲存的區別:順序儲存更適合查詢量大的程式設計;鏈式儲存更適合需要頻繁插入和刪除的程式
二、單連結串列
2.1 單連結串列的基本概念及描述
- 單連結串列結點構造:兩個域,一個存放資料資訊的\(info\)域;另一個指向該結點的後繼結點的\(next\)域
2.2 單連結串列的實現
- 單連結串列的常用操作:
- 建立一個空的單連結串列
- 輸出單連結串列中各個結點的值
- 在單連結串列中查詢第\(i\)個結點
2.2.1 單連結串列的儲存結構
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
2.2.2 單連結串列的插入操作(演算法)
- 演算法步驟(插入結點為\(p\),插入到結點\(q\)後面):
- 通過
find(head,i)
查詢\(q\)結點,查不到列印報錯資訊 - 給插入結點\(p\)分配空間,並設定資訊
- 如果在單連結串列的最前面插入新結點,讓單連結串列的首指標指向新插入的結點
p->next = head;
head = p;
- 如果在單連結串列中間插入新結點:
p->next = q->next;
q->next=p;
- 通過
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
node *insert(node *head, datatype x, int i) {
node *p, *q;
q = find(head, i); // 查詢第i個結點
if (!q && i != 0) {
printf("\n找不到第%d個結點,不能插入%d!", i, x);
} else {
p = (node *) malloc(sizeof(node)); // 分配空間
p->info = x; // 設定新結點
if (i == 0) // 插入的結點作為單連結串列的第一個結點
{
p->next = head;
head = p;
} else {
p->next = q->next; // 後插
q->next = p;
}
}
return head;
}
2.2.3 單連結串列的刪除操作(演算法)
- 演算法步驟(被刪除結點\(q\),被刪除結點前一個結點\(pre\))
- 判斷連結串列是否為空
- 迴圈查詢被刪除結點\(q\),並且設定一個結點\(pre\)標示被刪除結點的前一個結點
- 如果刪除結點為第一個結點
head = head->next;
free(p)
- 如果刪除結點為其他結點
pre->next = q->next;
free(p)
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
node *dele(node *head, datatype x) {
node *pre = NULL, *p;
if (!head) {
printf("單連結串列是空的");
return head;
}
p = head;
while (p && p->info != x) // 尋找被刪除結點p
{
pre = p; // pre指向p的前驅結點
p = p->next;
}
if (p) {
if (!pre) // 被刪除結點沒有上一個結點,則是要刪除的是第一個結點
{
head = head->next;
} else {
pre->next = p->next;
}
free(p)
}
return head;
}
三、帶頭結點的單連結串列
3.1 帶頭結點的單連結串列的基本概念及描述
- 頭結點的作用:單連結串列的插入和刪除需要對空的單連結串列進行特殊處理,因此可以設定 \(head\) 指標指向一個永遠不會被刪除的結點——頭結點
- 注:\(head\) 指示的是所謂的頭結點,它不是實際結點,第一個實際結點應該是
head->next
指示的
3.2 帶頭結點的單連結串列的實現
- 帶頭結點的單連結串列的常用操作:
- 建立一個空的帶頭結點的單連結串列
- 輸出帶頭結點的單連結串列中各個結點的值
- 在帶頭結點的單連結串列中查詢第 \(i\) 個結點
3.2.1 帶頭結點的單連結串列的儲存結構
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
3.2.2 帶頭結點的單連結串列的插入(演算法)
- 演算法步驟( \(p\) 為插入結點,\(q\) 為插入前一個結點):
- 通過
find(head,i)
查詢帶頭結點的單連結串列中的第 \(i\) 個結點( \(i=0\) 表示新結點插入在頭結點之後) - 如果沒找到結點 \(q\),列印報錯資訊
- 如果在非空的帶頭結點的單連結串列最前面插入一個新結點
p->next = q->next;
q->next = p;
- 如果在非空的帶頭結點的單連結串列的內部插入一個新結點
p->next = q->next;
q->next = p;
- 通過
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
node *insert(node *head, datatype x, int i) {
node *p, *q;
q = find(head, i); // 查詢帶頭結點的單連結串列中的第 i 個結點,i=0 時表示新結點插入在頭結點之後
if (!q) // 沒有找到
{
printf("\n帶頭結點的單連結串列中不存在第%d個結點!不能插入%d!", i, x);
return head;
}
p = (node *) malloc(sizeof(node)); // 為準備插入的新結點分配空間
p->info = x; // 為新結點設定值
p->next = q->next;
q->next = q; // i=0 時,本語句等價於 head->next=p
return head;
}
3.2.3 帶頭結點的單連結串列的刪除(演算法)
- 演算法步驟(被刪除結點為 \(q\),被刪除結點的前一個結點為 \(pre\)):
- 設定 \(pre\) 指向頭結點
- \(q\) 從帶頭結點的單連結串列的第一個實際結點開始迴圈尋找值為 \(x\) 的結點
- 刪除帶頭結點的單連結串列的第一個實際結點:
pre->next = q->next;
free(q)
- 刪除帶頭結點的單連結串列的內部結點:
pre->next = q->next;
free(q)
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
node *dele(node *head, datatype x) {
node *pre = head, *q; // pre 指向頭結點
q = head->next; // q 從帶頭結點的單連結串列的第一個實際結點開始找值為 x 的結點
while (q && q->info != x) // 迴圈查詢值為 x 的結點
{
pre = q; // pre 指向 q 的前驅
q = q->next;
}
if (q) {
pre->next = q->next; // 刪除
free(q); // 釋放記憶體空間
}
return head;
}
四、迴圈單連結串列
4.1 迴圈單連結串列的基本概念及描述
- 單連結串列存在的問題:從表中的某個結點開始,只能訪問該結點後面的結點
- 迴圈單連結串列解決的問題:從表中的任意一個結點開始,使其都能訪問到表中的所有的結點
- 迴圈單連結串列:在單連結串列的基礎上,設定表中最後一個結點的指標域指向表中的第一個結點
4.2 迴圈單連結串列的實現
- 迴圈單連結串列的常用操作:
- 建立一個空的迴圈單連結串列
- 獲得迴圈單連結串列的最後一個結點的儲存地址
- 輸出迴圈單連結串列中各個結點的值
- 在迴圈單連結串列中查詢一個值為 \(x\) 的結點
- 迴圈單連結串列的插入操作
- 迴圈單連結串列的刪除操作
- 迴圈單連結串列的整體插入與刪除操作
4.2.1 迴圈單連結串列的儲存結構
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
五、雙連結串列
5.1 雙連結串列的基本概念及描述
- 雙連結串列解決的問題:設定一個 \(llink\) 指標域,通過這個指標域直接找到每一個結點的前驅結點
5.2 雙連結串列的實現
- 雙連結串列的常用操作:
- 建立一個空的雙連結串列
- 輸出雙連結串列中各個結點的值
- 查詢雙連結串列中第 \(i\) 個結點
- 雙連結串列的插入操作
- 雙連結串列的刪除操作
5.2.1 雙連結串列的儲存結構
typedef int datatype;
typedef struct dlink_node {
datatype info;
struct dlink_node *llink, *rlink;
} dnode;
六、鏈式棧
6.1 鏈式棧的基本概念及描述
- 鏈式棧:使用鏈式儲存的棧
- 注:鏈式棧的棧頂指標一般用 \(top\) 表示
6.2 鏈式棧的實現
- 鏈式棧的常用操作:
- 建立一個空的鏈式棧
- 判斷鏈式棧是否為空
- 取得鏈式棧的棧頂結點值
- 輸出鏈式棧中各個結點的值
- 向鏈式棧中插入一個值為 \(x\) 的結點
- 刪除鏈式棧的棧頂節點
6.2.1 鏈式棧的儲存結構
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
七、鏈式佇列
7.1 鏈式佇列的基本概念及描述
- 鏈式佇列:使用鏈式儲存的佇列
- 注:佇列必須有隊首和隊尾指標,因此增加一個結構型別,其中的兩個指標域分別為隊首和隊尾指標
7.2 鏈式佇列的實現
- 鏈式佇列的常用操作:
- 建立一個空的鏈式佇列
- 判斷鏈式佇列是否為空
- 輸出鏈式佇列中各個結點的值
- 取得鏈式佇列的隊首結點值
- 向鏈式佇列中插入一個值為 \(x\) 的結點
- 刪除鏈式佇列中的隊首結點
7.2.1 鏈式佇列的儲存結構
typedef int datatype;
typedef struct link_node {
datatype info;
struct link_node *next;
} node;
typedef struct {
node *front, *rear; // 定義隊首和隊尾指標
} queue;
八、演算法設計題
8.1 求單連結串列中結點個數(演算法)
設計一個演算法,求一個單連結串列中的結點個數
typedef struct node {
int data;
struct node *next;
} linknode;
typedef linknode *linklist;
int count(linklist head) {
int c = 0;
linklist p = head; // head為實際的第一個結點
while (p) // 計數
{
c++;
p = p->next;
}
return c;
}
8.2 求帶頭結點的單連結串列中的結點個數(演算法)
設計一個演算法,求一個帶頭結點單連結串列中的結點個數
typedef struct node {
int data;
struct node *next;
} linknode;
typedef linknode *linklist;
int count(linlist head) {
int c = 0;
linklist = head->next; // head->next 為實際的第一個結點
while (p) // 計數
{
c++;
p = p->next;
}
return c;
}
8.3 在單連結串列中的某個結點前插一個新結點(演算法)
設計一個演算法,在一個單連結串列中值為 y 的結點前面插入一個值為 x 的結點。即使值為 x 的新結點成為值為 y 的結點的前驅結點
typedef struct node {
int data;
struct node *next;
} linknode;
typedef linknode *linklist;
void insert(linklist head, int y, int c) {
linklist pre, p, s; // 假設單鏈錶帶頭結點
pre = head;
p = head->next;
while (p && p->data != y) {
pre = p;
p = p->next;
}
if (p) // 找到了值為 y 的結點,即 p == y
{
s = (linklist) malloc(sizeof(linknode));
s->data = x;
s->next = p;
pre->next = s;
}
}
8.4 判斷單連結串列的各個結點是否有序(演算法)
設計一個演算法,判斷一個單連結串列中各個結點值是否有序
typedef struct node {
int data;
struct node *next;
} linknode;
typedef linknode *linklist;
int issorted(linklist head, char c) // c='a' 時為升序,c='d' 時為降序
{
int flag = 1;
linklist p = head->next;
switch (c) {
case 'a': // 判斷帶頭結點的單連結串列 head 是否為升序
while (p && p->next && flag) {
if (p->data <= p->next->data) p = p->next;
else flag = 0;
}
break;
case 'd': // 判斷帶頭結點的單連結串列 head 是否為降序
while (p && p->next && flag) {
if (p->data >= p->next->data) p = p->next;
else flag = 0
}
break;
}
return flag;
}
8.5 逆轉一個單連結串列(演算法)
設計一個演算法,利用單連結串列原來的結點空間將一個單連結串列就地轉置
- 核心思想:通過
head->next
保留上一個 \(q\) 的狀態 - 演算法步驟:
- 讓 \(p\) 指向實際的第一個結點
- 迴圈以下步驟:
- 讓 \(p\) 一直迴圈下去,直到走完整個連結串列,\(p\) 迴圈的時候,\(q\) 跟著 \(p\) 一起重新整理
- \(q\) 的 \(next\) 指標域始終指向
head->next;
head->next;
始終指向上一個 \(q\)
typedef struct node {
int data;
struct node *next;
} linknode;
typedef linknode *linklist;
void verge(linklist head) {
linlist p, q;
p = head->next;
head->next = NULL;
while (p) {
q = p;
p = p->next;
q->next = head->next; // 通過 head->next 保留上一個 q 的狀態
head->next = q;
}
}
8.6 拆分結點值為自然數的單連結串列,原連結串列保留值為偶數的結點,新連結串列存放值為奇數的結點(演算法)
設計一個演算法,將一個結點值自然數的單連結串列拆分為兩個單連結串列,原表中保留值為偶數的結點,而值為奇數的結點按它們在原表中的相對次序組成一個新的單連結串列
typedef struct node {
int data;
struct node *next;
} linknode;
typedef linknode *linklist;
linklist sprit(linklist head) {
linklist L, pre, p, r;
L = r = (linklist) malloc(sizeof(linknode));
r->next = NULL;
pre = head;
p = head->next;
while (p) {
if (p->data % 2 == 1) // 刪除奇數值結點,並用 L 連結串列儲存
{
pre->next = p->next;
r->next = p;
r = p; // 這樣使得 r 變成了 r->next
p = pre->next; // 這樣使得 p 變成了 head->next->next
} else // 保留偶數值結點
{
pre = p; // 書中的貌似多餘操作
p = p->next;
}
}
r->next = NULL; // 置返回的奇數連結串列結束標記
return L;
}
8.7 在有序單連結串列中刪除值大於 x 而小於 y 的結點(演算法)
設計一個演算法,對一個有序的單連結串列,刪除所有值大於 x 而不大於 y 的結點
typedef struct node {
int data;
struct node *next;
} linknode;
typedef linknode *linklist;
void deletedata(linklist head, datatype x, datatype y) {
linklist pre = head, p, q;
p = head->next;
// 找第 1 處大於 x 的結點位置
while (p && p->data <= x) {
pre = p;
p = p->next;
}
// 找第 1 處小於 y 的位置
while (p && p->data <= y) p = p->next;
// 刪除大於 x 而小於 y 的結點
q = pre->next;
pre->next = p; // 小於 x 的第一個結點指向大於 y 的第一個結點
pre = q->next;
// 釋放被刪除結點所佔用的空間
while (pre != p) { // 此時 p 已經指向了大於 y 的第一個結點
free(q);
q = pre;
pre = pre->next;
}
}
九、錯題集
- 在頭結點的單連結串列中查詢 \(x\) 應選擇的程式體是:
node *p = head; while (p && p->info != x) p = p->next; return p;
- 注:未找到時需要返回頭結點 \(head\),而不是返回一個 \(NULL\)
- 用不帶頭結點的單連結串列儲存佇列時,其隊頭指標指向隊頭結點,其隊尾指標指向隊尾結點,則在進行刪除操作時隊頭隊尾指標都可能要修改
- 注:鏈式佇列中只有一個結點是會出現該情況,插入時同理
- 若從鍵盤輸入 \(n\) 個元素,則建立一個有序單向連結串列的時間複雜度為 \(O(n^2)\)
- 注:第 \(1\) 個數:\(0\) 次查詢;第 \(2\) 個數:\(1\) 次查詢 \(,\cdots,\) 第 \(n\) 個數,\(n-1\) 次查詢,總共 \(n(n-1)/2\) 次