<!--time--2020/8/22-->
線性表的鏈式存儲結構
鏈表
單鏈表的特點:
1)將數據封裝成結點,由結點組成的線性表;
2)通過結點的指針域將結點連接到一起;
3)結點的內存空間不一定時連續的,但邏輯上是連續的;
4)有一個頭指針指向第一個數據元素;
5)鏈表是線性表的一種,是線性表的鏈式存儲結構的實現。
其中,鏈表中每個結點只有一個指針域指向后繼結點,這種鏈表稱為單鏈表或線性鏈表
單鏈表的結構:
頭結點:為了處理方便,會在鏈表的第一個元素前增加一個頭結點,該結點的數據域中不存儲線性表的數據元素,其作用是為了對鏈表進行操作時,可以對空表、非空表的情況以及對首元結點進行統一處理。
尾結點:最后一個結點的指針域為空指針
首元結點:鏈表中存儲第一個數據元素的結點。如上圖中data1結點。
頭指針:通常用head名稱定義,指向鏈表中第一個結點的指針,有頭結點時指向頭結點,若鏈表設計時不設頭結點,則指向首元結點。
單鏈表的定義
通過單鏈表的頭指針可以完成單鏈表的所有操作,而單鏈表的結點有兩個必須的屬性:
1)數據元素信息:節點中的數據元素基本信息;
2)指針域:結點之間通過指針進行邏輯關聯。用來存儲下一個結點的地址。
由此可定義出線性表的鏈式存儲的結構體類型:
typedef int ElementType;//鏈表中元素的類型,根據實際情況而定,此處以int為例
//單鏈表結構體定義
typedef struct link_node{
ElementType data;//結點的數據域
stuct link_node *next;//結點的指針域:(此時結構體尚未定義完整,不能用 NODE *next; )用該結構體類型定義一個指針,用來指向下一個結點的地址
}LINK_NODE,*LINK_LIST;
注意這里的定義,按照前面的分析,實際上只需要有LINK_NODE類型定義就行了,我們可以通過定義LINK_NODE *head 來定義一個頭指針,但為了提高程序的可讀性,在用typedef定義單鏈表時起了兩個別名,分別為LINK_NODE和LINK_LIST,而實際上LINK_LIST就是struct link_node * ,因此在定義單鏈表時可以定義為:
LINK_LIST list;
或者
LINK_NODE *list
當然,這里的list其實就是頭指針head;
單鏈表的初始化操作:
該步驟主要是為了完成頭結點的創建以及相關指針的置空(下一結點)
1、先通過malloc給頭結點分配空間
2、判斷內存是否分配失敗,失敗返回0
3、頭結點的next指針置空:(*L)->next = NULL;
4、操作結束,返回1
int init_list(LINK_LIST *L){//在函數內部去修改函數外部數據時要用指針
*L = (LINK_NODE *)malloc(sizeof(LINK_NODE));//分配頭結點空間
if (*L ==NULL)//如果內存分配失敗
{
return 0;
}
(*L)->next = NULL;//頭結點的next置為空
return 1;
}
獲取單鏈表的長度:
計算單鏈表中有效數據結點的個數(不包含頭結點)
1、定義變量length,用來統計有效結點個數;
2、定義tmp指針指向首元結點,首元結點為頭結點的下一結點:LINK_NODE *tmp = L->next;
3、通過while循環計算有效結點個數,循環條件:tmp指針為非空結點:tmp!=NULL
4、循環體內為length自增,tmp指針指向下一結點
5、返回長度length
int get_length(LINK_LIST L) {//這里不需要修改鏈表,直接用形參操作
int length = 0;
LINK_NODE *tmp = L->next;//從首元結點開始,而非頭結點
while (tmp!=NULL)
{
length++;
tmp = tmp->next;//指針指向下一個結點
}
return length;
}
單鏈表的遍歷操作:
1、先定義一個指針指向首元結點
2、通過while循環實現遍歷,循環條件為 tmp!=NULL,即鏈表為非空或tmp指針沒有超出鏈表范圍
3、打印數據域中tmp->data;
4、將tmp指針指向下一結點,實現循環,當tmp指向尾結點時,即tmp->next==NULL,在下一個循環開始時跳出循環
void show(LINK_LIST L) {
LINK_NODE *tmp = L->next;//從首元結點開始,而非頭結點
while (tmp != NULL)
{
printf("%d", tmp->data);//注意輸出數據類型,這里以int為例
if (tmp->next!=NULL)
{
printf(",");
}
else {
printf("\n");//對最后一個結點數據輸出后不加逗號處理
}
tmp = tmp->next;//將指針指向下一個結點
}
}
單鏈表插入操作
1、定義一個p_insert指針指向頭結點,用來存儲待插入結點的前一個結點
2、判斷插入位置 i 是否合理:即1< i < get_length(L) +1 (這里長度+1是將新結點插在末尾)
3、通過for循環移動p_insert指針,將p_insert指針指向插入位置 i 的前一個結點
4、創建新結點p_new,並給新結點分配空間。這里可以添加一個判斷,如果空間分配失敗,則返回0
5、給新結點的數據域賦值:p_new->data = e;
6、將第 i -1個結點所在地址(即 p_insert->next)賦值給新結點的next指針:p_new->next = p_ins->next;
7、將新結點的地址賦值給前一結點的next指針:p_insert->next = p_new;
//單鏈表的插入操作
int insert(LINK_LIST L,int i,ElementType e) {
LINK_NODE *p_ins = L;//先定義一個指針指向頭結點
//
if (i<1||i>get_length(L)+1)
{
return 0;
}
for (int j = 0; j < i-1; j++)//循環中可能會使得指針指向空,則意味着插入位置將會超過鏈表長度+1,使得操作失敗,
{
p_ins = p_ins->next;//將p_ins指向要插入結點的前一個結點
}
LINK_NODE *p_new = (LINK_NODE *)malloc(sizeof(LINK_NODE));
if (p_new ==NULL)//分配空間失敗,返回0
{
return 0;
}
p_new->data = e;//給數據域賦值
p_new->next = p_ins->next;//將新結點的next指向插入位置的原結點
p_ins->next = p_new;//將新結點鏈接到插入位置上
return 1;
}
單鏈表的刪除操作:
1、判斷刪除位置是否合理,不合理則返回0 :1<i<get_length(L)
2、定義一個指針指向頭結點,用來存儲刪除位置的前一個結點的地址:LINK_NODE *p_prev = L;
3、定義待刪除結點的指針:LINK_NODE *p_del;
4、移動p_prev 指針,將指針指向刪除節點 i 的前一個結點所在位置
5、通過待刪除結點的前一結點 p_prev 給待刪除結點 p_del 賦值地址
6、跳過刪除結點,將待刪除結點的下一結點指針地址p_del->next賦值給前一個結點的下一結點指針p_prev->next
7、釋放待刪除結點的空間:free(p_del),返回1,表示操作完成
//單鏈表的刪除操作
int delt(LINK_LIST L,int i) {
if (i<1||i>get_length(L))
{
printf("操作失敗,刪除位置不存在!");
return 0;//刪除失敗
}
LINK_NODE *p_prev = L;//previous 定義 前一個結點
LINK_NODE *p_del;//定義待刪除結點指針
for (int j = 0; j < i-1; j++)//移動i-1次,將prev指針指向要刪除結點 i 的前一個結點;
{
p_prev = p_prev->next;
}
p_del = p_prev->next;//通過待刪除結點的前一結點給待刪除結點賦值地址
p_prev->next = p_del->next;//跳過刪除結點,將待刪除結點的指針域賦值給前一結點的指針域
free(p_del);//釋放待刪除結點的空間
return 1;
}
單鏈表的修改操作
兩種方法,方法一:先創建一個新的結點,再將新結點覆蓋到需要需要修改的結點上,最后刪除舊結點;方法二:先找到要修改的結點,再直接修改該結點
int updata(LINK_LIST L,int i,ElementType e) { if (i<1||i>get_length(L))//判斷是否空表或位置是否合理 { printf("操作失敗,該位置不存在!"); return 0; } LINK_NODE *p_mod = L;//modify 定義指針指向頭結點。也可以指向首元結點,但要注意下面循環次數會發生變化 for (int j = 0; j < i;j++) {//因為頭指針指向頭結點,所以移動i次。若指向首元結點,則移動i-1次,條件改為j<i-1 p_mod = p_mod->next;//將指針指向需要修改的位置 } p_mod->data = e;//直接修改目標結點的數據域 return 1; }
單鏈表的查找操作
1、創建p_find指針指向起始節點
2、通過循環變臉查找,循環條件為p_find!=NULL;
3、判斷,如果當前指針指向結點的數據域與查找目標一致,返回當前結點的地址
4、p_find指針向后移一位,保證循環
5、跳出循環,返回NULL,說明未查找到數據
LINK_NODE *search(LINK_NODE *p_from,ElementType e) {//返回一個結點的指針 LINK_NODE *p_find = p_from;//指向起始結點 while (p_find!=NULL)//當前指向結點為有效結點 { if(p_find->data==e){//當前指針指向結點的數據域與查找目標一致 //printf("已查找到目標數據!該數據地址為:0x%p\n",p_find); return p_find; } p_find = p_find->next;//指針后移 } printf("未找到該數據!\n"); return NULL; }
至此,單鏈表的增刪改查結束。下面為調試的源碼:
源碼:
#include <stdio.h> #include <malloc.h> typedef int ElementType;//鏈表中元素的類型,根據實際情況而定,這里以int為例 //單鏈表結構體定義: typedef struct link_node { ElementType data;//結點的數據域 struct link_node* next;//結點的指針域 }LINK_NODE,*LINK_LIST;//這里LINK_LIST和 next 類型一致,都是struct link_node*類型 /*(LINK_LIST本身就是指針,因此定義頭指針時可以直接這樣 LINK_LIST head;) 也可以這樣定義:LINK_NODE * head; */ //單鏈表的初始化操作: //主要時為了完成頭結點的創建以及相關指針的置空 int init_list(LINK_LIST *L){//在函數內部去修改函數外部數據時要用指針 *L = (LINK_NODE *)malloc(sizeof(LINK_NODE));//分配頭結點空間 if (*L ==NULL)//如果內存分配失敗 { return 0; } (*L)->next = NULL;//頭結點的next置為空 return 1; } //獲取單鏈表長度:計算單鏈表中有效數據結點個數(不包含頭結點) int get_length(LINK_LIST L) {//這里不需要修改鏈表,直接用形參操作 int length = 0; LINK_NODE *tmp = L->next;//從首元結點開始,而非頭結點 while (tmp!=NULL) { length++; tmp = tmp->next;//指針指向下一個結點 } return length; } //單鏈表的遍歷操作 void show(LINK_LIST L) { LINK_NODE *tmp = L->next;//從首元結點開始,而非頭結點 while (tmp != NULL) { printf("%d", tmp->data);//注意輸出數據類型,這里以int為例 if (tmp->next!=NULL) { printf(","); } else { printf("\n");//對最后一個結點數據輸出后不加逗號處理 } tmp = tmp->next;//將指針指向下一個結點 } } //單鏈表的插入操作 int insert(LINK_LIST L,int i,ElementType e) { LINK_NODE *p_ins = L;//先定義一個指針指向頭結點 // if (i<1||i>get_length(L)+1) { return 0; } for (int j = 0; j < i-1; j++)//循環中可能會使得指針指向空,則意味着插入位置將會超過鏈表長度+1,使得操作失敗, { p_ins = p_ins->next;//將p_ins指向要插入結點的前一個結點 } LINK_NODE *p_new = (LINK_NODE *)malloc(sizeof(LINK_NODE)); if (p_new ==NULL)//分配空間失敗,返回0 { return 0; } p_new->data = e;//給數據域賦值 p_new->next = p_ins->next;//將新結點的next指向插入位置的原結點 p_ins->next = p_new;//將新結點鏈接到插入位置上 return 1; } //單鏈表的刪除操作 int delt(LINK_LIST L,int i) { if (i<1||i>get_length(L)) { printf("操作失敗,刪除位置不存在!"); return 0;//刪除失敗 } LINK_NODE *p_prev = L;//previous 定義 前一個結點 LINK_NODE *p_del;//定義待刪除結點指針 for (int j = 0; j < i-1; j++)//移動i-1次,將prev指針指向要刪除結點 i 的前一個結點; { p_prev = p_prev->next; } p_del = p_prev->next;//通過待刪除結點的前一結點給待刪除結點賦值地址 p_prev->next = p_del->next;//跳過刪除結點,將待刪除結點的指針域賦值給前一結點的指針域 free(p_del);//釋放待刪除結點的空間 return 1; } //單鏈表的修改操作 /*一:先刪除,再插入 1、先新建一個新結點 2、給新結點賦值 3、刪除要修改的結點 4、將新結點插入到刪除結點的位置上 二: 1、找到要修改的結點 2、直接修改該結點 */ int updata(LINK_LIST L,int i,ElementType e) { if (i<1||i>get_length(L))//判斷是否空表或位置是否合理 { printf("操作失敗,該位置不存在!"); return 0; } LINK_NODE *p_mod = L;//modify 定義指針指向頭結點。也可以指向首元結點,但要注意下面循環次數會發生變化 for (int j = 0; j < i;j++) {//因為頭指針指向頭結點,所以移動i次。若指向首元結點,則移動i-1次,條件改為j<i-1 p_mod = p_mod->next;//將指針指向需要修改的位置 } p_mod->data = e;//直接修改目標結點的數據域 return 1; } //單鏈表的查找操作 LINK_NODE *search(LINK_NODE *p_from,ElementType e) {//返回一個結點的指針 LINK_NODE *p_find = p_from;//指向起始結點 while (p_find!=NULL)//當前指向結點為有效結點 { if(p_find->data==e){//當前指針指向結點的數據域與查找目標一致 //printf("已查找到目標數據!該數據地址為:0x%p\n",p_find); return p_find; } p_find = p_find->next;//指針后移 } printf("未找到該數據!\n"); return NULL; } int main() { LINK_LIST list = NULL;//list為指針 //創建單鏈表 init_list(&list); //調用插入函數 //int rlt = insert(list,2,1);//rlt:0 第一個位置沒有元素,不能插入到第二個位置, insert(list,1,1); insert(list, 2, 2); insert(list, 3, 3); insert(list, 1, 4); insert(list, 3, 5); insert(list, 2, 99); insert(list, 3, 99); show(list); //調用刪除函數 int rlt1 = delt(list,6); printf("rlt = %d\n",rlt1); show(list); int rlt2 = delt(list, 4); printf("rlt2 = %d\n", rlt2); show(list); //調用修改函數 int rlt3 = updata(list,0,100); printf("rlt3 = %d\n",rlt3); int rlt4 = updata(list, 5, 100); printf("rlt4 = %d\n", rlt4); updata(list,1,99); show(list); updata(list, 2, 100); show(list); //調用查詢函數 LINK_NODE *p_rlt1 = search(list, 88); LINK_NODE *p_rlt2 = search(list,100); LINK_NODE *p_rlt3 = list; int index = 0; while (1)//循環查找所有重復數據 { p_rlt3 = search(p_rlt3->next,99);//從上一個找到的結點開始查詢 if (p_rlt3==NULL) { break; } index++; printf("已查找到第%d個數據的地址為0x%p\n",index,p_rlt3); } index = 0; return 0; }