02-1 鏈表

數(shù)據(jù)結(jié)構(gòu)與算法之美 - 學(xué)習(xí)筆記

鏈表(Linked list)

鏈表是一種物理存儲(chǔ)單元上非連續(xù)、非順序的存儲(chǔ)結(jié)構(gòu),數(shù)據(jù)元素的邏輯順序是通過(guò)鏈表中指針鏈接次序?qū)崿F(xiàn)的


常見(jiàn)緩存淘汰策略有三種:

緩存大小有限,當(dāng)緩存被用滿(mǎn)時(shí),哪些數(shù)據(jù)應(yīng)該被清理出去,哪些數(shù)據(jù)應(yīng)該被保留?就需要緩存淘汰策略決定

  1. 先進(jìn)先出 FIFO(first in,first out)
  2. 最少使用策略 LFU(least frequently used)
  3. 最近最少使用策略 LRU(least recently used)

而鏈表的經(jīng)典應(yīng)用場(chǎng)景,便是 LRU 緩存淘汰算法


與數(shù)組的區(qū)別

相比數(shù)組,鏈表是稍微復(fù)雜一點(diǎn)的數(shù)據(jù)結(jié)構(gòu)。

從底層存儲(chǔ)結(jié)構(gòu)來(lái)看:

數(shù)組需要一塊連續(xù)的內(nèi)存空間來(lái)存儲(chǔ),對(duì)內(nèi)存的要求比較高。可能出現(xiàn)的問(wèn)題就是:內(nèi)存總的剩余空間足夠,但是申請(qǐng)容量較大的數(shù)組時(shí)申請(qǐng)失敗;

鏈表恰恰相反,它不需要一塊連續(xù)的內(nèi)存空間,通過(guò)‘指針’將一組零散的內(nèi)存塊串聯(lián)起來(lái)使用。與數(shù)組不同,只要內(nèi)存剩余空間足夠,無(wú)論是否連續(xù),用鏈表來(lái)申請(qǐng)空間一定會(huì)成功。但它的內(nèi)存空間比數(shù)組大了將近一倍,因?yàn)殒湵碇械拿總€(gè)結(jié)點(diǎn)不僅要存儲(chǔ)數(shù)據(jù),還要存儲(chǔ)地址。

從使用場(chǎng)景來(lái)看:

鏈表適合插入、刪除,時(shí)間復(fù)雜度為 O(1);

數(shù)組適合查找,支持隨機(jī)訪問(wèn),根據(jù)下標(biāo)隨機(jī)訪問(wèn)的時(shí)間復(fù)雜度為 O(1)。


常見(jiàn)的鏈表結(jié)構(gòu):?jiǎn)捂湵?、雙鏈表、循環(huán)鏈表

單鏈表

單鏈表中的每個(gè)結(jié)點(diǎn)都必須具備兩個(gè)功能:存儲(chǔ)數(shù)據(jù),記錄下一個(gè)結(jié)點(diǎn)的地址

把第一個(gè)結(jié)點(diǎn)叫做頭結(jié)點(diǎn),最后一個(gè)結(jié)點(diǎn)叫做尾結(jié)點(diǎn)

尾結(jié)點(diǎn)的特點(diǎn):尾結(jié)點(diǎn)的后續(xù)指針 next 指向的不再是下一個(gè)結(jié)點(diǎn)。而是指向一個(gè)空地址 null

這樣做的好處是:防止尾結(jié)點(diǎn)的后續(xù)指針 next 成為一個(gè)野指針,導(dǎo)致遍歷停不下來(lái),或者出現(xiàn)一堆本不屬于該鏈表的垃圾數(shù)據(jù)等

循環(huán)鏈表

循環(huán)鏈表是一種特殊的單鏈表

單鏈表的尾結(jié)點(diǎn)指針指向 null,循環(huán)鏈表的尾結(jié)點(diǎn)指針指向頭結(jié)點(diǎn)。所以循環(huán)鏈表也叫做單循環(huán)鏈表

當(dāng)要處理的數(shù)據(jù)具有循環(huán)結(jié)構(gòu)特點(diǎn)時(shí),就特別適合采用循環(huán)鏈表。如,約瑟夫問(wèn)題/丟手絹問(wèn)題:N 個(gè)人圍成一個(gè)圈,從第一個(gè)開(kāi)始報(bào)數(shù),第 M 個(gè)將被殺掉,最后剩下一人,其余都被殺掉

雙向鏈表

