5、鏈表

數組在存儲結構上的弊端:

數組需要一塊連續的內存空間來存儲,對內存的要求比較高。如果我們申請一個100MB大小的數組,當內存中沒有連續的,足夠大的存儲空間時,即便內存的剩餘總可用空間大於100MB,仍然會申請失敗。

鏈表的內存結構?

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

單鏈表

兩個特殊結點:
頭結點用來記錄鏈表的基地址,有了它,我們就可以遍歷得到整條鏈表。
尾結點特殊的地方是:指針不是指向下一個結點,而是指向一個空地址NULL,表示這是鏈表上最後一個結點

插入和刪除:
因爲鏈表的存儲空間本身就不是連續的,所以插入和刪除操作不需要搬移節點,插入和刪除操作的時間複雜度都是O(1)

隨機訪問:
因爲鏈表的存儲空間並不是連續的,所以只能根據指針一個結點一個結點地有一次遍歷,直到找到相應的結點,時間複雜度爲O(n)

循環鏈表

循環鏈表和單鏈表的區別就在尾節點,循環鏈表的尾節點指針指向鏈表的頭結點
和單鏈表相比,循環鏈表的優點是從鏈尾到鏈頭比較方便。當要處理的數據具有環型結構特點時,就特別適合採用循環鏈表

雙向鏈表

首節點的前驅指針和尾節點的後繼指針均指向空地址

雙向鏈表相對於單鏈表的劣勢:

雙向鏈表需要額外的兩個空間來存儲後繼結點和前驅結點的地址,所以,如果存儲同樣多的數據,雙向鏈表要比單鏈表佔用更多的內存空間。

雙向鏈表相對於單鏈表的優勢:

刪除對比:

鏈表刪除數據有兩種情況:
① 刪除結點中”值等於某個給定值”的結點
單鏈表和雙鏈表時間複雜度都爲O(n),因爲刪除操作都爲O(1),但遍歷查找的時間複雜度爲O(n),根據時間複雜度的加法法則,總時間複雜度爲O(n)

② 刪除給定指針指向的結點
單鏈表:需要從頭結點開始遍歷鏈表找到刪除結點的前驅結點, 時間複雜度爲O(n)
雙向鏈表:要刪除的結點保存了前驅結點,所以不需要遍歷,雙向鏈表的時間複雜度爲O(1)

插入對比:

雙向鏈表時間複雜度爲O(1),單鏈表爲O(n)
總結:
雙向鏈表在某些情況下的插入和刪除等操作都要比單鏈表簡單,高效,但比較耗費內存。在實際軟件開發中,雙鏈表還是比單鏈表應用更加廣泛,Java中的LinkedHashMap就用到了雙向鏈表

雙向循環鏈表

首節點的前驅指針指向尾節點,尾節點的後繼指針指向首節點

鏈表VS數組性能大比拼

正是因爲數組和鏈表內存存儲的區別,他們插入,刪除,隨機訪問操作的時間複雜度正好相反。在實際的軟件開發中,不能僅僅利用複雜度分析就決定使用哪個數據結構來存儲數據

選擇數組還是鏈表?

1.插入、刪除和隨機訪問的時間複雜度
數組:插入、刪除的時間複雜度是O(n),隨機訪問的時間複雜度是O(1)。
鏈表:插入、刪除的時間複雜度是O(1),隨機訪問的時間複雜端是O(n)。

2.數組缺點
1)數組的聲明要佔用整塊連續內存空間,若申請內存空間很大,比如100M,但若內存空間沒有100M的連續空間時,則會申請失敗,儘管內存可用空間超過100M。
2)大小固定,若存儲空間不足,需進行擴容,一旦擴容就要進行數據複製,而這時非常費時的,Java中的ArrayList容器就是用數組實現的

3.數組優點:
數組支持隨機訪問

4.鏈表缺點
1)內存空間消耗更大,因爲需要額外的空間存儲指針信息。
2)對鏈表進行頻繁的插入和刪除操作,會導致頻繁的內存申請和釋放,容易造成內存碎片,如果是Java語言,還可能會造成頻繁的GC(自動垃圾回收器)操作。

5.鏈表優點:
鏈表支持動態擴容,插入刪除高效

