上篇博客實現動態順序表時,我們會發現它存在許多弊端,如:
? 中間/頭部的插?刪除,時間復雜度為O(N)? 增容需要申請新空間,拷?數據,釋放舊空間。會有不?的消耗。? 增容?般是呈2倍的增?,勢必會有?定的空間浪費。例如當前容量為100,滿了以后增容到200,我們再繼續插?了5個數據,后?沒有數據插?了,那么就浪費了95個數據空間。
這些問題該怎么解決呢?
我們需要學習線性表的另一種實現方式--鏈表。
目錄
1.鏈表的分類
2.單鏈表
2.1概念與結構
2.1.1結點
2.1.2鏈表的性質
2.2單鏈表的實現
2.2.1 SList.h
2.2.2 SList.c?
?3.雙向鏈表
3.1.概念與結構
?3.2雙向鏈表的實現
3.2.1List.h
?3.2.2 List.c
4.順序表與鏈表比較?
?
1.鏈表的分類
鏈表的結構?常多樣,以下情況組合起來有8種(2 x 2 x 2)鏈表結構:
鏈表可從三個維度分類組合:
- 單向與雙向:
- 單向鏈表:結點僅含一個指向下一結點的指針;
- 雙向鏈表:結點含前驅、后繼兩個指針。
- 帶頭與不帶頭:
- 帶頭鏈表有專門頭結點(不存數據),便于操作;
- 不帶頭鏈表首個結點即數據結點。
- 循環與不循環:
- 循環鏈表:最后結點指針指向頭結點(帶頭)或首結點(不帶頭);
- 不循環鏈表:最后結點指針為?
null
(單向)或后繼為?null
?且首結點前驅為?null
(雙向)。
2.單鏈表
2.1概念與結構
2.1.1結點
2.1.2鏈表的性質
1、鏈式機構在邏輯上是連續的,在物理結構上不?定連續2、結點?般是從堆上申請的3、從堆上申請來的空間,是按照?定策略分配出來的,每次申請的空間可能連續,可能不連續
struct SListNode {int data; //結點數據struct SListNode* next; //指針變量?保存下?個結點的地址
};
2.2單鏈表的實現
注意:以下函數的一些參數有一些是二級指針,不是一級指針,使用時要格外注意。使用二級指針是因為要改變實參(使用一級指針的話,形參的變化影響不了實參)。
2.2.1 SList.h
#pragma once#include<stdio.h>
#include<assert.h>
#include<stdlib.h>typedef int SLTDataType;typedef struct SListNode
{SLTDataType data; //結點數據struct SListNode* next; //指針保存下?個結點的地址
}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);//刪除pos結點
void SLTErase(SLTNode * *pphead, SLTNode * pos);//在指定位置之后插?數據
void SLTInsertAfter(SLTNode * pos, SLTDataType x);//刪除pos之后的結點
void SLTEraseAfter(SLTNode * pos);//銷毀鏈表
void SListDestroy(SLTNode * *pphead);
2.2.2 SList.c?
2.2.2.1創建結點,打印鏈表,銷毀鏈表
#define _CRT_SECURE_NO_WARNINGS#include "SList.h"// 測試函數:手動創建并遍歷一個單鏈表,最后釋放內存
void test1()
{// 手動創建4個節點并初始化數據(實際開發中建議封裝成函數)SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));n1->data = 1; // 節點1數據賦值為1SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));n2->data = 2; // 節點2數據賦值為2SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));n3->data = 3; // 節點3數據賦值為3SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));n4->data = 4; // 節點4數據賦值為4// 手動連接節點形成鏈表:1 -> 2 -> 3 -> 4 -> NULLn1->next = n2;n2->next = n3;n3->next = n4;n4->next = NULL; // 鏈表終止標志// 遍歷鏈表并打印數據SLTNode* pcur = n1; // 從鏈表頭節點開始遍歷while (pcur){printf("%d ", pcur->data); // 打印當前節點數據pcur = pcur->next; // 移動到下一個節點}printf("\n"); // 打印換行// 安全釋放鏈表內存(防止內存泄漏)SLTNode* current = n1; // 重新從頭節點開始while (current){SLTNode* temp = current; // 臨時保存當前節點地址current = current->next; // 先移動到下一個節點free(temp); // 釋放當前節點內存}// 注意:此時n1~n4已成為野指針,不可再訪問
}int main()
{test1(); // 執行測試函數return 0;
}
封裝函數:
1.創建結點:
SLTNode* CreateNode(int data)
{SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));if (node == NULL){perror("malloc");exit(EXIT_FAILURE);}node->data = data;node->next = NULL;return node;
}
2.打印鏈表
void SLTPrint(SLTNode* phead)
{assert(phead);SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
3.銷毀鏈表
void SListDestroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* current = *pphead;while (current){SLTNode* temp = current;current = current->next;free(temp);}*pphead = NULL;
}
?2.2.2.2尾部插入刪除
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* new_node = CreateNode(x);if (*pphead == NULL){*pphead = new_node;}else{SLTNode* pcur = *pphead;while (pcur->next){pcur = pcur->next;}pcur->next = new_node;}
}void SLTPopBack(SLTNode** pphead)
{assert(pphead && *pphead);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
2.2.2.3頭部插入刪除??
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* new_node = CreateNode(x);new_node->next = *pphead;*pphead = new_node;
}void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* tem = (*pphead)->next;free(*pphead);*pphead = tem;
}
2.2.2.3查找?
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* pcur = phead;while (pcur){if (pcur->data = x){return pcur;}pcur = pcur->next;}return NULL;
}
2.2.2.4在指定位置插入刪除
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);if (*pphead == pos){SLTPushFront(pphead,x);}else{SLTNode* new_node = CreateNode(x);SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}pcur->next = new_node;new_node->next = pos;}
}void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* new_node = CreateNode(x);new_node->next = pos->next;pos->next = new_node;
}void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);if (*pphead == pos){SLTPopFront(pphead);}else{SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;}
}void SLTEraseAfter(SLTNode* pos)
{assert(pos);SLTNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}
?3.雙向鏈表
3.1.概念與結構
帶頭雙向循環鏈表是一種具有特定結構與特性的線性數據結構。其每個節點包含三部分:數據域(存儲實際數據)、前驅指針(
?prev
,指向前驅節點)與后繼指針(next
,指向后繼節點),以此實現雙向鏈接。鏈表存在一個頭結點(通常為哨兵節點,不存儲有效數據),尾節點的?next
?指針指向頭結點,頭結點的 prev?指針指向尾節點,構成循環結構。從結構細節看:
?
- 頭結點(head):作為鏈表起始點,
next
?指向首個數據節點(如?d1
),prev?指向尾節點(如?d3
)。- 數據節點(如?
d1
、d2
、d3
):
d1
?的 prev?指向頭結點?head
,next
?指向?d2
;d2
?的 prev?指向?d1
,next
?指向?d3
;- 尾節點(
d3
):next
?指向頭結點?head
,prev?指向?d2
。
typedef int LTDataType;typedef struct listNode {LTDataType data;struct listNode* next;struct listNode* prev;
}LTNode;
?3.2雙向鏈表的實現
3.2.1List.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* next;struct listNode* prev;
}LTNode;LTNode* CreateNode(LTDataType x);//初始化
LTNode* LTInit2();
void LTInit1(LTNode** pphead);//打印鏈表
void LTPrint(LTNode* phead);//判斷鏈表是否為空
bool LTEmpty(LTNode* phead);//尾插及尾刪
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);//頭插及頭刪
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead, LTDataType x);//在pos位置之后插?數據
void LTInsert(LTNode* pos, LTDataType x);//刪除pos結點
void LTErase(LTNode* pos);//銷毀鏈表
void LTDestroy(LTNode* phead);
?3.2.2 List.c
3.2.2.1初始化鏈表,創建結點,打印鏈表,判斷鏈表是否無有效數據
LTNode* CreateNode(LTDataType x)
{LTNode* new_node = (LTNode*)malloc(sizeof(LTNode));if (new_node == NULL){perror("malloc");exit(1);}new_node->data = x;new_node->prev = new_node->next = new_node;return new_node;
}void LTInit1(LTNode** pphead)
{assert(pphead);*pphead = CreateNode(-1);
}LTNode* LTInit2()
{LTNode* phead = CreateNode(-1);return phead;
}void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d ", pcur->data);pcur = pcur->next;}printf("\n");
}bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}
3.2.2.2尾插及尾刪
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//phead->prev new_node pheadLTNode* new_node = CreateNode(x);new_node->next = phead;new_node->prev = phead->prev;phead->prev->next = new_node;phead->prev = new_node;
}void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//phead->prev->prev phead->prev pheadLTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);
}
尾插
?

