線性結構:有且只有一個根節點,且每個節點最多有一個直接前驅和一個直接后繼的非空數據結構
非線性結構:不滿足線性結構的數據結構
鏈表(單向鏈表的建立、刪除、插入、打印)
1、鏈表一般分為:
單向鏈表
雙向鏈表
環形鏈表
2、基本概念
鏈表實際上是線性表的鏈式存儲結構,與數組不同的是,它是用一組任意的存儲單元來存儲線性表中的數據,存儲單元不一定是連續的,
且鏈表的長度不是固定的,鏈表數據的這一特點使其可以非常的方便地實現節點的插入和刪除操作
鏈表的每個元素稱為一個節點,每個節點都可以存儲在內存中的不同的位置,為了表示每個元素與后繼元素的邏輯關系,以便構成“一個節點鏈着一個節點”的鏈式存儲結構,
除了存儲元素本身的信息外,還要存儲其直接后繼信息,因此,每個節點都包含兩個部分,第一部分稱為鏈表的數據區域,用於存儲元素本身的數據信息,這里用data表示,
它不局限於一個成員數據,也可是多個成員數據,第二部分是一個結構體指針,稱為鏈表的指針域,用於存儲其直接后繼的節點信息,這里用next表示,
next的值實際上就是下一個節點的地址,當前節點為末節點時,next的值設為空指針
1 struct link 2 { 3 int data; 4 struct link *next; 5 };
像上面這種只包含一個指針域、由n個節點鏈接形成的鏈表,就稱為線型鏈表或者單向鏈表,鏈表只能順序訪問,不能隨機訪問,鏈表這種存儲方式最大缺點就是容易出現斷鏈,
一旦鏈表中某個節點的指針域數據丟失,那么意味着將無法找到下一個節點,該節點后面的數據將全部丟失
3、鏈表與數組比較
數組(包括結構體數組)的實質是一種線性表的順序表示方式,它的優點是使用直觀,便於快速、隨機地存取線性表中的任一元素,但缺點是對其進行 插入和刪除操作時需要移動大量的數組元素,同時由於數組屬於靜態內存分配,定義數組時必須指定數組的長度,程序一旦運行,其長度就不能再改變,實際使用個數不能超過數組元素最大長度的限制,否則就會發生下標越界的錯誤,低於最大長度時又會造成系統資源的浪費,因此空間效率差
4、單向鏈表的建立
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 struct link *AppendNode (struct link *head); 5 void DisplyNode (struct link *head); 6 void DeletMemory (struct link *head); 7 8 struct link 9 { 10 int data; 11 struct link *next; 12 }; 13 14 int main(void) 15 { 16 int i = 0; 17 char c; 18 struct link *head = NULL; //鏈表頭指針 19 printf("Do you want to append a new node(Y/N)?"); 20 scanf_s(" %c", &c); 21 while (c == 'Y' || c == 'y') 22 { 23 head = AppendNode(head);//向head為頭指針的鏈表末尾添加節點 24 DisplyNode(head); //顯示當前鏈表中的各節點的信息 25 printf("Do your want to append a new node(Y/N)"); 26 scanf_s(" %c", &c); 27 i++; 28 } 29 printf("%d new nodes have been apended", i); 30 DeletMemory(head); //釋放所有動態分配的內存 31 32 return 0; 33 } 34 /* 函數功能:新建一個節點並添加到鏈表末尾,返回添加節點后的鏈表的頭指針 */ 35 struct link *AppendNode(struct link *head) 36 { 37 struct link *p = NULL, *pr = head; 38 int data; 39 p = (struct link *)malloc(sizeof(struct link));//讓p指向新建的節點 40 if (p == NULL) //若新建節點申請內存失敗,則退出程序 41 { 42 printf("No enough memory to allocate\n"); 43 exit(0); 44 } 45 if (head == NULL) //若原鏈表為空表 46 { 47 head = p; //將新建節點置為頭節點 48 } 49 else //若原鏈表為非空,則將新建節點添加到表尾 50 { 51 while (pr->next != NULL)//若未到表尾,則移動pr直到pr指向表尾 52 { 53 pr = pr->next; //讓pr指向下一個節點 54 } 55 pr->next = p; //讓末節點的指針指向新建的節點 56 } 57 printf("Input node data\n"); 58 scanf_s("%d", &data); //輸入節點數據 59 p->data = data; //將新建節點的數據域賦值為輸入的節點數據值 60 p->next = NULL; //將新建的節點置為表尾 61 return head; //返回添加節點后的鏈表的頭指針 62 } 63 /* 函數的功能:顯示鏈表中所有節點的節點號和該節點中的數據項的內容*/ 64 void DisplyNode (struct link *head) 65 { 66 struct link *p = head; 67 int j = 1; 68 while (p != NULL) //若不是表尾,則循環打印節點的數值 69 { 70 printf("%5d%10d\n", j, p->data);//打印第j個節點數據 71 p = p->next; //讓p指向下一個節點 72 j++; 73 } 74 } 75 //函數的功能:釋放head所指向的鏈表中所有節點占用的內存 76 void DeletMemory(struct link *head) 77 { 78 struct link *p = head, *pr = NULL; 79 while (p != NULL) //若不是表尾,則釋放節點占用的內存 80 { 81 pr = p; //在pr中保存當前節點的指針 82 p = p->next;//讓p指向下一個節點 83 free(pr); //釋放pr指向的當前節點占用的內存 84 } 85 }
上面的代碼使用了三個函數AppendNode、DisplyNode、DeletMemory
struct link *AppendNode (struct link *head);(函數作用:新建一個節點並添加到鏈表末尾,返回添加節點后的鏈表的頭指針)
void DisplyNode (struct link *head);(函數功能:顯示鏈表中所有節點的節點號和該節點中的數據項的內容)
void DeletMemory (struct link *head);(函數功能:釋放head所指向的鏈表中所有節點占用的內存)
(還使用了malloc函數和free函數)
5、malloc函數
作用:用於分配若干字節的內存空間,返回一個指向該內存首地址的指針,若系統不能提供足夠的內存單元,函數將返回空指針NULL,函數原型為void *malloc(unsigned int size)
其中size是表示向系統申請空間的大小,函數調用成功將返回一個指向void的指針(void*指針是ANSIC新標准中增加的一種指針類型,
具有一般性,通常稱為通用指針或者無類型的指針)常用來說明其基類型未知的指針,即聲明了一個指針變量,但未指定它可以指向哪一種基類型的數據,
因此,若要將函數調用的返回值賦予某個指針,則應先根據該指針的基類型,用強轉的方法將返回的指針值強轉為所需的類型,然后再進行賦值
1 int *pi; 2 pi = (int *)malloc(4);
其中malloc(4)表示申請一個大小為4字節的內存,將malloc(4)返回值的void*類型強轉為int*類型后再賦值給int型指針變量pi,即用int型指針變量pi指向這段存儲空間的首地址
若不能確定某種類型所占內存的字節數,則需使用sizeof()計算本系統中該類型所占的內存字節數,然后再用malloc()向系統申請相應字節數的存儲空間
pi = (int *)malloc(sizeof(int));
6、free函數
釋放向系統動態申請的由指針p指向的內存存儲空間,其原型為:Void free(void *p);該函數無返回值,唯一的形參p給出的地址只能由malloc()和calloc()申請內存時返回的地址,
該函數執行后,將以前分配的指針p指向的內存返還給系統,以便系統重新分配
為什么要用free釋放內存
(在程序運行期間,用動態內存分配函數來申請的內存都是從堆上分配的,動態內存的生存期有程序員自己來決定,使用非常靈活,但也易出現內存泄漏的問題,
為了防止內存泄漏的發生,程序員必須及時調用free()釋放已不再使用的內存)
7、單向鏈表的刪除操作
刪除操作就是將一個待刪除的節點從鏈表中斷開,不再與鏈表的其他節點有任何聯系
需考慮四種情況:
1.若原鏈表為空表,則無需刪除節點,直接退出程序
2.若找到的待刪除節點p是頭節點,則將head指向當前節點的下一個節點(p->next),即可刪除當前節點
3.若找到的待刪除節點不是頭節點,則將前一節點的指針域指向當前節點的下一節點(pr->next = p->next),即可刪除當前節點,當待刪除節點是末節點時,
由於p->next值為NULL,因此執行pr->next = p->next后,pr->next的值也變成NULL,從而使pr所指向的節點由倒數第2個節點變成了末節點
4.若已搜索到表尾(p->next == NULL),仍未找到待刪除節點,則顯示“未找到”,注意:節點被刪除后,只是將它從鏈表中斷開而已,它仍占用着內存,必須釋放其所占的內存,否則將出現內存泄漏
(頭結點不是頭指針,注意兩者區別)
8、頭節點和頭指針
頭指針存儲的是頭節點內存的首地址,頭結點的數據域可以存儲如鏈表長度等附加信息,也可以不存儲任何信息
參考鏈接---頭指針和頭節點:https://www.cnblogs.com/didi520/p/4165486.html
https://blog.csdn.net/qq_37037492/article/details/78453333
https://www.cnblogs.com/marsggbo/p/6622962.html
https://blog.csdn.net/hunjiancuo5340/article/details/80671298
(圖片出處:https://blog.csdn.net/hunjiancuo5340/article/details/80671298)
值得注意的是:
1.無論鏈表是否為空,頭指針均不為空。頭指針是鏈表的必要元素
2.鏈表可以沒有頭節點,但不能沒有頭指針,頭指針是鏈表的必要元素
3.記得使用free釋放內存
單向鏈表的刪除操作實現
1 struct link *DeleteNode (struct link *head, int nodeData) 2 { 3 struct link *p = head, *pr = head; 4 5 if (head == NULL) 6 { 7 printf("Linked table is empty!\n"); 8 return 0; 9 } 10 while (nodeData != p->data && p->next != NULL) 11 { 12 pr = p; /* pr保存當前節點 */ 13 p = p->next; /* p指向當前節點的下一節點 */ 14 } 15 if (nodeData == p->data) 16 { 17 if (p == head) /* 如果待刪除為頭節點 (注意頭指針和頭結點的區別)*/ 18 { 19 head = p->next; 20 } 21 else /* 如果待刪除不是頭節點 */ 22 { 23 pr->next = p->next; 24 } 25 free(p); /* 釋放已刪除節點的內存 */ 26 } 27 else /* 未發現節點值為nodeData的節點 */ 28 { 29 printf("This Node has not been found"); 30 } 31 32 return head; 33 }
9、單向鏈表的插入
向鏈表中插入一個新的節點時,首先由新建一個節點,將其指針域賦值為空指針(p->next = NULL),然后在鏈表中尋找適當的位置執行節點的插入操作,
此時需要考慮以下四種情況:
1.若原鏈表為空,則將新節點p作為頭節點,讓head指向新節點p(head = p)
2.若原鏈表為非空,則按節點值的大小(假設節點值已按升序排序)確定插入新節點的位置,若在頭節點前插入新節點,則將新節點的指針域指向原鏈表的頭節點(p->next = head),且讓head指向新節點(head =p)
3.若在鏈表中間插入新節點,則將新節點的指針域之下一節點(p->next = pr -> next),且讓前一節點的指針域指向新節點(pr->next = p)
4.若在表尾插入新節點,則末節點指針域指向新節點(p->next = p)
單向鏈表的插入操作實現
1 /* 函數功能:向單向鏈表中插入數據 按升序排列*/ 2 struct link *InsertNode(struct link *head, int nodeData) 3 { 4 struct link *p = head, *pr = head, *temp = NULL; 5 6 p = (struct link *)malloc(sizeof(struct link)); 7 if (p == NULL) 8 { 9 printf("No enough meomory!\n"); 10 exit(0); 11 } 12 p->next = NULL; /* 待插入節點指針域賦值為空指針 */ 13 p->data = nodeData; 14 15 if (head == NULL) /* 若原鏈表為空 */ 16 { 17 head = p; /* 插入節點作頭結點 */ 18 } 19 else /* 原鏈表不為空 */ 20 { 21 while (pr->data < nodeData && pr->next != NULL) 22 { 23 temp = pr; /* 保存當前節點的指針 */ 24 pr = pr->next; /* pr指向當前節點的下一節點 */ 25 } 26 if (pr->data >= nodeData) 27 { 28 if (pr == head) /* 在頭節點前插入新節點 */ 29 { 30 p->next = head; /* 新節點指針域指向原鏈表頭結點 */ 31 head = p; /* 頭指針指向新節點 */ 32 } 33 else 34 { 35 pr = temp; 36 p->next = pr->next; /* 新節點指針域指向下一節點 */ 37 pr->next = p; /* 讓前一節點指針域指向新節點 */ 38 } 39 } 40 else /* 若在表尾插入新節點 */ 41 { 42 pr->next = p; /* 末節點指針域指向新節點*/ 43 } 44 } 45 46 return head; 47 }