數據結構(3)線性表-鏈表-單鏈表

我們學習過順序表時,一旦對頭部或中間的數據進行處理,由于物理結構的連續性,為了不覆蓋,都得移,就導致時間復雜度為O(n),還有一個潛在的問題就是擴容,假如我們擴容前是100個大小的元素空間,一旦擴容以后(我們就說二倍的擴),那就是200,但我們實際上就只存105個元素,那不就有95個元素所對應的空間被浪費了嗎?怎樣才能解決這樣的問題呢?

帶著疑問,來學習線性表的其中之一,鏈表。

一、鏈表

鏈表是物理存儲結構上非連續的線性表,數據元素的邏輯順序是通過鏈表中的指針來實現的。

按照指針方向鏈表分為單向鏈表,雙向列表和循環鏈表。

按內存管理的話還是靜態鏈表和動態鏈表

二、單鏈表

本次就進行單鏈表的學習。

1.單鏈表概述

單鏈表一般作Single List

單鏈表是什么東西呢?

單鏈表就好像是火車一樣,火車的每個單位是車廂,車頭是動力,車廂與車廂之間存在有結構連接。

為什么要這么對比呢?

單鏈表的基本單位就是結點,結點與結點之間由指針連接起來,這是靜態的看;火車在淡旺季可以選擇加車廂和減車廂,而我們的單鏈表對應的操作就是增加或者刪除結點。

單鏈表屬于鏈表,根據指針的方向取名單鏈表,單鏈表結點間的指針方向是只有一個的。

我們在申請順序表的大小的時候用的是realloc,第一個參數是需要修改的內存的地址,第二個參數是改為的內存空間的大小,這個內存空間我們申請的時候直接是一整塊全部申請下來,但是對于單鏈表則不同,如上面畫的圖,我們每次申請都僅僅申請一個結點,任意取出來邏輯上相鄰的兩個結點:

現在視角落在存儲了整型2的這個結點上,這個結點存儲了兩個東西,一個是數據,一個是下一個結點的地址,由此我們的單鏈表的結點就是一個結構體變量,一個是存儲的數據,一個是結點結構體類型的指針(因為通過這個指針訪問下一個結點時,還是一個結點結構體,所以必須是結點結構體類型的指針變量)。

2.鏈表的性質

由這個簡略圖給出鏈表的通用特性:

①鏈表在邏輯上是連續的,物理空間上不一定連續

②結點一般是在堆上申請的

③從堆上申請的空間不一定連續

了解即可。

3.單鏈表結點結構的創建

非常清晰了,直接給出來:

為了單鏈表存儲數據類型的可修改性,一個typedef,為了后續與結點這個結構體操作的簡便性,再來一個typedef,這點在順序表中已經見過了,不多解釋。

不管是先前的圖片還是后續的創建,都展示出了結點的兩個屬性,一般存放數據的被稱作數據域,存放下一個結點地址的叫做指針域。

4.單鏈表的打印

有了單鏈表的結點,如果要實現我們所畫的圖片的效果應該是這樣的:

為每一個結點創建內存空間,并且賦值,實際上發現,我們邏輯上的1,2,3,4是通過指針來實現的。

如果對于這樣的一個鏈表,我們該如何實現遍歷打印呢?

通過plist開始訪問鏈表的數據,進去打印該結點的數據,并且將指針該為下一個結點的地址,最終下一個結點的next指針為空則遍歷完成。

進去就打印,并且跳到下一個鏈表結點里,如果不為空則繼續打印,為空說明已經遍歷到尾,可以停止。

當然,其實可以把assert換一下,因為如果鏈表為空,直接打印個NULL,也可以:

5.單鏈表的插入結點操作

①單鏈表的尾插

先想尾插的參數,肯定得把單鏈表的地址傳過去吧,然后你插什么總得告訴我吧。

所以就倆參數,一個是單鏈表的地址(如上面的plist),還有想要插入的元素是什么。

