數據結構之單鏈表的實現
在上一節 :數據結構之順序表
我們提到了順序表的一些缺陷,那有沒有什么數據結構可以減少這些問題呢?
答案自然就是今天我們所要說的鏈表。
本節大綱:
- 鏈表的概念與結構
- 單鏈表的實現
- 完整代碼展示
一.鏈表的概念與結構
1.概念
鏈表是一種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的 。
2.結構及種類
鏈表有好多種:單雙向鏈表,帶頭不帶頭鏈表,循環非循環鏈表 ;它們兩兩組合可以組成 8 種鏈表結構,如下圖所示:
鏈表又包括 數據域 和 指針域。
雖然有這么多的鏈表的結構,但是我們實際中最常用還是兩種結構:
1. 無頭單向非循環鏈表:
結構簡單,一般不會單獨用來存數據。實際中更多是作為其他數據結構的子結構,如哈希桶、圖的鄰接表等等。
另外這種結構在筆試面試中出現很多。
2. 帶頭雙向循環鏈表:
結構最復雜,一般用在單獨存儲數據。實際中使用的鏈表數據結構,都是帶頭雙向循環鏈表。
另外這個結構雖然結構復雜,但是使用代碼實現以后會發現結構會帶來很多優勢,實現反而簡單了。
所以我們今天從 無頭單向非循環鏈表 開始:
三.單鏈表的實現
1.自定義數據類型
首先我們肯定要先創建一個自定義數據類型:
//進行自定義類型,方便修改 typedef int DataType; //進行自定義數據類型 typedef struct SLTNode { DataType data;//存放數據 struct SLTNode* next;//指向下一塊空間 }SLTNode;
2.打印函數
假設我們現在有一個數據完全的單鏈表,但是我們想在看到它的值,這時就需要一個打印函數了。
現在我們來想一想我們剛剛所提到的單鏈表結構,它是由數據域和指針域組合而成,而它中的指針指向的是下一塊儲存空間,如果該指針為NULL,就說明該鏈表已結束。
//單鏈表打印函數 void SLTPrint(SLTNode *p) { if (p == NULL)//如果該鏈表為空 { printf("該鏈表中並無值可供打印\n"); return;//直接退出 } //正常情況下 //cur指向當前位置 SLTNode *cur = p; while (cur)//如果cur是NULL,循環就停止 { printf("%d -> ", cur->data); cur = cur->next;//cur指向下一塊空間 } printf("NULL\n"); }
在這會有一些人問 :while 中的判斷條件是否可以寫為 cur->next ,我們來看一下:
從上圖中,我們可以明顯看出:若循環條件為 cur->next ,這樣循環到達最后一個節點就退出了,最后一個節點內的值並未打印,當然要是你在循環結束后,再加一句printf,那自然也沒錯
3.創建節點函數:
因后面的頭插、尾插、任意位置插入,都需要去創建節點,所以在這就把它單獨列一個模塊:
//創建節點函數 SLTNode *BuySLTNode(DataType data) { //動態開辟一塊空間 SLTNode *new_node = (SLTNode *) malloc(sizeof(SLTNode)); if (new_node == NULL)//如果未開辟成功 { perror("BuyNode Error!");//打印錯誤信息 exit(-1); } else { //新結點的值為傳進來的值 new_node->data = data; new_node->next = NULL; } //返回新建立的節點 return new_node; }
4.單鏈表尾插
尾插,我們肯定要先找到該鏈表的尾,然后將尾的next指向我們新建立的結點,將新節點的next指向NULL;
//單鏈表尾插函數 void SLTPushBack(SLTNode *p, DataType InsertData) { //創建一個新節點 SLTNode *new_node = BuySLTNode(InsertData); if (p == NULL)//說明該鏈表為空 { p = new_node; } else//非空就找尾 { SLTNode *cur = p; while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur // 因為我們要用最后一個節點的next // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面 { cur = cur->next;//指向下一個 } cur->next = new_node;//尾插 } }
可是,上面這段代碼有沒有問題呢? 我們來運行一下:

void test1() { SLTNode *p = NULL; SLTPushBack(p,0); SLTPushBack(p,1); SLTPushBack(p,2); SLTPushBack(p,3); SLTPrint(p); }
我們明明尾插了4次,最后為什么還是無值可供打印呢? 我們再來調試一下:
我們發現,它在函數里都換過了,卻出了函數又變為了NULL。我們思來想去總覺得這種情形好像在哪見過 --- 哦,原來曾在這個例子中見過:
#include<stdio.h> void swap(int a, int b) { int c = a; a = b; b = c; } int main() { int a = 2, b = 3; printf("before:\na=%d, b=%d\n", a, b); swap(a, b); printf("after:\na=%d, b=%d\n", a, b); return 0; }
當時,我們便已提到,改變其值時,要傳值,但要改變其指向,要傳址;
在舉一個例子:

void change(char *p) { p[0] = '!'; } void change_2(char **p) { static char *a = "12334"; *p = a; } int main() { char a[] = "dadasfsfa"; char *p = a; //改變指針所指向的值 change(p); printf("%s\n", p);//!adasfsfa //改變指針的指向 change_2(&p); printf("%s\n", p);//12334 return 0; }
所以正確的尾插怎么寫呢:
1.函數參數為STLNode** ,即改為址傳遞,因為當 p為空的時候,我們需要改變其指向,指向我們新建的new_node
//單鏈表尾插函數 void SLTPushBack(SLTNode **p, DataType InsertData) { //創建一個新節點 SLTNode *new_node = BuySLTNode(InsertData); if (*p == NULL)//說明該鏈表為空 { *p = new_node;//改變p的指向 } else//非空就找尾 { SLTNode *cur = *p; while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur // 因為我們要用最后一個節點的next // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面 { cur = cur->next;//指向下一個 } cur->next = new_node;//尾插 } }
5.單鏈表頭插
現在有了之前的錯誤經驗,是不是一下就知道這個肯定是傳址啊:
//單鏈表頭插函數 void SLTPushFront(SLTNode **p, DataType InsertData) { SLTNode *new_node = BuySLTNode(InsertData);//創建新節點 //在頭插中,並不關心*p為不為空,反正最后它們的處理都是一樣的 //將原鏈表內容連接在new_node上 new_node->next = *p; //改變指向 *p = new_node; }
6.單鏈表尾刪
循環遍歷找尾,釋放,置NULL
//單鏈表尾刪函數 void SLTPopBack(SLTNode **p) { if (*p == NULL)//表示該鏈表為空 { printf("該鏈表中並無值可供刪除!\n"); return; } else if ((*p)->next == NULL)//表示該鏈表中只有一個值,刪除之后就為空 { free(*p);//釋放空間 *p = NULL;//置為NULL; } else { //循環遍歷找尾 SLTNode *cur = *p;//指向當前位置 SLTNode *prev = NULL;//指向前一個位置 while (cur->next) { prev = cur; cur = cur->next; } free(cur);//釋放空間 cur = NULL;//及時置NULL //刪除之后prev便是最后一個節點了 prev->next = NULL; } }
7.單鏈表頭刪
釋放改指向
//單鏈表頭刪函數 void SLTPopFront(SLTNode **p) { if (*p == NULL)//表示該鏈表為空 { printf("該鏈表中並無值可供刪除!\n"); return; } else { SLTNode* pop_node=*p;//指向頭節點 *p=(*p)->next;//指向下一個元素 free(pop_node);//釋放空間 pop_node=NULL;//及時置NULL } }
8.單鏈表查找函數
循環遍歷
//單鏈表查找函數 SLTNode *SLTFindNode(SLTNode *p, DataType FindData) { assert(p);//確保傳進來的指針不為空 SLTNode *cur = p; while (cur) { if (cur->data == FindData) { return cur;//找到就返回該節點的地址 } cur = cur->next;//循環遍歷 } return NULL;//找不到就返回NULL }
9.單鏈表修改函數
//單鏈表修改函數 void SLTModify(SLTNode *pos, DataType ModifyData) { assert(pos);//確保pos不為空 pos->data = ModifyData;//修改 }
10.任意位置插入
1.單鏈表在pos位置之后插入insertdata
//單鏈表在pos位置之后插入InsertData void SLTInsertAfter(SLTNode* pos,DataType InsertData) { assert(pos);//保證pos不為NULL SLTNode* new_node=BuySLTNode(InsertData);//創建一個新節點 //該節點的下一個指向原本該位置的下一個 new_node->next=pos->next; //該位置的下一個就是new_node pos->next=new_node; }
2.單鏈表在pos位置之前插入insertdata
這時就要考慮到若位置是第一個位置時,那就相當於頭插了
此外,我們還得找到它的前一個位置
//單鏈表在pos位置之前插入InsertData void SLTInsertBefore(SLTNode **p, SLTNode *pos, DataType InsertData) { assert(pos); SLTNode *new_node = BuySLTNode(InsertData); if (pos == *p)//這就說明是第一個位置前插入 { //頭插 new_node->next = *p; *p = new_node; } else { SLTNode *cur = *p; SLTNode *prev = NULL; //新節點的下一個指向這個位置 new_node->next = pos; //循環遍歷找到它的前一個位置 while (cur != pos) { prev = cur; cur = cur->next; } //前一個位置指向新節點 prev->next = new_node; } }
除此之外,還有哦另外一種方法,可以不用去知道它鏈表的第一個位置
我們在傳過去的那個位置,來進行一個后插,再交換這兩個節點的數據 ,這也是一種方法:
//單鏈表在pos位置之前插入InsertData void SLTInsertBefore_2(SLTNode *pos, DataType InsertData) { assert(pos); SLTNode *new_node = BuySLTNode(InsertData); new_node->next = pos->next; pos->next = new_node; //進行了一個后插 //交換這兩個變量的值 DataType tmp = pos->data; pos->data = new_node->data; new_node->data = tmp; }
如圖:
11.任意位置刪除
1.刪除pos位后的數據
//刪除pos位后的數據 void SLTEraseAfter(SLTNode *pos) { assert(pos); if (pos->next == NULL) { //說明后面並沒有元素了 printf("該位置后無數據可以刪除\n"); } else { //創建一個指針指向下一個位置 SLTNode *next = pos->next; //原位置的下一個位置,指向 下一個位置 的下一個位置 pos->next = next->next; //釋放內存 free(next); next = NULL; } }
2.刪除pos位的數據 --- 這就包含了頭刪
//刪除pos位的數據 --- 含有頭刪 void SLTEraseCur(SLTNode **p, SLTNode *pos) { assert(pos); if (*p == pos) { //說明是頭刪 *p = pos->next; free(pos); pos = NULL; } else { //普通位置 SLTNode *prev = *p; SLTNode *cur = *p; //找到頭一個位置 while (cur != pos) { prev = cur; cur = cur->next; } //頭一個位置指向下下個位置 prev->next = pos->next; free(pos); pos = NULL; } }
3.同樣可以用之前所提到的方法進行帶有頭刪的任意位置刪除
1.把該位置 后一個節點的數據存下;
2.刪除該位置后的節點
3.將頭節點的值改為之前存下的數據
單鏈表的實現到這便就結束了
三.完整代碼展示

//確保頭文件只包含一次 #ifndef FINASLT_SLT_H #define FINASLT_SLT_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <assert.h> //進行自定義類型,方便修改 typedef int DataType; //進行自定義數據類型 typedef struct SLTNode { DataType data;//存放數據 struct SLTNode *next;//指向下一塊空間 } SLTNode; //單鏈表打印函數 void SLTPrint(SLTNode *p); //創建節點函數 SLTNode *BuySLTNode(DataType data); //單鏈表尾插函數 void SLTPushBack(SLTNode **p, DataType InsertData); //單鏈表頭插函數 void SLTPushFront(SLTNode **p, DataType InsertData); //單鏈表尾刪函數 void SLTPopBack(SLTNode **p); //單鏈表頭刪函數 void SLTPopFront(SLTNode **p); //單鏈表查找函數 SLTNode *SLTFindNode(SLTNode *p, DataType FindData); //單鏈表修改函數 void SLTModify(SLTNode *pos, DataType ModifyData); //單鏈表在pos位置之后插入InsertData void SLTInsertAfter(SLTNode *pos, DataType InsertData); //單鏈表在pos位置之前插入InsertData void SLTInsertBefore(SLTNode **p, SLTNode *pos, DataType InsertData); //單鏈表在pos位置之前插入InsertData void SLTInsertBefore_2( SLTNode *pos, DataType InsertData); //刪除pos位后的數據 void SLTEraseAfter(SLTNode *pos); //刪除pos位的數據 --- 含有頭刪 void SLTEraseCur(SLTNode **p, SLTNode *pos); #endif //FINASLT_SLT_H

#include "SLT.h" //單鏈表打印函數 void SLTPrint(SLTNode *p) { if (p == NULL)//如果該鏈表為空 { printf("該鏈表中並無值可供打印\n"); return;//直接退出 } //正常情況下 //cur指向當前位置 SLTNode *cur = p; while (cur)//如果cur是NULL,循環就停止 { printf("%d -> ", cur->data); cur = cur->next;//cur指向下一塊空間 } printf("NULL\n"); } //創建節點函數 SLTNode *BuySLTNode(DataType data) { //動態開辟一塊空間 SLTNode *new_node = (SLTNode *) malloc(sizeof(SLTNode)); if (new_node == NULL)//如果未開辟成功 { perror("BuyNode Error!");//打印錯誤信息 exit(-1); } else { //新結點的值為傳進來的值 new_node->data = data; new_node->next = NULL; } //返回新建立的節點 return new_node; } ////單鏈表尾插函數 //void SLTPushBack(SLTNode *p, DataType InsertData) //{ // //創建一個新節點 // SLTNode *new_node = BuySLTNode(InsertData); // if (p == NULL)//說明該鏈表為空 // { // p = new_node; // } // else//非空就找尾 // { // SLTNode *cur = p; // while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur // // 因為我們要用最后一個節點的next // // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面 // { // cur = cur->next;//指向下一個 // } // cur->next = new_node;//尾插 // } //} //單鏈表尾插函數 void SLTPushBack(SLTNode **p, DataType InsertData) { //創建一個新節點 SLTNode *new_node = BuySLTNode(InsertData); if (*p == NULL)//說明該鏈表為空 { *p = new_node;//改變p的指向 } else//非空就找尾 { SLTNode *cur = *p; while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur // 因為我們要用最后一個節點的next // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面 { cur = cur->next;//指向下一個 } cur->next = new_node;//尾插 } } //單鏈表頭插函數 void SLTPushFront(SLTNode **p, DataType InsertData) { SLTNode *new_node = BuySLTNode(InsertData);//創建新節點 //在頭插中,並不關心*p為不為空,反正最后它們的處理都是一樣的 //將原鏈表內容連接在new_node上 new_node->next = *p; //改變指向 *p = new_node; } //單鏈表尾刪函數 void SLTPopBack(SLTNode **p) { if (*p == NULL)//表示該鏈表為空 { printf("該鏈表中並無值可供刪除!\n"); return; } else if ((*p)->next == NULL)//表示該鏈表中只有一個值,刪除之后就為空 { free(*p);//釋放空間 *p = NULL;//置為NULL; } else { //循環遍歷找尾 SLTNode *cur = *p;//指向當前位置 SLTNode *prev = NULL;//指向前一個位置 while (cur->next) { prev = cur; cur = cur->next; } free(cur);//釋放空間 cur = NULL;//及時置NULL //刪除之后prev便是最后一個節點了 prev->next = NULL; } } //單鏈表頭刪函數 void SLTPopFront(SLTNode **p) { if (*p == NULL)//表示該鏈表為空 { printf("該鏈表中並無值可供刪除!\n"); return; } else { SLTNode *pop_node = *p;//指向頭節點 *p = (*p)->next;//指向下一個元素 free(pop_node);//釋放空間 pop_node = NULL;//及時置NULL } } //單鏈表查找函數 SLTNode *SLTFindNode(SLTNode *p, DataType FindData) { assert(p);//確保傳進來的指針不為空 SLTNode *cur = p; while (cur) { if (cur->data == FindData) { return cur;//找到就返回該節點的地址 } cur = cur->next;//循環遍歷 } return NULL;//找不到就返回NULL } //單鏈表修改函數 void SLTModify(SLTNode *pos, DataType ModifyData) { assert(pos);//確保pos不為空 pos->data = ModifyData;//修改 } //單鏈表在pos位置之后插入InsertData void SLTInsertAfter(SLTNode *pos, DataType InsertData) { assert(pos);//保證pos不為NULL SLTNode *new_node = BuySLTNode(InsertData);//創建一個新節點 //該節點的下一個指向原本該位置的下一個 new_node->next = pos->next; //該位置的下一個就是new_node pos->next = new_node; } //單鏈表在pos位置之前插入InsertData void SLTInsertBefore(SLTNode **p, SLTNode *pos, DataType InsertData) { assert(pos); SLTNode *new_node = BuySLTNode(InsertData); if (pos == *p)//這就說明是第一個位置前插入 { //頭插 new_node->next = *p; *p = new_node; } else { SLTNode *cur = *p; SLTNode *prev = NULL; //新節點的下一個指向這個位置 new_node->next = pos; //循環遍歷找到它的前一個位置 while (cur != pos) { prev = cur; cur = cur->next; } //前一個位置指向新節點 prev->next = new_node; } } //單鏈表在pos位置之前插入InsertData void SLTInsertBefore_2(SLTNode *pos, DataType InsertData) { assert(pos); SLTNode *new_node = BuySLTNode(InsertData); new_node->next = pos->next; pos->next = new_node; //進行了一個后插 //交換這兩個變量的值 DataType tmp = pos->data; pos->data = new_node->data; new_node->data = tmp; } //刪除pos位后的數據 void SLTEraseAfter(SLTNode *pos) { assert(pos); if (pos->next == NULL) { //說明后面並沒有元素了 printf("該位置后無數據可以刪除\n"); } else { //創建一個指針指向下一個位置 SLTNode *next = pos->next; //原位置的下一個位置,指向 下一個位置 的下一個位置 pos->next = next->next; //釋放內存 free(next); next = NULL; } } //刪除pos位的數據 --- 含有頭刪 void SLTEraseCur(SLTNode **p, SLTNode *pos) { assert(pos); if (*p == pos) { //說明是頭刪 *p = pos->next; free(pos); pos = NULL; } else { //普通位置 SLTNode *prev = *p; SLTNode *cur = *p; //找到頭一個位置 while (cur != pos) { prev = cur; cur = cur->next; } //頭一個位置指向下下個位置 prev->next = pos->next; free(pos); pos = NULL; } }

#include "SLT.h" void test1() { SLTNode *p = NULL; SLTPushBack(&p, 0); SLTPushBack(&p, 1); SLTPushBack(&p, 2); SLTPrint(p); SLTPushFront(&p, 0); SLTPushFront(&p, 1); SLTPushFront(&p, 2); SLTPrint(p); SLTPopBack(&p); SLTPrint(p); SLTPopFront(&p); SLTPrint(p); SLTNode *pos = SLTFindNode(p, 1); if (pos == NULL) { printf("該鏈表中無所查找的值!\n"); } else { SLTModify(pos,123); } SLTPrint(p); SLTInsertAfter(pos,321); SLTPrint(p); SLTInsertBefore(&p,pos,000); SLTPrint(p); SLTInsertBefore_2(SLTFindNode(p,1),666); SLTPrint(p); SLTEraseAfter(SLTFindNode(p,1)); SLTEraseAfter(SLTFindNode(p,666)); SLTEraseCur(&p,SLTFindNode(p,0)); SLTEraseCur(&p,SLTFindNode(p,123)); SLTEraseCur(&p,SLTFindNode(p,321)); SLTEraseCur(&p,SLTFindNode(p,0)); SLTEraseCur(&p,SLTFindNode(p,0)); SLTEraseCur(&p,SLTFindNode(p,666)); SLTPrint(p); } int main() { setbuf(stdout, NULL); test1(); return 0; }
|--------------------------------------------------------
關於單鏈表的知識到這便結束了
因筆者水平有限,若有錯誤,還望指正!