日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

02-链表结构(Linked list)

發布時間:2023/12/31 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 02-链表结构(Linked list) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

鏈表

  • 1.五花八門的鏈表結構
    • 1.1 底層的存儲結構
    • 1.2 三種最常見的鏈表結構
      • 1.2.1 單鏈表
        • 1.2.1.1 單鏈表的查找、插入、刪除
      • 1.2.2 循環鏈表
      • 1.2.3 雙向鏈表
        • 1.2.3.1 雙向鏈表刪除操作
      • 1.2.4 雙向循環鏈表
  • 2. 鏈表 VS 數組性能大比拼
  • 3. 如何基于鏈表實現 LRU 緩存淘汰算法?

1.五花八門的鏈表結構

相比數組,鏈表是一種稍微復雜一點的數據結構。這兩個非常基礎、非常常用的數據結構,我們常常會放到一塊兒來比較。所以我們先來看,這兩者有什么區別。

1.1 底層的存儲結構

我們先從底層的存儲結構上來看一看。

為了直觀地對比,我畫了一張圖。從圖中我們看到,數組需要一塊連續的內存空間來存儲,對內存的要求比較高。如果我們申請一個 100MB 大小的數組,當內存中沒有連續的、足夠大的存儲空間時,即便內存的剩余總可用空間大于 100MB,仍然會申請失敗。

而鏈表恰恰相反,它并不需要一塊連續的內存空間,它通過“指針”將一組零散的內存塊串聯起來使用,所以如果我們申請的是 100MB 大小的鏈表,根本不會有問題。

1.2 三種最常見的鏈表結構

三種最常見的鏈表結構,它們分別是:單鏈表、雙向鏈表和循環鏈表。

1.2.1 單鏈表

鏈表通過指針將一組零散的內存塊串聯在一起。其中,我們把內存塊稱為鏈表的“結點”。為了將所有的結點串起來,每個鏈表的結點除了存儲數據之外,還需要記錄鏈上的下一個結點的地址。如圖所示,我們把這個記錄下個結點地址的指針叫作后繼指針 next。


其中有兩個結點是比較特殊的,它們分別是第一個結點和最后一個結點。我們習慣性地把第一個結點叫作頭結點,把最后一個結點叫作尾結點。其中,頭結點用來記錄鏈表的基地址。有了它,我們就可以遍歷得到整條鏈表。而尾結點特殊的地方是:指針不是指向下一個結點,而是指向一個空地址 NULL,表示這是鏈表上最后一個結點。

1.2.1.1 單鏈表的查找、插入、刪除

與數組一樣,鏈表也支持數據的查找、插入和刪除操作。
我們知道,在進行數組的插入、刪除操作時,為了保持內存數據的連續性,需要做大量的數據搬移,所以時間復雜度是 O(n)。而在鏈表中插入或者刪除一個數據,我們并不需要為了保持內存的連續性而搬移結點,因為鏈表的存儲空間本身就不是連續的。所以,在鏈表中插入和刪除一個數據是非常快速的。

從圖中我們可以看出,針對鏈表的插入和刪除操作,我們只需要考慮相鄰結點的指針改變,所以對應的時間復雜度是 O(1)。

但是,有利就有弊。鏈表要想隨機訪問第 k 個元素,就沒有數組那么高效了。因為鏈表中的數據并非連續存儲的,所以無法像數組那樣,根據首地址和下標,通過尋址公式就能直接計算出對應的內存地址,而是需要根據指針一個結點一個結點地依次遍歷,直到找到相應的結點。

數組隨機訪問的時間復雜度為O(1),數組隨機訪問的效率特別特別高。 但是鏈表是不支持隨機訪問的。當我們想要訪問鏈表中的第5個結點時。數組可以根據尋址公式,一下就定位到第5個元素所在的地址。而鏈表不行,要想訪問鏈表中的第5個結點,我們只能從頭節點開始遍歷。順序訪問。一個一個往下找。所以鏈表"隨機訪問"的時間復雜度為O(n). 更為準確的來講,鏈表的訪問只能叫順序訪問,而不能叫隨機訪問。

