數據結構入門 (二):掙脫連續空間的束縛 —— 單向鏈表詳解

@TOC(目錄)

引言:整齊的代價

在上一篇文章中,我們一起探索了數據結構大家族的第一位成員——順序表。我們了解到,順序表作為一種線性結構,其最大的特點在于邏輯順序與物理順序的一致性,即元素之間不僅存在邏輯上的前后關系,在物理存儲空間中也以連續的方式存放。

這種“整齊”帶來了顯而易見的好處,比如我們可以通過索引值快速定位到任何一個元素,就像報出房間號就能立刻找到人一樣。但是,凡事有利也有弊。這種對連續空間近乎苛刻的要求,也給它帶來了麻煩。

在這里插入圖片描述

圖1

如圖1,左邊是我們的內存空間,右邊是待存儲的順序表。雖然內存空閑的總空間綽綽有余,但我們卻找不到一塊足夠大且連續的區域來放置這個順序表。這就是順序表的局限性之一:對連續空間要求高,容易導致空間浪費

那么,有沒有一種方法,可以解決這個問題呢?我們很容易就能想到:如果我們能讓數據元素“見縫插針”,不考慮相鄰位置,哪里有空位就插一個,這樣不就能充分利用空間了嗎?然后用一根“線”把它們按邏輯串起來,不就能完美解決這個問題了嗎?

至此,我們已經初窺鏈表,相比起順序表,它的組織形式更加靈活、自由。

一、溫故知新:順序表的“雙刃劍”

在探討鏈表之前,我們再總結一下“順序表”的優缺點。

核心優點

  • 隨機訪問效率高: 存儲空間連續,可以通過索引直接定位元素,時間復雜度為O(1)
  • 緩存友好: 連續的內存布局對CPU緩存更友好,遍歷速度通常更快。

核心痛點

  • 操作成本高(時間維度): 為了維持物理上的連續性,在中間插入或刪除一個元素后,需要移動大量后續元素,時間復雜度為 O(n)。這就像隊伍中間插進一個人,后面所有人都得挪一步。且數據量越大,成本就越高。
  • 空間限制大(空間維度):
    • 必須預分配: 數組通常需要預先分配固定大小的空間。如果空間小了,需要經歷一個“申請新空間 -> 復制全部數據 -> 釋放舊空間”的昂貴擴容過程。
    • 容易浪費或失敗: 如果空間大了,則造成內存浪費。更糟糕的是,如引言所述,在內存碎片化的情況下,即使總空間足夠,也可能因找不到足夠大的連續塊而申請失敗。

二、鏈式存儲結構:一次聰明的“解耦”

既然順序表的“物理連續”帶來了諸多不便,我們何不換個思路?那便是將元素的‘邏輯順序’與它們的‘物理位置’徹底解耦。這正是鏈表設計的核心思想:它放棄了對物理連續的執著,轉而選擇利用物理上不連續的空間,來表示邏輯上的連續

1.鏈表的“細胞”——節點

與順序結構不同,鏈式結構中引入了它的基本單元——節點。每個節點除了要存儲數據元素信息外,還要存儲它的后繼元素的存儲地址。它的節點包含兩部分:

  1. 數據域:存儲數據元素信息。
  2. 指針域:存儲下一個節點的內存地址。

!圖二(一個方塊被一條直線分成兩部分,分別寫上data,next)

圖2.鏈表節點結構示例圖

這里給出鏈表節點結構的定義:

typedef int Element_t;typedef struct _node {Element_t val;  // 數據域struct _node *next;  // 指針域
} node_t;

2.”節點“成“鏈”

有了節點這個基本單元,我們就可以把它們串聯起來了。將所有節點鏈結成一個鏈表,這就是線性表的鏈式存儲結構,因為此鏈表的每個節點只包含一個指針域,所以叫做單鏈表

對于線性表來說,它一定有頭也有尾。一般而言,我們會設定一個特殊的指針,它永遠指向鏈表的第一個節點,我們叫它頭指針。有時為了方便,我們會在單鏈表的第一個節點前添加一個節點,稱為頭節點使用頭節點的情況下,頭指針指向的就是這個頭節點,而不是第一個實際存儲數據的節點。頭節點的指針域存儲指向第一個節點的指針,數據域可以不存儲信息,也可以存儲如線性表長度等附加信息。單鏈表的最后一個節點,指針域通常指向NULL或者None,也就是“空”。