6.數組與鏈表最大區別:
數組與鏈表最大的區別作者認爲是,數組是大小固定,一經聲明就要佔用整塊連續內存空間而鏈表本身沒有大小的限制,天然地支持動態擴容。

7.如何選擇?
數組簡單易用,在實現上使用連續的內存空間,可以藉助CPU的緩衝機制預讀數組中的數據,所以訪問效率更高,而鏈表在內存中並不是連續存儲,所以對CPU緩存不友好,沒辦法預讀。
如果代碼對內存的使用非常苛刻,那數組就更適合。因爲鏈表中的每個結點都需要消耗額外的存儲空間去存儲一份指向下一個結點的指針,所以內存消耗會翻倍。而且,對鏈表進行頻繁的插入,刪除操作,會導致頻繁的內存申請和釋放,容易造成內存碎片,如果是Java語言,就有可能會導致頻繁的GC,所以,在我們實際的開發中,針對不同類型的項目,要根據具體情況,權衡究竟是選擇數組還是鏈表

擴展:
CPU在從內存讀取數據的時候,會先把讀取到的數據加載到CPU的緩存中。而CPU每次從內存讀取數據並不是只讀取那個特定要訪問的地址,而是讀取一個數據塊(這個大小我不太確定。。)並保存到CPU緩存中,然後下次訪問內存數據的時候就會先從CPU緩存開始查找,如果找到就不需要再從內存中取。這樣就實現了比內存訪問速度更快的機制,也就是CPU緩存存在的意義:爲了彌補內存訪問速度過慢與CPU執行速度快之間的差異而引入。

對於數組來說,存儲空間是連續的,所以在加載某個下標的時候可以把以後的幾個下標元素也加載到CPU緩存這樣執行速度會快於存儲空間不連續的鏈表存儲

設計思想:

用空間換時間:
當內存空間充足的時候,如果我們更加追求代碼的執行速度,我們就可以選擇空間複雜度相對較高,但時間複雜度相對很低的算法或者數據結構。

時間換空間:
如果內存比較緊缺,比如代碼跑在手機或者單片機上,這個時候,就要反過來用時間換空間的設計思路

思考題:

如何用鏈表來實現LRU緩存淘汰策略呢?
緩存實際上就是利用了空間換時間的設計思想。如果我們把數據存儲在硬盤上,會比較節省內存,但每次查找數據都要詢問一次硬盤,會比較慢。但如果我們通過緩存技術,事先將數據加載在內存中,雖然會比較耗費內存空間,但是每次數據查詢的速度就大大提高了

如何輕鬆寫出正確的鏈表代碼

  1. 理解指針或引用的含義
    將某個變量賦值給指針,實際上就是將這個變量的地址賦值給指針

  2. 警惕指針丟失和內存泄漏
    插入和刪除鏈表結點時,要記得手動釋放結點對應的內存空間,否則,就會出現內存泄漏的問題

  3. 利用哨兵簡化實現難度
    針對鏈表的插入,刪除操作,需要對插入第一個結點和刪除最後一個結點的情況進行特殊處理,雖然哨兵可能只是少做一次判斷

    如果,我們引入哨兵結點,在任何時候,不管鏈表是不是空,head指針都會一直指向這個哨兵結點。我們也把這種有哨兵結點的鏈表叫做帶頭鏈表。沒有哨兵結點的鏈表叫做不帶頭鏈表

哨兵結點是不存儲數據的,因爲哨兵結點一直存在,所以插入第一個結點和插入其他結點,刪除最後一個結點和刪除其他結點,都可以統一爲相同的代碼實現邏輯

  1. 重點留意邊界處理條件
    經常用來檢查鏈表是否正確的邊界4個邊界條件:
    1.如果鏈表爲空時,代碼是否能正常工作?
    2.如果鏈表只包含一個節點時,代碼是否能正常工作?
    3.如果鏈表只包含兩個節點時,代碼是否能正常工作?
    4.代碼邏輯在處理頭尾節點時是否能正常工作?

5.舉例畫圖,輔助思考
核心思想:釋放腦容量,留更多的給邏輯思考,這樣就會感覺到思路清晰很多。

6.多寫多練,沒有捷徑
五個常見的鏈表操作:
1.單鏈表反轉
2.鏈表中環的檢測
3.兩個有序鏈表合併
4.刪除鏈表倒數第n個節點
5.求鏈表的中間節點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章