你可以把鏈表想象成一個隊伍,隊伍中的每個人都只知道自己后面的人是誰,所以當我們希望知道排在第 k 位的人是誰的時候,我們就需要從第一個人開始,一個一個地往下數。所以,鏈表隨機訪問的性能沒有數組好,需要 O(n) 的時間復雜度。

1.2.2 循環鏈表

循環鏈表是一種特殊的單鏈表。實際上,循環鏈表也很簡單。它跟單鏈表唯一的區別就在尾結點。我們知道,單鏈表的尾結點指針指向空地址,表示這就是最后的結點了。而循環鏈表的尾結點指針是指向鏈表的頭結點。

和單鏈表相比,循環鏈表的優點是從鏈尾到鏈頭比較方便。當要處理的數據具有環型結構特點時,就特別適合采用循環鏈表。比如著名的約瑟夫問題。盡管用單鏈表也可以實現,但是用循環鏈表實現的話,代碼就會簡潔很多。

1.2.3 雙向鏈表

在實際的軟件開發中,也更加常用的鏈表結構:雙向鏈表。

單向鏈表只有一個方向,結點只有一個后繼指針 next 指向后面的結點。而雙向鏈表,顧名思義,它支持兩個方向,每個結點不止有一個后繼指針 next 指向后面的結點,還有一個前驅指針 prev 指向前面的結點。

圖中可以看出來,雙向鏈表需要額外的兩個空間來存儲后繼結點和前驅結點的地址。所以,如果存儲同樣多的數據,雙向鏈表要比單鏈表占用更多的內存空間。雖然兩個指針比較浪費存儲空間,但可以支持雙向遍歷,這樣也帶來了雙向鏈表操作的靈活性。

從結構上來看,雙向鏈表可以支持 O(1) 時間復雜度的情況下找到前驅結點,正是這樣的特點,也使雙向鏈表在某些情況下的插入、刪除等操作都要比單鏈表簡單、高效。

找到鏈表中某個結點的前驅結點: 單鏈表的平均時間復雜度為O(n),因為需要從頭節點開始找。 而雙向鏈表的時間復雜度為O(1),因為這個結點中的prev前驅結點指針域存放的是前驅結點的地址,通過這個地址,我們一步就找到了該結點的前驅結點,所以時間復雜度O(1)

1.2.3.1 雙向鏈表刪除操作

在實際的軟件開發中,從鏈表中刪除一個數據無外乎這兩種情況:

  • 刪除結點中“值等于某個給定值”的結點;
  • 刪除給定指針指向的結點。

對于第一種情況,不管是單鏈表還是雙向鏈表,為了查找到值等于給定值的結點,都需要從頭結點開始一個一個依次遍歷對比,直到找到值等于給定值的結點,然后再通過我前面講的指針操作將其刪除。

盡管單純的刪除操作時間復雜度是 O(1),但遍歷查找的時間是主要的耗時點,對應的時間復雜度為 O(n)。根據時間復雜度分析中的加法法則,刪除值等于給定值的結點對應的鏈表操作的總時間復雜度為 O(n)。

很多書中把鏈表的插入和刪除操作的時間復雜度寫為O(1),是沒有考慮插入、刪除操作之前查找元素的過程(時間復雜度O(n)),只考慮存粹的插入和刪除操作時間復雜度自然是O(1)。

對于第二種情況,我們已經找到了要刪除的結點,但是刪除某個結點 q 需要知道其前驅結點,而單鏈表并不支持直接獲取前驅結點,所以,為了找到前驅結點,我們還是要從頭結點開始遍歷鏈表,直到 p->next=q,說明 p 是 q 的前驅結點。

但是對于雙向鏈表來說,這種情況就比較有優勢了。因為雙向鏈表中的結點已經保存了前驅結點的指針,不需要像單鏈表那樣遍歷。所以,針對第二種情況,單鏈表刪除操作需要 O(n) 的時間復雜度,而雙向鏈表只需要在 O(1) 的時間復雜度內就搞定了!

