1. 程式人生 > >演算法筆記-散列表3

演算法筆記-散列表3

為什麼散列表和連結串列會經常一起使用

LRU快取淘汰演算法

總結連結串列那篇文章中,曾經實現過LRU快取淘汰演算法,但是當時的刪除插入操作時間複雜是 O(n)。

如果用散列表加連結串列的方式實現LRU快取淘汰演算法,時間複雜度就變成O(1)。

資料的存貯我們用散列表實現,雜湊衝突用連結串列法解決。如果有相同雜湊值的元素,我們就將其插入到該雜湊值對應位置連結串列的尾部,連結串列上的節點都有一個 hnext 指標,指向該連結串列的後後繼節點。

插入元素的排列順序,我們用雙向連結串列來維護。也就是說,散列表中的所有元素,除了因為雜湊衝突的原因,導致相同雜湊值元素之間有連結串列維護以外,還有一個雙向連結串列將所有的元素串聯起來。

查詢某一元素的時候,根據雜湊值可以迅速定位元素位置,時間複雜度 O(1)。

刪除某一元素的時候,查詢的時間複雜度是 O(1),又因為該節點維護在雙向連結串列中,所以刪除操作時間複雜度也是 O(1),總的操作時間複雜度是 O(1)。

插入某一元素的時候,首先在散列表中查詢該元素,若找到該元素則將其移動到雙向連結串列的尾部,時間複雜度為 O(1),如果沒有找到,則計算雜湊值存入散列表中,並將其維護到雙向連結串列的尾部,時間複雜度為 O(1)。

上述操作中,隱含了一些條件,比如需要明確知道頭結點,尾節點指標,這樣才能保證插入雙向連結串列尾部和頭部的時間複雜度為 O(1)。

Redis有序集合

跳錶那篇文章中總結到 Redis 的有序集合是通過跳錶實現的,其實不然。

Redis 的有序集合中的元素有兩個很重要的屬性:key 和 score,我們不僅通過 key 查詢資料還通過 score 查詢資料。

舉例:在有序集合中儲存學生以及成績資訊,key 代表的是學生的學號,score 代表的是學生的成績。那麼對學生資訊會有以下操作:
1:查詢某個學生資訊。
2:刪除某個學生資訊。
3:新增某個學生資訊。
4:查詢分數在某個區間的學生資訊。
5:按照分數對學生排序。

如果單以跳錶去儲存維護學生分數資訊,雖然保證了有序,但是查詢學生資訊的時間複雜度就是複雜了。

如果以學號為關鍵值用散列表儲存學生的資訊,查詢時間複雜度為 O(1) 了,但是實現上述 4,5 的操作就複雜的多了。

所以近似於上述 LRU快取淘汰演算法 的資料結構儲存學生資訊,就能兼顧對學生資訊的各種高效操作了,這就是 Redis 有序集合的實現原理。

Java LinkedHashMap

我在公司開發用到的主要語言就是 Java 其中的 LinkedHashMap 操作類經常用到,下面對其進行簡單分析。

先看程式碼

HashMap<Integer, Integer> m = new LinkedHashMap<>();
m.put(3, 11);
m.put(1, 12);
m.put(5, 23);
m.put(2, 22);

for (Map.Entry e : m.entrySet()) {
  System.out.println(e.getKey());
}

輸出結果是 3 1 5 2。首先從類名就看的出該操作類底層是 HashMap 實現的,但是輸出元素居然是有序的,說明該結構中有除了用到散列表還用到了連結串列,用連結串列去維護資料的有序性。

再看程式碼:

// 10 是初始大小,0.75 是裝載因子,true 是表示按照訪問時間排序
HashMap<Integer, Integer> m = new LinkedHashMap<>(10, 0.75f, true);
m.put(3, 11);
m.put(1, 12);
m.put(5, 23);
m.put(2, 22);

m.put(3, 26);
m.get(5);

for (Map.Entry e : m.entrySet()) {
  System.out.println(e.getKey());
}

首先,當 m 分別放入 3,1,2,5這三個元素的時候,我們是知道這些資料是有序的,按照插入散列表中時間循序排列。當再次插入 key 值為 3 的元素的時候,LinkedHashMap 內部是先找到 key = 3 的元素,將其刪除,然後插入新的 key = 3 的元素,並將其維護在連結串列的尾部。當訪問 key = 5 的元素的時候,查詢到該元素,並將其維護在連結串列的尾部。所以最終的輸出結果是 1 2 3 5。

經過上述兩段程式碼,我們可以發現 Java 中的 LinkedHashMap 其實實現的就是LRU快取淘汰演算法。

總結

其實,總結了這麼多突然發現重要的其實就是兩種資料結構:陣列和連結串列。陣列支援隨機訪問,但是插入刪除不夠高效,連結串列插入刪除快,但是隨機訪問卻低效。陣列需要的是連續空間,對記憶體要求比較高,連結串列佔用的是隨機記憶體地址,提高了記憶體利用率。將陣列與連結串列相互結合起來,取長補短,就能實現高效能的增刪改查功能。

本文創作靈感來源於 極客時間 王爭老師的《資料結構與演算法之美》課程,通過課後反思以及借鑑各位學友的發言總結,現整理出自己的知識架構,以便日後溫故知新,查漏補缺。

初入演算法學習,必是步履蹣跚,一路磕磕絆絆跌跌撞撞。看不懂別慌,也別忙著總結,先讀五遍文章先,無他,唯手熟爾~
與諸君共勉

關注本人公眾號,第一時間獲取最新文章釋出,每日更新一篇技術文章。

在這裡插入圖片描述