與單鏈表的區(qū)別:?jiǎn)捂湵淼慕Y(jié)點(diǎn)只有兩個(gè)域,數(shù)據(jù)域、存儲(chǔ)后續(xù)結(jié)點(diǎn)的指針域。雙向鏈表有三個(gè)域,數(shù)據(jù)域、存儲(chǔ)后續(xù)結(jié)點(diǎn)的指針域(next)、存儲(chǔ)前驅(qū)結(jié)點(diǎn)地址的指針域(prev)

結(jié)構(gòu)上來(lái)看,雙向鏈表支持 O(1)時(shí)間復(fù)雜度的情況下找到前驅(qū)結(jié)點(diǎn),正是這樣的特點(diǎn),雙向鏈表在某些情況下的插入、刪除等操作都比單鏈表簡(jiǎn)單、高效


用空間換時(shí)間的設(shè)計(jì)思想

當(dāng)內(nèi)存空間充足的時(shí)候,如果更加追求代碼的執(zhí)行速度,可以選擇空間復(fù)雜度相對(duì)較高、但時(shí)間復(fù)雜度相對(duì)很低的算法或者數(shù)據(jù)結(jié)構(gòu)。相反,如果內(nèi)存比較緊缺,比如代碼跑在手機(jī)或者單片機(jī)上,就要用時(shí)間換空間的設(shè)計(jì)思路


寫(xiě)出正確鏈表代碼技巧

理解指針或引用的含義

將某個(gè)變量賦值給指針,實(shí)際上就是將這個(gè)變量的地址賦值給指針(指針存儲(chǔ)的是變量的內(nèi)存地址)

警惕指針丟失和內(nèi)存泄露

內(nèi)存泄露:
申請(qǐng)內(nèi)存空間進(jìn)行使用,用完之后忘記歸還,并且申請(qǐng)的那塊內(nèi)存空間自己也訪問(wèn)不了(可能是將地址弄丟了),而系統(tǒng)也沒(méi)辦法把他分給需要的程序

內(nèi)存溢出:
1.要求分配的內(nèi)存超出系統(tǒng)能夠給與的,系統(tǒng)不能滿(mǎn)足,于是產(chǎn)生溢出。
2.申請(qǐng) int 大小的內(nèi)存,但存放 long 大小的數(shù)據(jù),導(dǎo)致內(nèi)存不夠用,也會(huì)產(chǎn)生內(nèi)存溢出

示例:在ab結(jié)點(diǎn)之間插入x

p->next = x;  // 將p的next指針指向x結(jié)點(diǎn);
x->next = p->next;  // 將x的結(jié)點(diǎn)的next指針指向b結(jié)點(diǎn);

p->next 指針在完成第一步操作之后,已經(jīng)不再指向結(jié)點(diǎn) b 了,而是指向結(jié)點(diǎn) x。第 2 行代碼相當(dāng)于將 x 賦值給 x->next,自己指向自己。因此,整個(gè)鏈表也就斷成了兩半,從結(jié)點(diǎn) b 往后的所有結(jié)點(diǎn)都無(wú)法訪問(wèn)到了

在插入結(jié)點(diǎn)時(shí),要注意操作順序。防止丟失指針,導(dǎo)致內(nèi)存泄露。示例代碼順序顛倒即正確

利用哨兵簡(jiǎn)化實(shí)現(xiàn)難度

如果我們引入哨兵結(jié)點(diǎn),在任何時(shí)候,不管鏈表是不是空,head 指針都會(huì)一直指向這個(gè)哨兵結(jié)點(diǎn)。我們也把這種有哨兵結(jié)點(diǎn)的鏈表叫帶頭鏈表。相反,沒(méi)有哨兵結(jié)點(diǎn)的鏈表就叫作不帶頭鏈表

哨兵結(jié)點(diǎn)是不存儲(chǔ)數(shù)據(jù)的。因?yàn)樯诒Y(jié)點(diǎn)一直存在,所以插入第一個(gè)結(jié)點(diǎn)和插入其他結(jié)點(diǎn),刪除最后一個(gè)結(jié)點(diǎn)和刪除其他結(jié)點(diǎn),都可以統(tǒng)一為相同的代碼實(shí)現(xiàn)邏輯了

重點(diǎn)留意邊界條件處理

判斷鏈表代碼是否正確的邊界條件:

  1. 鏈表為空,代碼是否正常工作
  2. 鏈表只包含一個(gè)結(jié)點(diǎn),代碼是否正常工作
  3. 只包含兩個(gè)結(jié)點(diǎn),代碼是否正常工作
  4. 代碼邏輯在處理頭結(jié)點(diǎn)和尾結(jié)點(diǎn)的時(shí)候,能否正常工作

舉例畫(huà)圖,賦值思考

