鏈表結構深度解析:從單向無頭到雙向循環的實現全指南

上篇博客實現動態順序表時,我們會發現它存在許多弊端,如:

? 中間/頭部的插?刪除,時間復雜度為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)鏈表結構:

鏈表可從三個維度分類組合:

  1. 單向與雙向
    • 單向鏈表:結點僅含一個指向下一結點的指針;
    • 雙向鏈表:結點含前驅、后繼兩個指針。
  2. 帶頭與不帶頭
    • 帶頭鏈表有專門頭結點(不存數據),便于操作;
    • 不帶頭鏈表首個結點即數據結點。
  3. 循環與不循環
    • 循環鏈表:最后結點指針指向頭結點(帶頭)或首結點(不帶頭);
    • 不循環鏈表:最后結點指針為?null(單向)或后繼為?null?且首結點前驅為?null(雙向)。

雖然有這么多的鏈表的結構,但是我們實際中最常用還是兩種結構: 單鏈表雙向帶頭循環鏈表。
1. 無頭單向非循環鏈表:結構簡單,?般不會單獨用來存數據。實際中更多是作為其他數據結構的子結構,如哈希桶、圖的鄰接表等等。另外這種結構在筆試面試中出現很多。
2. 帶頭雙向循環鏈表:結構最復雜,?般用在單獨存儲數據。實際中使用的鏈表數據結構,都是帶頭雙向循環鏈表。另外這個結構雖然結構復雜,但是使用代碼實現以后會發現結構會帶來很多優勢,實現反而簡單了,后?我們代碼實現了就知道了。

2.單鏈表

2.1概念與結構

概念:鏈表是?種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。
舉個例子,這是一列火車:

淡季時?次的?廂會相應減少,旺季時?次的?廂會額外增加?節。只需要將???的某節?廂去掉/加上,不會影響其他?廂,每節?廂都是獨?存在的。
在鏈表?,每節“?廂”是什么樣的呢?

2.1.1結點

與順序表不同的是,鏈表?的每節"?廂"都是獨?申請下來的空間,我們稱之為“結點”
結點的組成主要有兩個部分:當前結點要保存的數據和保存下?個結點的地址(指針變量)。
圖中指針變量 plist保存的是第?個結點的地址,我們稱plist此時“指向”第?個結點,如果我們希望
plist“指向”第?個結點時,只需要修改plist保存的內容為0x0012FFA0。
鏈表中每個結點都是獨?申請的(即需要插?數據時才去申請?塊結點的空間),我們需要通過指針變量來保存下?個結點位置才能從當前結點找到下?個結點。

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)。
  • 數據節點(如?d1d2d3
    • d1?的 prev?指向頭結點?headnext?指向?d2
    • d2?的 prev?指向?d1next?指向?d3
  • 尾節點(d3next?指向頭結點?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)
只需修改指針指向
插?
動態順序表,空間不夠時需要擴容和空間浪費
沒有容量的概念,按需申請釋放,不存在空間浪費
應?場景
元素?效存儲+頻繁訪問
任意位置?效插?和刪除

?

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/80079.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/80079.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/80079.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

@PostConstruct @PreDestroy

PostConstruct 是 Java EE&#xff08;現 Jakarta EE&#xff09;中的一個注解&#xff0c;用于標記一個方法在對象初始化完成后立即執行。它在 Spring 框架、Java Web 應用等場景中廣泛使用&#xff0c;主要用于資源初始化、依賴注入完成后的配置等操作。 1. 基本作用 執行時…

【ArcGIS微課1000例】0146:將多個文件夾下的影像移動到一個目標文件夾(以Landscan數據為例)

本文講述將多個文件夾下的影像移動到一個目標文件夾,便于投影變換、裁剪等操作。 文章目錄 一、數據準備二、解壓操作三、批量移動四、查看效果五、ArcGIS操作一、數據準備 全球人口數據集Landscan2000-2023如下所示,每年數據位一個壓縮包: 二、解壓操作 首先將其解壓,方…

專業級 GIF 制作工具深度解析:Gifski 與 GIPHY CAPTURE 的技術對比與實戰指南

《Gifski 與 GIPHY CAPTURE&#xff1a;GIF 制作工具的深度對比與實戰應用》 最近在嘗試做一些培訓文檔&#xff0c;需要使用GIF圖做動態效果&#xff0c;把工具選型過程給大家做一下分享。 先看一張對比表&#xff0c;具體如下&#xff1a; 場景 Windows macOS Linux 移…

selenium替代----playwright

安裝 好處特點&#xff1a;這個東西不像selenium需要固定版本的驅動 pip config set global.index-url https://mirrors.aliyun.com/pypi/simplepip install --upgrade pippip install playwright playwright installplaywright install ffmpeg (處理音視頻的)驗證&#x…

Python代碼編程基礎

字符串 str.[]實現根據下標定位實現對元素的截取 for 循環可以實現遍歷 while 循環可以在實現遍歷的同時實現對某一下標數值的修改 字符串前加 r 可以實現對字符串的完整內容輸出 字符串前加 f 可以實現對字符串內{}中包裹內容的格式化輸出&#xff0c;僅在 v3.6 之后可用…

5月9號.

v-for: v-bind: v-if&v-show: v-model: v-on: Ajax: Axios: async&await: Vue生命周期: Maven: Maven坐標:

Spring 必會之微服務篇(1)

目錄 引入 單體架構 集群和分布式架構 微服務架構 挑戰 Spring Cloud 介紹 實現方案 Spring Cloud Alibaba 引入 單體架構 當我們剛開始學開發的時候&#xff0c;基本都是單體架構&#xff0c;就是把一個項目的所有業務的實現功能都打包在一個 war 包或者 Jar 包中。…

計算機的基本組成

#靈感# 記錄下基礎知識&#xff0c;此處專指計算機硬件方面&#xff0c;捎帶記下芯片知識。 綜述&#xff1a; 計算機硬件的基本組成包括運算器、控制器、存儲器、輸入設備和輸出設備五大部分。其中&#xff0c;集成在一起的運算器和控制器稱為 CPU&#xff08;處理器&#x…

【Python 列表(List)】

Python 中的列表&#xff08;List&#xff09;是最常用、最靈活的有序數據集合&#xff0c;支持動態增刪改查操作。以下是列表的核心知識點&#xff1a; 一、基礎特性 有序性&#xff1a;元素按插入順序存儲可變性&#xff1a;支持增刪改操作允許重復&#xff1a;可存儲重復元…

Qt 的原理及使用(1)——qt的背景及安裝

1. Qt 背景介紹 1.1 什么是 Qt Qt 是?個 跨平臺的 C 圖形??界?應?程序框架 。它為應?程序開發者提供了建?藝術級圖形 界?所需的所有功能。它是完全?向對象的&#xff0c;很容易擴展。Qt 為開發者提供了?種基于組件的開發模 式&#xff0c;開發者可以通過簡單的拖拽…

多分類問題softmax傳遞函數+交叉熵損失

在多分類問題中&#xff0c;Softmax 函數通常與交叉熵損失函數結合使用。 Softmax 函數 Softmax 函數是一種常用的激活函數&#xff0c;主要用于多分類問題中。它將一個實數向量轉換為概率分布&#xff0c;使得每個元素的值在 0 到 1 之間&#xff0c;且所有元素的和為 1。 …

數智讀書筆記系列032《統一星型模型--一種敏捷靈活的數據倉庫和分析設計方法》

引言 在當今數字化時代,數據倉庫作為企業數據管理的核心基礎設施,承擔著整合、存儲和提供企業數據的關鍵角色。隨著商業環境的快速變化和業務需求的日益復雜,數據倉庫的設計方法也在不斷演進,以適應新的挑戰和要求。 背景與意義 數據倉庫領域長期存在著兩種主流方法論之…

RT-Thread 深入系列 Part 1:RT-Thread 全景總覽

摘要&#xff1a; 本文將從 RTOS 演進、RT-Thread 的版本分支、內核架構、核心特性、社區與生態、以及典型產品應用等多維度&#xff0c;全面呈現 RT-Thread 的全景圖。 關鍵詞&#xff1a;RT-Thread、RTOS、微內核、組件化、軟件包管理、SMP 1. RTOS 演進與 RT-Thread 定位 2…

[docker基礎一]docker簡介

目錄 一 消除恐懼 1) 什么是虛擬化&#xff0c;容器化 2)案例 3)為什么需要虛擬化&#xff0c;容器化 二 虛擬化實現方式 1)應用程序執行環境分層 2)虛擬化常見類別 3)常見虛擬化實現 一&#xff09;主機虛擬化(虛擬機)實現 二&#xff09;容器虛擬化實現 一 消除恐…

