?文章來源地址http://www.zghlxwxcb.cn/news/detail-598091.html
本章內(nèi)容
1.什么是鏈表
2.鏈表常見幾種形式
3.無頭單向非循環(huán)鏈表的實現(xiàn)
3.1結點結構的定義
3.2函數(shù)接口的實現(xiàn)
3.2.1尾插
3.2.2尾刪
4. 帶頭雙向循環(huán)鏈表的實現(xiàn)
4.1結點結構的定義
4.2函數(shù)接口的實現(xiàn)
5.兩種鏈表的差異
①尾插與尾刪的時間復雜度
②頭插與頭刪的時間復雜度
③函數(shù)形參為何一個是二級指針,一個是一級指針?
完整源碼
無頭單向非循環(huán)鏈表
SList.h
SList.c
test.c
帶頭雙向循環(huán)鏈表
List.h
List.c
test.c
?
?
1.什么是鏈表
像數(shù)組一樣,鏈表也用來表示一系列的元素。事實上,能用數(shù)組來做的事情,一般也可以用鏈表來做。然而,鏈表的實現(xiàn)跟數(shù)組是不一樣的,在不同場景它們會有不同的性能表現(xiàn)。
計算機的內(nèi)存就像一大堆格子,每格都可以用來保存比特形式的數(shù)據(jù)。當要創(chuàng)建數(shù)組時,程序會在內(nèi)存中找出一組連續(xù)的空格子,給它們起個名字,以便你的應用存放數(shù)據(jù)。
我們之前說過,計算機能夠直接跳到數(shù)組的某一索引上。如果代碼要求它讀取索引 4的值,那么計算機只需一步就可以完成任務。重申一次,之所以能夠這樣,是因為程序事先知道了數(shù)組開頭所在的內(nèi)存地址——例如地址是 1000——當它想去索引 4時,便會自動跳到 1004處。
與數(shù)組不同的是,組成鏈表的格子不是連續(xù)的。它們可以分布在內(nèi)存的各個地方。這種不相鄰的格子,就叫作結點。
那么問題來了,計算機怎么知道這些分散的結點里,哪些屬于這個鏈表,哪些屬于其他鏈表呢?這就是鏈表的關鍵了:每個結點除了保存數(shù)據(jù),它還保存著鏈表里的下一結點的內(nèi)存地址。
這份用來指示下一結點的內(nèi)存地址的額外數(shù)據(jù),被稱為鏈。鏈表如下圖所示。?
注意:
1.從上圖可以看出,鏈式結構在邏輯上是來連續(xù)的,但是在物理上不一定連續(xù)。
2.現(xiàn)實中的結點一般都是從堆上來申請的。
3.從堆上申請的空間,是按照一定的策略來分配的,兩次申請的空間可能連續(xù),也可能不連續(xù)。
2.鏈表常見幾種形式
①單向或者雙向
②帶頭或者不帶頭?
?③循環(huán)或者非循環(huán)
雖然有這么多的鏈表的結構,但是我們實際中最常用到的還是這兩種結構:
1. 無頭單向非循環(huán)鏈表
結構簡單,一般不會用來單獨進行存儲數(shù)據(jù)。實際中更多是作為其它數(shù)據(jù)結構的子結構,如哈希表、圖的鄰接表等等。另外這種結構在筆試面試中出現(xiàn)比較多。
2. 帶頭雙向循環(huán)鏈表
結構最復雜,一般用于單獨存儲數(shù)據(jù)。實際中使用的鏈表數(shù)據(jù)結構,都是帶頭雙向循環(huán)鏈表。另外這個結構雖然結構復雜,但是使用代碼實現(xiàn)以后發(fā)現(xiàn)結構會帶來很多優(yōu)勢,實現(xiàn)反而簡單了。
3.無頭單向非循環(huán)鏈表的實現(xiàn)
就我個人而言,我認為雖然無頭單向非循環(huán)鏈表是這幾個鏈表中結構最簡單的,但是實現(xiàn)卻是最復雜的。我們學會了頭單向非循環(huán)鏈表的實現(xiàn),別的鏈表實現(xiàn)應當不在話下。
3.1結點結構的定義
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
如上文所講,一個結點包含的內(nèi)容有數(shù)據(jù)data和保存下一個結點的地址的指針。
鏈表正式由一個個這樣的結點串連起來的,所以我們只需要記住排在最前面的頭結點的位置,就能訪問鏈表里任意一個結點。
注意:無頭單向非循環(huán)鏈表里的頭結點指的是鏈表中的第一個結點,它本身也存儲著有效數(shù)據(jù)。而后面所講的帶頭雙向循環(huán)鏈表中的頭結點僅僅是作為鏈表起始的標志,并不會存儲有效數(shù)據(jù)。
3.2函數(shù)接口的實現(xiàn)
//申請一個結點
SLTNode* BuySLTNode(SLTDataType data);
//創(chuàng)建一個鏈表,包含數(shù)據(jù)為0~n
SLTNode* CreateSList(int n);
//釋放內(nèi)存
void SLTDestroy(SLTNode** pphead);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType data);
//尾刪
void SLTPopBack(SLTNode** pphead);
//頭插
void SLTPushFront(SLTNode** pphead, SLTDataType data);
//頭刪
void SLTPopFront(SLTNode** pphead);
//查找data的結點
SLTNode* SLTFind(SLTNode** pphead, SLTDataType data);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType data);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType data);
//刪除pos結點
void SLTErase(SLTNode** pphead, SLTNode* pos);
//打印鏈表內(nèi)容
void SLTPrint(SLTNode* phead);
由于代碼量過大,為了避免影響閱讀體驗太差,我將完整代碼置于文章末尾。此處,我們來細致的學習兩個函數(shù)尾插與尾刪,其它函數(shù)與其原理幾乎相同。
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType data);
//尾刪
void SLTPopBack(SLTNode** pphead);
3.2.1尾插
void SLTPushBack(SLTNode** pphead, SLTDataType data)
{
SLTNode* newNode = BuySLTNode(data);
//若為第一次插入,則分情況處理
if (*pphead==NULL)
{
*pphead = newNode;
return;
}
//找尾
SLTNode* tail = *pphead;
while(tail->next)
{
tail = tail->next;
}
//找到了,進行尾插
tail->next = newNode;
}
?首先將tail也指向頭結點,通過變化tail來找到尾結點,如下圖所示為找尾的過程:
此時,tail所指向的結點的next為NULL,則循環(huán)結束,進行尾插。尾插只需要將tail所指向的結點的next指針賦值為newNode即可。
3.2.2尾刪
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//分情況處理
if (*pphead == NULL)
{
free(*pphead);
*pphead = NULL;
}
//找尾
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
//找到了,進行尾刪
free(tail->next);
tail->next = NULL;
}
同樣的尾刪也是先找尾,但與尾插的找尾有一點不同的是while循環(huán)的判斷條件不相同。原因是我們刪掉尾結點后,還有重要的一步是將尾結點的前一個結點(也就是新的尾結點)的next置為NULL。所以這里的tail指向的是尾結點的前一個結點。
此時tail->next->next?為NULL,所以循環(huán)結束,進行尾刪。尾刪只需要釋放掉尾結點,并將新的尾結點的next置為NULL。
4. 帶頭雙向循環(huán)鏈表的實現(xiàn)
帶頭雙向循環(huán)鏈表看似結構復雜,其實在寫代碼時你會感到很輕松。其關鍵就在于它的頭結點不一般。此處的頭結點不存儲有效數(shù)據(jù)。
4.1結點結構的定義
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;//指向前一個結點
struct ListNode* next;//指向后一個結點
}LTNode;
既然是雙向循環(huán)鏈表,那就得要求每個結點既保存下一個結點的地址,又要保存上一個結點的地址。結構中,prev指向前一個結點,next指向后一個結點。
4.2函數(shù)接口的實現(xiàn)
//申請一個結點
LTNode* BuyListNode(LTDataType data);
//初始化頭結點
LTNode* LTInit();
//釋放申請的內(nèi)存
void LTDestroy(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType data);
//尾刪
void LTPopBack(LTNode* phead);
//頭插
void LTPushFront(LTNode* phead, LTDataType data);
//頭刪
void LTPopFront(LTNode* phead);
//查找data
LTNode* LTFind(LTNode* phead, LTDataType data);
//在pos位置前插入
void LTInsert(LTNode* phead, LTNode* pos, LTDataType data);
//刪除pos位置
void LTErase(LTNode* phead, LTNode* pos);
//判斷鏈表是否為空
bool LTEmpty(LTNode* phead);
//統(tǒng)計鏈表中數(shù)據(jù)的個數(shù)
size_t LTSize(LTNode* phead);
//打印鏈表存儲的數(shù)據(jù)
void LTPrint(LTNode* phead);
同樣的,由于代碼過長,為了避免影響閱讀體驗,我將完整源碼置于文章末尾。帶頭雙向循環(huán)鏈表的各個函數(shù)實現(xiàn)都比不帶頭的單鏈表簡單很多。因此學會了之前單鏈表的實現(xiàn),雙向鏈表的實現(xiàn)自然輕松無比。這里就不做贅述。
5.兩種鏈表的差異
①尾插與尾刪的時間復雜度
對于單鏈表而言,尾插與尾刪都是它的劣勢。因為計算機無法直接訪問到鏈表的尾結點,必須遍歷完整個鏈表才能找到尾結點。所以單鏈表的尾插與尾刪都為O(N)。
對于帶頭雙向循環(huán)鏈表,找尾是極其方便的,因為尾結點就在頭結點的前一個,可以一步就訪問到。所以,帶頭雙向循環(huán)鏈表的尾插與尾刪都為O(1)。
②頭插與頭刪的時間復雜度
兩種鏈表頭插與頭刪都為O(1)。對于鏈表這種數(shù)據(jù)結構而言,頭插與頭刪都是其優(yōu)勢。上一章中提到順序表的尾插與尾刪效率極高,但是頭插與頭刪卻比較劣勢。而鏈表正好相反,頭插與頭刪效率很高。因此面對不同的場景,要學會使用合適的數(shù)據(jù)結構。
③函數(shù)形參為何一個是二級指針,一個是一級指針?
單鏈表,我們需要對phead的值做修改。那么就得利用phead的指針。而phead本身就是一個指針,所以函數(shù)的形參為pphead的二級指針。
雙向循環(huán)鏈表,并不需要對phead做修改,而只是需要訪問它prev和next所指向的結點,所以只用一級指針即可。
完整源碼
無頭單向非循環(huán)鏈表
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;
//申請一個結點
SLTNode* BuySLTNode(SLTDataType data);
//創(chuàng)建一個鏈表,包含數(shù)據(jù)為0~n
SLTNode* CreateSList(int n);
//釋放內(nèi)存
void SLTDestroy(SLTNode** pphead);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType data);
//尾刪
void SLTPopBack(SLTNode** pphead);
//頭插
void SLTPushFront(SLTNode** pphead, SLTDataType data);
//頭刪
void SLTPopFront(SLTNode** pphead);
//查找data的結點
SLTNode* SLTFind(SLTNode** pphead, SLTDataType data);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType data);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType data);
//刪除pos結點
void SLTErase(SLTNode** pphead, SLTNode* pos);
//打印鏈表內(nèi)容
void SLTPrint(SLTNode* phead);
SList.c
#define _CRT_SECURE_NO_DEPRECATE 1
#include"SList.h"
SLTNode* BuySLTNode(SLTDataType data)
{
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
//檢查是否申請成功
if (newNode == NULL)
{
perror("malloc fail");
exit(-1);
}
//對newNode進行初始化
newNode->data = data;
newNode->next = NULL;
//返回申請成功的結點
return newNode;
}
SLTNode* CreateSList(int n)
{
SLTNode* phead = NULL;
SLTNode* ptail = NULL;
for (int i = 0; i < n; i++)
{
SLTNode* newNode = BuySLTNode(i);
//若為第一個插入,則分情況處理
if (phead == NULL)
{
phead = ptail = newNode;
}
else
{
ptail->next = newNode;
ptail = newNode;
}
}
return phead;
}
void SLTDestroy(SLTNode** pphead)
{
assert(*pphead);
SLTNode* cur = *pphead;
//cur判斷何時結束
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
void SLTPushBack(SLTNode** pphead, SLTDataType data)
{
SLTNode* newNode = BuySLTNode(data);
//若為第一次插入,則分情況處理
if (*pphead==NULL)
{
*pphead = newNode;
return;
}
//找尾
SLTNode* tail = *pphead;
while(tail->next)
{
tail = tail->next;
}
//找到了,進行尾插
tail->next = newNode;
}
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//分情況處理
if (*pphead == NULL)
{
free(*pphead);
*pphead = NULL;
}
//找尾
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
//找到了,進行尾刪
free(tail->next);
tail->next = NULL;
}
void SLTPushFront(SLTNode** pphead, SLTDataType data)
{
SLTNode* newNode = BuySLTNode(data);
//進行頭插
newNode->next = *pphead;
*pphead = newNode;
}
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead);
//進行頭刪
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
SLTNode* SLTFind(SLTNode** pphead, SLTDataType data)
{
assert(*pphead);
SLTNode* cur = *pphead;
while (cur)
{
//找到了,返回cur
if (cur->data == data)
{
return cur;
}
cur = cur->next;
}
//沒找到,返回NULL
return NULL;
}
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType data)
{
assert(pos);
SLTNode* newNode = BuySLTNode(data);
//若pos恰好是phead,相當于進行頭插
if (pos == *pphead)
{
SLTPushFront(pphead, data);
return;
}
//找pos前一個指針
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//找到了,進行插入
prev->next = newNode;
newNode->next = pos;
}
void SLTInsertAfter(SLTNode* pos, SLTDataType data)
{
assert(pos);
SLTNode* newNode = BuySLTNode(data);
SLTNode* next = pos->next;
pos->next = newNode;
newNode->next = next;
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//若pos恰好就是phead,則相當于頭刪
if (pos == *pphead)
{
SLTPopFront(pphead);
return;
}
//找pos前一個結點
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//找到了,進行刪除
prev->next = pos->next;
free(pos);
pos = NULL;
}
void SLTPrint(SLTNode* phead)
{
assert(phead);
SLTNode* cur = phead;
//cur控制何時結束
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
test.c
#define _CRT_SECURE_NO_DEPRECATE 1
#include"SList.h"
void test()
{
SLTNode* phead = CreateSList(10);
SLTPushBack(&phead, 0);
SLTPushBack(&phead, 0);
SLTPushBack(&phead, 0);
SLTPushBack(&phead, 0);
SLTPushBack(&phead, 0);
SLTPrint(phead);
SLTPushFront(&phead, 1);
SLTPushFront(&phead, 1);
SLTPushFront(&phead, 1);
SLTPushFront(&phead, 1);
SLTPushFront(&phead, 1);
SLTPrint(phead);
SLTNode* pos = SLTFind(&phead, 3);
SLTInsert(&phead, pos, 300);
SLTInsertAfter(pos, 30);
SLTPrint(phead);
SLTPopBack(&phead);
SLTPopBack(&phead);
SLTPopBack(&phead);
SLTPopBack(&phead);
SLTPopBack(&phead);
SLTPopFront(&phead);
SLTPopFront(&phead);
SLTPopFront(&phead);
SLTPopFront(&phead);
SLTPopFront(&phead);
SLTPrint(phead);
SLTDestroy(&phead);
}
int main()
{
test();
return 0;
}
帶頭雙向循環(huán)鏈表
List.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;//指向前一個結點
struct ListNode* next;//指向后一個結點
}LTNode;
//申請一個結點
LTNode* BuyListNode(LTDataType data);
//初始化頭結點
LTNode* LTInit();
//釋放申請的內(nèi)存
void LTDestroy(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType data);
//尾刪
void LTPopBack(LTNode* phead);
//頭插
void LTPushFront(LTNode* phead, LTDataType data);
//頭刪
void LTPopFront(LTNode* phead);
//查找data
LTNode* LTFind(LTNode* phead, LTDataType data);
//在pos位置前插入
void LTInsert(LTNode* phead, LTNode* pos, LTDataType data);
//刪除pos位置
void LTErase(LTNode* phead, LTNode* pos);
//判斷鏈表是否為空
bool LTEmpty(LTNode* phead);
//統(tǒng)計鏈表中數(shù)據(jù)的個數(shù)
size_t LTSize(LTNode* phead);
//打印鏈表存儲的數(shù)據(jù)
void LTPrint(LTNode* phead);
List.c
#define _CRT_SECURE_NO_DEPRECATE 1
#include"List.h"
LTNode* BuyListNode(LTDataType data)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL)
{
perror("malloc fail");
exit(-1);
}
newNode->data = data;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->prev = phead;
phead->next = phead;
return phead;
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
void LTPushBack(LTNode* phead, LTDataType data)
{
assert(phead);
LTNode* newNode = BuyListNode(data);
LTNode* tail = phead->prev;
newNode->next = phead;
newNode->prev = tail;
tail->next = newNode;
phead->prev = newNode;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
tail->prev->next = phead;
phead->prev = tail->prev;
free(tail);
}
void LTPushFront(LTNode* phead, LTDataType data)
{
assert(phead);
LTNode* newNode = BuyListNode(data);
newNode->next = phead->next;
phead->next->prev = newNode;
phead->next = newNode;
newNode->prev = phead;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* cur = phead->next;
phead->next = cur->next;
cur->next->prev = phead;
free(cur);
}
LTNode* LTFind(LTNode* phead, LTDataType data)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data==data)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void LTInsert(LTNode* phead, LTNode* pos, LTDataType data)
{
assert(phead);
LTNode* newNode = BuyListNode(data);
pos->prev->next = newNode;
newNode->prev = pos->prev;
newNode->next = pos;
pos->prev = newNode;
}
void LTErase(LTNode* phead,LTNode* pos)
{
assert(phead);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
}
bool LTEmpty(LTNode* phead)
{
assert(phead);
if (phead->next == phead)
{
return true;
}
return false;
}
size_t LTSize(LTNode* phead)
{
assert(phead);
size_t size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("\n");
}
test.c
#define _CRT_SECURE_NO_DEPRECATE 1
#include"List.h"
void test()
{
LTNode* phead = LTInit();
LTPushBack(phead, 2);
LTPushBack(phead, 1);
LTPushBack(phead, 1);
LTPushBack(phead, 1);
LTPushBack(phead, 1);
LTPushBack(phead, 1);
LTPushFront(phead, 0);
LTPushFront(phead, 0);
LTPushFront(phead, 0);
LTPushFront(phead, 0);
LTPushFront(phead, 0);
LTPrint(phead);
LTPopFront(phead);
LTPopFront(phead);
LTPopFront(phead);
LTPrint(phead);
LTPopBack(phead);
LTPopBack(phead);
LTPopBack(phead);
LTPrint(phead);
LTNode* pos = LTFind(phead, 2);
LTInsert(phead, pos, 200);
LTInsert(phead, pos, 200);
LTInsert(phead, pos, 200);
LTPrint(phead);
LTErase(phead, pos);
LTPrint(phead);
LTDestroy(phead);
}
int main()
{
test();
return 0;
}
文章來源:http://www.zghlxwxcb.cn/news/detail-598091.html
?
到了這里,關于『初階數(shù)據(jù)結構 ? C語言』⑨ - 基于結點的數(shù)據(jù)結構——鏈表(單鏈表&&雙向循環(huán)鏈表)附完整源碼的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!