目錄
引言
學習目標:
1.什么是鏈表
2.鏈表的分類
2.1?單向鏈表和雙向鏈表
(1)單向鏈表
(2)雙向鏈表
2.2 帶頭結點鏈表和不帶頭結點鏈表
(1)帶頭結點鏈表
(2)不帶頭結點鏈表
2.3?循環鏈表和不循環鏈表
(1)循環鏈表
(2)非循環鏈表
3.鏈表的實現
3.1 單鏈表的功能
3.2單鏈表的定義
3.3單鏈表功能實現
1.打印單鏈表數據
2.單鏈表頭插
(1)創建節點
(2)頭插代碼
3.單鏈表頭刪
4.單鏈表尾插
代碼實現
5.單鏈表尾刪
6.單鏈表數據查找
7.單鏈表數據修改
代碼實現
8.指定位置數據插入
(1)指定位置之前插入數據
(2)指定位置之后插入數據
9.指定位置數據刪除
(1)刪除pos節點
(2)刪除pos之后的節點
10.銷毀單鏈表
完整代碼SList.h
引言
在上一篇文章中?[數據結構——lesson2.順序表]我們學習了數據結構中的順序表,我們知道了順序表的空間是連續存儲的,這與數組非常類似。但是我們也遺留了一些關于順序表的問題:
當我們同時當插入數據時可能存在移動數據與擴容的情況,這大大增加我們的時間與空間成本。因此我們接下來學習新的數據結構——鏈表。
學習目標:
- 什么是鏈表?
- 鏈表的分類
- 鏈表的實現
1.什么是鏈表
鏈表(Linked List)是一種常見的數據結構,它通過一系列的節點(Node)來存儲數據元素。這些節點之間通過指針或引用相互連接,從而形成一個鏈狀結構。每個節點通常包含兩部分:一部分用于存儲數據元素,另一部分用于存儲指向下一個節點的指針或引用。
注意:
1.從上圖可以看出,鏈式結構在邏輯上是連續的,但在物理上不一定連續;
鏈表是一種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。鏈表中的每個節點通過指針指向下一個節點,從第一個節點開始,依次沿著指針可以訪問到后續節點,形成一個邏輯上連續的序列。但在物理內存中,這些節點可能分布在不同的位置,并不要求存儲單元是連續的。
2.鏈表的結點一般都是從堆上申請的;
鏈表中的結點是獨立申請的空間,通常是在需要插入數據時才去申請一塊節點的空間。操作系統的內存管理中,堆是一塊可供程序動態分配的內存區域,它允許程序在運行時根據需要申請和釋放內存,適合鏈表這種動態數據結構,因為鏈表的長度不固定,需要隨時根據數據的插入和刪除來分配和釋放節點空間。
3.從堆上申請的空間,是按照一定的策略來分配的,兩次省請的空間可能連續,可能不連續。
堆內存的分配由操作系統的內存分配器負責,它會根據一定的算法和策略來管理和分配內存,這些算法會根據堆內存的當前使用情況來尋找合適的空閑塊分配給程序。由于堆內存的使用是動態變化的,每次申請內存時,內存分配器找到的空閑塊位置是不確定的,所以兩次申請的空間可能連續,也可能不連續。
2.鏈表的分類
鏈表可以分為幾種不同的類型,其中最常見的有八種,它們大致可以分為三類:
2.1?單向鏈表和雙向鏈表
(1)單向鏈表
在單向鏈表中,每個節點只包含一個指向下一個節點的指針。
(2)雙向鏈表
在雙向鏈表中,每個節點除了包含數據元素外,還包含兩個指針:一個指向下一個節點(稱為后繼節點),另一個指向前一個節點(稱為前驅節點)。
2.2 帶頭結點鏈表和不帶頭結點鏈表
(1)帶頭結點鏈表
帶頭結點的單鏈表是在鏈表的第一個數據元素結點之前附加一個結點,稱為頭結點。
(2)不帶頭結點鏈表
非帶頭結點的單鏈表不包含頭結點,鏈表的第一個結點即為數據元素結點,頭指針直接指向鏈表的第一個數據元素結點。
2.3?循環鏈表和不循環鏈表
(1)循環鏈表
循環鏈表是一種特殊類型的鏈表,其中最后一個節點指向第一個節點,形成一個循環。這種結構使得鏈表在邏輯上成為一個環狀結構。
(2)非循環鏈表
雖然有這么多的鏈表的結構,但是我們實際中最常用還是兩種結構:
?
無頭單向非循環鏈表
?帶頭雙向循環鏈
本篇博客將詳細講解一下無頭單向非循環鏈表。
無頭單向非循環鏈表是一種鏈表結構,其特點是沒有頭結點,且鏈表中的節點單向連接,不形成循環。這種鏈表結構相對簡單,一般不會單獨用來存儲數據,而是作為其他數據結構的子結構,如哈希桶、圖的鄰接表等。
3.鏈表的實現
3.1 單鏈表的功能
單鏈表一般要實現如下幾個功能:
- 打印單鏈表中的數據。
- 對單鏈表進行頭插(開頭插入數據)。
- 對單鏈表進行頭刪(開頭刪除數據)。
- 對單鏈表進行尾插(末尾插入數據)。
- 對單鏈表進行尾刪(末尾刪除數據)。
- 對單鏈表進行查找數據。
- 對單鏈表數據進行修改。
- 對指定位置的數據刪除。
- 對指定位置的數據插入。
- 銷毀單鏈表。
3.2單鏈表的定義
單鏈表的結點定義方式與我們以往定義的方式都不同,它是一個結構體中包含兩個成員。一個是存儲數值,一個存放下一個結點的地址。
//定義節點的結構
//數據+指向下一節點的指針
typedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;
3.3單鏈表功能實現
1.打印單鏈表數據
遍歷鏈表并打印每個節點的數據來展示鏈表的內容,直到遇到鏈表的末尾。
代碼實現
void SLTPrint(SLTNode* phead)
{// 初始化一個指針pcur,指向鏈表的頭節點SLTNode* pcur = phead;// 使用while循環遍歷鏈表,直到pcur指向NULL(即鏈表末尾) while (pcur != NULL){printf("%d->", pcur->data);pcur = pcur->next;}// 循環結束后,打印"NULL"以明確表示鏈表的結束 printf("NULL\n");
}
2.單鏈表頭插
(1)創建節點
由于單鏈表每次插入都需要插入一個節點,因此我們可以寫一個創建節點的函數方便后續調用。
//申請內存
SLTNode* SLTCreateNode(SLTDataType x)
{// 為新的單鏈表節點分配內存空間 SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail:");exit(1);}// 初始化新節點的數據字段 newnode->data = x;newnode->next = NULL;return newnode;
}
(2)頭插代碼
在這里我們需要注意的是:單鏈表傳參需要二級指針。因為頭插數據需要改變一級指針plist的指向,而形參的改變無法影響實參,所以需要二級指針才能改變一級指針plist的指向。
//頭插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{// 斷言確保傳入的頭節點指針的地址非空,避免解引用空指針assert(pphead);// 創建一個新的節點,并初始化其數據為x SLTNode* newnode = SLTCreateNode(x);// 將新節點的next指針指向當前頭節點newnode->next = *pphead;// 更新頭節點指針,使其指向新節點*pphead = newnode;
}
一、對?assert(pphead)
?的理解
核心作用:確保二級指針?pphead
?不為空,避免解引用空指針導致程序崩潰。
- 邏輯背景:
代碼中存在?*pphead
(解引用二級指針),而對空指針執行解引用操作(如?*NULL
)會觸發未定義行為(通常導致程序崩潰)。
assert(pphead)
?會在程序運行時檢查?pphead
?是否為?NULL
:- 若?
pphead
?為?NULL
,程序會報錯并終止,提示開發者問題所在; - 若?
pphead
?非空,則繼續執行后續代碼。
- 若?
- 本質目的:通過斷言提前暴露潛在的空指針風險,提高代碼健壯性。
二、為什么需要傳遞二級指針?
核心原因:需要通過函數修改原始指針(如鏈表頭節點指針?head
)的地址本身。
- 指針操作的本質:
- 若僅傳遞一級指針?
head
(即結構體指針),函數內部只能修改?head
?指向的結構體內容,無法改變?head
?本身存儲的地址值(類似 “傳值” 效果)。 - 若需要修改?
head
?本身的地址(例如頭節點變更、鏈表初始化等場景),必須傳遞?head
?的地址,即二級指針?pphead
(類似 “傳址” 效果)。
- 若僅傳遞一級指針?
3.單鏈表頭刪
頭刪與頭插類似,都可能需要改變plist的指向,所以也需要二級指針。并且也需要防止鏈表為空的情況發生。
代碼實現
//頭刪
void SLTPopFront(SLTNode** pphead)
{// 鏈表不能為空,確保pphead不是空指針// 且*pphead(即頭節點)也不是空指針assert(pphead && *pphead);// 保存頭節點的下一個節點的地址,// 以便刪除頭節點后能夠更新頭指針SLTNode* next = (*pphead)->next;free(*pphead);// 更新頭節點*pphead = next;
}
4.單鏈表尾插
代碼實現
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);// *pphead就是指向第一個節點的指針// 處理空鏈表SLTNode* newnode = SLTCreateNode(x);// 判斷鏈表是否為空if (*pphead == NULL){// 如果鏈表為空,則新節點即為頭節點*pphead = newnode;}else{// 找尾SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}// 循環結束,ptail指向尾節點ptail->next = newnode;}
}
5.單鏈表尾刪
與單鏈表尾插類似,當鏈表只有一個頭結點時需要將plist置為空,所以也需要二級指針。并且也需要防止鏈表為空的情況發生。
//尾刪
void SLTPopBack(SLTNode** pphead)
{// 鏈表不能為空,確保pphead不是空指針,// 且*pphead(即頭節點)也不是空指針assert(pphead && *pphead);// 如果鏈表只有一個節點if ((*pphead)->next == NULL) //->優先級高于*{free(*pphead);*pphead = NULL;}else{// 鏈表有多個節點SLTNode* prev = *pphead;SLTNode* ptail = *pphead;// 遍歷鏈表直到找到尾節點while (ptail->next){prev = ptail; // 更新prev節點ptail = ptail->next; // 移動到下一個節點}free(ptail); // 釋放尾節點prev->next = NULL;}
}
6.單鏈表數據查找
如果找到了就返回這個節點的地址,否則就返回空指針NULL。
代碼實現
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;// 遍歷鏈表,直到當前節點為空(即到達鏈表末尾)while (pcur){if (pcur->data == x){return pcur; // 找到當前節點的指針}pcur = pcur->next;}return NULL;
}
7.單鏈表數據修改
代碼實現
//數據修改
void SLTModifity(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos); SLTNode* cur = pphead;while (cur){if (cur == pos){cur->data = x;break;}cur = cur->next;}
}
8.指定位置數據插入
(1)指定位置之前插入數據
//在指定位置之前插入數據
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLTCreateNode(x);// 如果插入位置是鏈表的第一個位置(即pos等于頭節點)// 則調用頭插函數if (pos == *pphead){SLTPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}
(2)指定位置之后插入數據
//在指定位置之后插入數據
void SLTnsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLTCreateNode(x);// 將新節點的next指針指向pos的下一個節點newnode->next = pos->next;// 將pos的next指針指向新節點,完成插入操作pos->next = newnode;
}
9.指定位置數據刪除
(1)刪除pos節點
//刪除pos節點
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pos);assert(pphead && *pphead);// 如果刪除的節點是頭節點if (pos == *pphead){SLTPopFront(pphead); // 頭刪}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}// 將prev的next指針指向pos的下一個節點// 從而繞過pos節點,實現刪除prev->next = pos->next;free(pos);pos = NULL;}
}
(2)刪除pos之后的節點
//刪除pos之后的節點
void SLTraseAfter(SLTNode* pos)
{assert(pos && pos->next);// 創建一個臨時指針del,指向pos之后的節點,即要刪除的節點SLTNode* del = pos->next;// 將pos的next指針指向del的下一個節點,從而繞過del節點,實現刪除pos->next = del->next;free(del);del = NULL;
}
10.銷毀單鏈表
銷毀鏈表需要依次遍歷釋放節點。
//銷毀鏈表
void SListDesTory(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){// 保存當前節點的下一個節點的指針// 以便在釋放當前節點后能夠繼續遍歷SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
完整代碼
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//定義節點的結構
//數據+指向下一節點的指針
typedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;//打印
void SLTPrint(SLTNode* phead);
//尾插
void SLTPushBack(SLTNode** phead, SLTDataType);
//頭插
void SLTPushFront(SLTNode** phead, SLTDataType);
//尾刪
void SLTPopBack(SLTNode** pphead);
//頭刪
void SLTPopFront(SLTNode** pphead);//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//在指定位置之前插入數據
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在制定位置之后插入數據
void SLTnsertAfter(SLTNode* pos, SLTDataType x);//刪除pos節點
void SLTErase(SLTNode** pphead, SLTNode* pos);
//刪除pos之后的節點
void SLTraseAfter(SLTNode* pos);//銷毀鏈表
void SListDesTory(SLTNode** pphead);//數據修改
void SLTModifity(SLTNode* pphead, SLTNode* pos, SLTDataType x);
SList.c
#include"SList.h"void SLTPrint(SLTNode* phead)
{// 初始化一個指針pcur,指向鏈表的頭節點SLTNode* pcur = phead;// 使用while循環遍歷鏈表,直到pcur指向NULL(即鏈表末尾) while (pcur != NULL){printf("%d->", pcur->data);pcur = pcur->next;}// 循環結束后,打印"NULL"以明確表示鏈表的結束 printf("NULL\n");
}//申請內存
SLTNode* SLTCreateNode(SLTDataType x)
{// 為新的單鏈表節點分配內存空間 SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail:");exit(1);}// 初始化新節點的數據字段 newnode->data = x;newnode->next = NULL;return newnode;
}//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);// *pphead就是指向第一個節點的指針// 處理空鏈表SLTNode* newnode = SLTCreateNode(x);// 判斷鏈表是否為空if (*pphead == NULL){// 如果鏈表為空,則新節點即為頭節點*pphead = newnode;}else{// 找尾SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}// 循環結束,ptail指向尾節點ptail->next = newnode;}
}//頭插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{// 斷言確保傳入的頭節點指針的地址非空,避免解引用空指針assert(pphead);// 創建一個新的節點,并初始化其數據為x SLTNode* newnode = SLTCreateNode(x);// 將新節點的next指針指向當前頭節點newnode->next = *pphead;// 更新頭節點指針,使其指向新節點*pphead = newnode;
}//尾刪
void SLTPopBack(SLTNode** pphead)
{// 鏈表不能為空,確保pphead不是空指針,// 且*pphead(即頭節點)也不是空指針assert(pphead && *pphead);// 如果鏈表只有一個節點if ((*pphead)->next == NULL) //->優先級高于*{free(*pphead);*pphead = NULL;}else{// 鏈表有多個節點SLTNode* prev = *pphead;SLTNode* ptail = *pphead;// 遍歷鏈表直到找到尾節點while (ptail->next){prev = ptail; // 更新prev節點ptail = ptail->next; // 移動到下一個節點}free(ptail); // 釋放尾節點prev->next = NULL;}
}//頭刪
void SLTPopFront(SLTNode** pphead)
{// 鏈表不能為空,確保pphead不是空指針// 且*pphead(即頭節點)也不是空指針assert(pphead && *pphead);// 保存頭節點的下一個節點的地址,// 以便刪除頭節點后能夠更新頭指針SLTNode* next = (*pphead)->next;free(*pphead);// 更新頭節點*pphead = next;
}//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;// 遍歷鏈表,直到當前節點為空(即到達鏈表末尾)while (pcur){if (pcur->data == x){return pcur; // 找到當前節點的指針}pcur = pcur->next;}return NULL;
}//數據修改
void SLTModifity(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos); SLTNode* cur = pphead;while (cur){if (cur == pos){cur->data = x;break;}cur = cur->next;}
}//在指定位置之前插入數據
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLTCreateNode(x);// 如果插入位置是鏈表的第一個位置(即pos等于頭節點)// 則調用頭插函數if (pos == *pphead){SLTPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}//在指定位置之后插入數據
void SLTnsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLTCreateNode(x);// 將新節點的next指針指向pos的下一個節點newnode->next = pos->next;// 將pos的next指針指向新節點,完成插入操作pos->next = newnode;
}//刪除pos節點
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pos);assert(pphead && *pphead);// 如果刪除的節點是頭節點if (pos == *pphead){SLTPopFront(pphead); // 頭刪}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}// 將prev的next指針指向pos的下一個節點// 從而繞過pos節點,實現刪除prev->next = pos->next;free(pos);pos = NULL;}
}//刪除pos之后的節點
void SLTraseAfter(SLTNode* pos)
{assert(pos && pos->next);//創建臨時變量delSLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//銷毀鏈表
void SListDesTory(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){// 保存當前節點的下一個節點的指針// 以便在釋放當前節點后能夠繼續遍歷SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
結束語
本節內容學習了鏈表的種類和單鏈表的實現。
感謝大家三連支持!!!