進去第一件事可不是去插入,就像順序表插入一樣,你總得看看有沒有地方讓你插入吧,類比過來,就得先生成個結點,這個結點專門用來存放本次插入的值,指針的話由于尾插,所以肯定是NULL。

有了這個結點以后就得把它像連接火車車廂一樣接上去,很顯然,需要先找到鏈表尾,其實我們打印的時候用的while循環結束后,指針已經到了單鏈表的尾了,所以拿過來即可,找到尾以后把原來的尾的指針從NULL變成新創建的結點的地址即可。

思路有了,代碼表達:

逢開辟必檢查。

而鏈表的插入分為空鏈表的尾插和非空鏈表的尾插,至于為什么,先往下看:

非空:

拿著鏈表地址,遍歷到最后,把創建的新結點地址賦給尾結點即可。

但是如果是空鏈表呢:

你非空確實傳過來地址,我順著地址修改值就行,但是如果是空的鏈表呢?你傳過來的是什么,是NULL,那你再修改,對實際的鏈表根本就是無濟于事啊。所以為了對真正的鏈表的里面的值進行修改,我們不能傳值了,只能傳過來這個空鏈表的指針的地址(為了通過這個地址去修改這個指針)

地址的地址,或者說一級指針的地址應該用二級指針來承接,所以形參寫的時候應該是二級指針,修改為:

其中,鏈表不為空需要遍歷到鏈表尾部的地址,因為后續需要用這個地址去修改NULL,這樣的話到尾結點就該停,而尾結點與其他結點的不同就是下一個結點的地址為空。

測試代碼如下:

說明確實插入了。

當然,還得防范一下傳過來的指針,畢竟plist可以為空(即鏈表為空),但是pplist存的可是鏈表的地址的啊,這玩意為空算哪門子事:

加個assert防范一下。

②單鏈表的頭插

還是有了經驗以后,參數還是pplist,并且分開鏈表為空和鏈表不為空的情況:

首先還是不為空:

讓plist指向我們的newnode,newnode指向我們的第一個結點,這樣的話賦值就得先把plist的值賦給newnod->next了,然后再把plist的值修改了:

而且發現,如果鏈表為空,plist為NULL,賦過去,newnode地址給plist,也沒毛病。

調試完畢,沒啥問題

6.單鏈表的刪除結點操作

①單鏈表的尾刪

釋放最后一塊空間,并將尾結點的前一個結點的next指針改成NULL。

這就要求你必須找到兩個位置的指針,一個是ptail,一個是ptail前的prev,我們以這樣的思路來想辦法遍歷,示意圖:

ptail開始指向第一個元素,prev由于必須在ptail前,此時應該為空;

進入檢測以后看看這個結點是不是尾結點,是的話就該停止遍歷了,即判斷條件為ptail->next != NULL。

問題就在于循環體如何實現ptail往后走的同時來讓prev在ptail前,很簡單,可以說是keep up with:

如果不是尾結點的話,prev先站到現在ptail的位置,因為下一步就是后移,這樣出了循環就是一個在前一個在后的效果,如:

當然不能忘了釋放空間和防范野指針:

最后三條解釋一下,prev->next = NULL確實是修改了鏈表的實際結構,因為prev是尾結點前那個結點的地址,順著地址肯定能修改成功實際的鏈表;有了尾結點地址,free不用多解釋;重點在于ptail = NULL,它可不是真的把尾結點指針置空了,或者說它即使置空也不影響實際鏈表中的地址還存在(如果存了的話),只是free以后習慣置空,作用是防止在函數內部再被調用。

畫圖解釋:

正如我們插入操作的時候需要防范鏈表是不是空鏈表一樣,對應過來就是,我們刪除的恰好就是唯一的一個結點,即第一個結點,代碼思路還行不行得通:

就盯著這個看,很明顯,上來就是NULL,這么一搞,prev直接就是隨機值。

