數據結構(C語言篇):(七)雙向鏈表

目錄

前言

一、概念與結構

二、雙向鏈表的實現

2.1? 頭文件的準備

2.2? 函數的實現

2.2.1? LTPushBack( )函數(尾插)

(1)LTBuyNode( )

(2)LTInit( )

(3)LTPrint( )

(4)LTPushBack( )

2.2.2? LTPushFront( )函數(頭插)

2.2.3? LTPopBack( )函數(尾刪)

2.2.4? LTPopFront( )函數(頭刪)

2.2.5? LTInsert( )函數(在pos位置之后插入數據)

2.2.6? LTErase( )函數(刪除pos位置的結點)

2.2.7? LTFind( )函數(查找結點)

2.2.8? LTDestroy( )函數(銷毀)

三、順序表與鏈表的分析

總結


前言

????????數據結構作為計算機科學的核心基礎之一,其高效性與靈活性直接影響程序性能。雙向鏈表以其獨特的雙指針結構脫穎而出,既繼承了單鏈表的動態內存管理優勢,又通過前驅指針實現了逆向遍歷與快速節點刪除。這種結構在操作系統內核、數據庫索引及LRU緩存淘汰算法等場景中展現關鍵價值。本文將深入剖析雙向鏈表的實現原理、時間復雜度權衡及典型應用場景,下面就讓我們正式開始吧!


一、概念與結構

? ? ? ? 如上圖所示,帶頭鏈表里的頭結點,實際為“哨兵位”,哨兵位結點不存儲任何有效元素,只是站在這里“放哨”的。

? ? ? ? 需要注意的是,這里的“帶頭”和前面博客中提到的“頭結點”是兩個概念,實際前面的在單鏈表階段稱呼是不嚴謹的,但是為了更好地幫助大家理解,我們才直接稱為單鏈表的頭結點。

二、雙向鏈表的實現

2.1? 頭文件的準備

typedef int LTDataType;
typedef struct ListNode
{struct ListNode* next; //指針保存下?個結點的地址struct ListNode* prev; //指針保存前?個結點的地址LTDataType data;
}LTNode;//void LTInit(LTNode** pphead);
LTNode* LTInit();
void LTDestroy(LTNode* phead);
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);
//在pos位置之后插?數據
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
LTNode *LTFind(LTNode* phead,LTDataType x);

2.2? 函數的實現

2.2.1? LTPushBack( )函數(尾插)

? ? ? ? 我們先來畫圖分析一下:

? ? ? ? 當然,在正式實現尾插函數之前,我們照舊還得先寫一下雙向鏈表的創建結點函數、鏈表初始化函數和鏈表打印函數 —— LTBuyNode( )、LTInit( )和LTPrint( ),如下所示:

(1)LTBuyNode( )

? ? ? ? 實現邏輯如下:

  1. 內存分配:為新節點分配內存空間

  2. 內存檢查:檢查內存分配是否成功

  3. 數據賦值:將數據存儲到新節點

  4. 指針初始化:將前驅和后繼指針都指向自己(循環鏈表特性)

? ? ? ? 完整代碼如下:

LTNode* LTBuyNode(LTDataType x) {// 1. 內存分配LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));// 2. 內存分配失敗檢查if (newnode == NULL) {perror("malloc fail!");  // 打印錯誤信息exit(1);                 // 退出程序}// 3. 數據賦值newnode->data = x;// 4. 指針初始化(雙向循環鏈表的關鍵)newnode->next = newnode->prev = newnode;return newnode;
}
(2)LTInit( )

? ? ? ? 該函數的實現邏輯如下:

  1. 創建哨兵節點:使用LTBuyNode函數創建特殊節點

  2. 返回鏈表頭:返回指向哨兵節點的指針

  3. 建立空鏈表:初始化一個標準的空雙向循環鏈表

? ? ? ? 完整代碼如下:

// 初始化雙向循環鏈表
LTNode* LTInit() {// 1. 創建哨兵節點,通常使用特殊值(如-1)標記LTNode* phead = LTBuyNode(-1);// 2. 返回哨兵節點作為鏈表頭return phead;
}
(3)LTPrint( )

? ? ? ? 該函數的實現邏輯如下:

  1. 遍歷鏈表:從第一個有效節點開始遍歷

  2. 打印數據:輸出每個節點的數據值

  3. 循環檢測:利用哨兵節點作為循環終止條件

  4. 格式化輸出:使用箭頭表示節點間的連接關系