多寫(xiě)多練,沒(méi)有捷徑


五個(gè)常見(jiàn)的鏈表操作

序號(hào)對(duì)應(yīng) Leetcode 上題序

  1. 單鏈表反轉(zhuǎn) 206
  2. 鏈表中環(huán)的檢測(cè) 141
  3. 兩個(gè)有序的鏈表合并 21
  4. 刪除鏈表倒數(shù)第 n 個(gè)結(jié)點(diǎn) 19
  5. 求鏈表的中間結(jié)點(diǎn) 876

這里簡(jiǎn)述一下我的解題方式,具體代碼實(shí)現(xiàn)移步下篇

單鏈表反轉(zhuǎn):

創(chuàng)建三個(gè)指針,prev 指向前一個(gè)結(jié)點(diǎn)(反轉(zhuǎn)鏈表頭)、cur 當(dāng)前待反轉(zhuǎn)結(jié)點(diǎn)、next 待反轉(zhuǎn)下一個(gè)結(jié)點(diǎn),主要作用為存儲(chǔ)指針,避免造成內(nèi)存泄露

遍歷鏈表,next 存儲(chǔ)下一個(gè)結(jié)點(diǎn)、更改 cur 指針指向、更改前結(jié)點(diǎn)、更新當(dāng)前待反轉(zhuǎn)結(jié)點(diǎn)

鏈表中環(huán)的檢測(cè):

遍歷鏈表,給每個(gè)結(jié)點(diǎn)做標(biāo)記,當(dāng)標(biāo)記結(jié)點(diǎn)在下一次出現(xiàn),則有環(huán)

兩個(gè)有序的鏈表合并:

創(chuàng)建兩個(gè)指針,新鏈表(存儲(chǔ)合并鏈表),不斷比較兩個(gè)排序鏈表頭結(jié)點(diǎn),將較小結(jié)點(diǎn)放入合并鏈表中,移動(dòng)被放入結(jié)點(diǎn)的鏈表指針

當(dāng)兩個(gè)鏈表中指針存在指向 null 時(shí),將另一個(gè)鏈表接到排列完成的鏈表之后,排列結(jié)束

刪除鏈表倒數(shù)第 n 個(gè)結(jié)點(diǎn)

創(chuàng)建兩個(gè)指針,快指針走 n+1 步后,慢指針開(kāi)始走,直至快指針走到底

此時(shí),慢指針位置為倒數(shù)第 n+1 個(gè)結(jié)點(diǎn),操作這個(gè)結(jié)點(diǎn)的 next 指向下下個(gè)結(jié)點(diǎn)

求鏈表的中間結(jié)點(diǎn)

創(chuàng)建兩個(gè)指針,快指針每次走兩步,慢指針每次走一步,當(dāng)快指針走到底時(shí),慢指針位置為中間結(jié)點(diǎn)位置

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 數(shù)據(jù)結(jié)構(gòu)02-鏈表(單/雙/向普通及循環(huán)鏈表) 鏈表通常由一連串節(jié)點(diǎn)組成,每個(gè)節(jié)點(diǎn)包含任意的實(shí)例數(shù)據(jù)(data f...
    sanpintian閱讀 830評(píng)論 0 1
  • 線(xiàn)性表 定義:線(xiàn)性表就是數(shù)據(jù)排成像一條線(xiàn)一樣的結(jié)構(gòu)。每個(gè)線(xiàn)性表上的數(shù)據(jù)最多只有前和后兩個(gè)方向。其實(shí)除了數(shù)組,鏈表、...
    竹blue閱讀 410評(píng)論 0 0
  • 基本概念 鏈表的含義: 鏈表是一種用于存儲(chǔ)數(shù)據(jù)集合的數(shù)據(jù)結(jié)構(gòu),具有以下屬性 相鄰元素之間通過(guò)指針相連 最后一個(gè)元素...
    古劍誅仙閱讀 1,054評(píng)論 0 3
  • 1.鏈表的存儲(chǔ)結(jié)構(gòu) 相比數(shù)組,鏈表是一種稍微復(fù)雜一點(diǎn)的數(shù)據(jù)結(jié)構(gòu)。對(duì)于初學(xué)者來(lái)說(shuō),掌握起來(lái)也要比數(shù)組稍難一些。這兩個(gè)...
    青漾閱讀 328評(píng)論 0 0
  • 前言:本篇文章只是記錄王爭(zhēng)的數(shù)據(jù)結(jié)構(gòu)與算法之美[https://time.geekbang.org/column/...
    沉江小魚(yú)閱讀 759評(píng)論 0 10

友情鏈接更多精彩內(nèi)容