所以如果只有一個結點的話,直接free并且賦plist為空即可(當然,在函數里是*pplist)。

完善:

測試:

兩端代碼都成功。

②單鏈表的頭刪

頭刪其實沒什么可多說的,把plist修改為原plist->next,再對原plist的空間釋放即可:

測試代碼:

沒啥好講的,連圖都不用畫,空想一下就能想出來。

7.單鏈表的查找操作

參數:你想查哪個鏈表,地址給我,你想查鏈表里哪個元素,元素也給我,所以就倆參數,而且不會對鏈表的結構產生改變,傳值即;上面都沒說返回值的事,因為不管是打印還是對鏈表結構的改變,不需要返回什么,但是查找,你找到了給我個地址讓我知道啊,找不到你總得告訴我找不到吧。

查找我寫了兩個版本:
第一個版本是這樣的:

最開始實際上我寫的是:

while (phead->data != x)
{
?? ?phead = phead->next;
}

if(phead == NULL)

? ? ? ? return NULL;

else

? ? ? ? return phead;

思路就是,要是這個結點的值不是我要的值,就往死里給我遍歷,跳出循環以后有倆情況,一個就是為空了都,那說明找完都沒找到,直接返回NULL就行;一個就是找到了,跳出循環即可。

問題有倆,一個是遍歷到尾了,phead就是NULL,再解引用去判斷data與x相等不相等就不禮貌了,再來就是其實我寫的if-else語句其實就是return phead的意思,(我剛開始就意識到了if-else的化簡,絲毫沒有發現第一個問題,最后代碼報錯檢查了檢查才發現)

第一次檢查修改為:

while (phead->data != x)
{
?? ?phead = phead->next;
}

? ? ? ? return phead;

然后發現空指針解引用問題:

while (phead->data != x && phead != NULL)
{
?? ?phead = phead->next;
}

? ? ? ? return phead;

加上了還給我整事,后來想了想,這就是當時學邏輯運算符的短路問題,如果遍歷完都找不到你先解引用才去檢驗是不是空,這樣代碼當然還是會報錯,所以還得:

while (phead != NULL && phead->data != x)
{
?? ?phead = phead->next;
}

? ? ? ? return phead;

才沒毛病。

第二個版本是這樣的:

while循環瘋狂遍歷,每次遍歷到的結點去檢測一下data,如果遍歷完都沒找到,直接返回NULL。

這個版本寫出來其實是因為想放棄第一個版本了,但是又因為自己不服氣,就有了上面的修改過程。

8.單鏈表指定位置的插入和刪除操作

①指定位置之前插入數據

指定位置的插入和刪除,肯定是在某個特定的值前才進行,所以鏈表在這種情況下不會為空

這點意味著要在3這個結點前插結點,創建新結點就不說了,來看指針怎么變,這種情況下傳的肯定是鏈表中某個節點的地址,這樣看來的話newnode->next不成問題,但是前面一個結點的next指針可就炸缸了啊,因為你給我的是3的指針,給了這個我往后遍歷順著走就行,往前遍歷可就得想想辦法了,還是得寫一循環遍歷,當該指針next為3這個結點就停手,給目標節點起名pos,給目標節點前一個結點起名prev(取自previous)。

準備工作:

準備好結點,準備好要改變的值以后,就是賦值

最后:

但有一特例,如果指定位置前是鏈表的頭呢:

這個時候的prev->next永遠不會是pos,就會無休止的找下去,這樣的話會導致出錯,干脆如果plist==pos,直接調頭插去,懶得再寫邏輯了,反正也跟寫頭插一樣:

測試代碼:

不管頭插還是中間的插入都沒啥大毛病。

②指定位置之后插入數據

注意參數即可,因為鏈表的結點的成員是一個data,一個next指針,所以往后插根本不需要指定位置的前一結點的地址,也就不需要鏈表的頭結點地址。

先按照正常元素往中間插的思路寫:

newnode->next指向pos->next,pos->next指向newnode即可,一定要注意順序。

newnode->next = pos ->next;

pos->next = newnode;

pos->next = newnode;

newnode ->next= pos ->next;

真按照下面的干了,直接炸了,因為pos->next直接被你用newnode覆蓋了,導致第二句代碼newnode->next指的還是newnode自己。

直接寫出來就行,但是插入還得小心點(空就不用考慮了,肯定是查找后的指針,不可能為空,即有pos就不用驗空),想想第一個結點插入,由于是之后,那跟找中間的插也沒啥區別;想想尾結點插入:

對著代碼看,也沒啥毛病。

測試一下:

沒犯啥毛病,成功了。

③刪除指定位置的結點

一個遍歷到pos前,一個賦值改指針指向,一個釋放:

然后就是看看首或者尾會不會出幺蛾子:

上來就發現了,我們的邏輯是prev->next是不是pos,根本無法檢測pos剛好為頭的情況,所以還是寫個if-else去頭刪:

尾結點沒炸。

測試:

④刪除指定位置后的結點

還是說,指定位置后可以直接通過pos找到,就不需要傳plist了。

不想寫那么多next就將要刪去的結點的地址存到del里,方便自己看以及方便改指針和釋放:

這么寫增加代碼可讀性。

想想頭尾會不會出啥問題,頭的話頭后刪,跟我們從中間找一個結點刪效果一樣;尾結點后面沒結點,想了想指定位置后結點刪去不能有尾結點,所以加到assert里:

測試:

9.單鏈表的銷毀

銷毀單鏈表就把頭結點的地址傳過來。

很明顯,如果直接free掉plist那么后面的結點全部都丟了,所以在free結點前記錄下一個結點,直到下一個結點為NULL再停止:

三、對比順序表和單鏈表

剛剛學完,可謂是手感火熱,順序表和單鏈表都有插入和刪除操作。

其中順序表的頭插頭刪由于其空間的連續性,所以必須先將已有元素后移,才能插入,后移就會用一個循環,時間復雜度是O(n);

順序表的尾插尾刪,不需要移動數據,所以也不需要循環,插就一句根據地址賦值的事,尾刪更簡單,直接size--即可。時間復雜度是O(1)

單鏈表的頭插頭刪,頭插由于不需要將其他元素后移,直接針對鏈表頭結點所給指針改變一下next指針;頭刪就先改頭結點指向地址,再free即可。

單鏈表的尾插尾刪,最影響時間復雜度的就是while循環去找尾結點的指針ptail,導致時間復雜度為O(n)。

我們現在就學了這兩種數據結構,已經可以看出來,沒有哪一種數據結構就能一招鮮吃遍天,各有各的優點,你尾部頻繁的增刪改,那就用順序表,頭部頻繁增刪改那就單鏈表。

解釋這個就是為了解釋我最開始所說的,學習各種各樣的數據結構,是為了算法里選擇最優的數據結構。

另外,單鏈表由于每個結點都是在堆區申請獨立的空間,所以不會存在內存浪費的情況,這種優勢是針對順序表來說的,因為在順序表里有一開辟內存的函數叫CheckCapacity,二倍擴容空間如果不用,那就會浪費。

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

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

相關文章

【Unity】DOTween的常用函數解釋

DOTween插件常用函數解釋 1.DOTween.To(通用變化動畫) 解釋:將某一個值在一定的時間內變化到另一個值(通用的函數),可用于大部分的動畫變化 使用示例: using UnityEngine; using DG.Tweenin…

數據結構測試模擬題(1)

1、約瑟夫問題 #include<bits/stdc.h> using namespace std; const int N25; int e[N],ne[N],head-1,idx1; int n,m; void add_to_head(int x){e[idx]x;ne[idx]head;headidx; } void add(int k,int x){e[idx]x;ne[idx]ne[k];ne[k]idx; } int main(){cin>>n>>…