尾刪
?
3.2.2.3頭插及頭刪
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* new_node = CreateNode(x);//phead new_node phead->nextnew_node->next = phead->next;new_node->prev = phead;phead->next->prev = new_node;phead->next = new_node;
}void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}

頭插

頭刪
?3.2.2.4查找,在指定位置插入及刪除
?
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//在pos位置之后插?數據
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* new_node = CreateNode(x);new_node->next = pos->next;new_node->prev = pos;pos->next->prev = new_node;pos->next = new_node;
}void LTErase(LTNode* pos)
{assert(pos);assert(!(pos->prev == pos && pos->next == pos));pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);
}

在指定位置之后插入
?

刪除指定位置結點
3.2.2.5銷毀鏈表
?
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* del = pcur;pcur = pcur->next;free(del);}free(phead);
}
4.順序表與鏈表比較?
不同點 | 順序表 | 鏈表(單鏈表) |
存儲空間上 | 物理上?定連續 | 邏輯上連續,但物理上不?定連續 |
隨機訪問 | ?持O(1) | 不?持:O(N) |
任意位置插?或者刪除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指針指向 |
插? | 動態順序表,空間不夠時需要擴容和空間浪費 | 沒有容量的概念,按需申請釋放,不存在空間浪費 |
應?場景 | 元素?效存儲+頻繁訪問 | 任意位置?效插?和刪除 |
?
?