? ? ? ? 完整代碼如下:

void LTPrint(LTNode* phead) {// 1. 從第一個有效節點開始(跳過哨兵節點)LTNode* pcur = phead->next;// 2. 遍歷鏈表,直到回到哨兵節點while (pcur != phead) {printf("%d -> ", pcur->data);  // 打印當前節點數據pcur = pcur->next;            // 移動到下一個節點}// 3. 打印換行,結束輸出printf("\n");
}
(4)LTPushBack( )

? ? ? ? 該函數的實現邏輯如下:

  1. 參數驗證:確保頭結點phead不為NULL。

    assert(phead);
  2. 創建新節點:使用LTBuyNode函數創建新結點,新結點包含數據x,prev和next指針初始化

    LTNode* newnode = LTBuyNode(x);
  3. 設置新結點的指針newnode->prev?指向原來的尾節點(即?phead->prev);newnode->next?指向頭節點?phead。

    newnode->prev = phead->prev;
    newnode->next = phead;
  4. 更新相鄰結點的指針:將原來的尾結點的next指向新結點,將頭結點的prev指向新結點(現在的新結點稱為新的尾結點)。

    phead->prev->next = newnode;
    phead->prev = newnode;

? ? ? ? ????????完整代碼如下:

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//phead phead->prev newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}

2.2.2? LTPushFront( )函數(頭插)

? ? ? ? 畫圖分析如下:

? ? ? ? 函數實現邏輯如下:

????????1.參數驗證:確保頭結點phead不為NULL。

????????2.創建新結點:調用LTBuyNode函數創建新結點。

????????3.設置新結點的指針:newnode->next?指向原來的第一個數據節點(即?phead->next);newnode->prev?指向頭節點?phead。

newnode->next = phead->next;
newnode->prev = phead;

????????4.更新相鄰結點的指針:將原來的第一個數據節點的?prev?指向新節點;將頭節點的?next?指向新節點(現在新節點成為新的第一個數據節點)。

phead->next->prev = newnode;
phead->next = newnode;

? ? ? ? 完整代碼如下:

//頭插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//phead newnode phead->nextnewnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}

2.2.3? LTPopBack( )函數(尾刪)

? ? ? ? 首先我們要先來實現一個判空函數LTEmpty():

bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}

? ? ? ? 下面來畫圖分析一下:

? ? ? ? 實現邏輯分析如下:

????????1.前置條件檢查:使用LTEmpty?函數檢查鏈表是否為空;如果鏈表為空(只有頭節點),則斷言失敗,不能刪除;確保鏈表至少有一個數據節點可刪除。

assert(!LTEmpty(phead));

? ? ? ??2.定位要刪除的結點:尾結點就是頭結點的?prev?指向的節點;將尾節點保存到?del?變量中。

LTNode* del = phead->prev;

????????3.更新指針連接:

  • del->prev->next = phead:將尾節點的前一個節點的?next?指向頭節點

  • phead->prev = del->prev:將頭節點的?prev?指向尾節點的前一個節點

del->prev->next = phead;
phead->prev = del->prev;

????????4.釋放內存:釋放被刪除結點的內存;將指針置為NULL,避免野指針。

free(del);
del = NULL;

? ? ? ? 完整代碼如下:

//尾刪
void LTPopBack(LTNode* phead)
{assert(!LTEmpty(phead));LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}

2.2.4? LTPopFront( )函數(頭刪)

? ? ? ? 畫圖分析如下:

? ? ? ? 函數實現邏輯如下:

????????1.前置條件檢查:使用?LTEmpty?函數檢查鏈表是否為空。

????????2.定位要刪除的結點:第一個數據節點就是頭結點的next指向的結點;將該結點保存到?del?變量中。

LTNode* del = phead->next;

????????3.更新指針連接:

  • del->next->prev = phead:將第二個數據節點的?prev?指向頭節點

  • phead->next = del->next:將頭節點的?next?指向第二個數據節點

  • 這樣就跳過了要刪除的第一個數據節點

    del->next->prev = phead;
    phead->next = del->next;

    4.釋放內存:釋放被刪除節點的內存;將指針置為?NULL,避免野指針。

? ? ? ? 完整代碼如下:

//頭刪
void LTPopFront(LTNode* phead)
{assert(!LTEmpty(phead));LTNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}

