# 面試官: 既然已經有數組了,為什么還要鏈表


面試官: 既然已經有數組了,為什么還要鏈表

本文發布於微信平台: 程序員面試官

超過20w字的「前端面試與進階指南」可以移步github


對於不少開發者而言,鏈表(linked list)這種數據結構既熟悉又陌生,熟悉是因為它確實是非常基礎的數據結構,陌生的原因是我們在業務開發中用到它的幾率的確不大.

在很多情況下,我們用數組就能很好的完成工作,而且不會產生太多的差異,那么鏈表存在的意義是什么?鏈表相比於數組有什么優勢或者不足嗎?

什么是鏈表

鏈表是一種常見的基礎數據結構,是一種線性表,但是並不會按線性的順序存儲數據,而是在每一個節點里存到下一個節點的指針(Pointer).

從本質上來講,鏈表與數組的確有相似之處,他們的相同點是都是線性數據結構,這與樹和圖不同,而它們的不同之處在於數組是一塊連續的內存,而鏈表可以不是連續內存,鏈表的節點與節點之間通過指針來聯系.

鏈表vs數組

當然,鏈表也有不同的形態,主要分為三種:單向鏈表、雙向鏈表、循環鏈表.

單向鏈表

單向鏈表的節點通常由兩個部分構成,一個是節點儲存的值val,另一個就是節點的指針next.

單向鏈表

鏈表與數組類似,也可以進行查找、插入、刪除、讀取等操作,但是由於鏈表與數組的特性不同,導致不同操作的復雜度也不同.

查找性能

單向鏈表的查找操作通常是這樣的:

  1. 從頭節點進入,開始比對節點的值,如果不同則通過指針進入下一個節點
  2. 重復上面的動作,直到找到相同的值,或者節點的指針指向null

鏈表的查找性能與數組一樣,都是時間復雜度為O(n).

插入刪除性能

鏈表與數組最大的不同就在於刪除、插入的性能優勢,由於鏈表是非連續的內存,所以不用像數組一樣在插入、刪除操作的時候需要進行大面積的成員位移,比如在a、b節點之間插入一個新節點c,鏈表只需要:

  1. a斷開指向b的指針,將指針指向c
  2. c節點將指針指向b,完畢

這個插入操作僅僅需要移動一下指針即可,插入、刪除的時間復雜度只有O(1).

鏈表的插入操作如下:

插入操作

鏈表的刪除操作如下:

刪除操作

讀取性能

鏈表相比之下也有劣勢,那就是讀取操作遠不如數組,數組的讀取操作之所以高效,是因為它是一塊連續內存,數組的讀取可以通過尋址公式快速定位,而鏈表由於非連續內存,所以必須通過指針一個一個節點遍歷.

比如,對於一個數組,我們要讀取第三個成員,我們僅需要arr[2]就能快速獲取成員,而鏈表則需要從頭部節點進入,在通過指針進入后續節點才能讀取.

應用場景

由於雙向鏈表的存在,單向鏈表的應用場景比較少,因為很多場景雙向鏈表可以更出色地完成.

但是單向鏈表並非無用武之地,在以下場景中依然會有單向鏈表的身影:

  1. 撤銷功能,這種操作最多見於各種文本、圖形編輯器中,撤銷重做在編輯器場景下屬於家常便飯,單向鏈表由於良好的刪除特性在這個場景很適用
  2. 實現圖、hashMap等一些高級數據結構

雙向鏈表

我們上文已經提到,單向鏈表的應用場景並不多,而真正在生產環境中被廣泛運用的正是雙向鏈表.

雙向鏈表與單向鏈表相比有何特殊之處?

雙向鏈表

我們看到雙向鏈表多了一個新的指針prev指向節點的前一個節點,當然由於多了一個指針,所以雙向鏈表要更占內存.

別小看雙向鏈表多了一個前置指針,在很多場景里由於多了這個指針,它的效率更高,也更加實用.

比如編輯器的「undo/redo」操作,雙向鏈表就更加適用,由於擁有前后指針,用戶可以自由得進行前后操作,如果這個是一個單向鏈表,那么用戶需要遍歷鏈表這時的時間復雜度是O(n).

真正生產級應用中的編輯器采用的數據結構和設計模式更加復雜,比如Word就是采用Piece Table數據結構加上Command queue模式實現「undo/redo」的,不過這是后話了.

循環鏈表

循環鏈表,顧名思義,他就是將單向鏈表的尾部指針指向了鏈表頭節點:

循環鏈表

循環鏈表一個應用場景就是操作系統的分時問題,比如有一台計算機,但是有多個用戶使用,CPU要處理多個用戶的請求很可能會出現搶占資源的情況,這個時候計算機會采取分時策略來保證每個用戶的使用體驗.

每個用戶都可以看成循環鏈表上的節點,CPU會給每個節點分配一定的處理時間,在一定的處理時間后進入下一個節點,然后無限循環,這樣可以保證每個用戶的體驗,不會出現一個用戶搶占CPU而導致其他用戶無法響應的情況.

當然,約瑟夫環的問題是單向循環鏈表的代表性應用,感興趣的可以深入了解下.

當然如果是雙向鏈表首尾相接呢?這就是雙向循環鏈表.

在Node中有一類場景,沒有查詢,但是卻有大量的插入和刪除,這就是Timer模塊。
幾乎所有的網絡I/O請求,都會提供timeout操作控制socket的超時狀況,這里就會大量使用到setTimeout,並且這些timeout定時器,絕大部分都是用不到的(數據按時正常響應),那么又會有響應的大量clearTimeout操作,因此node采用了雙向循環鏈表來提高Timer模塊的性能,至於其中的細節就不再贅述了.

                                           插入!
TimersList <-----> timer1 <-----> timer2 <-----> timer4 <-----> timer3 <-----> ......
                1000ms后執行     1050ms后執行    1100ms后執行    1200ms后執行

小結

至此,我們對鏈表這個數據結構有了一定的認知,由於其非連續內存的特性導致鏈表非常適用於頻繁插入、刪除的場景,而不見長於讀取場景,這跟數組的特性恰好形成互補,所以現在也可以回到題目中的問題了,鏈表的特性與數組互補,各有所長,而且鏈表由於指針的存在可以形成環形鏈表,在特定場景也非常有用,因此鏈表的存在是很有必要的。

那么,現在有一個非常常見的一個面試向的思考題:

我們平時在用的微信小程序會有最近使用的功能,時間最近的在最上面,按照時間順序往后排,當用過的小程序大於一定數量后,最不常用的小程序就不會出現了,你會如何設計這個算法?

2019-09-07-01-20-00


2019-09-20-11-23-16


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM