如何使用散列表實現一個O(1)時間複雜度的LRU緩存算法

1.散列表

 

    什麼是散列表呢?我舉這樣一個例子,記得小時候家裏只有一個座機,但是這個座機不能存電話號碼,於是只能將要聯繫的人的電話號碼寫在一個本子上。時間久了本子上的電話號碼越來越多。然後這個時候要去找某一個指定的聯繫人的時候發現很難找到。如果是你你想想一下應該怎麼樣才能快速找到呢?

    其實我們每次新增一個聯繫人的時候可以將他的姓的首字母取出來,然後所以首字母相同的都在一個區間,也就是做一個目錄。

    例如張三,我們就將他放在Z字母的地方,同時如果Z字母的聯繫人又在520頁的話,我們要找張三時直接找到Z,然後知道在520頁。這樣是不是比一頁一頁去找要快很多。

    我們可以使用一個數組,首先通過Hash運算,也就是取姓的首字母得到Z,然後可以根據ASCII碼計算Z是90,所以存放在下標爲90的位置,然後要找張三時只需要通過Hash運算得到Z然後再找到ASCII的90也就是取下標爲90的數據即可。

 

2.散列衝突

 

   首先散列表是作用於數組上的,因爲數組支持隨機訪問,所以能夠達到O(1)的時間複雜度,而散列表本身就是要達到O(1)的時間複雜度,可是如果散列衝突了怎麼辦呢?就像我們前面舉例那樣,比如張三首字母爲Z,也就是ASCII的90,我們存放在一個數組下標90中,而鄭立也是Z那麼這種情況怎麼辦呢?如果直接存放是不是就把原來的張三給覆蓋掉了呢。

 

 

2.1.開放尋址法

 

   開放尋址法的核心思想是,如果出現了散列衝突,我們就重新探測一個空閒位置,將其插入。那如何重新探測新的位置呢?當我們往散列表中插入數據時,如果某個數據經過散列函數散列之後,存儲位置已經被佔用了,我們就從當前位置開始,依次往後查找,看是否有空閒位置,直到找到爲止。如果數組整個都沒有空位置,這個時候就需要對數組進行擴容操作。

    而我們要獲取數據的時候就需要先Hash運算,然後得到下標後再去拿值,拿到值後要比對是不是要拿的數據,因爲有可能Hash衝突了,此時的值並不是你想要的,如果是就直接取出,不是的話就需要重新遍歷數組,直到找到對應的數。

從上面可以明顯的看出來開發尋址法並不是一種好的方案,當最好的情況時查詢數據時間複雜度爲O(1),而最壞的情況時就需要遍歷整個數組從而退化爲O(n),平均時間複雜度爲O(1)。

 

2.2.鏈表法

 

    而鏈表法就是如果衝突的話直接形成一個鏈表,相當於掛在了上一個元素上,我們獲取數據的時候只需要Hash運算後拿到下標,然後拿到鏈表比對是否爲獲取的數據即可,可能眨眼一看好像複雜度和開放尋址法也差不多。其實不然,首先Hash衝突並不是每次都會發生,其次因爲會不斷的進行動態擴容所以碰撞機率會減少,所以衝突的鏈表並不會像開放尋址法的數組那樣長。

    像JDK1.7的HashMap就是採用的這種方式來解決衝突的,而到了JDK1.8以後則換成了紅黑樹,原因就是因爲紅黑樹查詢的時間複雜度是比鏈表要快的。所以實際上我們是說的鏈表法,實際上我們還可以採用紅黑樹或者把鏈表改爲跳錶。

    看到這兒你或許應該明白了爲什麼Java中的HashMap無論是負載因子還是2的n次方擴容,都是因爲減少Hash衝突,而減少Hash衝突的原因就是讓時間複雜度降低到O(1),因爲一旦Hash衝突時間複雜度可能就不在是O(1)。

 

2.3.負載因子

 

    上面說過如果數據量到達一定量的時候我們是需要進行擴容的,而擴容實際上我們不需要等沒有位置的時候才進行,我們只要插入數據達到一定數量時就可以提前擴容。比如我們我們默認數組爲16,然後只要達到12時就就行擴容,然後我們可以算出其中比例是0.75,也就是負載因子。

    熟悉HashMap的人應該知道HashMap就是這樣操作的,但是這個負載因子實際上是要取一個合適的值的,如果你取1的話實際上很容易發生衝突,相當於滿了才進行擴容。而如果取太低的話又會出現空間的浪費,比如取0.5,實際上才一半就擴容了。

 

3.LRU緩存淘汰算法

 

   什麼是LRU緩存淘汰算法呢?我舉個例子,作爲一個Java的開發人員,時常會買一些技術書籍來看,但是家裏的書架只能放下10本,那麼如果我現在已經有了10本,又重新買了一本,我應該怎麼放呢?我這樣子操作,我把最近最少使用的書給扔掉,然後把新的書放上去就行了,但是怎麼看最近最少使用呢?我們只要每次看過的書都放在最上面,然後最下面的一本就是最近最少看的了。

   我們看一下LeetCode的第146題,對應的就是LRU緩存題目

 

 

   實際上我們可以有很多種解法來實現LRU緩存,但是題目中要達到時間複雜度爲O(1),如果使用鏈表或者數組都是不能實現的,這個時候就可以使用散列表了,每次get的時候如果存在此數據,那麼我們就將它移動到鏈表的尾部,這樣在淘汰時我們只需要刪除鏈表的首地址就行了,而鏈表的刪除操作時間複雜度也是O(1)的,所以採用散列表加鏈表就可以實現。

 

   下面我寫了兩個版本,第一個是採用了Java中自帶的HashTable來作爲散列,然後自定一個鏈表來實現,而另一個版本就是自定義一個散列表同時自定義一個鏈表來實現。使用自定義散列表和自定義鏈表的方案比較複雜實現圖如下。

 

 

   其中prer是指上一個的地址,而next就是下一個的地址,data爲存放數據的,可能最難理解的就是hnext,其實hnext是爲了解決hash衝突的,一旦衝突了我們就把他掛在與之對應衝突數據的hnext上,這樣我們去獲取數據時只需要先Hash運算,然後再去看data的值是不是我們要找的值,如果不是就再通過hnext去找即可。

 

   使用HashTable加雙向鏈表實現代碼如下

 

 

 使用自定義HashMap加雙向鏈表實現,前方高能

 

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