2.2.5? LTInsert( )函數(在pos位置之后插入數據)

? ? ? ? 畫圖分析如下:

? ? ? ? 實現邏輯:

????????1. 參數驗證:確保?pos?節點不為?NULL。

????????2.創建新結點:調用?LTBuyNode?函數創建新節點。

????????3.設置新結點的指針:

  • newnode->prev?指向?pos?節點(前驅節點)

  • newnode->next?指向?pos?節點原來的下一個節點

    newnode->prev = pos;
    newnode->next = pos->next;

    4.更新相鄰結點的指針:

  • 將?pos?節點原下一個節點的?prev?指向新節點

  • 將?pos?節點的?next?指向新節點

pos->next->prev = newnode;
pos->next = newnode;

? ? ? ? 完整代碼如下:

//在pos位置之后插?數據
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);//pos newnode pos->nextnewnode->prev = pos;newnode->next = pos->next;pos->next->prev = newnode;pos->next = newnode;
}

2.2.6? LTErase( )函數(刪除pos位置的結點)

? ? ? ? 先畫圖分析一下:

? ? ? ? 實現邏輯分析如下:

? ? ? ??1.參數驗證:確保?pos?節點不為?NULL。

? ? ? ? 2.更新指針連接(跳過要刪除的節點):

  • pos->prev->next = pos->next:將前驅節點的?next?指向后繼節點

  • pos->next->prev = pos->prev:將后繼節點的?prev?指向前驅節點

  • 這樣就完全跳過了要刪除的?pos?節點

    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;

    3.釋放內存:釋放被刪除節點的內存。

    free(pos);
    pos = NULL;

    完整代碼如下所示:

    //刪除pos位置的節點
    void LTErase(LTNode* pos)
    {assert(pos);//pos->prev pos pos->nextpos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
    }

    2.2.7? LTFind( )函數(查找結點)

? ? ? ? 實現邏輯如下:

????????1.參數驗證:確保頭節點?phead?不為?NULL

????????2.初始化遍歷指針:創建當前指針?pcur?并初始化為第一個數據節點(phead->next);跳過哨兵頭節點,從第一個數據節點開始遍歷。

LTNode* pcur = phead->next;

????????3.遍歷鏈表查找數據:循環條件?pcur != phead:當回到頭節點時停止(完成一圈遍歷);對每個數據節點檢查其?data?是否等于目標值?x;如果找到匹配的節點,立即返回該節點的指針。

while (pcur != phead)
{if (pcur->data == x){return pcur;}pcur = pcur->next;
}

????????4.未找到的情況:如果遍歷完所有數據節點都沒有找到匹配的節點;返回?NULL?表示查找失敗。

return NULL;

? ? ? ? 完整代碼如下:

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;
}

2.2.8? LTDestroy( )函數(銷毀)

? ? ? ? 畫圖分析如下:

? ? ? ? 函數實現邏輯:

????????1.初始化遍歷指針:創建當前指針?pcur?并初始化為第一個數據節點;從頭節點的下一個節點開始遍歷。

LTNode* pcur = phead->next;

????????2.遍歷并釋放所有數據結點:

  • 循環條件pcur != phead?—— 當回到頭節點時停止;

  • 保存下一個節點:在釋放當前節點前,先保存下一個節點的指針;

  • 釋放當前節點:使用?free()?釋放當前數據節點的內存;

  • 移動到下一個節點:將?pcur?指向之前保存的下一個節點。

while (pcur != phead)
{LTNode* next = pcur->next;free(pcur);pcur = next;
}

????????3.釋放頭結點:釋放頭節點(哨兵節點)的內存;將指針置為?NULL,避免野指針。

free(phead);
phead = NULL;

? ? ? ? 完整代碼如下:

//銷毀
void LTDesTroy(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}//銷毀頭結點free(phead);phead = NULL;
}

三、順序表與鏈表的分析

不同點順序表

鏈表(單鏈表)

存儲空間上物理上?定連續邏輯上連續,但物理上不?定連續
隨機訪問?持O(1)不?持:O(N)
任意位置插?或者刪除元素可能需要搬移元素,效率低O(N)只需修改指針指向
插?動態順序表,空間不夠時需要擴
容和空間浪費
沒有容量的概念,按需申請釋放,不存在
空間浪費
應?場景元素?效存儲+頻繁訪問任意位置?效插?和刪除

