猶豫了幾天,看了很多大牛寫的關於c語言連結串列,感觸很多,終於下定決心,把自己對於連結串列的理解隨之附上,可用與否,自行裁奪。由於作者水平有限也是第一次寫,不足之處,竭誠希望得到各位大神的批評指正。製作不易,不喜勿噴,謝謝!!!
在正文開始之前,我先對陣列和連結串列進行簡單的對比分析。
連結串列也是一種很常見的資料結構,不同於陣列的是它是動態進行儲存分配的一種結構。陣列存放資料時,必須要事先知道元素的個數。舉個例子,比如一個班有40個人,另一個班有100個人,如果要用同一個陣列先後來存放這兩個班的學生資料,那麼必須得定義長度為100的陣列。如果事先不確定一個班的人數,只能把陣列定義的足夠大,以能存放任何班級的學生資料。這樣就很浪費記憶體,而且陣列對於記憶體的要求必須是是連續的,資料小的話還好說,資料大的話記憶體分配就會失敗,陣列定義當然也就失敗。還有陣列對於插入以及刪除元素的效率也很低這就不一一介紹了。然而連結串列就相對於比較完美,它很好的解決了陣列存在的那些問題。它儲存資料時就不需要分配連續的空間,對於元素的插入以及刪除效率就很高。可以說連結串列對於記憶體就是隨用隨拿,不像陣列要事先申請。當然,有優點就必然有缺點,就比如說連結串列裡每一個元素裡面都多包含一個地址,或者說多包含一個存放地址的指標變數,所以記憶體開銷就很大。還有因為連結串列的記憶體空間不是連續的,所以想找到其中的某一個資料就沒有陣列那麼方便,必須先得到該元素的上一個元素,根據上一個元素提供的下一元素地址去找到該元素。所以不提供“頭指標”(下文中“頭指標”為“PHead”),那麼整個連結串列將無法訪問。連結串列就相當於一條鐵鏈一環扣一環(這個稍後會詳細的說)。
連結串列
上面我提到過連結串列是動態進行儲存分配的一種結構。連結串列中的每一個元素稱為“結點”,每個結點都包括兩部分:一部分為使用者需要的實際資料,另一部分為下一結點的地址。連結串列有一個“頭指標(PHead)”變數,存放著一個地址,該地址指向第一個結點,第一個結點裡面存放著第二個結點的地址,第二個結點又存放著第三個結點地址。就這樣頭指標指向第一個結點,第一個結點又指向第二個......直到最後一個結點。最後一個結點不再指向其他結點,地址部分存放一個“NULL”。 見下圖:(表中有一個尾指標(PEnd)其作用後面會解釋)
c語言單項鍊表尾新增整體程式碼如下:(詳解附後)
#include <stdio.h> #include <stdlib.h> #include <malloc.h> //函式宣告 //尾新增 void wei_tian_jia(struct NODE** PHEAD, struct NODE** PEND, int shu_ju); //尾新增(沒有尾指標) void wei_tian_jia_(struct NODE** PHEAD, int shu_ju); //釋放連結串列 void shi_fang_lian_biao(struct NODE* PHEAD); //釋放連結串列(並是頭指標(PHead)尾指標(PEnd)指向空) void shi_fang_lian_biao_free(struct NODE** PHEAD, struct NODE** PEnd); //輸出連結串列 void shu_chu(struct NODE* PHEAD); //定義一個連結串列結構體 struct NODE { int shu_ju; //使用者需要的實際資料 struct NODE* PNext; //下一結點的地址 }; int main(void) { //建立頭尾指標 struct NODE* PHead = NULL; struct NODE* PEnd = NULL; //尾新增 wei_tian_jia(&PHead, &PEnd, 17); wei_tian_jia(&PHead, &PEnd, 21); wei_tian_jia(&PHead, &PEnd, 34); wei_tian_jia(&PHead, &PEnd, 8); wei_tian_jia(&PHead, &PEnd, 24); //尾新增(沒有尾指標) //wei_tian_jia_(&PHead, 23); //wei_tian_jia_(&PHead, 17); //wei_tian_jia_(&PHead, 11); //輸出連結串列 shu_chu(PHead); //釋放連結串列 //shi_fang_lian_biao(PHead); //釋放連結串列(並是頭指標(PHead)尾指標(PEnd)指向空) shi_fang_lian_biao_free(&PHead, &PEnd); system("pause"); return 0; }
//尾新增 void wei_tian_jia(struct NODE** PHEAD, struct NODE** PEND, int SHU_JU) { //建立結點 struct NODE* PTEMP = (struct NODE*)malloc(sizeof(struct NODE)); if (PTEMP != NULL) { //節點賦值 PTEMP->shu_ju = SHU_JU; PTEMP->PNext = NULL; //把結點連起來 if (NULL == *PHEAD) // 因為PHEAD如果是NULL的話 PEND也就是NULL 所以條件裡面不必要寫 { *PHEAD = PTEMP; *PEND = PTEMP; } else { (*PEND)->PNext = PTEMP; *PEND = PTEMP; } } } //尾新增(沒有尾指標) void wei_tian_jia_(struct NODE** PHEAD1, int SHU_JU) { //建立結點 struct NODE* PTEMP = (struct NODE*)malloc(sizeof(struct NODE)); if (PTEMP != NULL) { //結點成員賦值 PTEMP->shu_ju = SHU_JU; PTEMP->PNext = NULL; //把結點連一起 if (NULL == *PHEAD1) { *PHEAD1 = PTEMP; } else { struct NODE* PTEMP2 = *PHEAD1; while (PTEMP2->PNext != NULL) { PTEMP2 = PTEMP2->PNext; } PTEMP2->PNext = PTEMP; } } } //輸出連結串列 void shu_chu(struct NODE* PHEAD) { while (PHEAD != NULL) { printf("%d\n", PHEAD->shu_ju); PHEAD = PHEAD->PNext; } } //釋放連結串列 void shi_fang_lian_biao(struct NODE* PHEAD) { struct NODE* P = PHEAD; while (PHEAD != NULL) { struct NODE* PTEMP = P; P = P->PNext; free(PTEMP); } free(PHEAD); } //釋放連結串列(並是頭指標(PHead)尾指標(PEnd)指向空) void shi_fang_lian_biao_free(struct NODE** PHEAD, struct NODE** PEnd) { while (*PHEAD != NULL) { struct NODE* PTEMP = *PHEAD; *PHEAD = (*PHEAD)->PNext; free(PTEMP); } *PHEAD = NULL; *PHEAD = NULL; }
部分程式碼詳解:
(再次申明:由於作者水平有限,所以有的變數名用的拼音。見笑,莫怪!!!為了簡單明瞭,方便起見,我定義了一個實際資料。)
“頭指標”(PHead)以及“尾指標”(PEnd):
頭指標很好理解指向首結點用於遍歷整個陣列,而尾指標呢?我們先看下面兩段程式碼一段是有尾指標的一段是沒有尾指標的:
顯然這是一段有尾指標的程式碼。這裡的思想就是當寫入第一個成員進連結串列的時候,此時連結串列就一個成員,即是頭(PHEAD),也是尾(PEND),當寫入第二個成員的時候,連結串列頭(PHEALD)不動連結串列尾(PEND)向後移,指向最後一個結點。
//尾新增 void wei_tian_jia(struct NODE** PHEAD, struct NODE** PEND, int SHU_JU) { //建立一個結點 struct NODE* PTEMP = (struct NODE*)malloc(sizeof(struct NODE)); if (PTEMP != NULL) { //節點成員賦值(一定要每個成員都要賦值) PTEMP->shu_ju = SHU_JU; PTEMP->PNext = NULL; //把結點連起來 if (NULL == *PHEAD) // 因為PHEAD如果是NULL的話 PEND也就是NULL 所以條件裡面不必要寫 { *PHEAD = PTEMP; *PEND = PTEMP; } else { //把尾指標向後移 (*PEND)->PNext = PTEMP; *PEND = PTEMP; } } }
那麼下面這段程式碼是沒有尾指標的。它的思想就是頭指標一直指向第一個結點,然後通過遍歷來找到最後一個結點,從而使最後一個結點裡面的指標指向所要插入的元素。
//尾新增(沒有尾指標) void wei_tian_jia_(struct NODE** PHEAD1, int SHU_JU) { //建立結點 struct NODE* PTEMP = (struct NODE*)malloc(sizeof(struct NODE)); if (PTEMP != NULL) { //結點成員賦值 PTEMP->shu_ju = SHU_JU; PTEMP->PNext = NULL; //把結點連一起 if (NULL == *PHEAD1) { *PHEAD1 = PTEMP; } else { struct NODE* PTEMP2 = *PHEAD1; while (PTEMP2->PNext != NULL) { PTEMP2 = PTEMP2->PNext; } PTEMP2->PNext = PTEMP; } } }
我把上面程式碼裡面的一段摘出來說明一下。
這段程式碼裡面可以看到我又定義了一個PTEMP2指標變數,為什麼呢?前面我提到過沒有尾指標的時候新增結點的思想就是要遍歷陣列,從而找到最後一個結點然後讓它指向我們要插入的結點,如果沒有這個PHEAD2,我們遍歷完連結串列以後我們的頭指標PHEAD1就已經指向了最後一個結點了,單項鍊表如果頭指標移動了,資料就會找不到了。所以我定義了一箇中間變數裝著頭指標然後去遍歷連結串列,讓頭指標永遠指向連結串列的頭。
else { struct NODE* PTEMP2 = *PHEAD1; while (PTEMP2->PNext != NULL) { PTEMP2 = PTEMP2->PNext; } PTEMP2->PNext = PTEMP; }
可以看到有尾指標的程式碼和沒有尾指標的程式碼裡面,有尾指標的連結串列裡面我每次新增完資料都讓尾指標指向最後一個結點,然後通過尾指標來新增資料。而沒有尾指標的連結串列裡面每次新增資料都要通過迴圈來遍歷連結串列找到最後一個結點然後指向所新增的結點。如果一個連結串列裡面有幾萬個結點,每次都通過迴圈遍歷連結串列來新增資料,那麼速度就相對於有尾指標的連結串列慢很多。總而言之,還是看個人愛好吧。不管黑貓還是白貓能抓到耗子都是好貓。