PostgreSQL 的 pg_advisory_lock 函數

PostgreSQL 的 pg_advisory_lock 函數 pg_advisory_lock 是 PostgreSQL 提供的一種應用級鎖機制&#xff0c;它不鎖定具體的數據庫對象&#xff08;如表或行&#xff09;&#xff0c;而是通過數字鍵值來協調應用間的并發控制。 鎖的基本概念 PostgreSQL 提供兩種咨詢鎖(advi…

SGLang 實戰介紹 (張量并行 / Qwen3 30B MoE 架構部署)

一、技術背景 隨著大語言模型&#xff08;LLM&#xff09;的飛速發展&#xff0c;如何更高效、更靈活地駕馭這些強大的模型生成我們期望的內容&#xff0c;成為了開發者們面臨的重要課題。傳統的通過拼接字符串、管理復雜的狀態和調用 API 的方式&#xff0c;在處理復雜任務時…

微服務中 本地啟動 springboot 無法找到nacos配置 啟動報錯

1. 此處的環境變量需要匹配nacos中yml配置文件名的后綴 對于粗心的小伙伴在切換【測試】【開發】環境的nacos使用時會因為這里導致項目總是無法啟動成功

Lua從字符串動態構建函數

在 Lua 中&#xff0c;你可以通過 load 或 loadstring&#xff08;Lua 5.1&#xff09;函數從字符串動態構建函數。以下是一個示例&#xff1a; 示例 1&#xff1a;基本動態函數構建 -- 動態構建一個函數 local funcStr "return function(a, b) return a b end"-…

【Python】?Python單元測試框架unittest總結

1. 本期主題&#xff1a;Python單元測試框架unittest詳解 unittest是Python內置的單元測試框架&#xff0c;遵循Java JUnit的"測試驅動開發"&#xff08;TDD&#xff09;理念&#xff0c;通過繼承TestCase類實現測試用例的模塊化組織。本文聚焦于獨立測試腳本的編寫…

【Python 實戰】---- 使用Python批量將 .ncm 格式的音頻文件轉換為 .mp3 格式

1. 前言 .ncm 格式是網易云音樂專屬的加密音頻格式,用于保護版權。這種格式無法直接播放,需要解密后才能轉換為常見的音頻格式。本文將介紹如何使用 Python 批量將 .ncm 格式的音頻文件轉換為 .mp3 格式。 2. 安裝 ncmdump ncmdump 是一個專門用于解密 .ncm 文件的工具。它…