總結

? ? ? ? 以上就是本期博客的全部內容啦!本期我為大家介紹了雙向鏈表的實現邏輯以及順序表與鏈表的對比分析,希望能夠對大家學習數據結構有所幫助,謝謝大家的支持~!

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

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

相關文章

從拿起簡歷(resume)重新找工作開始聊起

經濟蕭條或經濟衰退在經濟相關學術上似乎有著嚴格的定義,我不知道我們的經濟是否已經走向了衰退或者蕭條,但有一點那是肯定的,那就現在我們的經濟肯定是不景氣的。經濟不景氣會怎么樣?是的,會有很多人失業,…

OS+MySQL+(其他)八股小記

魯迅先生曾經說過,每天進步一點點,媽媽夸我小天才。 依舊今日八股,這是我在多個文檔整合一起的,可能格式有些問題,請諒解。 操作系統 1.進程和線程的區別? 進程是代碼在數據集合的一次執行活動,…

Transformer的并行計算與長序列處理瓶頸總結

🌟 第0層:極簡版(30秒理解)一句話核心:Transformer像圓桌會議——所有人都能同時交流(并行優勢),但人越多會議越混亂(長序列瓶頸)。核心問題 并行優勢&#x…

Vue 3 useId 完全指南:生成唯一標識符的最佳實踐

📖 概述 useId() 是 Vue 3 中的一個組合式 API 函數,用于生成唯一的標識符。它確保在服務端渲染(SSR)和客戶端渲染之間生成一致的 ID,避免水合不匹配的問題。 🎯 基本概念 什么是 useId? useId…

CGroup 資源控制組 + Docker 網絡模式

1 CGroup 資源控制組1.1 為什么需要 CGroup - 容器本質 宿主機上一組進程 - 若無資源邊界,一個暴走容器即可拖垮整機 - CGroup 提供**內核級硬限制**,比 ulimit、nice 更可靠1.2 核心概念 3 件套 | 概念 | 一句話解釋 | 查看方式 | | Hierarchy | 樹…

【ArcGIS微課1000例】0150:如何根據地名獲取經緯度坐標

本文介紹了三種獲取地理坐標的方法:1)在ArcGIS Pro中通過搜索功能定位目標點(如月牙泉)并查看其WGS84坐標;2)使用ArcGIS內置工具獲取坐標;3)推薦三個在線工具(maplocation、地球在線、yanue)支持批量查詢和多地圖源坐標轉換。強調了使用WGS84坐標系以減少誤差,并展示…

HTML應用指南:利用GET請求獲取MSN財經股價數據并可視化

隨著數字化金融服務的不斷深化,及時、準確的財經信息已成為投資者決策與市場分析的重要支撐。MSN財經股價數據服務作為廣受信賴的金融信息平臺,依托微軟強大的技術架構與數據整合能力,持續為全球用戶提供全面、可靠的證券市場數據。平臺不僅提…

雅思聽力第四課:配對題核心技巧與詞匯深化

現在,請拿出劍橋真題,開始你的刻意練習! 內容大綱 課程核心目標舊題回顧與基礎鞏固配對題/匹配題核心解題策略考點總結與精聽訓練表 一、課程核心目標 掌握第二部分配對題的解題策略攻克第三部分匹配題的改寫難點系統整理高頻場景詞匯與特…

SQL Server從入門到項目實踐(超值版)讀書筆記 25

第12章 存儲過程的應用 🎉學習指引 存儲過程(Stored Procedure)是在大型數據庫系統中,一組為了完成特定功能的SQL語句集,存儲過程時數據庫中的一個重要對象,它代替了傳統的逐條執行SQL語句的方式。本章就來…

20.29 QLoRA適配器實戰:24GB顯卡輕松微調650億參數大模型

QLoRA適配器實戰:24GB顯卡輕松微調650億參數大模型 QLoRA 適配器配置深度解析 一、QLoRA 適配器核心原理 QLoRA 作為當前大模型微調領域的前沿技術,通過量化與低秩適配的協同設計,在保證模型效果的前提下實現了顯存占用的革命性降低。其核心由三大技術支柱構成: 4位量化…

QMainWindow使用QTabWidget添加多個QWidget

