?
?🔥個人主頁:艾莉絲努力練劍
?專欄傳送門:《C語言》、《數據結構與算法》、C語言刷題12天IO強訓、LeetCode代碼強化刷題
🍉學習方向:C/C++方向
??人生格言:為天地立心,為生民立命,為往圣繼絕學,為萬世開太平
前言:本篇文章,我們復盤順序表和鏈表相關的知識點,在初階的數據結構與算法階段,我們把知識點分成三部分,復雜度作為第一部分,順序表和鏈表、棧和隊列、二叉樹為第二部分,排序為第二部分,我們之前已經介紹完了第一部分:算法復雜度,本文我們繼續學習第二部分中的順序表和鏈表部分內容啦。
? ? ? ? 半個多月前,博主更新了頭插、尾刪、頭刪、隨機位置插入、隨機位置刪除、查找、修改、菜單等內容,本篇文章,我們就來復盤一下動態順序表的內容,博主會添加很多新內容,希望對大家的順序表學習有所幫助。
??
目錄
正文
三、單鏈表
(二)實現單鏈表
3、增刪查改
增
(1)尾插
(2)頭插
(3)在指定位置之前插入數據
(4)在指定位置之后插入數據
刪
(1)尾刪
(2)頭刪
(3)刪除pos節點
(4)刪除pos之后的節點
查
(1)查找
改?
(1)修改
?銷毀鏈表
(1)銷毀鏈表
4、完整代碼?
(1)SList.h:
(2)SList.c:
(3)test.c:
結尾
正文
提醒:為什么我們要學那么多的數據結構?這是因為沒有一種數據結構能夠去應對所有場景。我們在不同的場景需要選擇不同的數據結構,所以數據結構沒有誰好誰壞之分,而評估數據結構的好壞要針對場景,如果在一種場景下我們需要頻繁地對頭部進行插入刪除操作,那么這個時候我們用鏈表;但是如果對尾部進行插入刪除操作比較頻繁,那我們用順序表比較好。
????????因此,不同的場景我們選擇不同的數據結構。
三、單鏈表
(二)實現單鏈表
3、增刪查改
增
(1)尾插
我們要申請新的節點(需要malloc),我們單獨封裝一個函數。
?
現在新節點就申請好了,我們要讓5和4節點連起來:
?
這就是為什么我們明明已經有phead這個指針,還要額外再定義一個指針pcur——
?
這樣一來pcur在不斷變化,phead保持不變,phead始終保存的是第一個節點的地址。在這里我不想改變phead,phead始終指向第一個節點,方便我們后面遍歷完了如果還要再從頭開始遍歷的時候我們能夠找到第一個節點的地址。
我們定義pcur,只要pcur不為空,我們就進入循環,pcur為空我們就跳出循環。
?
我們這邊調用test02:?
?
?這是SList.c尾插的代碼:
//尾插
void SLTPushBack(?SLTNode* phead, SLTDatatype x)
{//申請新節點?SLTNode* newnode = SLTBuyNode(x);//鏈表為空——要特殊處理if (phead == NULL){phead = newnode;}?SLTNode* ptail = phead;while (ptail->next != NULL){ptail = ptail->next;}//找到了尾節點 ptail newnodeptail->next = newnode;
}
這邊其實代碼還是有問題的,我們先運行一下看看:
?
尾插:?
SList.c:
//尾插
void SLTPushBack(?SLTNode** pphead, SLTDatatype x)
{//申請新節點?SLTNode* newnode = SLTBuyNode(x);//鏈表為空——要特殊處理if (*pphead == NULL){*pphead = newnode;}else{?SLTNode* ptail = *pphead;while (ptail->next != NULL){ptail = ptail->next;}//找到了尾節點 ptail newnodeptail->next = newnode;}
}
test.c:?
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
?
尾插的時間復雜度:O(N) 。
(2)頭插
?
頭插:
SList.c:
//頭插
void SLTPushFront(?SLTNode** pphead, SLTDatatype x)
{assert(pphead);?SLTNode* newnode = SLTBuyNode(x);//newnode *ppheadnewnode->next = *pphead;*pphead = newnode;
}
test.c:?
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPrint(plist);SLTPushFront(&plist, 2);SLTPrint(plist);SLTPushFront(&plist, 3);SLTPrint(plist);SLTPushFront(&plist, 4);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
?
頭插的時間復雜度:O(1) 。?
(3)在指定位置之前插入數據
函數形參中同樣需要用二級指針傳入鏈表地址,還要傳入指定位置的地址和需要插入的數據。
在函數中,需要先找到指定位置的前一個節點,然后把需要添加的數據插入到這兩個節點之間:
SList.c:
void SLTPushBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && pos);//當pos為頭節點時 相當于頭插if(*pphead == pos){SLTPushFront(pphead, x);}else{SLTNode* newNode = SLTBuyNode(x);//找到pos前一個節點SLTNode* pre = *pphead;while(pre->next != pos){pre = pre->next;}//把新節點放在pre和pos之間newNode->next = pre->next;//等價于newNode->next = pospre->next = newNode;}}
test.c:?
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);?SLTNode* pos = SLTFind(plist, 4);SLTInsert(&plist, pos, 100);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
在指定位置之前插入數據時間復雜度:O(N)。?
(4)在指定位置之后插入數據
和類似,我們找到指定位置的后一個節點,把新節點放在這兩個節點之間——
4后面插入一個100:?
1后面插入一個100:
SList.c:
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && pos);if(pos->next == NULL){//相當于尾插SLTPushBack(pphead, x);}else{SLTNode* newNode = SLTBuyNode(x);newNode->next = pos->next;pos->next = newNode;//順序不能顛倒 否則會先修改pos->next}
}
test.c:?
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);?SLTNode* pos = SLTFind(plist, 1);//SLTInsert(&plist, pos, 100);SLTInsertAfter(pos, 100);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
在指定位置之后插入數據時間復雜度:O(1)。??
刪
(1)尾刪
pphead和phead的關系:
本文中的pphead是形參,phead是畫圖時定義的一個指針。
phead是指向第一個結點的指針;
在程序里面,指向第一個節點的指針在形參里面是*pphead。
函數形參中二級指針存放表頭地址。
如果鏈表只有一個元素需要釋放頭節點的空間,并把鏈表指針置為空;
如果有多個元素需要找到倒數第二個節點和最后一個節點,釋放最后一個節點,并把倒數第二個節點的next指針置為空。
?出問題了:?
?
萬一鏈表只有一個節點,我們要注意這種情況——
尾刪:
SList.c:
//尾刪
void SLTPopBack(?SLTNode** pphead)
{//鏈表為空不能刪除assert(pphead && *pphead);//pphead是*pphead的地址//pphead是一個二級指針,我們對pphead解引用一次,*pphead就是指向第一個節點的地址//*pphead為空說明鏈表為空//鏈表只有一個節點的情況if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{?SLTNode* prev = NULL;?SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}//prev ptailprev->next = NULL;free(ptail);ptail = NULL;}
}
test.c:
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);//SLTPushFront(&plist, 1);//SLTPrint(plist);//SLTPushFront(&plist, 2);//SLTPrint(plist);//SLTPushFront(&plist, 3);//SLTPrint(plist);//SLTPushFront(&plist, 4);//SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
?
鏈表為空,如果還要再刪一次,程序就會assert(斷言)報錯:
?
?
表達式為假,因為*pphead為空了,在69行斷言出現了報錯。
如果初始將prev置為*pphead也要討論:
這樣兩個指針都指向這個節點,讓prev下一個節點置為空——本身它下一個節點就為空,現在把ptail 給 free掉,prev就變成了野指針。
如果只有一個節點,prev->next = NULL;這一行代碼就可以不要了,而且ptail和prev都要free。如果不止一個節點的話,那這又是一套邏輯,這種寫法會更復雜一些。
博主給出的這種寫法會簡單一些。?
尾刪的時間復雜度:O(N) 。?
(2)頭刪
與尾刪相似,讓頭指針指向第二個節點,并釋放頭節點——
?
頭刪:
SList.c:
//頭刪
void SLTPopFront(?SLTNode** pphead)
{assert(pphead && *pphead);?SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
test.c:
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);//頭刪SLTPopFront(&plist);SLTPrint(plist);SLTPopFront(&plist);SLTPrint(plist);SLTPopFront(&plist);SLTPrint(plist);SLTPopFront(&plist);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
再刪一次就會斷言報錯——
?
?
頭刪的時間復雜度:O(1) 。
(3)刪除pos節點
讓pos前一個節點指向pos后一個節點——
SList.c:
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && pos && *pphead);//pos為頭節點if(pos == *pphead){free(*pphead);*pphead = NULL;pos = NULL;}else{SLTNode* prev = *pphead;while(prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}
test.c:
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);?SLTNode* pos = SLTFind(plist, 3);SLTErase(&plist, pos);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
(4)刪除pos之后的節點
找到pos之后的節點del,連接pos節點和del->next,再釋放del——
SList.c:
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{assert(pphead && pos && pos->next && *pphead);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
test.c:
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);?SLTNode* pos = SLTFind(plist, 3);SLTErase(&plist, pos);SLTPrint(plist);
}int main()
{/*test01();*/test02();return 0;
}
?
查
(1)查找
來個不存在的數據測試一下——?
查找?
SList.c:
//查找
?SLTNode* SLTFind(?SLTNode* phead, SLTDatatype x)
{?SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;}pcur = pcur->next;}//未找到return NULL;
}
test.c:
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);?SLTNode* pos = SLTFind(plist, 100);if (pos){printf("找到了!\n");}else{printf("未找到!\n");}
}int main()
{/*test01();*/test02();return 0;
}
改?
(1)修改
修改指定位置的數據:直接修改該節點data的值——
SList.c:
void SLTChangeData(SLTNode* pos, SLTDataType x)
{assert(pos);pos->data = x;
}
?銷毀鏈表
遍歷鏈表,釋放每一個節點,由于需要修改指向頭節點的指針,因此函數形參中要用二級指針——
(1)銷毀鏈表
SList.c:
void SLTDestroy(SLTNode** pphead)
{assert(pphead );SLTNode* pcur = *pphead;while(pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
這里pos=NULL、pcur=NULL、next=NULL加上置為空是養成好習慣,這里是默認置為空。?
test.c:?
void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);?SLTNode* pos = SLTFind(plist, 3);SListDestory(&plist);
}int main()
{/*test01();*/test02();return 0;
}
4、完整代碼?
(1)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;//typedef struct SListNode SLTNode;void SLTPrint(?SLTNode* phead);//尾插
void SLTPushBack(?SLTNode** pphead, SLTDatatype x);//頭插
void SLTPushFront(?SLTNode** pphead, SLTDatatype x);//尾刪
void SLTPopBack(?SLTNode** pphead);//頭刪
void SLTPopFront(?SLTNode** pphead);//查找
?SLTNode* SLTFind(?SLTNode* phead, SLTDatatype x);//在指定位置之前插入數據
void SLTInsert(?SLTNode** pphead, ?SLTNode* pos, SLTDatatype x);//在指定位置之后插入數據
void SLTInsertAfter(?SLTNode* pos, SLTDatatype x);//刪除pos節點
void SLTErase(?SLTNode** pphead, ?SLTNode* pos);//刪除pos之后的節點
void SLTEraseAfter(?SLTNode* pos);//修改
void SLTChangeData(?SLTNode* pos, SLTDatatype x);//銷毀鏈表
void SListDestory(?SLTNode** pphead);
(2)SList.c:
#define _CRT_SECURE_NO_WARNINGS 1#include"SList.h"void SLTPrint(?SLTNode* phead)
{?SLTNode* pcur = phead;while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}//后續我們要申請新節點就直接調用SLTBuyNode方法
?SLTNode* SLTBuyNode(SLTDatatype x)
{?SLTNode* newnode = (?SLTNode*)malloc(sizeof(?SLTNode));//malloc不一定申請成功,我們判斷一下if (newnode == NULL){printf("malloc fail!");exit(1);}//初始化一下newnode->data = x;newnode->next = NULL;return newnode;
}//尾插
void SLTPushBack(?SLTNode** pphead, SLTDatatype x)
{//申請新節點?SLTNode* newnode = SLTBuyNode(x);//鏈表為空——要特殊處理if (*pphead == NULL){*pphead = newnode;}else{?SLTNode* ptail = *pphead;while (ptail->next != NULL){ptail = ptail->next;}//找到了尾節點 ptail newnodeptail->next = newnode;}
}//頭插
void SLTPushFront(?SLTNode** pphead, SLTDatatype x)
{assert(pphead);?SLTNode* newnode = SLTBuyNode(x);//newnode *ppheadnewnode->next = *pphead;*pphead = newnode;
}//尾刪
void SLTPopBack(?SLTNode** pphead)
{//鏈表為空不能刪除assert(pphead && *pphead);//pphead是*pphead的地址//pphead是一個二級指針,我們對pphead解引用一次,*pphead就是指向第一個節點的地址//*pphead為空說明鏈表為空//鏈表只有一個節點的情況if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{?SLTNode* prev = NULL;?SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}//prev ptailprev->next = NULL;free(ptail);ptail = NULL;}
}//頭刪
void SLTPopFront(?SLTNode** 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 SLTInsert(?SLTNode** pphead, ?SLTNode* pos, SLTDatatype x)
{assert(pphead && pos);?SLTNode* newnode = SLTBuyNode(x);//pos指向頭節點if (pos == *pphead){//頭插SLTPushFront(pphead, x);}else{//找pos前一個節點?SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//prev newnode posprev->next = newnode;newnode->next = pos;}
}//在指定位置之后插入數據
void SLTInsertAfter(?SLTNode* pos, SLTDatatype x)
{assert(pos);?SLTNode* newnode = SLTBuyNode(x);//pos newnode pos->nextnewnode->next = pos->next;pos->next = newnode;
}//刪除pos節點
void SLTErase(?SLTNode** pphead, ?SLTNode* pos)
{assert(pphead && pos);//pos剛好是頭節點——頭刪if (pos==*pphead){SLTPopFront(pphead);}else{?SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//prev pos pos->nextprev->next = pos->next;free(pos);pos = NULL;}
}//刪除pos之后的節點
void SLTEraseAfter(?SLTNode* pos)
{assert(pos);//pos del del->next?SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//修改
void SLTChangeData(?SLTNode* pos, SLTDatatype x)
{assert(pos);pos->data = x;
}//銷毀鏈表
void SListDestory(?SLTNode** pphead)
{//一個一個銷毀assert(pphead);?SLTNode* pcur = *pphead;while (pcur){?SLTNode* next = pcur->next;free(pcur);pcur = next;}//*pphead是野指針,要置為空*pphead = NULL;
}
(3)test.c:
#define _CRT_SECURE_NO_WARNINGS 1#include"SList.h"int test01()
{//創建一個鏈表——實際上是創建一個一個節點,再把節點連起來?SLTNode*node1=(?SLTNode*)malloc(sizeof(?SLTNode));?SLTNode*node2=(?SLTNode*)malloc(sizeof(?SLTNode));?SLTNode*node3=(?SLTNode*)malloc(sizeof(?SLTNode));?SLTNode*node4=(?SLTNode*)malloc(sizeof(?SLTNode));node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;?SLTNode* plist = node1;//打印鏈表SLTPrint(plist);
}void test02()
{//創建空鏈表?SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);////SLTPushFront(&plist, 1);//SLTPrint(plist);//SLTPushFront(&plist, 2);//SLTPrint(plist);//SLTPushFront(&plist, 3);//SLTPrint(plist);//SLTPushFront(&plist, 4);//SLTPrint(plist);////SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);////頭刪
// SLTPopFront(&plist);
// SLTPrint(plist);
// SLTPopFront(&plist);
// SLTPrint(plist);
// SLTPopFront(&plist);
// SLTPrint(plist);
// SLTPopFront(&plist);
// SLTPrint(plist);?SLTNode* pos = SLTFind(plist, 3);//if (pos)//{// printf("找到了!\n");//}//else//{// printf("未找到!\n");//}//SLTInsert(&plist, pos, 100);/*SLTInsertAfter(pos, 100);*///SLTErase(&plist, pos);//SLTPrint(plist);SListDestory(&plist);
}int main()
{/*test01();*/test02();return 0;
}
結尾
往期回顧:
【數據結構與算法】數據結構初階:詳解順序表和鏈表(三)——單鏈表(上
本期內容需要回顧的C語言知識如下面的截圖中所示(指針博主寫了6篇,列出來有水字數嫌疑了,就只放指針第六篇的網址,博主在指針(六)把指針部分的前五篇的網址都放在【往期回顧】了,點擊【傳送門】就可以看了)。
大家如果對前面部分的知識點印象不深,可以去上一篇文章的結尾部分看看,博主把需要回顧的知識點相關的博客的鏈接都放在上一篇文章了,上一篇文章的鏈接博主放在下面了:
?【數據結構與算法】數據結構初階:詳解順序表和鏈表(三)——單鏈表(上)
結語:本篇文章到這里就結束了,對數據結構的單鏈表知識感興趣的友友們可以在評論區留言,博主創作時可能存在筆誤,或者知識點不嚴謹的地方,大家多擔待,如果大家在閱讀的時候發現了行文有什么錯誤歡迎在評論區斧正,再次感謝友友們的關注和支持!