當雜湊表遇上鍊表會擦除什麼火花?

公眾號程式設計師學長發表於2021-07-23

      在資料結構中,雜湊表和連結串列經常會組合在一塊使用,如果你對java很熟悉,你會發現LinkedHashMap這樣一個常用的容器,也把雜湊表和連結串列組合起來使用。那雜湊表和連結串列是如何組合使用的,他們組合在一起能碰撞出什麼火花,請跟隨我的腳步,一起一探究竟。

     我們先思考這麼一個問題,如何使用連結串列來實現LRU快取呢?如果對LRU不熟,可以看這篇文章。頁面置換演算法你學會了嗎?

     我們可以維護一個有序的單連結串列,越靠近連結串列尾部的結點是越早訪問的。當有一個新的資料被訪問時,我們從連結串列頭開始順序遍歷。遍歷的結果有兩種情況。

  1. 如果此資料之前就已經被快取在連結串列中,我們遍歷得到這個資料對應的結點,然後將其從這個位置移動到連結串列的頭部。

  2. 如果此資料不在連結串列中,又會分為兩種情況。如果此時快取連結串列沒有滿,我們直接將該結點插入連結串列頭部。如果此時快取連結串列已經滿了,我們從連結串列尾部刪除一個結點,然後將新的資料結點插入到連結串列頭部。

      這樣我們就用連結串列實現了一個LRU快取,我們接下來分析一下快取訪問的時間複雜度。因為不管快取有沒有滿,我們都需要遍歷一遍連結串列,所以基於連結串列實現的LRU快取,快取訪問的時間複雜度是O(n)。

 

 

        那有沒有什麼方法可以減低時間複雜度呢?我們先來分析一下快取的常用操作。對於一個快取來說,主要涉及以下三種操作:

  1. 往快取新增一個元素。

  2. 從快取中刪除一個元素。

  3. 在快取中查詢一個元素。

 

      這三個操作都會涉及到查詢的操作,如果單純的使用連結串列,時間複雜度只能是O(n)。大家都知道雜湊表的查詢操作是O(1),那我們能不能把雜湊表和連結串列結合起來使用,將快取的這三個常用操作的時間複雜度減低到O(1)呢?答案是肯定的,我們來看一下他們是如何組合在一起的。

       如圖所示,我們使用雙向連結串列來儲存資料,連結串列中的每個結點除了資料(data)、前驅指標(pre)、後繼指標(next)之外,還新增了一個特殊的欄位 hnext。這個hnext有什麼作用呢?因為我們的雜湊表是通過連結串列法解決雜湊衝突的,所以每個結點會在兩條鏈中。一個鏈是剛剛我們提到的雙向連結串列,另一個鏈是雜湊表中的拉鍊。前驅和後繼指標是為了將結點串在雙向連結串列中,hnext 指標是為了將結點串在雜湊表的拉鍊中。

      瞭解了這個雜湊表和雙向連結串列的組合儲存結構之後,我們再來看,前面講到的快取的三個操作,是如何做到時間複雜度是 O(1) 的?

       首先,我們來看如何查詢一個資料。我們前面講過,雜湊表中查詢資料的時間複雜度接近 O(1),所以通過雜湊表,我們可以很快地在快取中找到一個資料。當找到資料之後,我們還需要將它移動到雙向連結串列的尾部。

       其次,我們來看如何刪除一個資料。我們需要找到資料所在的結點,然後將結點刪除。藉助雜湊表,我們可以在 O(1) 時間複雜度裡找到要刪除的結點。因為我們的連結串列是雙向連結串列,雙向連結串列可以通過前驅指標 O(1) 時間複雜度獲取前驅結點,所以在雙向連結串列中,刪除結點只需要 O(1) 的時間複雜度。

      最後,我們來看如何新增一個資料。新增資料到快取稍微有點麻煩,我們需要先看這個資料是否已經在快取中。如果已經在其中,需要將其移動到雙向連結串列的頭部;如果不在其中,還要看快取有沒有滿。如果滿了,則將雙向連結串列尾部的結點刪除,然後再將資料放到連結串列的頭部;如果沒有滿,就直接將資料放到連結串列的頭部。這整個過程涉及的查詢操作都可以通過雜湊表來完成。

       其他的操作,比如刪除頭結點、連結串列尾部插入資料等,都可以在 O(1) 的時間複雜度內完成。所以,這三個操作的時間複雜度都是 O(1)。至此,我們就通過雜湊表和雙向連結串列的組合使用,實現了一個高效的、支援 LRU 快取淘汰演算法的快取系統原型。

       所以,可以通過雜湊表和連結串列結合的方式,實現一個時間複雜度為O(1)的LRU快取。

       更多硬核知識,請關注公眾號”程式設計師學長"。

 

相關文章