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

演算法筆記-散列表2

如何打造一個工業級別的散列表

在散列表中:雜湊函式,擴容機制,衝突解決等是決定散列表效能的重要因素,下面針對這三者分析它們是如何影響散列表的效能的。

雜湊函式

雜湊函式的設計不能太過複雜,否則計算雜湊值的時候勢必會消耗更多的時間。
雜湊函式生成的值要儘可能隨機且自由分佈,否則勢必會引起更多的雜湊衝突

雜湊函式的設計過程中,我們要綜合考慮資料關鍵字的型別,長度,規模以及分佈。上篇文章中,針對運動員的編號分析創建出雜湊函式,這種方法叫做“資料分析法”。當然還有其他直接定址法,平方取中法,摺疊法,隨機數法等等。

散列表的擴容

在散列表中,裝載因子的高低說明了散列表中元素的多少,空閒地址的多少,引發雜湊衝突概率的大小。同時,也體現了在散列表中插入刪除以及查詢資料的快慢。

對於靜態資料集合,可以設計出穩定高效的雜湊函式,保證散列表的高效能。

對於動態資料集合,由於資料規模的變化,裝載因子也跟著變化。當資料規模大的時候,雜湊衝突就愈發明顯,整個散列表的效能機會急劇下降。這個時候,我們就需要對散列表擴容。擴容之後,我們對資料搬移的過程中,就要重新計算雜湊值,重新確定元素在散列表中的位置。

散列表的擴容,是因為裝載因子值過大,雜湊衝突高頻發生。如果,裝載因子過小,說明散列表中空閒地址過多,造成了資源浪費,所以就需要縮蓉。所以,裝載因子的設定,要充分考慮到散列表中雜湊衝突發生的概率以及散列表記憶體地址的高效使用。

低效散列表的擴容:當往散列表中插入資料的時候發現散列表需要擴容,這個時候直接申請記憶體空間,搬移散列表資料,最後在新的散列表中插入資料。這個插入過程不僅包含了插入操作,還包含了資料搬移的工作,插入時間相當長,這種一次性擴容的方法效率不高。

高效散列表的擴容:擴容的時候,先申請記憶體空間,然後不要一次性把所有的元素搬移到新的散列表中。每次插入新資料的時候,插入到新的散列表的中,並且從舊散列表中取出一個數據放入新的散列表中。每次查詢資料的時候,先從舊的散列表中查詢,若未查到,再到新的散列表中查詢。經過多次重複這樣的操作,舊散列表中的資料就會一點一點的轉移到新的散列表中。

雜湊衝突的解決

開放定址法:例如 Java 中 ThreadLocalMap
開發定址法中,散列表中的資料都儲存在陣列中,可以有效利用CPU快取加速查詢。這種方法實現的散列表,序列化方便。缺點就是衝突代價更高,裝載因子不能大於 1。試用場景就是:當資料規模較小,裝載因子小的時候,適合採用這種方法。

連結串列法:例如 Java 中 LinkedHashMap
連結串列法的記憶體利用率高,不用像陣列那樣提前申請號記憶體空間,而是在需要的時候再申請記憶體節點。對於雜湊衝突的元素,該雜湊值位置對應的是一個連結串列,可以直接將資料插入的連結串列的尾部,所以用連結串列法實現的散列表,裝載因子可以很大。即便裝載因子過大,連結串列資料過多,我們可以將連結串列轉化為跳錶或者是紅黑樹,查詢資料的時間複雜度就是 O(logn) 不會是 O(n)。當然,它的缺點就是記憶體訪問不友好,佔用的記憶體都是零散的記憶體地址,不想陣列那般是一個連續的記憶體空間。所以,連結串列法實現的散列表適合大資料規模,優化策略是可以將連結串列轉化為跳錶或者紅黑樹這種結構。

工業散列表的分析

Java 中的 HashMap:
初始大小:16,也可以修改預設值設定初始大小。
裝載因子和動態擴容:裝載因子是 0.75,每次擴容都是擴大為原來的兩倍。
雜湊衝突解決辦法:底層採用連結串列法解決雜湊衝突,JDK1.8以後修改了連結串列的資料結構。當連結串列中的元素個數大於 8 的時候,連結串列轉化為紅黑樹,當連結串列元素個數小於 6 的時候轉化為連結串列。
雜湊函式:見程式碼

int hash(Object key) {
    int h = key.hashCode()return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}

public int hashCode() {
  int var1 = this.hash;
  if(var1 == 0 && this.value.length > 0) {
    char[] var2 = this.value;
    for(int var3 = 0; var3 < this.value.length; ++var3) {
      var1 = 31 * var1 + var2[var3];
    }
    this.hash = var1;
  }
  return var1;
}
如何打造一個工業級別的散列表

一個工業級別的散列表應該具備以下特點:
效能穩定,極端情況下效能也不會退化到難以接受的地步(時間複雜度的考量)。
記憶體空間使用合理,不浪費過多的記憶體空間(空間複雜度的考量)。
支援高效的插入刪除查詢等操作(綜合考量時間複雜度和空間複雜度)。

為了保證以上特點,我們就要考慮設計一個合適的雜湊函式,保證資料隨機性以及均勻分佈。定義合適的裝載因子值,設計動態擴容縮容的策略,不浪費空間又保證雜湊衝突發生低頻性。選擇合適的雜湊衝突解決方式,綜合考量資料的規模特點等等。

總結

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

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

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

在這裡插入圖片描述