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

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

本文發佈於微信平臺: 程序員面試官

超過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

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