Helm配置之為特定Deployment配置特定Docker倉庫(覆蓋全局配置)

文章目錄 Helm配置之為特定Deployment配置特定Docker倉庫(覆蓋全局配置)需求方法1:使用Helm覆蓋值方法2: 在Lens中臨時修改Deployment配置步驟 1: 創建 Docker Registry Secret步驟 2: 在 Deployment 中引用 Secret參考資料Helm配置之為特定Deployment配置特定Docker倉庫(覆…

BERT 作為Transformer的Encoder 為什么采用可學習的位置編碼

摘要 BERT 在位置編碼上與原始 Transformer 論文中的 sin/cos 公式不同&#xff0c;選擇了可學習&#xff08;learned&#xff09;的位置嵌入方案。本文將從 Transformer 原始位置編碼選項入手&#xff0c;分析 BERT 選擇 learned positional embeddings 的四大核心原因&#x…

【Linux 學習計劃】-- gcc、g++、動靜態庫鏈接

目錄 什么是gcc、g gcc、g 相關操作詳解 預處理、編譯、匯編、鏈接來源 動靜態鏈接是什么 結語 什么是gcc、g gcc、g其實就是編譯器&#xff0c;是幫助我們從.c或者.cc&#xff0c;.cpp文件編譯成可執行程序的 其中&#xff0c;我們如果要編譯c語言文件的話&#xff0c;…

前端讀取本地項目中 public/a.xlsx 文件中的數據 vue3

前端讀取本地項目中 public/a.xlsx 文件中的數據 vue3 項目中需要在 Vue3 項目中讀取 public/a.xlsx 文件&#xff0c;可以使用 fetch API 來獲取文件內容 一、安裝 xlsx 首先&#xff0c;你需要安裝 xlsx 庫&#xff1a; npm install xlsx二、在需要用的頁面里引入xlsx im…

MySQL:to many connections連接數過多

當你遇到 MySQL: Too many connections 錯誤時&#xff0c;意味著當前連接數已達到 MySQL 配置的最大限制。這通常是由于并發連接過多或連接未正確關閉導致的。 一、查看當前連接數 查看 MySQL 當前允許的最大連接數 SHOW VARIABLES LIKE max_connections;查看當前使用的最大…

2024年熱門AI趨勢及回顧

人工智能的崛起 2024 年可能會被銘記為人工智能不再是一種技術新奇事物&#xff0c;而是成為現實的一年。微軟、Salesforce 和 Intuit 等巨頭將人工智能融入主流企業解決方案&#xff1b;從文案寫作到數據分析&#xff0c;專門的人工智能應用程序和服務如雨后春筍般涌現&#…

LangFlow技術深度解析:可視化編排LangChain應用的新范式 -(2)流編輯器系統

Flow Editor System | langflow-ai/langflow | DeepWiki 流編輯器系統 相關源文件 流編輯器系統是 Langflow 的核心交互式組件&#xff0c;允許用戶直觀地創建、編輯和管理 LLM 驅動的應用程序。它提供了一個直觀的畫布&#xff0c;用戶可以在其中添加節點、將其與邊緣連接并…

驅動-定時-秒-字符設備

文章目錄 目的相關資料參考實驗驅動程序-timer_dev.c編譯文件-Makefile測試程序-timer.c分析 加載驅動-運行測試程序總結 目的 通過定時器timer_list、字符設備、規避競爭關系-原子操作&#xff0c;綜合運用 實現一個程序&#xff0c;加深之前知識的理解。 實現字符設備驅動框…

[Java實戰]Spring Boot整合Kafka:高吞吐量消息系統實戰(二十七)

[Java實戰]Spring Boot整合Kafka&#xff1a;高吞吐量消息系統實戰&#xff08;二十七&#xff09; 一、引言 Apache Kafka作為一款高吞吐量、低延遲的分布式消息隊列系統&#xff0c;廣泛應用于實時數據處理、日志收集和事件驅動架構。結合Spring Boot的自動化配置能力&…

Kotlin Multiplatform--04:經驗總結(持續更新)

Kotlin Multiplatform--04&#xff1a;經驗總結&#xff08;持續更新&#xff09; 引言 引言 本章用來記載筆者開發過程中的一些經驗總結 一、Ktor設置Header 在官方文檔中&#xff0c;想要設置Header的示例代碼如下&#xff1a; client.get("https://ktor.io&qu…

在 Ubuntu 系統中,將 JAR 包安裝為服務

在 Ubuntu 系統中&#xff0c;將 JAR 包安裝為服務可以通過 systemd 來實現。以下是詳細的操作步驟&#xff1a; 準備工作 確保 JAR 文件路徑和 Java 運行時環境已準備好。驗證 Java 是否可用&#xff1a; java -version創建 systemd 服務文件 systemd 的服務文件通常位于 …

電商項目-商品微服務-品牌管理微服務開發

一、功能分析 品牌管理微服務包括&#xff1a; &#xff08;1&#xff09;查詢全部列表數據 &#xff08;2&#xff09;根據ID查詢實體數據 &#xff08;3&#xff09;增加 &#xff08;4&#xff09;修改 &#xff08;5&#xff09;刪除 &#xff08;6&#xff09;分頁…

Spring Boot開發—— 整合Lucene構建輕量級毫秒級響應的全文檢索引擎

文章目錄 一、為什么選擇 Lucene?輕量級搜索的底層密碼二、核心原理:Lucene 的倒排索引2.1 倒排索引:速度之源2.2 段合并優化策略三、Spring Boot集成Lucene實戰3.1 依賴配置3.2 實體與索引設計3.3 核心索引服務(含異常處理)3.4 使用示例(測試類)四、高級優化技巧4.1 索…

SpringBootDay1|面試題

目錄 一、springboot框架 1、什么是springboot 2、Spring Boot的主要優點 3、springboot核心注解 4、定義banner&#xff08;springboot的logo&#xff09; 5、springboot配置文件 6、springboot 整合 jdbc 二、面試題 1&#xff09;springmvc的作用 ?編輯 2&#x…

jQuery Ajax中dataType 和 content-type 參數的作用詳解

jQuery Ajax中dataType與contentType參數解析 一、核心概念對比 參數作用對象數據類型默認值dataType響應數據預期接收的數據格式jQuery自動判斷&#xff08;根據響應頭MIME類型&#xff09;contentType請求數據發送數據的編碼格式application/x-www-form-urlencoded 二、da…

幾款常用的虛擬串口模擬器

幾款常用的虛擬串口模擬器&#xff08;Virtual Serial Port Emulator&#xff09;&#xff0c;適用于 Windows 系統&#xff0c;可用于開發和調試串口通信應用&#xff1a; 1. com0com (開源免費) 特點&#xff1a; 完全開源免費&#xff0c;無功能限制。 可創建多個虛擬串口…

LLM筆記(六)線性代數

公式速查表 1. 向量與矩陣&#xff1a;表示、轉換與知識存儲的基礎 向量表示 (Vectors): 語義的載體 在LLM中&#xff0c;向量 x ∈ R d \mathbf{x}\in\mathbb{R}^d x∈Rd 是信息的基本單元&#xff0c;承載著豐富的語義信息&#xff1a; 詞嵌入向量 (Word Embeddings)&am…

[特殊字符] Word2Vec:將詞映射到高維空間,它到底能解決什么問題?

一、在 Word2Vec 之前,我們怎么處理語言? 在 Word2Vec 出現之前,自然語言處理更多是“工程方法”,例如字符串匹配、關鍵詞提取、正則規則...。但這些表示通常缺乏語義,詞與詞之間看不出任何聯系以及非常淺顯。當然,技術沒有好壞,只有適合的場景。例如: 關鍵詞匹配非常…