前言:
🎈個人主頁:🎈 :???初階牛???
🐻推薦專欄: 🍔🍟🌯 C語言進階
🔑個人信條: 🌵知行合一
🍉本篇簡介:>:講解數據結構中鏈表的知識,;鏈表的分類,c語言實現單鏈表常見接口等.
金句分享:
?山不向我走來,我便向山走去.?
目錄
- 前言:
- 一、鏈表介紹
- 1.1 鏈表結構圖:
- 1.2 鏈表分類(圖解分析)
- 二、單鏈表實現:
- 2.1 鏈表的"結點"聲明:
- 2.2 "插入"元素操作.
- 單鏈表的"尾插":
- 單鏈表的"頭插"
- 指定位置之后"插入"新節點
- "申請新節點"函數
- 2.3 "刪除"元素操作.
- 單鏈表的"尾刪"
- 單鏈表的"頭刪":
- 單鏈表的"刪除"指定的目標結點
- 2.4 "查找"目標結點函數
- 2.5 單鏈表的"打印"
- 2.6 單鏈表的"銷毀"
- 總結:"鏈表"與"順序表"的區別
- 三、總代碼
- 測試區(test.c)
- 接口實現區(SList.c)
- 函數聲明區(SList.h)
一、鏈表介紹
順序表缺點:
- 中間/頭部的插入刪除,時間復雜度為O(N),因為需要移動數據.
- 增容需要申請新空間,特別是異地擴容,拷貝數據,釋放舊空間。消耗不小。
- 增容不是一次增容到位,而是每次增容后判斷是否符合要求,并且增容一般是2倍增容,一次次擴容消耗太大.
- 除此之外,還可能有一定的空間浪費。
例如:當前容量為200,如果有201個待插入數據,勢必要增容到400(原來容量的兩倍),這樣就浪費了199個空間.
我們不妨來看一下鏈表的存儲結構.
鏈表的概念:
概念:鏈表是一種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的.也是屬于線性表的一種.
🌰栗子
單鏈表的結點聲明:
typedef int DateType;
typedef struct SListN0de
{DateType Date;//數據域struct SListN0de* next;//指針域
}SLTNode;
結點:
1.1 鏈表結構圖:

通過上圖我們不難知道:
- 鏈表在邏輯上是連續的(next指針,指向下一個結點,鏈接在一起),而物理上可能連續,也可能不連續.
- 鏈表是由一個個結點鏈接在一起組成的,每個結點其實是
malloc
在堆區上申請的,所以地址可能連續,也可能不連續.
1.2 鏈表分類(圖解分析)
共有八種鏈表,我們主要學習不帶頭單向鏈表與帶頭雙向鏈表,學會這兩種,其它的大同小異,寫起來并不苦難.
單向、雙向:
不帶頭、帶頭:
帶頭與不帶頭的區別在于:
帶頭:鏈表是通過一個特殊的結點—頭結點指向鏈表的第一個有效結點.
不帶頭:通過結點指針指向鏈表的第一個有效結點.
頭結點作用:傳送門
不須換、循環:
重點掌握:
- 無頭單向非循環鏈表(本篇重點):結構簡單,一般不會單獨用來存數據。實際中更多是作為其他數據結構的子結構,如哈希桶、圖的鄰接表等等。另外這種結構在筆試面試中出現很多,因為單鏈表不能回頭,可以考察的地方很多.
- 帶頭雙向循環鏈表:結構最復雜,一般用在單獨存儲數據。實際中使用的鏈表數據結構,都是帶頭雙向循環鏈表。這種結構的鏈表雖然結構復雜,但是優勢也很明顯,并且實現起來反而很簡單.后續跟著學到了就可以理解了.
二、單鏈表實現:
2.1 鏈表的"結點"聲明:
typedef int DateType;
typedef struct SListN0de
{DateType Date;//數據域struct SListN0de* next;//指針域
}SLTNode;
單鏈表初始化:
單鏈表是不需要初始化操作的,只需要創建一個結點指針就行.初始狀態:指針指向NULL
.
頭指針:
SLTNode* plist=NULL;
2.2 "插入"元素操作.
我們需要插入數據時,只需要申請一個結點,將數據放入結點,然后將結點鏈接起來就行.
單鏈表的"尾插":
單鏈表的尾插步驟:
- 找尾:
由于單鏈表的結點之間不一定是連續存儲的,不支持向順序表那樣的隨機訪問,需要通過遍歷才能找到目標結點. - 將最后一個結點的next指向新節點.
圖解:
那如果鏈表本身就是空鏈表呢?
此時需要修改頭指針,將頭指針指向這個新節點.
注意:
-
需要傳二級指針:
這點很重要,因為需要修改頭指針,而頭指針的類型是:SLTNode*
,相信友友們學到這里應該知道,如果想要在函數中形參的改變影響實參,則需要傳址調用,通過地址來影響實參.
那么,指針的地址是?二級指針相信友友們應該沒有忘記.😂😂😂 -
斷言,這里需要靈活斷言.
"尾插"函數聲明:
void PushBack(SLTNode** pphead, DateType x)
pphead需要斷言:
pphead是指向 *pphead(一級指針/頭指針)的指針,即值存儲的是頭指針的地址,只要頭指針存在,則不為空.而頭指針一定存在.
*phead不能斷言:
*phead是頭指針,頭指針在鏈表為空時,頭指針的值是NULL,所以不能斷言.
鏈表中有數據時,指向第一個結點,值是第一個結點的地址.
- 頭指針是很重要的一個指針,我們都是通過頭指針找到鏈表的,所以,除了頭插需要修改頭指針以外,其他插入都不能修改頭指針,所以我們需要創建一個臨時指針:
SLTNode*tail = *pphead
代替頭指針找尾.
代碼:
void PushBack(SLTNode** pphead, DateType x)
{assert(pphead);//如果頭指針的地址為NULL,則報錯.SLTNode*tail = *pphead;//創建一個指針代替頭指針遍歷SLTNode* newnode = BuyNode(x);//*pphead代表代表頭指針,phead表示頭指針的地址//如果*pphead指向NULL,說明為空鏈表if (*pphead == NULL){//這里可以使用tail代替*phead嗎?//不能,因為這里要改變的是,頭結點的指針,需要用二級指針(解引用)來改變*pphead = newnode;//空鏈表是將頭指針指向新節點}else{//找尾巴,畫圖解析//這里可以使用tail,是因為,要改變的是結構體的內容,只需要用結構體指針(解引用)就行while ( tail->next != NULL)//如果該節點的下一個節點是空指針則停止循環{tail = tail->next;}tail->next = newnode;//讓尾節點的next指向新節點.}
}
單鏈表的"頭插"
尾插是不是顯得有些麻煩?那我們試著看一下頭插.
頭插步驟:
- 創建一個新節點.
- 將新節點指向頭指針指向的結點.
- 更新頭指針(頭指針指向新節點)
圖解:
代碼實現:
//寫法1
void PushFront(SLTNode** pphead, DateType x)
{assert(pphead);SLTNode* newnode = BuyNode(x);//下面兩句的順序不能變newnode->next = *pphead;*pphead = newnode;
}
寫法2:
//寫法2
void PushFront(SLTNode** pphead, DateType x)
{assert(pphead);SLTNode* newnode = BuyNode(x);SLTNode* phead = *pphead;//創建一個臨時指針,保存一下頭指針指向的頭結點.//順序隨便改*pphead = newnode;newnode->next = phead;
}
兩種方法都比較好理解,也很簡單,單鏈表的頭插效率很高,不需要遍歷,
指定位置之后"插入"新節點
該函數很簡單,只需要通過查找目標結點函數找到目標結點的位置,然后將將新節點鏈接上去就行了.
步驟:
- 將新節點的指針域(
next
)指向指定結點的下一個結點. - 將指定位置的結點的指針域(
next
)指向新節點,
圖解:
//使用此函數之前可以先使用,查找目標結點函數(SListFind),找到位置先
void SLTInsertBack( SLTNode* pos, DateType x)
{assert(pos);SLTNode* newnode = BuyNode(x);newnode->next = pos->next;pos->next = newnode;
}
"申請新節點"函數
新節點都是使用malloc
函數動態申請的.函數實現很簡單,相信聰明的友友們可以理解,牛牛就不過介紹了.
SLTNode* BuyNode(DateType x)//創建新結點
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));assert(newnode);//防止申請結點失敗newnode->Date = x;newnode->next = NULL;return newnode;
}
2.3 "刪除"元素操作.
因為鏈表的結點都是動態申請的,所以鏈表的刪除操作需要將目標結點釋放,同時為了保護原有的鏈表結構,需要將受目標結點的其他結點也靈活修改.
單鏈表的"尾刪"
"刪除結點"步驟:
- 處理特殊情況,如果頭指針指向NULL,空鏈表不能執行刪除操作.
- 找倒數第二個結點,方法:
tail->next->next != NULL
因為最后一個結點的next=NULL
;
數據結構記得多畫圖哦,有助于我們理解. - 先釋放尾結點(
tail->next
),再將倒數第二個結點的nex
t置空NULL
- 處理特殊情況:如果鏈表就只有一個結點,就不存在倒數第二個結點,此時直接釋放頭結點,并將頭結點置空.
圖解:
//尾刪
void PopBack(SLTNode** pphead)
{assert(pphead);//二級指針不可能為空,如果為空就一定是傳錯了assert(*pphead);//防止空鏈表的刪除操作SLTNode* tail = *pphead;//創建一個指針代替頭指針遍歷if (tail->next == NULL) {free(tail);tail= NULL;}else {while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}}
單鏈表的"頭刪":
同樣,單鏈表的"頭刪"也是很簡單的操作.
步驟:
- 將頭結點記錄一下.
- 將頭指針指向第二個結點.
- 釋放頭結點.
void PopFront(SLTNode** pphead)
{assert(pphead);//二級指針不可能為空,如果為空就一定是傳錯了assert(*pphead);//防止空鏈表的刪除操作SLTNode* head = *pphead;*pphead = ( * pphead)->next;free(head);
}
思考:
需不需要單獨去考慮,如果鏈表只有一個結點的特殊情況?
答案:
不需要,因為如果鏈表只有一個結點,頭刪將頭指針指向第二個結點,剛好是指向NULL,也是符合要求的.
單鏈表的"刪除"指定的目標結點
步驟:
- 通過查找目標結點函數
SListFind
(下面牛牛講了),找到目標結點的地址. - 將目標結點的前驅結點指向目標結點的后繼結點.
- 釋放目標結點.
- 特殊情況:如果是頭刪,需要修改頭結點,讓其指向第二個結點.
圖解:
代碼實現:
//告訴位置(建議配合SListFind函數一起使用),刪除第一個出現該值的節點
void SlitErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);//二級指針不可能為空,如果為空就一定是傳錯了assert(*pphead);//防止空鏈表的刪除操作assert(pos);SLTNode* cur = *pphead;//創建一個指針代替頭指針遍歷if (cur == pos) {//如果目標結點是頭結點這種特殊情況SLTNode* next = cur->next;free(cur);*pphead = next;}else {while (cur->next != pos && cur->next != NULL)//遍歷尋找目標結點{cur = cur->next;}cur->next = pos->next;//將目標結點的前驅指向目標結點的后繼free(pos);}
}
2.4 "查找"目標結點函數
單鏈表查找目標結點只需要遍歷一遍這個鏈表即可,如果目標結點有多個,則只返回第一個遇到的目標結點,找不到目標結點則返回NULL
.
函數很簡單,牛牛不過多介紹了.
SLTNode* SListFind(SLTNode* phead, DateType x)
{SLTNode* cur = phead;//代替頭指針遍歷鏈表while (cur){if (cur->Date == x){return cur;}cur = cur ->next;}return NULL;
}
2.5 單鏈表的"打印"
單鏈表的打印很簡單,遍歷打印就行了.
void Print(SLTNode* phead)//鏈表的打印
{//assert(phead);SLTNode* cur = phead;while (cur){printf("%d->", cur->Date);cur = cur->next;}printf("NULL\n\n");
}
2.6 單鏈表的"銷毀"
步驟:
- 創建
next
指針保存要刪除節點的下一個結點. - 將要刪除的結點釋放.
- 將要刪除的結點更新到
next
- 繼續執行1
//單鏈表的銷毀
void SLTDestroy(SLTNode* phead)//這個函數不會將頭指針置空,要使用該函數的人自己置空
{SLTNode* del = phead;SLTNode* next = phead;//用于記錄下一個結點while (next){next = next->next;free(del);del = next;}//保持習慣置空next == NULL;del = NULL;
}
總結:"鏈表"與"順序表"的區別
順序表 | 鏈表 | 區別 |
---|---|---|
物理上必定是連續的 | 邏輯上連續,但是物理上不一定連續 | 物理存儲空間 |
訪問其中的某個結點支持隨機訪問( O(1) ) | 不支持隨機訪問 | 訪問元素 |
大多數情況下需要移動數據,效率低( O(N) ) | 只需要改變目標指針的指向,但是需要找到該結點 | 刪除和插入新節點(任意位置) |
容量不夠時,動態順序表需要動態擴容,效率不高 | 沒有容量的概念不需要擴容,資源利用率高 | 插入數據時 |
元素的存儲很高效+頻繁訪問 | 頻繁的對任意位置的插入和刪除 | 使用場景 |
由于無物理上連續存在,利用率高 | 利用率低 | 緩存利用率 |
希望這篇文章對大家有幫助。歡迎小伙伴們私信提意見和提問哦!
最后,小伙伴們的點贊就是給牛牛最大的支持,能不能給牛牛來一個一鍵三連呢?謝謝支持。
三、總代碼
測試區(test.c)
//test.c 主函數區域,用于測試接口
#include "SList.h"
void test1()
{SLTNode* plist=NULL;printf("插入0,1,2,3,4,5,6,7,8,9之后:\n");PushBack(&plist, 1);PushBack(&plist, 2);PushBack(&plist, 3);PushBack(&plist, 4);PushBack(&plist, 5);PushBack(&plist, 6);PushBack(&plist, 7);PushBack(&plist, 8);PushBack(&plist, 9);//頭插PushFront(&plist, 0);Print(plist);printf("尾刪一次后:\n");PopBack(&plist);Print(plist);printf("頭刪一次后:\n");PopFront(&plist);Print(plist);printf("刪除第一次出現元素7的結點后:\n");SlitErase(&plist, SListFind(plist, 7));Print(plist);printf("在第一個出現5值的結點后面插入一個值為666的結點\n");SLTInsertBack(SListFind(plist, 5), 666);Print(plist);SLTDestroy(plist);plist == NULL;
}
int main()
{test1();return 0;
}
接口實現區(SList.c)
#include "SList.h"SLTNode* BuyNode(DateType x)//創建新結點
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));assert(newnode);newnode->Date = x;newnode->next = NULL;return newnode;
}
void PushBack(SLTNode** pphead, DateType x)
{assert(pphead);SLTNode*tail = *pphead;//創建一個指針代替頭指針遍歷SLTNode* newnode = BuyNode(x);//cur代表代表頭指針,phead表示頭指針的地址//如果cur指向NULL,說明為空鏈表if (*pphead == NULL){//這里可以使用tail代替*phead嗎?//不能,因為這里要改變的是,頭結點的指針,需要用二級指針(解引用)來改變*pphead = newnode;//空鏈表是將頭指針指向新節點}else{//找尾巴,畫圖解析//這里可以使用tail,是因為,要改變的是結構體的內容,只需要用結構體指針(解引用)就行while ( tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}//頭插(錯誤示例)
//void PushFront(SLTNode** pphead, DateType x)
//{
// assert(pphead);
// SLTNode* phead = *pphead;
// SLTNode* newnode = BuyNode(x);
// //下面兩句的順序不能變,除非再創一個結點保phead
// newnode->next = phead;
// phead= newnode;
//}
//
正確寫法1
//void PushFront(SLTNode** pphead, DateType x)
//{
// assert(pphead);
// SLTNode* newnode = BuyNode(x);
// //下面兩句的順序不能變,除非再創一個結點保phead
// newnode->next = *pphead;
// *pphead = newnode;
//}//寫法2
void PushFront(SLTNode** pphead, DateType x)
{assert(pphead);SLTNode* newnode = BuyNode(x);SLTNode* phead = *pphead;//順序隨便改*pphead = newnode;newnode->next = phead;
}
//尾刪
void PopBack(SLTNode** pphead)
{assert(pphead);//二級指針不可能為空,如果為空就一定是傳錯了assert(*pphead);//防止空鏈表的刪除操作SLTNode* tail = *pphead;//創建一個指針代替頭指針遍歷if (tail->next == NULL) {free(tail);tail= NULL;}else {while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}}
void PopFront(SLTNode** pphead)
{assert(pphead);//二級指針不可能為空,如果為空就一定是傳錯了assert(*pphead);//防止空鏈表的刪除操作SLTNode* head = *pphead;*pphead = ( * pphead)->next;free(head);
}SLTNode* SListFind(SLTNode* phead, DateType x)
{SLTNode* cur = phead;while (cur){if (cur->Date == x){return cur;}cur = cur ->next;}printf("找不到:\n");return NULL;
}
void SlitErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);//二級指針不可能為空,如果為空就一定是傳錯了assert(*pphead);//防止空鏈表的刪除操作assert(pos);SLTNode* cur = *pphead;//創建一個指針代替頭指針遍歷if (cur == pos) {//如果目標結點時頭結點SLTNode* next = cur->next;free(cur);*pphead = next;}else {while (cur->next != pos && cur->next != NULL)//遍歷尋找目標結點{cur = cur->next;}cur->next = pos->next;//將目標結點的前驅指向目標結點的后繼free(pos);}
}void SLTInsertBack( SLTNode* pos, DateType x)
{assert(pos);SLTNode* newnode = BuyNode(x);newnode->next = pos->next;pos->next = newnode;}void Print(SLTNode* phead)//鏈表的打印
{//assert(phead);SLTNode* cur = phead;while (cur){printf("%d->", cur->Date);cur = cur->next;}printf("NULL\n\n");
}void SLTDestroy(SLTNode* phead)//這個函數不會將頭指針置空,要使用該函數的人自己置空
{SLTNode* del = phead;SLTNode* next = phead;//用于記錄下一個結點while (next){next = next->next;free(del);del = next;}//保持習慣置空next == NULL;del = NULL;
}
函數聲明區(SList.h)
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int DateType;
typedef struct SListN0de
{DateType Date;struct SListN0de* next;
}SLTNode;//尾插
void PushBack(SLTNode** pphead, DateType x);
//尾刪
void PopBack(SLTNode** pphead);
//頭插
void PushFront(SLTNode** pphead, DateType x);
//頭刪
void PopFront(SLTNode** pphead);//告訴值,返回結點的地址
SLTNode* SListFind(SLTNode* phead, DateType x);//告訴位置(建議配合SListFind函數一起使用),刪除第一個出現該值的節點void SlitErase(SLTNode** pphead, SLTNode* pos);//告訴位置,在位置后面插入
void SLTInsertBack( SLTNode* pos, DateType x);struct SListN0de* BuyNode(DateType x);//創建新節點void Print(SLTNode* phead);//鏈表的打印// 單鏈表的銷毀
void SLTDestroy(SLTNode* phead);