線性結構:有且只有一個根節點,且每個節點最多有一個直接前驅和一個直接後繼的非空數據結構。
非線性結構:不滿足線性結構的數據結構。
目錄
鏈表(單向鏈表的建立、刪除、插入、打印)
1、鏈表分類
單向鏈表
雙向鏈表
環形鏈表
2、基本概念
鏈表實際上是線性表的鏈式存儲結構,與數組不同的是,它是用一組任意的存儲單元來存儲線性表中的數據,存儲單元不一定是連續的,且鏈表的長度不是固定的,鏈表數據的這一特點使其可以非常的方便地實現節點的插入和刪除操作
鏈表的每個元素稱爲一個節點,每個節點都可以存儲在內存中的不同的位置,爲了表示每個元素與後繼元素的邏輯關係,以便構成“一個節點鏈着一個節點”的鏈式存儲結構,
除了存儲元素本身的信息外,還要存儲其直接後繼信息,因此,每個節點都包含兩個部分,第一部分稱爲鏈表的數據區域,用於存儲元素本身的數據信息,這裏用data表示,它不侷限於一個成員數據,也可是多個成員數據,第二部分是一個結構體指針,稱爲鏈表的指針域,用於存儲其直接後繼的節點信息,這裏用next表示,
next的值實際上就是下一個節點的地址,當前節點爲末節點時,next的值設爲空指針
1 struct link 2 { 3 int data; 4 struct link *next; 5 };
像上面這種只包含一個指針域、由n個節點鏈接形成的鏈表,就稱爲線型鏈表或者單向鏈表,鏈表只能順序訪問,不能隨機訪問,鏈表這種存儲方式最大缺點就是容易出現斷鏈,
一旦鏈表中某個節點的指針域數據丟失,那麼意味着將無法找到下一個節點,該節點後面的數據將全部丟失
3、鏈表與數組比較
數組(包括結構體數組)的實質是一種線性表的順序表示方式,它的優點是使用直觀,便於快速、隨機地存取線性表中的任一元素,但缺點是對其進行 插入和刪除操作時需要移動大量的數組元素,同時由於數組屬於靜態內存分配,定義數組時必須指定數組的長度,程序一旦運行,其長度就不能再改變,實際使用個數不能超過數組元素最大長度的限制,否則就會發生下標越界的錯誤,低於最大長度時又會造成系統資源的浪費,因此空間效率差
4、單向鏈表的建立
#include <stdio.h>
#include <stdlib.h>
struct link *AppendNode (struct link *head);
void DisplyNode (struct link *head);
void DeletMemory (struct link *head);
struct link
{
int data;
struct link *next;
};
nt main(void)
{
int i = 0;
char c;
struct link *head = NULL; //鏈表頭指針
printf("Do you want to append a new node(Y/N)?");
scanf_s(" %c", &c);
while (c == 'Y' || c == 'y')
{
head = AppendNode(head);//向head爲頭指針的鏈表末尾添加節點
DisplyNode(head); //顯示當前鏈表中的各節點的信息
printf("Do your want to append a new node(Y/N)");
scanf_s(" %c", &c);
i++;
}
printf("%d new nodes have been apended", i);
DeletMemory(head); //釋放所有動態分配的內存
return 0;
}
/* 函數功能:新建一個節點並添加到鏈表末尾,返回添加節點後的鏈表的頭指針 */
struct link *AppendNode(struct link *head)
{
struct link *p = NULL, *pr = head;
int data;
p = (struct link *)malloc(sizeof(struct link));//讓p指向新建的節點
if (p == NULL) //若新建節點申請內存失敗,則退出程序
{
printf("No enough memory to allocate\n");
exit(0);
}
if (head == NULL) //若原鏈表爲空表
{
head = p; //將新建節點置爲頭節點
}
else //若原鏈表爲非空,則將新建節點添加到表尾
{
while (pr->next != NULL)//若未到表尾,則移動pr直到pr指向表尾
{
pr = pr->next; //讓pr指向下一個節點
}
pr->next = p; //讓末節點的指針指向新建的節點
}
printf("Input node data\n");
scanf_s("%d", &data); //輸入節點數據
p->data = data; //將新建節點的數據域賦值爲輸入的節點數據值
p->next = NULL; //將新建的節點置爲表尾
return head; //返回添加節點後的鏈表的頭指針
}
/* 函數的功能:顯示鏈表中所有節點的節點號和該節點中的數據項的內容*/
void DisplyNode (struct link *head)
{
struct link *p = head;
int j = 1;
while (p != NULL) //若不是表尾,則循環打印節點的數值
{
printf("%5d%10d\n", j, p->data);//打印第j個節點數據
p = p->next; //讓p指向下一個節點
j++;
}
}
//函數的功能:釋放head所指向的鏈表中所有節點佔用的內存
void DeletMemory(struct link *head)
{
struct link *p = head, *pr = NULL;
while (p != NULL) //若不是表尾,則釋放節點佔用的內存
{
pr = p; //在pr中保存當前節點的指針
p = p->next;//讓p指向下一個節點
free(pr); //釋放pr指向的當前節點佔用的內存
}
}
上面的代碼使用了三個函數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
值得注意的是:
1.無論鏈表是否爲空,頭指針均不爲空。頭指針是鏈表的必要元素
2.鏈表可以沒有頭節點,但不能沒有頭指針,頭指針是鏈表的必要元素
3.記得使用free釋放內存
單向鏈表的刪除操作實現
struct link *DeleteNode (struct link *head, int nodeData)
{
struct link *p = head, *pr = head;
if (head == NULL)
{
printf("Linked table is empty!\n");
return 0;
}
while (nodeData != p->data && p->next != NULL)
{
pr = p; /* pr保存當前節點 */
p = p->next; /* p指向當前節點的下一節點 */
}
if (nodeData == p->data)
{
if (p == head) /* 如果待刪除爲頭節點 (注意頭指針和頭結點的區別)*/
{
head = p->next;
}
else /* 如果待刪除不是頭節點 */
{
pr->next = p->next;
}
free(p); /* 釋放已刪除節點的內存 */
}
else /* 未發現節點值爲nodeData的節點 */
{
printf("This Node has not been found");
}
return head;
}
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)
單向鏈表的插入操作實現
/* 函數功能:向單向鏈表中插入數據 按升序排列*/
struct link *InsertNode(struct link *head, int nodeData)
{
struct link *p = head, *pr = head, *temp = NULL;
p = (struct link *)malloc(sizeof(struct link));
if (p == NULL)
{
printf("No enough meomory!\n");
exit(0);
}
p->next = NULL; /* 待插入節點指針域賦值爲空指針 */
p->data = nodeData;
if (head == NULL) /* 若原鏈表爲空 */
{
head = p; /* 插入節點作頭結點 */
}
else /* 原鏈表不爲空 */
{
while (pr->data < nodeData && pr->next != NULL)
{
temp = pr; /* 保存當前節點的指針 */
pr = pr->next; /* pr指向當前節點的下一節點 */
}
if (pr->data >= nodeData)
{
if (pr == head) /* 在頭節點前插入新節點 */
{
p->next = head; /* 新節點指針域指向原鏈表頭結點 */
head = p; /* 頭指針指向新節點 */
}
else
{
pr = temp;
p->next = pr->next; /* 新節點指針域指向下一節點 */
pr->next = p; /* 讓前一節點指針域指向新節點 */
}
}
else /* 若在表尾插入新節點 */
{
pr->next = p; /* 末節點指針域指向新節點*/
}
}
return head;
}
(tips:上面的代碼中將頭節點中的數據當作第一個元素,大多數情況頭節點是不存儲數據的(當時沒注意···),讀者可自行嘗試修改代碼讓頭結點不存儲數據,頭節點的後一個節點作爲第一個元素)
下面附上頭結點不存儲數據的代碼(區別不是很大,就是多用了一個子函數來初始化頭結點)
#include <stdio.h>
#include <stdlib.h>
struct link *AppendNode (struct link *head);
void DisplyNode (struct link *head);
void DeletMemory (struct link *head);
struct link *init (struct link *head);
struct link
{
int data;
struct link *next;
};
int main(void)
{
int i = 0;
char c;
struct link *head = NULL; //鏈表頭指針
head = init(head); /* 初始化隊列 */
printf("Do you want to append a new node(Y/N)?");
scanf_s(" %c", &c); //%c前有一個空格
while (c == 'Y' || c == 'y')
{
head = AppendNode(head);//向head爲頭指針的鏈表末尾添加節點
DisplyNode(head); //顯示當前鏈表中的各節點的信息
printf("Do your want to append a new node(Y/N)");
scanf_s(" %c", &c); //%c前有一個空格
i++;
}
printf("%d new nodes have been apended", i);
DeletMemory(head); //釋放所有動態分配的內存
return 0;
}
//函數功能:初始化鏈表,即新建一個頭結點(此處頭結點不放數據,原則上不放,實際還是可以放數據)
struct link *init (struct link *head)
{
struct link *p = NULL;
p = (struct link *)malloc(sizeof(struct link));
if (p == NULL)
{
printf("初始化鏈表失敗\n");
exit(0);
}
head = p;
p->next = NULL;
return head;
}
//函數功能:新建一個節點並添加到鏈表末尾,返回添加節點後的鏈表的頭指針
struct link *AppendNode(struct link *head)
{
struct link *p = NULL, *pr = head;
int data;
p = (struct link *)malloc(sizeof(struct link));//讓p指向新建的節點
if (p == NULL) //若新建節點申請內存失敗,則退出程序
{
printf("No enough memory to allocate\n");
exit(0);
}
if (head->next == NULL) //若原鏈表爲空表(只有頭節點,頭節點不存儲數據爲空表)
{
printf("Input node data");
scanf_s("%d", &data);
head->next = p; /* 讓頭結點的指針指向新建節點 */
p->data = data;
p->next = NULL; /* 新建結點置爲表尾 */
return head;
}
else //若原鏈表爲非空,則將新建節點添加到表尾
{
while (pr->next != NULL)//若未到表尾,則移動pr直到pr指向表尾
{
pr = pr->next; //讓pr指向下一個節點
}
pr->next = p; //讓末節點的指針指向新建的節點
printf("Input node data");
scanf_s("%d", &data); //輸入節點數據
p->data = data; //將新建節點的數據域賦值爲輸入的節點數據值
p->next = NULL;//將新建的節點置爲表尾
return head; //返回添加節點後的鏈表的頭指針
}
}
//函數的功能:顯示鏈表中所有節點的節點號和該節點中的數據項的內容
void DisplyNode (struct link *head)
{
struct link *p = head;
int j = 1;
p = p->next;
while (p != NULL) //若不是表尾,則循環打印節點的數值
{
printf("%5d%10d\n", j, p->data);//打印第j個節點數據
p = p->next; //讓p指向下一個節點
j++;
}
}
//函數的功能:釋放head所指向的鏈表中所有節點佔用的內存
void DeletMemory(struct link *head)
{
struct link *p = head, *pr = NULL;
while (p != NULL) //若不是表尾,則釋放節點佔用的內存
{
pr = p; //在pr中保存當前節點的指針
p = p->next;//讓p指向下一個節點
free(pr); //釋放pr指向的當前節點佔用的內存
}
}