1. 鏈表的概念及結構
概念:鏈表是?種物理存儲結構上?連續、?順序的存儲結構,數據元素的邏輯順序是通過鏈表 中的指針鏈接次序實現的 。
鏈表也是線性表的一種。
鏈表的結構跟???廂相似,淡季時?次的?廂會相應減少,旺季時?次的?廂會額外增加?節。只 需要將???的某節?廂去掉/加上,不會影響其他?廂,每節?廂都是獨?存在的。
?廂是獨?存在的,且每節?廂都有??。想象?下這樣的場景,假設每節?廂的??都是鎖上的狀 態,需要不同的鑰匙才能解鎖,每次只能攜帶?把鑰匙的情況下如何從?頭?到?尾?
最簡單的做法:每節?廂?都放?把下?節?廂的鑰匙。?
在鏈表?,每節“?廂”是什么樣的呢?
與順序表不同的是,鏈表?的每節"?廂"都是獨?申請下來的空間,我們稱之為“結點/節點”
節點的組成主要有兩個部分:當前節點要保存的數據和保存下?個節點的地址(指針變量)。
圖中指針變量 plist保存的是第?個節點的地址,我們稱plist此時“指向”第?個節點,如果我們希 望plist“指向”第?個節點時,只需要修改plist保存的內容為0x0012FFA0。
為什么還需要指針變量來保存下?個節點的位置?
鏈表中每個節點都是獨?申請的(即需要插?數據時才去申請?塊節點的空間),我們需要通過指針變量來保存下?個節點位置才能從當前節點找到下?個節點。
結合前?學到的結構體知識,我們可以給出每個節點對應的結構體代碼:
假設當前保存的節點為整型:
struct SListNode
{int data; //節點數據struct SListNode* next; //指針變量?保存下?個節點的地址
};
當我們想要保存?個整型數據時,實際是向操作系統申請了?塊內存,這個內存不僅要保存整型數 據,也需要保存下?個節點的地址(當下?個節點為空時保存的地址為空)。 當我們想要從第?個節點?到最后?個節點時,只需要在前?個節點拿上下?個節點的地址(下?個 節點的鑰匙)就可以了。
給定的鏈表結構中,如何實現節點從頭到尾的打印?
思考:當我們想保存的數據類型為字符型、浮點型或者其他?定義的類型時,該如何修改?
補充說明:
1、鏈式機構在邏輯上是連續的,在物理結構上不?定連續
2、節點?般是從堆上申請的
3、從堆上申請來的空間,是按照?定策略分配出來的,每次申請的空間可能連續,可能不連續
2. 單鏈表的實現
我們和順序表一樣定義三個文件,一個頭文件我們取個名字叫SList.h,兩個源文件分別取名叫SList.c和text.c,我們的SList.c用來實現鏈表的方法,text.c用來測試。
我們把單鏈表的功能函數全部在頭文件里面聲明一下,以及我們結構體的定義,還有我們需要用到的頭文件都放在.h文件里面。
.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>//定義節點的結構
typedef int SLTDataType;typedef struct SListNode
{SLTDataType date;//數據struct SListNode* next;//指向節點的地址}SLTNode;void SLTPrint(SLTNode* phead);//打印void SLTBuyBode(SLTNode** pphead, SLTDataType x);//尾插void SLPushFront(SLTNode** pphead, SLTDataType x);//頭插void SLTPopBack(SLTNode** pphead);//尾刪void SLTPopFront(SLTNode** pphead);//頭刪SLTNode* SLFind(SLTNode* phead, SLTDataType x);//查找void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//指定位置之前插入數據void SLTnesertAfter(SLTNode* pos, SLTDataType x);//指定位置之后插入數據void SLTErase(SLTNode** pphead, SLTNode* pos);//刪除pos節點void SLTEreaseAfter(SLTNode* pos);//刪除pos之后的節點void SListDesTroy(SLTNode** pphead);//銷毀鏈表
以上就是我們的聲明了,要注意我們的類型,我們這里用typedef?定義了一個int類型給它取了個名字叫SLTDataType,當我們需要改類型的時候直接把int改成我們想要的類型就行。
我們和順序表一樣先來實現尾插,我們插入數據的時候需要申請節點,所以我們先來實現申請節點的函數。
節點的申請
SList.c
SLTNode* SLBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->date = x;newnode->next = NULL;return newnode;
}
申請空間可能導致申請失敗所以我們需要判斷一下,x是我們節點里面的數據。
現在來實現尾插
//尾插
void SLTBuyBode(SLTNode** pphead, SLTDataType x)
{assert(pphead);//*pphead就是指向第一個節點的指針//空鏈表和非空鏈表SLTNode* newnode = SLBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾節點ptail->next = newnode;}
}
如果我們傳過來一個NULL,我們可以對他解引用嗎?那肯定是不行的,所以我們加個斷言。
我們要插入數據那肯定是需要申請節點的,我們把需要插入的數據x傳過去,然后進行找尾找到后讓我們的尾節點的next指向我們的新節點。
這里我們創建了一個指針變量讓它進行找尾的操作,如果用我們的頭節點不斷往后最后會指向我們的尾節點。
尾插實現好了我們用測試文件來測試一下
text.c
打印怎么實現的我們上面已經說了。我把打印的代碼直接放這了(SList.c)
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->date);pcur = pcur->next;}printf("NULL\n");
}
頭插的實現
//頭插
void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = SLBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}
頭插的實現其實就是把我們申請好的節點讓它的next指向我們的頭節點。?
測試一下:
尾刪的實現
//尾刪
void SLTPopBack(SLTNode** pphead)
{assert(pphead && *pphead);//鏈表只有一個節點if ((*pphead)->next == NULL)//-->優先級*{free(*pphead);*pphead = NULL;}else{//鏈表有多個節點SLTNode* prev = *pphead; //尾節點的前一個節點SLTNode* ptail = *pphead;//尾節點while (ptail->next){prev = ptail;//尾節點的前一個節點ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;//讓我的尾節點指向空指針}
}
既然我們要刪除節點那么節點不能為空,所以需要斷言一下。
當鏈表只有一個節點的時候我們直接把它釋放就行了。
有多個節點的時候我們需要用一個指針變量用來保存我們的前一個節點。?
測試一下:
頭刪的實現
//頭刪
void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
我們要把頭節點給釋放掉,所以我們先把頭節點的next指向下一個節點的地址給保存起來。
測試一下:
查找的實現
//查找
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->date == x){return pcur;}pcur = pcur->next;}return NULL;
}
如果找到了我們就直接返回找到的節點,沒找到返回NULL。
測試一下:
指定位置之前插入數據的實現
//指定位置之前插入數據
void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLBuyNode(x);if (pos == *pphead){//如果pos等于我們的頭節點我們直接使用頭插SLPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}
pos是我們指定的位置,所以不能為空我們斷言一下。
插入數據肯定是要申請節點的。
如果pos等于我們的頭節點那么我們直接調用頭插就可以了。
我們定義一個指針變量用來保存前一個節點,然后用我們申請好了的節點的next指向我們的pos,在用我們前一個節點的next指向我們申請好了的節點。
測試一下:
指定位置之后插入數據
//指定位置之后插入數據
void SLTnesertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}
這里我們用不到頭節點,所以只有兩個參數,這里的pos是指定位置所以不能為空我們要加個斷言,這里直接用申請好的節點的next指向pos的下一個節點,然后再讓pos的next指向我們申請好的節點,這樣就插入進去了。
?測試:
刪除pos節點
/刪除pos節點
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);if (pos == *pphead){//如果等于說明要刪除的就是頭節點,所以我們直接頭刪SLTPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}
刪除指定pos節點的實現和我們在指定位置之前插入數據的實現有點相似。
當跳出while循序的時候說明prev->next已經等于pos了,所以我們讓perv->next指向pos的下一個節點,然后把pos釋放掉。
測試:
刪除pos之后的節點
//刪除pos之后的節點
void SLTEreaseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
刪除pos之后的節點,直接把pos指向的下一個節點的地址保存起來,這里面我們定義了一個指針變量用來存放下一個節點,然后把del節點的next指向的節點給pos->next,最后釋放掉del。
測試:
銷毀鏈表
//銷毀鏈表
void SListDesTroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
通過循序對節點一個一個的釋放。
最后置為NULL
3.單鏈表的所有代碼
SList.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>//定義節點的結構
typedef int SLTDataType;typedef struct SListNode
{SLTDataType date;//數據struct SListNode* next;//指向節點的地址}SLTNode;void SLTPrint(SLTNode* phead);//打印void SLTBuyBode(SLTNode** pphead, SLTDataType x);//尾插void SLPushFront(SLTNode** pphead, SLTDataType x);//頭插void SLTPopBack(SLTNode** pphead);//尾刪void SLTPopFront(SLTNode** pphead);//頭刪SLTNode* SLFind(SLTNode* phead, SLTDataType x);//查找void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//指定位置之前插入數據void SLTnesertAfter(SLTNode* pos, SLTDataType x);//指定位置之后插入數據void SLTErase(SLTNode** pphead, SLTNode* pos);//刪除pos節點void SLTEreaseAfter(SLTNode* pos);//刪除pos之后的節點void SListDesTroy(SLTNode** pphead);//銷毀鏈表
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->date);pcur = pcur->next;}printf("NULL\n");
}//判斷是否為非空鏈表
SLTNode* SLBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->date = x;newnode->next = NULL;return newnode;
}//尾插
void SLTBuyBode(SLTNode** pphead, SLTDataType x)
{assert(pphead);//*pphead就是指向第一個節點的指針//空鏈表和非空鏈表SLTNode* newnode = SLBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾節點ptail->next = newnode;}
}//頭插
void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = SLBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}//尾刪
void SLTPopBack(SLTNode** pphead)
{assert(pphead && *pphead);//鏈表只有一個節點if ((*pphead)->next == NULL)//-->優先級*{free(*pphead);*pphead = NULL;}else{//鏈表有多個節點SLTNode* prev = *pphead; //尾節點的前一個節點SLTNode* ptail = *pphead;//尾節點while (ptail->next){prev = ptail;//尾節點的前一個節點ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;//讓我的尾節點指向空指針}
}
//頭刪
void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}//查找
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->date == x){return pcur;}pcur = pcur->next;}return NULL;
}//指定位置之前插入數據
void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLBuyNode(x);if (pos == *pphead){//如果pos等于我們的頭節點我們直接使用頭插SLPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}//指定位置之后插入數據
void SLTnesertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}//刪除pos節點
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);if (pos == *pphead){//如果等于說明要刪除的就是頭節點,所以我們直接頭刪SLTPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}//刪除pos之后的節點
void SLTEreaseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//銷毀鏈表
void SListDesTroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
text.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListText02()
{SLTNode* plist = NULL;//尾插SLTBuyBode(&plist, 1);SLTBuyBode(&plist, 2);SLTBuyBode(&plist, 3);SLTBuyBode(&plist, 4);SLTPrint(plist);//打印//測試頭插SLPushFront(&plist, 0);SLTPrint(plist);//測試尾刪SLTPopBack(&plist);SLTPrint(plist);//測試頭刪SLTPopFront(&plist);SLTPrint(plist);//測試查找SLTNode* find = SLFind(plist,1);if (find == NULL){printf("沒找到\n");}else{printf("找到了\n");}//指定位置之前插入數據SLTnsert(&plist,find,6);SLTPrint(plist);//指定位置之后插入數據SLTnesertAfter(find, 7);SLTPrint(plist);//刪除pos節點SLTErase(&plist,find);SLTPrint(plist);//刪除pos之后的節點SLTEreaseAfter(find);SLTPrint(plist);//銷毀鏈表SListDesTroy(&plist);
}
int main()
{SListText02();return 0;
}
以上就是單鏈表的基本操作了,如有表述不準確或理解不深刻的地方,還請各位看客不吝指正。
?