刪除給定指針指向的結點: 這種情況是已經找到了要刪除的元素,我們只需要執行刪除操作即可. 針對單鏈表而言: 單鏈表如果要刪除一個結點q.必須要知道這個結點的前驅結點是誰,修改前驅結點的指針指向即可.單鏈表找某個結點的前驅結點,只能從頭開始遍歷. 臨界值 p->next == q;說明p就是q的前驅結點.所以在單鏈表中,找這個前驅結點的平均時間復雜度為O(n),然后執行刪除操作的時間復雜度為O(1).
根據時間復雜度分析的加法法則: 刪除給定指針指向的結點 --> 單鏈表的總的時間復雜度為O(n).

針對雙鏈表而言: 雙鏈表要刪除一個結點q.也必須得知道這個結點的前驅結點和后繼結點. 修改前驅結點的后繼指針next和后繼結點的前驅指針prev即可.而針對雙鏈表而言,找q的前驅結點和q的后繼結點的時間復雜度都為O(1).而執行刪除操作(修改指針指向)的時間復雜度也為O(1).
根據時間復雜度分析的加法法則: 刪除給定指針指向的結點 --> 雙鏈表的總的時間復雜度為O(1).

除了插入、刪除操作有優勢之外,對于一個有序鏈表,雙向鏈表的按值查詢的效率也要比單鏈表高一些。因為,我們可以記錄上次查找的位置 p,每次查詢時,根據要查找的值與 p 的大小關系,決定是往前還是往后查找,所以平均只需要查找一半的數據。

實際上,這里有一個更加重要的知識點需要掌握,那就是用空間換時間的設計思想。當內存空間充足的時候,如果我們更加追求代碼的執行速度,我們就可以選擇空間復雜度相對較高、但時間復雜度相對很低的算法或者數據結構。相反,如果內存比較緊缺,比如代碼跑在手機或者單片機上,這個時候,就要反過來用時間換空間的設計思路。

1.2.4 雙向循環鏈表

了解了循環鏈表和雙向鏈表,如果把這兩種鏈表整合在一起就是一個新的版本:雙向循環鏈表。

2. 鏈表 VS 數組性能大比拼

數組和鏈表是兩種截然不同的內存組織方式。正是因為內存存儲的區別,它們插入、刪除、隨機訪問操作的時間復雜度正好相反。

不過,數組和鏈表的對比,并不能局限于時間復雜度。而且,在實際的軟件開發中,不能僅僅利用復雜度分析就決定使用哪個數據結構來存儲數據。

3. 如何基于鏈表實現 LRU 緩存淘汰算法?

學習鏈表有什么用呢?為了回答這個問題,我們先來討論一個經典的鏈表應用場景,那就是 LRU 緩存淘汰算法。

緩存的大小有限,當緩存被用滿時,哪些數據應該被清理出去,哪些數據應該被保留?這就需要緩存淘汰策略來決定。常見的策略有三種:先進先出策略 FIFO(First In,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。

這些策略你不用死記,我打個比方你很容易就明白了。假如說,你買了很多本技術書,但有一天你發現,這些書太多了,太占書房空間了,你要做個大掃除,扔掉一些書籍。那這個時候,你會選擇扔掉哪些書呢?對應一下,你的選擇標準是不是和上面的三種策略神似呢?

我的思路是這樣的:我們維護一個有序單鏈表,越靠近鏈表尾部的結點是越早之前訪問的。當有一個新的數據被訪問時,我們從鏈表頭開始順序遍歷鏈表。

1). 如果此數據之前已經被緩存在鏈表中了,我們遍歷得到這個數據對應的結點,并將其從原來的位置刪除,然后再插入到鏈表的頭部。
2) 如果此數據沒有在緩存鏈表中,又可以分為兩種情況:

  • 如果此時緩存未滿,則將此結點直接插入到鏈表的頭部;
  • 如果此時緩存已滿,則鏈表尾結點刪除,將新的數據結點插入鏈表的頭部。

這樣我們就用鏈表實現了一個 LRU 緩存。

現在我們來看下緩存訪問的時間復雜度是多少。因為不管緩存有沒有滿,我們都需要遍歷一遍鏈表,所以這種基于鏈表的實現思路,緩存訪問的時間復雜度為 O(n)。

實際上,我們可以繼續優化這個實現思路,比如引入散列表(Hash table)來記錄每個數據的位置,將緩存訪問的時間復雜度降到 O(1)。

總結

以上是生活随笔為你收集整理的02-链表结构(Linked list)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。