- 第 99 篇 -
Date: 2025 - 05 - 25
Author: 鄭龍浩/仟墨
【數據結構】
續上一篇: 線性表之“單鏈表”
文章目錄
- “雙鏈表(帶頭雙向循環鏈表)” 的實現:
- 分步解釋所有函數:
- test.c
- DListNode.h
- DListNode.c
“雙鏈表(帶頭雙向循環鏈表)” 的實現:
- 帶頭:帶頭結點
- 循環:鏈表中的結點是循環的,頭結點前驅是最后一個結點,最后一個結點后繼是頭結點
- 雙向:每一個結點都有存儲前驅結點和后繼結點的指針域
分步解釋所有函數:
-
申請新的結點
封裝這個函數是因為在后邊插入的時候多次使用了下面的代碼,造成了代碼冗余,所以將申請新結點的代碼封裝起來,用的時候直接調用即可
malloc
申請新的空間,寬度為sizef(DListNode)
- 判斷
malloc
內存分配是否成功,失敗則退出(概論很小) - 設置結點數據域,將元素
x
存入該結點的data
中 - 初始化指針域,前驅后繼全部都指向自己(完成閉環)
- 返回申請的結點的地址
// 1 申請新的結點 DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申請新的結點if (NewNode == NULL) {printf("內存申請失敗!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存儲數據NewNode->prev = NewNode; // 初始時指向自己NewNode->next = NewNode; // 初始時指向自己return NewNode; }
-
帶頭結點的雙向循環鏈表初始化 – 創建并返回頭結點,即返回哨兵結點
這個函數的作用和
BuyDListNode
很像,但是意思上不一樣!-
DListInit()
有很強的語義約束 –> 專門用于初始化,而非開辟新的結點的內存空間 -
返回的頭結點指針具有特殊作用(哨兵結點),不是普通節點(普通結點的
data
存儲數據,而哨兵結點的data
不存儲數據,默認為0)返回創建的頭結點指針,該頭結點代表一個空鏈表
-
寫函數的時候,可以調用申請結點的函數來實現頭節點的創建
步驟:
- 調用
BuyDListNode(0)
創建頭結點(data
無實際意義) - 由于
BuyDListNode
已讓prev
和next
指向自己,無需額外操作 - 返回頭結點指針
// 2 帶頭結點的雙向循環鏈表初始化 -- 創建并返回頭結點,即返回哨兵結點 DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申請新的結點 // 頭結點的 data 無意義,僅作哨兵// 因為BuyDListNode(0_中已經指向了自己所以下面代碼可寫可不寫//NewNode->prev = NewNode; // 新結點前驅指向自己//NewNode->next = NewNode; // 新結點后繼指向自己return NewNode; // 返回頭結點(哨兵結點) }
-
-
雙向鏈表清除 – 只釋放頭節點以外的結點
清除的意思是保留雙鏈表本身,但是雙鏈表中保存數據的結點要清除(不清除頭結點)
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 釋放哨兵結點以外的結點(存儲數據的結點)
- 初始化指針域,將哨兵結點的前驅和后繼全部指向自己,完成閉環
// 3 雙向鏈表清除 -- 只釋放頭節點以外的結點 void DListClear(DListNode* phead) {assert(phead); // 防止空指針DListNode* cur = phead->next; // 存放當前的結點的地址while (cur != phead) { // 釋放處頭結點(哨兵結點)以外的結點DListNode* next = cur->next; // 保存下一個結點地址(防止釋放后地址丟失)free(cur); // 釋放當前結點內存空間cur = next; // 指向下一個結點}cur->next = cur->prev = cur; }
- 檢查頭結點有效性(assert),若
-
雙向鏈表銷毀
在
DListClear
基礎上增加頭結點內存釋放但是銷毀的時候要用到二級指針,因為二級指針才能將存放頭結點地址的二級指針變量置為NULL
步驟:
- 檢查頭結點有效性(assert),
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 釋放哨兵結點以外的結點(存儲數據的結點),使用
DListClear
- 釋放頭結點內存空間
phead
初始化,即phead
中存儲的地址為NULL
// 4 雙向鏈表銷毀 -- 銷毀要用雙指針 void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指針DListClear(*phead); // 只釋放頭節點以外的結點free(*phead); // 釋放頭結點*phead = NULL; // 重新置為NULL }
- 檢查頭結點有效性(assert),
-
頭插
頭插操作是在 哨兵結點(頭結點)與 第一個結點之間插入一個新的結點,并且將新的結點與頭結點和原第一個結點連接起來
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 申請新的結點
- 新結點前驅指向頭結點
- 新節點后繼指向原第一個結點
- 原第一結點前驅指向新的結點
- 頭結點后繼指向新的結點
// 5 頭插 void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULL// 因為帶頭雙向循環鏈表是有頭結點的,而頭結點不存儲數據始終為NULL,即不存在頭結點給頭結點添加數據的情況DListNode* NewNode = BuyDListNode(x); // 申請新的結點// 頭插就是讓新的結點插到 “哨兵結點(頭結點)”與“第一個結點”之間DListNode* first = phead->next; // 表示第一個結點NewNode->prev = phead; // 1 新結點前驅指向頭結點NewNode->next = first; // 2 新節點后繼指向原第一個結點first->prev = NewNode; // 3 原第一結點前驅指向新的結點phead->next = NewNode; // 4 頭結點后繼指向新的結點 }
- 檢查頭結點有效性(assert),若
-
尾插
尾插操作就是在最后一個結點后邊插入一個新的結點,并且將新節點與原最后一個結點和頭結點連接起來
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 申請新的結點
- 新節點前驅指向尾節點
- 新節點后繼指向頭節點
- 尾節點后繼指向新節點
- 頭節點前驅指向新節點
// 6 尾插 void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申請新的結點// 尾插就是將新的結點放到 tail 的后邊DListNode* tail = phead->prev; // 尾結點NewNode->prev = tail; // 1 新節點前驅指向尾節點NewNode->next = phead; // 2 新節點后繼指向頭節點tail->next = NewNode; // 3 尾節點后繼指向新節點phead->prev = NewNode; // 4 頭節點前驅指向新節點 }
- 檢查頭結點有效性(assert),若
-
頭刪
頭刪就是將第一個結點(不是刪除哨兵結點)的內存空間釋放,并且將頭結點與原第二個結點連接起來
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 檢查鏈表是否只有一個頭結點(哨兵結點),如果是的話,則不刪除,直接
return
- 頭結點后繼指向第二個結點
- 第二個結點前驅指向頭結點
- 釋放第一個結點
// 7 頭刪 void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回// 切記:頭刪不是刪除“頭結點(哨兵結點)”,而是刪除第一個結點DListNode* first = phead->next; // 第一個結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* second = first->next; // 第二個結點phead->next = second; // 1 頭結點后繼指向第二個結點second->prev = phead; // 2 第二個結點前驅指向頭結點free(first); // 3 釋放第一個結點 }
- 檢查頭結點有效性(assert),若
-
尾刪
尾刪就是將最后一個結點內存空間釋放, 并且將原倒數第二個結點與頭結點連接起來
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 檢查鏈表是否只有一個頭結點(哨兵結點),如果是的話,則不刪除,直接
return
- 頭結點前驅指向倒數第二個結點
- 倒數第二個結點后繼指向頭結點
- 釋放尾結點內存空間
// 8 尾刪 void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回DListNode* tail = phead->prev; // 尾結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* TailPrev = tail->prev; // 倒數第二個結點phead->prev = TailPrev; // 1 頭結點前驅指向倒數第二個結點TailPrev->next = phead; // 2 倒數第二個結點后繼指向頭結點free(tail); // 3 釋放尾結點 }
- 檢查頭結點有效性(assert),若
-
查找 找到返回結點地址,找不到返回NULL
從第一個結點(不是哨兵結點)到 最后一個結點找結點
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 從第一個結點找到最后一個結點,如果找到了
data
為x
的結點,就返回該結點的地址 - 沒找到則返回NULL
// 9 查找 找到返回結點地址,找不到返回NULL DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指針,確保phead不是NULL// 切記:頭結點不存儲數據,查找無需找頭結點DListNode* cur = phead->next;// 從第一個結點到最后一個結點 (結束條件為:cur 走到了頭結點)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一個結點}return NULL; // 若沒找到,則返回NULL }
- 檢查頭結點有效性(assert),若
-
在 pos 的前面進行插入
pos 前面插入 數據為 x 的結點,就是在 pos 前驅與 pos 之間插入數據,并將新節點 與 pos 前驅后繼連接起來
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 驗證pos是否屬于某個鏈表(通過檢查閉環)
- 申請新的結點
- 新結點的前驅指向 原pos前驅結點
- 新節點的后繼指向 pos結點
- 原pos的前驅結點后繼指向 新的結點
- 原pos的前驅指向 新的結點
// 10 在pos的前面進行插入(不包括頭結點,因為頭結點不存儲數據) void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos); // 確保不是孤立節點DListNode* NewNode = BuyDListNode(x); // 申請新的結點// pos 前面插入 數據為 x 的結點,就是在 pos 前驅與 pos 之間插入數據DListNode* PosPrev = pos->prev; // pos 的前驅結點NewNode->prev = PosPrev; // 1 新結點的前驅指向 原pos前驅結點NewNode->next = pos; // 2 新節點的后繼指向 pos結點PosPrev->next = NewNode; // 3 原pos的前驅結點后繼指向 新的結點pos->prev = NewNode; // 4 原pos的前驅指向 新的結點 }
- 檢查頭結點有效性(assert),若
-
刪除 pos 位置的節點
刪除pos結點就是將pos的前驅結點和后繼結點連接起來,并且釋放pos結點
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 驗證pos是否屬于某個鏈表(通過檢查閉環)
- 新結點的前驅指向 原pos前驅結點
- 新節點的后繼指向 pos結點
- 原pos的前驅結點后繼指向 新的結點
- 原pos的前驅指向 新的結點
// 11 刪除pos位置的結點(不包括頭結點,因為頭結點不存儲數據) void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL && 斷言指向頭結點(哨兵結點)// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos); // 確保不是孤立節點// 刪除pos結點就是將pos的前驅結點和后繼結點連接起來,并且釋放pos結點pos->prev->next = pos->next; // 1 pos的前驅結點后繼指向pos的后繼結點pos->next->prev = pos->prev; // 2 pos的后繼結點前驅指向pos的前驅結點free(pos); // 3 釋放 pos 結點的內存空間 }
- 檢查頭結點有效性(assert),若
-
打印
從第一個結點打印到最后一個結點
步驟:
- 檢查頭結點有效性(assert),若
phead
中存儲的地址為NULL,則試圖訪問phead->next
會導致程序崩潰,所以需要攔截非法操作 - 驗證pos是否屬于某個鏈表(通過檢查閉環)
- 檢查是否只有一個結點(哨兵結點),如果是的話則打印
Empty List
,并且跳過后邊代碼,直接return
- 檢查是否只有一個結點(哨兵結點),如果是的話則打印
- 從第一個結點打印到最后一個結點(結束條件為cur為哨兵結點「頭結點」)
- 打印結尾
HEAD
// 12 打印 void DListPrint(DListNode* phead) {assert(phead); // 避免空指針,檢查頭指針是否為NULLDListNode* cur = phead->next; // 存儲當前結點地址// 若是頭結點if (phead->next == phead) {printf("Empty List\n");return;}// 從第一個結點打印到最后一個結點(結束條件為cur為哨兵結點「頭結點」)while (cur != phead) {printf("%d -> ", cur->data); // 打印當前結點的數據cur = cur->next; // 指向下一個結點}printf("HEAD\n"); // 最后打印結束 }
- 檢查頭結點有效性(assert),若
test.c
#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"
void Check1();
int main(void) {Check1();return 0;
}// 測試1
void Check1() {// 創建鏈表DListNode* L = DListInit(); // 新建鏈表 -- 必須要讓L頭結點為DListInit(),如果指向NULL會越界訪問// 尾插 1 ~ 5printf("尾插1 2 3 4 5\n");for (int i = 1; i <= 5; i++) {DListPushBack(L, i); // 每次尾插}DListPrint(L); // 打印鏈表// 頭插 1 ~ 5printf("頭插11 22 33 44 55\n");for (int i = 1; i <= 5; i++) {DListPushFront(L, i*10 + i); // 每次頭插}DListPrint(L); // 打印鏈表// 頭刪 5 次printf("頭刪 5 次\n");for (int i = 1; i <= 5; i++) {DListPopFront(L); // 每次頭刪}DListPrint(L); // 打印鏈表// 尾刪 2 次printf("尾刪 2 次\n");for (int i = 1; i <= 2; i++) {DListPopBack(L); // 每次尾插}DListPrint(L); // 打印鏈表// 在 3 之前插入 33printf("在 3 之前插入 33\n");DListInsert(DListFind(L, 3), 23);DListPrint(L); // 打印鏈表// 刪除數據為 23 的結點printf("刪除數據為 23 的結點\n");DListErase(DListFind(L, 23));DListPrint(L); // 打印鏈表// 清除雙鏈表(保留哨兵)printf("清除雙鏈表(保留哨兵)\n");DListClear(L);if (L->next == L && L->prev == L) {printf("鏈表已經清除\n");}// 銷毀雙鏈表printf("銷毀雙鏈表(不保留哨兵)\n");DListDestroy(&L);if (L == NULL) {printf("鏈表已經銷毀\n");}
}
DListNode.h
#define _CRT_SECURE_NO_WARNINGS
#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
typedef int ElemType;
typedef struct DListNode {ElemType data; // 數據struct DListNode* prev; // 指向前驅節點(前驅結點的地址)struct DListNode* next; // 指向后驅結點(后驅結點的地址)
}DListNode;// 申請新的結點
DListNode* BuyDListNode(ElemType x);
// 帶頭結點的雙向循環鏈表初始化 -- 創建并返回頭結點,即返回哨兵結點
DListNode* DListInit();
// 雙向鏈表清除 -- 只釋放頭節點以外的結點
void DListClear(DListNode* phead);
// 雙向鏈表銷毀
void DListDestroy(DListNode** phead);
// 頭刪頭插要重新更新頭指針
// 頭插
void DListPushFront(DListNode* phead, ElemType x);
// 尾插
void DListPushBack(DListNode* phead, ElemType x);
// 頭刪
void DListPopFront(DListNode* phead);
// 尾刪
void DListPopBack(DListNode* phead);
// 查找 找到返回結點地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x);
// 在phead的前面進行插入
void DListInsert(DListNode* pos, ElemType x);
// 刪除phead位置的節點
void DListErase(DListNode* pos);
// 打印
void DListPrint(DListNode* phead);
DListNode.c
#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"// 1 申請新的結點
DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申請新的結點if (NewNode == NULL) {printf("內存申請失敗!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存儲數據NewNode->prev = NewNode; // 初始時指向自己NewNode->next = NewNode; // 初始時指向自己return NewNode;
}
// 2 帶頭結點的雙向循環鏈表初始化 -- 創建并返回頭結點,即返回哨兵結點
DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申請新的結點 // 頭結點的 data 無意義,僅作哨兵// 因為BuyDListNode(0_中已經指向了自己所以下面代碼可寫可不寫//NewNode->prev = NewNode; // 新結點前驅指向自己//NewNode->next = NewNode; // 新結點后繼指向自己return NewNode; // 返回頭結點(哨兵結點)
}
// 3 雙向鏈表清除 -- 只釋放頭節點以外的結點
void DListClear(DListNode* phead) {assert(phead); // 防止空指針DListNode* cur = phead->next; // 存放當前的結點的地址while (cur != phead) { // 釋放處頭結點(哨兵結點)以外的結點DListNode* next = cur->next; // 保存下一個結點地址(防止釋放后地址丟失)free(cur); // 釋放當前結點內存空間cur = next; // 指向下一個結點}cur->next = cur->prev = cur;
}
// 4 雙向鏈表銷毀 -- 銷毀要用雙指針
void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指針DListClear(*phead); // 只釋放頭節點以外的結點free(*phead); // 釋放頭結點*phead = NULL; // 重新置為NULL
}
// 5 頭插
void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULL// 因為帶頭雙向循環鏈表是有頭結點的,而頭結點不存儲數據始終為NULL,即不存在頭結點給頭結點添加數據的情況DListNode* NewNode = BuyDListNode(x); // 申請新的結點// 頭插就是讓新的結點插到 “哨兵結點(頭結點)”與“第一個結點”之間DListNode* first = phead->next; // 表示第一個結點NewNode->prev = phead; // 1 新結點前驅指向頭結點NewNode->next = first; // 2 新節點后繼指向原第一個結點first->prev = NewNode; // 3 原第一結點前驅指向新的結點phead->next = NewNode; // 4 頭結點后繼指向新的結點
}
// 6 尾插
void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申請新的結點// 尾插就是將新的結點放到 tail 的后邊DListNode* tail = phead->prev; // 尾結點NewNode->prev = tail; // 1 新節點前驅指向尾節點NewNode->next = phead; // 2 新節點后繼指向頭節點tail->next = NewNode; // 3 尾節點后繼指向新節點phead->prev = NewNode; // 4 頭節點前驅指向新節點
}
// 7 頭刪
void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回// 切記:頭刪不是刪除“頭結點(哨兵結點)”,而是刪除第一個結點DListNode* first = phead->next; // 第一個結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* second = first->next; // 第二個結點phead->next = second; // 1 頭結點后繼指向第二個結點second->prev = phead; // 2 第二個結點前驅指向頭結點free(first); // 3 釋放第一個結點
}
// 8 尾刪
void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回DListNode* tail = phead->prev; // 尾結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* TailPrev = tail->prev; // 倒數第二個結點phead->prev = TailPrev; // 1 頭結點前驅指向倒數第二個結點TailPrev->next = phead; // 2 倒數第二個結點后繼指向頭結點free(tail); // 3 釋放尾結點
}
// 9 查找 找到返回結點地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指針,確保phead不是NULL// 切記:頭結點不存儲數據,查找無需找頭結點DListNode* cur = phead->next;// 從第一個結點到最后一個結點 (結束條件為:cur 走到了頭結點)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一個結點}return NULL; // 若沒找到,則返回NULL
}
// 10 在pos的前面進行插入(不包括頭結點,因為頭結點不存儲數據)
void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos); // 確保不是孤立節點DListNode* NewNode = BuyDListNode(x); // 申請新的結點// pos 前面插入 數據為 x 的結點,就是在 pos 前驅與 pos 之間插入數據DListNode* PosPrev = pos->prev; // pos 的前驅結點NewNode->prev = PosPrev; // 1 新結點的前驅指向 原pos前驅結點NewNode->next = pos; // 2 新節點的后繼指向 pos結點PosPrev->next = NewNode; // 3 原pos的前驅結點后繼指向 新的結點pos->prev = NewNode; // 4 原pos的前驅指向 新的結點
}
// 11 刪除pos位置的結點(不包括頭結點,因為頭結點不存儲數據)
void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL && 斷言指向頭結點(哨兵結點)// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos); // 確保不是孤立節點// 刪除pos結點就是將pos的前驅結點和后繼結點連接起來,并且釋放pos結點pos->prev->next = pos->next; // 1 pos的前驅結點后繼指向pos的后繼結點pos->next->prev = pos->prev; // 2 pos的后繼結點前驅指向pos的前驅結點free(pos); // 3 釋放 pos 結點的內存空間
}
// 12 打印
void DListPrint(DListNode* phead) {assert(phead); // 避免空指針,檢查頭指針是否為NULLDListNode* cur = phead->next; // 存儲當前結點地址// 若是頭結點if (phead->next == phead) {printf("Empty List\n");return;}// 從第一個結點打印到最后一個結點(結束條件為cur為哨兵結點「頭結點」)while (cur != phead) {printf("%d -> ", cur->data); // 打印當前結點的數據cur = cur->next; // 指向下一個結點}printf("HEAD\n"); // 最后打印結束
}