!圖三(頭指針的鏈表)

圖3.帶頭節點的單向鏈表

帶頭指針的單向鏈表

圖4.帶頭指針的單向鏈表

3.頭指針 vs 頭結點

特性頭指針頭節點
本質一個指針變量一個實際的節點對象
空表示head == NULLhead->next == NULL
用途標識鏈表的起始位置簡化插入/刪除操作
插入/刪除對第一個節點的操作是特殊的,需改動頭指針本身所有位置的操作邏輯完全統一
優點節省一個節點的內存代碼邏輯更簡單、健壯,不易出錯
缺點增刪邏輯復雜,需特殊判斷增加了一個節點的內存開銷
是否必要

結論:在工程實踐中,為了代碼的簡潔和魯棒性,強烈推薦使用帶頭結點的鏈表。這點微小的空間犧牲是完全值得的。接下來的所有操作,我們都將基于帶頭結點的鏈表進行。

為了更好地管理鏈表,我們通常會再封裝一個結構體來代表整個鏈表:

// 定義鏈表頭結構
typedef struct {node_t head;  // 頭節點int count;  // 當前鏈表中有效數據節點的數量
} LinkList_t;

三、鏈表的基本功:核心操作詳解

現在,讓我們基于帶頭結點的鏈表 LinkList_t,來實現它的核心功能。

1.定義鏈表結構

鏈表的結構在第二部分已有說明,這里給出完整版:

typedef int Element_t;
// 1.定義鏈表節點結構
typedef struct _node {Element_t val;struct _node *next;
} node_t;// 2。定義鏈表頭結構
typedef struct {node_t head;int count;
} LinkList_t;

2.創建鏈表

首先,我們需要一個函數來創建一個空的鏈表。這包括為 LinkList_t 結構體分配內存,并初始化其內部的頭結點和計數器。

LinkList_t *createLinkList()
{// 1. 聲明一個指向LinkList_t的指針變量table,并初始化為NULL,防止野指針。LinkList_t *table = NULL;// 2. 在堆上申請一塊大小為`sizeof(LinkList_t)`的內存空間,將內存地址賦值給`table`。table = malloc(sizeof(LinkList_t));if (table == NULL)  {return NULL;   }// 3. 初始化鏈表結構體成員table->head.val = 0;table->head.next = NULL;table->count = 0;return table;
}

3.遍歷鏈表

由于鏈表物理上不連續,我們無法隨機訪問。唯一的辦法就是“順藤摸瓜”:從頭節點開始,沿著next指針一路走下去,直到遇到NULL

void showLinkList(const LinkList_t* table)
{node_t *p = table->head.next; // 頭節點的下一個節點,也就是鏈表中第一個有效數據節點的指針,將p初始化為這個節點,作為遍歷的起點printf("link list:%d\n",table->count);while (p) {printf("%d\n",p->val); // 訪問當前節點的數據p = p->next; // 更新p為后繼節點}printf("\n"); // 打印換行符,使輸出更整潔
}

4.插入節點

插入新節點有四個步驟:

  1. 引入一個輔助指針*pp找到待插入位置的前一個位置的節點。
  2. 創建新節點。
  3. 更新新節點。
  4. 最后更新老節點。

插入操作詳解

頭插入(時間復雜度 O(1))
  • 頭指針

    帶頭指針的單向鏈表的頭插入

圖5.帶頭指針的單向鏈表的頭插入示例圖
操作步驟:
  1. 新節點直接指向原頭節點 x1
  2. 更新頭指針 x1 指向新節點
new_node->next = x1;
x1 = new_node;
  • 頭節點

    帶頭節點的單向鏈表的頭插入

圖6.帶頭節點的單向鏈表的頭插入示例圖
操作步驟:
  1. 新節點指向頭節點的下一個節點
  2. 頭節點指向新節點
new_node->next = p->next;
p->next = new_node;

優勢: 頭插入是最高效的操作,時間復雜度恒為 O(1),無需遍歷鏈表

任意位置插入(時間復雜度 O(n))

鏈表任意位置插入示例圖

圖7.單向鏈表任意位置插入示例圖

這個指針修改動作本身是O(1)的,這正是鏈表效率高的原因。當然,找到這個p節點需要遍歷,耗時 O(n)。鏈表插入的精髓就在于這兩句指針操作:

new_node->next = p->next;
p->next = new_node;

這兩句代碼的順序是絕對不能調換的。

解讀這兩句代碼,可以看出是先把p的后繼節點改成新節點的后繼節點,再把新節點變成p的后繼節點。

如果把兩句代碼交換一下順序,會怎么樣?

第一句將p->next覆蓋成new_node的地址了。第二句new_node->next = p->next就相當于new_node->next = new_node,鏈表斷裂了。

尾插入(時間復雜度 O(n))

尾插入示例圖

圖7.單向鏈表尾插入示例圖
p->next = new_node;                   
new_node->next = NULL;   

C語言完整實現

頭插法
int insertLinkListHeader(LinkList_t* table, Element_t val)
{node_t *p = &table->head;  // p指針指向頭節點node_t *new_node = malloc(sizeof(node_t));  // 創建新節點if (new_node == NULL)return -1;new_node->val = val;new_node->next = p->next;p->next = new_node;++table->count;return 0;
}
任意位置插入
int insertLinkListPos (LinkList* table, int pos, Element_t val)
{// 1.判斷邊界值if (pos < 0 || pos > table->count){printf("insert invalid!\n");return -1;}// 2.找到插入位置的前驅節點node_t *p = &table->head;int index = -1;while (index < pos - 1){p = p->next;index++;}if (p == NULL) {return -1;}// 3.創建新節點node_t *new_node = malloc(sizeof(node_t));new_node->val = val;// 4.插入新節點new_node->next = p->next;p->next = new_node;// 5.更新鏈表長度table->count++;return 0;
}

5.按值刪除節點

當我們要刪除某個節點時:

  1. 引入輔助指針p,p找到待刪除位置的前一個位置

  2. 引入輔助指針備份待刪除位置tmp

tmp = p->next;
p->next = tmp->next;
free(tmp);

!圖

圖8.單向鏈刪除節點示例圖
int deleteLinkListElement(LinkList_t* table, Element_t val)
{node_t *p = &table->head;while (p->next){if (p->next->val == val){break;}p = p->next;}if (p->next == NULL){printf("Not Found!\n");return -1;}node_t *tmp = p->next;p->next = tmp->next;free(tmp);table->count--;return 0;
}

6.銷毀鏈表

由于鏈表是節點和節點串聯起來的,當銷毀鏈表時,也需要逐個節點釋放內存,最后再釋放鏈表管理結構體本身的內存。

void releaseLinkList(LinkList_t* table)
{if (table){// 循環刪除每個節點node_t *p = &table->head;node_t *tmp; // 臨時保存要釋放的節點while (p->next){tmp = p->next;p->next = tmp->next;free(tmp);--table->count;}printf("LinkList have %d node!\n",table->count);free(table);}
}

四、順序表 vs. 單向鏈表

特性順序表單向鏈表
存儲方式物理連續物理離散
訪問方式隨機訪問 (O(1))順序訪問 (O(n))
插入/刪除效率低,需移動元素 (O(n))效率高,只需修改指針 (O(1)) (不含查找)
空間管理預分配,易浪費或需擴容按需分配,靈活,但有指針額外開銷
適用場景數據量固定,頻繁查找,很少增刪數據量不固定,頻繁增刪,不關心隨機訪問

五、總結

今天,我們深入了解了單向鏈表。它通過犧牲隨機訪問的能力,換來了極其靈活的插入、刪除操作和高效的空間利用率。它與順序表并非“誰取代誰”的關系,不能簡單地說哪個好,哪個不好,需要根據實際情況來選擇。

掌握鏈表的關鍵,在于理解“指針”或“引用”如何將離散的內存串聯成一個邏輯整體。

但是,單向鏈表也并非完美。我們順著它只能一路向前,無法回頭,這在某些場景下非常不便。而且,它尾部節點的next指針永遠指向NULL,是不是有點“浪費”呢?

下一篇,我們將探索功能更強大的單向循環鏈表。同時,我們將嘗試僅使用頭指針來管理鏈表。

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

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

相關文章

AI-視頻一致性與多幀控制在AIGC中的技術挑戰與突破!

全文目錄&#xff1a;開篇語前言1. 視頻中人物一致性建模的難點與現有解決方案**人物一致性建模的挑戰****現有解決方案****案例代碼&#xff1a;基于姿態估計的多幀一致性保持**2. 光照/紋理/姿態跨幀保持方法剖析**跨幀光照與紋理一致性****跨幀姿態一致性**3. 幀間插值與關鍵…

基于Qwen2.5-3B-Instruct的LoRA微調與推理實戰指南

前言 大語言模型(LLM)的微調是當前AI領域的熱門話題&#xff0c;而參數高效微調方法(如LoRA)因其低成本和高效率備受關注。本文將手把手教你如何使用Qwen2.5-3B-Instruct模型進行LoRA微調&#xff0c;并構建完整的推理流程。 一、環境準備 1.1 硬件要求 ? GPU: 至少16GB顯存(如…

電腦插上u盤不顯示怎么回事

對于經常使用電腦的用戶來說&#xff0c;U盤是一種再熟悉不過的存儲工具。不管是拷貝資料、備份文件&#xff0c;還是制作啟動盤&#xff0c;U盤都發揮著重要作用。然而&#xff0c;有時候你可能會遇到這樣的情況&#xff1a;“U盤插上電腦&#xff0c;燈亮了&#xff0c;但電腦…

2025年6月GESP(C++二級): 冪和數

2025年6月GESP(C++二級): 冪和數 題目描述 對于正整數 n n n,如果 n n n 可以表為兩個

Windows、macOS、liunx下使用qemu搭建riscv64/linux

背景 在Windows、macOS和Linux環境下使用QEMU搭建RISC-V 64位Linux系統&#xff0c;網絡上存在大量過時、不完整或錯誤的教程。且部分AI生成的內容“幻覺”現象嚴重&#xff0c;導致關鍵步驟錯誤且難以進行。為確保可靠性&#xff0c;本教程基于最新實測驗證&#xff0c;涵蓋三…

簡單使用MCP

1、說明# 測試環境服務器 CPU數量&#xff1a;2核 內存&#xff1a;4GB 磁盤&#xff1a;50GB# 補充 如果不想使用Docker進行操作&#xff0c;只需要跳過Docker相關命令操作 即&#xff1a;使用Ollama運行模型&#xff0c;使用Python來創建MCP2、安裝Docker# 安裝Docker https:…

電腦裝機軟件一鍵安裝管理器

軟件使用 現在的裝機軟件很多&#xff0c;主要幾種類型就是辦公、看圖、影音、下載等&#xff0c;如果每次裝機之后&#xff0c;手動一個一個去安裝&#xff0c;費時費力還容易安裝到全家桶。 就有人整理了網絡上常用的一系列裝機軟件純凈和諧版本&#xff0c;并打包到一起&a…

深度學習入門-深度學習簡介

深度學習是加深了層的深度神經網絡。只需通過疊加層&#xff0c;就可以創建深度網絡。1、 加深網絡將深度學習中的重要技術&#xff08;構成神經網絡的各種層、學習時的有效技巧、對圖像特別有效的CNN、參數的最優化方法等&#xff09;匯總起來&#xff0c;創建一個深度網絡&am…

Linux 下安裝DM8數據庫詳細教程

Linux 下安裝DM8數據庫詳細教程 一、環境準備 1.操作系統要求 DM 數據庫支持多種操作系統,如 Windows、Linux 等。對于 Linux 系統,確保內核版本符合要求,例如 CentOS 7 或更高版本。同時,要保證系統有足夠的磁盤空間(建議至少 10GB 以上)和內存(至少 1GB 以上)。 對…

搭建基于Gitee文檔筆記自動發布

搭建基于Gitee文檔筆記自動發布由于現在gitee不支持代理靜態頁面&#xff0c;并且github.io需要VPN&#xff0c;實際使用的話gitee更為方便。一、為服務器和個人PC添加免密push和pull 參考鏈接&#xff1a;https://help.gitee.com/base/account/SSH%E5%85%AC%E9%92%A5%E8%AE%BE…

【Lua】閉包可能會導致的變量問題

先思考下面這個問題&#xff1a;local function counter()local count 0return function()count count 1return countend endlocal a counter() local b counter()print(a()) --> ? print(a()) --> ? print(b()) --> ? print(a()) --> ?輸出結果&#xff…

可觀測性、OpenTracing、OpenCensus、OpenTelemetry、Jaeger

監控與觀測 隨著軟件應用從單片架構向分布式微服務體系轉變&#xff0c;應用監控(Monitoring)和觀測(Observability)的需求也隨之提升。兩者存在相同的定義&#xff0c;目的都是為了發現應用程序中的問題。但還是有差別&#xff1a; 監控&#xff1a;目的是為了捕獲已知的問題…

Linux下使用原始socket收發數據包

在Linux系統中&#xff0c;使用非原始的socket&#xff0c;可以收發TCP或者UDP等網絡層數據包。如果要處理網絡層以下的數據包&#xff0c;比如ICMP、ARP等&#xff0c;或者更底層&#xff0c;比如鏈路層數據包&#xff0c;就得使用原始socket了。 創建socket 創建socket要使用…

暑期自學嵌入式——Day05補充(C語言階段)

接續上文&#xff1a;暑期自學嵌入式——Day05&#xff08;C語言階段&#xff09;-CSDN博客 主頁點關注不迷路喲。你的點贊、收藏&#xff0c;一鍵三連&#xff0c;是我持續更新的動力喲&#xff01;&#xff01;&#xff01; 主頁&#xff1a; 一位搞嵌入式的 genius-CSDN博…

.NET Core EFCore零基礎快速入門簡單使用

一、什么是 Entity Framework (EF) Core Entity Framework (EF) Core 是輕量化、可擴展和跨平臺版的對象關系映射程序 (O/RM)數據訪問技術&#xff0c;。 它將開發人員從編寫大量 SQL 語句中解放出來。 二、EF的相關程序包 Microsoft.EntityFrameworkCore 核心程序包&#x…

AAC音頻格式

目錄 AAC音頻格式介紹 主要特點 技術優勢 常見文件擴展名 應用領域 AAC與PCM的區別與優勢對比 基本概念差異 主要技術區別 各自優勢 PCM的優勢 AAC的優勢 應用場景選擇 AAC音頻數據格式解析 1. AAC 文件格式 (1) ADIF (Audio Data Interchange Format) (2) ADT…

pom.xml文件中的${}變量從哪里傳值

在 Maven 的 pom.xml 文件中&#xff0c;${} 格式的變量&#xff08;稱為屬性占位符&#xff09;的值來源主要有以下幾種途徑&#xff1a; 1. ?內置屬性&#xff08;Maven 預定義&#xff09;?? ${project.basedir}&#xff1a;項目根目錄${project.version}&#xff1a;項…

【人工智能】項目案例分析:使用TensorFlow進行大規模對象檢測

????歡迎大家來到我們的天空???? ?? 作者簡介:我們的天空 ??《頭銜》:大廠高級軟件測試工程師,阿里云開發者社區專家博主,CSDN人工智能領域新星創作者。 ??《博客》:人工智能,深度學習,機器學習,python,自然語言處理,AIGC等分享。 所屬的專欄:TensorF…

C++---cout、cerr、clog

在C編程里&#xff0c;cout、cerr和clog是標準庫提供的重要輸出流對象&#xff0c;在數據輸出方面發揮著關鍵作用。 一、cout&#xff1a;標準輸出流 cout 是 std::ostream 類的對象&#xff0c;其作用是向標準輸出設備&#xff08;一般是控制臺&#xff09;輸出數據。它和 C 語…

脈沖神經網絡(Spiking Neural Network, SNN)與知識蒸餾(Knowledge Distillation, KD)

目錄 脈沖神經網絡&#xff08;Spiking Neural Network, SNN&#xff09; 知識蒸餾&#xff08;Knowledge Distillation, KD&#xff09; 三種類別 三種變體 脈沖神經網絡&#xff08;Spiking Neural Network, SNN&#xff09; 收到生物神經系統的啟發&#xff0c;設計的&a…