QTabWidget添加其它Wdiget的2個函數如下&#xff1a; QTabWidget的介紹可參考官網QTabWidget Class | Qt Widgets | Qt 6.9.1 直接上代碼&#xff0c;代碼如下&#xff1a; #include <QMainWindow>#include <QApplication> #include <QVBoxLayout> #includ…

AI學習機哪個好?選這幾款步步高就對了

隨著新教改政策的推進&#xff0c;教育對孩子的綜合素養提出了更高要求。英語更重聽說、數學更重思維&#xff0c;這讓許多家長在輔導孩子時感到壓力倍增。因此&#xff0c;如何選擇一款能真正幫助孩子提升能力的學習機&#xff0c;成為了大家普遍關心的問題。面對市場上功能各…

【設計模式】--重點知識點總結

題1 1、工廠和產品之間是依賴關系 2、工廠方法模式&#xff1a;工廠方法不能為靜態方法。如果是靜態方法&#xff0c;子類無法重寫行為。 簡單工廠可以用靜態方法 3、采用設計模式&#xff0c;以保證成功的設計和體系結構 4、建造者模式&#xff1a;&#xff08;1&#xf…

輕量實現 OCPP 1.6 JSON 協議(歐洲版)的充電樁調試平臺

1 項目概覽 1.1 目標與適用場景 1.1.1 簡介 本文介紹的開源項目 ocpp_charge&#xff0c;是一個 自研輕量實現 OCPP 1.6 JSON 協議&#xff08;歐洲版&#xff09; 的充電樁調試平臺。 它沒有依賴官方 OCPP 1.6J 庫&#xff0c;而是從零實現協議解析與會話管理&#xff0c;適…

Ubuntu 搭建 Solana 區塊鏈開發環境 + Anchor 智能合約完整教程

文章目錄簡介特征核心概念Solana 的工作原理&#xff08;簡單版&#xff09;為什么人們選擇 Solana開發環境準備Solana 官網Solana 文檔Anchor 文檔GithubRust SDK快速安裝 Solana&#xff08;推薦&#xff09;單獨安裝 Solana安裝依賴項安裝 Solana CLI安裝 Anchor CLI安裝 AV…

curl 介紹及使用教程

文章目錄 什么是 curl? 1. 解析用戶輸入與初始化 2. 建立網絡連接 3. 構建并發送請求 4. 接收并處理響應 5. 清理資源 核心特點總結 基本語法 常用功能及示例 1. 基本 HTTP 請求 2. 發送 GET 請求 3. 發送 POST 請求 4. 設置請求頭 5. 處理認證 6. 斷點續傳 7. 跟隨重定向 8. …

【第十一章】Python 隊列全方位解析:從基礎到實戰

Python 隊列全方位解析&#xff1a;從基礎到實戰 本文將從基礎概念到高級應用&#xff0c;用 “文字解釋 代碼示例 圖表對比 實戰案例” 的方式&#xff0c;全面覆蓋 Python 隊列知識&#xff0c;零基礎也能輕松掌握。 文章目錄Python 隊列全方位解析&#xff1a;從基礎到實…

跨平臺開發框架實測:React Native vs Flutter vs Kotlin Multiplatform

本文聚焦 React Native、Flutter 和 Kotlin Multiplatform 三大跨平臺開發框架&#xff0c;從性能表現、開發效率、生態系統、跨平臺一致性及學習成本五個關鍵維度展開實測對比。通過具體場景的測試數據與實際開發體驗&#xff0c;剖析各框架的優勢與短板&#xff0c;為開發者在…

【網弧軟著正版】2025最強軟著材料AI生成系統,基于GPT5.0

軟著材料AI一鍵生成系統 網址&#xff1a;AI軟著材料生成平臺 | 一鍵生成全套軟著文檔 - 網絡弧線 產品簡介&#xff1a; 專業的軟件著作權材料AI生成平臺&#xff0c;基于GPT-5模型開發&#xff0c;自2022年運營至今已服務數萬用戶成功獲得軟著證書。輸入軟件名稱即可自動生成…

存儲掉電強制拉庫引起ORA-01555和ORA-01189/ORA-01190故障處理---惜分飛

機房存儲突然掉電導致Oracle數據庫訪問存儲異常,數據庫報出大量的ORA-27072: File I/O error,Linux-x86_64 Error: 5: Input/output error,ORA-15081: failed to submit an I/O operation to a disk等錯誤,實例直接crash Wed Aug 27 07:11:53 2025 Errors in file /u01/app/ora…