當散列表趕上鍊表會發生什麼呢?

      在數據結構中,散列表和鏈表常常會組合在一塊使用,若是你對java很熟悉,你會發現LinkedHashMap這樣一個經常使用的容器,也把散列表和鏈表組合起來使用。那散列表和鏈表是如何組合使用的,他們組合在一塊兒能碰撞出什麼火花,請跟隨個人腳步,一塊兒一探究竟。java

     咱們先思考這麼一個問題,如何使用鏈表來實現LRU緩存呢?若是對LRU不熟,能夠看這篇文章。頁面置換算法你學會了嗎?程序員

     咱們能夠維護一個有序的單鏈表,越靠近鏈表尾部的結點是越早訪問的。當有一個新的數據被訪問時,咱們從鏈表頭開始順序遍歷。遍歷的結果有兩種狀況。算法

  1. 若是此數據以前就已經被緩存在鏈表中,咱們遍歷獲得這個數據對應的結點,而後將其從這個位置移動到鏈表的頭部。緩存

  2. 若是此數據不在鏈表中,又會分爲兩種狀況。若是此時緩存鏈表沒有滿,咱們直接將該結點插入鏈表頭部。若是此時緩存鏈表已經滿了,咱們從鏈表尾部刪除一個結點,而後將新的數據結點插入到鏈表頭部。數據結構

      這樣咱們就用鏈表實現了一個LRU緩存,咱們接下來分析一下緩存訪問的時間複雜度。由於無論緩存有沒有滿,咱們都須要遍歷一遍鏈表,因此基於鏈表實現的LRU緩存,緩存訪問的時間複雜度是O(n)。spa

 

 

        那有沒有什麼方法能夠減低時間複雜度呢?咱們先來分析一下緩存的經常使用操做。對於一個緩存來講,主要涉及如下三種操做:指針

  1. 往緩存添加一個元素。blog

  2. 從緩存中刪除一個元素。get

  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緩存。

       更多硬核知識,請關注公衆號」程序員學長"。

相關文章
相關標籤/搜索