【從蛋殼到滿天飛】JS 數據結構解析和算法實現-哈希表

思惟導圖

前言

【從蛋殼到滿天飛】JS 數據結構解析和算法實現,所有文章大概的內容以下: Arrays(數組)、Stacks(棧)、Queues(隊列)、LinkedList(鏈表)、Recursion(遞歸思想)、BinarySearchTree(二分搜索樹)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(優先隊列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(哈希表)html

源代碼有三個:ES6(單個單個的 class 類型的 js 文件) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)git

所有源代碼已上傳 github,點擊我吧,光看文章可以掌握兩成,動手敲代碼、動腦思考、畫圖才能夠掌握八成。github

本文章適合 對數據結構想了解而且感興趣的人羣,文章風格一如既往如此,就以爲手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時間跨度也算將近半年時間了,但願對想學習數據結構的人或者正在學習數據結構的人羣有幫助。算法

哈希表

  1. 哈希表相對於以前實現的那些數據結構來講數組

    1. 哈希表是一個相對比較簡單的數據結構,
    2. 對於哈希表來講也有許多相對比較複雜的研究,
    3. 不過對於這些研究大多數都是比較偏數學的,
    4. 對於普通的軟件工程軟件開發來說,
    5. 使用哈希表瞭解哈希表的底層實現,並不須要知道那麼多的複雜深奧的內容,
  2. 經過 leetcode 上的題目來看哈希表緩存

    1. leetcode 上第 387 號問題,在解決這個問題的時候,
    2. 開闢的一個 26 個空間的數組就是哈希表,
    3. 實際上真正想作是每個字符和一個數字之間進行一個映射的關係,
    4. 這個數字是這個字符在字符串中出現的頻率,
    5. 使用一個數組就能夠解決這個問題,
    6. 那是由於將每個字符都和一個索引進行了對應,
    7. 以後直接用這個索引去數組中尋找相應的對應信息,也就是映射的內容,
    8. 二十六的字符對應的索引就是數組中的索引下標,
    9. 當每個字符與索引對應了,
    10. 那麼對這個字符所對應的對應的內容增刪改查都是 O(1)級別的,
    11. 那麼這就是哈希表這種數據結構的巨大優點,
    12. 它的本質其實就是將你真正關心的內容轉換成一個索引,
    13. 如字符對應的內容轉換成一個索引,而後直接使用數組來存儲相應的內容,
    14. 因爲數組自己是支持隨機訪問的,
    15. 因此可使用 O(1)的時間複雜度來完成各項操做,
    16. 這就是哈希表。
    // 答題
    class Solution {
       // leetcode 387. 字符串中的第一個惟一字符
       firstUniqChar(s) {
          /** * @param {string} s * @return {number} */
          var firstUniqChar = function(s) {
             const hashTable = new Array(26);
    
             for (var i = 0; i < hashTable.length; i++) hashTable[i] = 0;
    
             for (const c of s) hashTable[c.charCodeAt(0) - 97]++;
    
             for (var i = 0; i < hashTable.length; i++)
                if (hashTable[s[i].charCodeAt(0) - 97] === 1) return i;
    
             return -1;
          };
          /** * @param {string} s * @return {number} */
          var firstUniqChar = function(s) {
             const hashTable = new Array(26);
             const letterTable = {};
    
             for (var i = 0; i < hashTable.length; i++) {
                letterTable[String.fromCharCode(i + 97)] = i;
                hashTable[i] = 0;
             }
    
             for (const c of s) hashTable[letterTable[c]]++;
    
             for (var i = 0; i < s.length; i++)
                if (hashTable[letterTable[s[i]]] === 1) return i;
    
             return -1;
          };
    
          return firstUniqChar(s);
       }
    }
    複製代碼
  3. 哈希表是對於你所關注的內容將它轉化成索引安全

    1. 如上面的題目中,
    2. 你關注的是字符它所對應的頻率,
    3. 那麼對於每個字符來講必須先把它轉化成一個索引,
    4. 更通常的在一個哈希表中是能夠存儲各類數據類型的,
    5. 對於每種數據類型都須要一個方法把它轉化成一個索引,
    6. 那麼相應的關心的這個類型轉換成索引的這個函數就稱之爲是哈希函數,
    7. 在上面的題目中,哈希函數能夠寫成fn(char1) = char1 -'a'
    8. 這 fn 就是函數,char1 就是給定的字符,
    9. 經過這個函數 fn 就把 char1 轉化成一個索引,
    10. 這個轉化的方法體就是char1 -'a'
    11. 有了哈希函數將字符轉化爲索引以後,以後就只須要在哈希表中操做便可,
    12. 在上面的題目中只是簡單的將鍵轉化爲索引,因此很是的容易,
    13. 還有如一個班裏有 30 名學生,從 1-30 給這個學生直接編號便可,
    14. 而後在數組中去存取這個學生的信息時直接用編號-1
    15. 做爲數組的索引這麼簡單,經過-1 就將鍵轉化爲了索引,太容易了。
    16. 在大多數狀況下處理的數據是很是複雜的,
    17. 如一個城市的居民的信息,那麼就會使用居民的身份證號來與之對應,
    18. 可是居民的身份證號有 18 位數,那麼就不能直接用它做爲數組的索引,
    19. 複雜的還有字符串,如何將一個字符串轉換爲哈希表中的一個索引,
    20. 還有浮點數,或者是一個複合類型好比日期年月日時分秒,
    21. 那麼這些類型就須要先將它們轉化爲一個索引纔可使用,
    22. 相應的就須要合理的設計一個哈希函數,
    23. 那麼多的數據類型,因此很難作到每個經過哈希函數
    24. 都能轉化成不一樣的索引從而實現一一對應,
    25. 並且這個索引的值它要很是適合做爲數組所對應的索引。
  4. 這種狀況下不少時候就不得不處理一個在哈希表中很是關鍵的問題數據結構

    1. 兩個不一樣的鍵經過哈希函數它能對應一樣一個索引,
    2. 這就是哈希衝突,
    3. 因此在哈希表上的操做也就是在解決這種哈希衝突,
    4. 若是設計的哈希函數很是好都是一一對應的,
    5. 那麼對哈希表的操做也會很是的簡單,
    6. 不過對於更通常的狀況,在哈希表上的操做主要考慮怎麼解決哈希衝突問題。
  5. 哈希表充分的體現了算法設計領域的經典思想dom

    1. 使用空間來換取時間。
    2. 不少算法問題不少經典算法在本質上就是使用空間來換取時間,
    3. 不少時候多存儲一些東西或者預處理一些東西緩存一些東西,
    4. 那麼在實際執行算法任務的時候完成這個任務獲得這個結果就會快不少,
    5. 對於哈希表就很是完美的體現了這一點,
    6. 例如鍵對應了身份證號,假如能夠開闢無限大的空間,
    7. 這個空間大小有 18 個 9 那麼大,而且它仍是一個數組,
    8. 那麼徹底就可使用O(1)的時間完成各項操做,
    9. 可是很難開闢一個這麼大的空間,就算空間中每個位置只存儲 32 位的整型,
    10. 一個字節八個位,就是 4 個字節,4byte 乘以 18 個九,
    11. 也就是接近 37 萬 TB 的空間,太大了。
    12. 相反,若是空間的大小隻有 1 這麼大,
    13. 那麼就表明了存儲的全部內容都會產生哈希衝突,
    14. 把全部的內容都堆在惟一的數組空間中,
    15. 假設以鏈表的方式來組織總體的數據,
    16. 那麼相應的各項操做完成的時間複雜度就會是O(n)級別。
    17. 以上就是設計哈希表的極端狀況,
    18. 若是有無限的空間,各項操做都能在O(1)的時間完成,
    19. 若是隻有 1 的空間,各項操做只能在O(n)的時間完成。
    20. 哈希表總體就是在這兩者之間產生一個平衡,
    21. 哈希表是時間和空間之間的平衡。
  6. 對哈希表總體來講這個數組能開多大空間是很是重要的ide

    1. 雖然如此,哈希表總體,哈希函數的設計依然是很是重要的,
    2. 不少數據類型自己並不能很是天然的和一個整型索引相對應,
    3. 因此必須想辦法讓諸如字符串、浮點數、複合類型日期
    4. 可以跟一個整型把它看成索引來對應。
    5. 就算你能開無限的空間,可是把身份證號做爲索引,
    6. 可是 18 位如下及 18 位以上的空間所有都是浪費掉的,
    7. 因此對於哈希表來講,還但願,
    8. 對於每個經過哈希函數獲得索引後,
    9. 這個索引的分佈越均勻越好。

哈希函數的設計

  1. 哈希表這種數據結構

    1. 其實就是把所關心的鍵經過哈希函數轉化成一個索引,
    2. 而後直接把內容存到一個數組中就行了。
  2. 對於哈希表來講,關心的主要有兩部份內容

    1. 第一部分就是哈希函數的設計,
    2. 第二部分就是解決哈希函數生成的索引相同的衝突,
    3. 也就是解決哈希衝突如何處理的問題。
  3. 哈希函數的設計

    1. 經過哈希函數獲得的索引分佈越均勻越好。
    2. 雖然很好理解,可是想要達到這樣的條件是很是難的,
    3. 對於數據的存儲的數據類型是五花八門,
    4. 因此對於一些特殊領域,有特殊領域的哈希函數設計方式,
    5. 甚至有專門的論文來討論如何設計哈希函數,
    6. 也就說明哈希函數的設計實際上是很是複雜的。
  4. 最通常的哈希函數設計原則

    1. 將全部類型的數據相應的哈希函數的設計都轉化成是
    2. 對於整型進行一個哈希函數的過程。
    3. 小範圍的正整數直接使用它來做爲索引,
    4. 如 26 個字母的 ascll 碼或者一個班級的學生編號。
    5. 小範圍的負整數進行偏移,對於數組來講索引都是天然數,
    6. 也就是大於等於 0 的數字,作一個簡單的偏移便可,
    7. 將它們都變完成天然數,如-100~100,讓它們都加 100,
    8. 變成0~200就能夠了,很是容易。
    9. 大整數如身份證號轉化爲索引,一般作法是取模運算,
    10. 好比取這個大整數的後四位,等同於mod 10000
    11. 可是這樣就存在陷阱,這個哈希表的數組最大隻有一萬空間,
    12. 對於哈希表來講空間越大,就越難發生哈希衝突,
    13. 那麼你能夠取這個大整數的後六位,等同於mod 1000000
    14. 可是對於身份證後四位來講,
    15. 這四位前面的八位實際上是一我的的生日,
    16. 如 110108198512166666,取模後六位就是 166666,
    17. 這個 16 實際上是日期,數值只在 1-31 之間,永遠不可能取 99,
    18. 而且只取模後六位,並無利用身份證上全部的信息,
    19. 因此就會形成分佈不均勻的狀況。
  5. 取模的數字選擇很重要,

    1. 因此纔會對哈希函數的設計,不一樣的領域有不一樣的作法,
    2. 就算對身份證號的哈希函數設計的時候都要具體問題具體分析,
    3. 哈希函數設計在不少時候很難找到通用的通常設計原則,
    4. 具體問題具體分析在特殊的領域是很是重要的,
    5. 像身份證號,有一個簡單的解決方案能夠解決分佈不均勻的問題,
    6. 模一個素數,一般狀況模一個素數都能更好的解決分佈均勻的問題,
    7. 因此就能夠更有效的利用這個大整數中的信息,
    8. 之因此模一個素數能夠更有效的解決這個問題,
    9. 這是因爲它背後有必定的數學理論作支撐,它自己屬於數論領域,
    10. 以下圖所示,模 4 就致使了分佈不均勻、哈希衝突,
    11. 可是模 7 就不同了,分佈更加均勻減小了哈希衝突,
    12. 因此須要看你存儲的數據是否有規律,
    13. 一般狀況下模一個素數獲得的結果會更好,
    14. http://planetmath.org/goodhashtableprimes
    15. 能夠從這個網站中看到,根據你的數據規模,你取模多大一個素數是合適的,
    16. 例如你存儲的數據在 2^5 至 2^6 時,你能夠取模 53,哈希衝突的機率是 10.41667,
    17. 例如你存儲的數據在 2^23 至 2^24 你能夠取模 12582917,衝突機率是 0.000040,
    18. 這些都有人研究的,因此你能夠從這個網站中去看。
    19. 不用去深究,只要瞭解這個大的基本原則便可。
    // 10 % 4 ---> 2 10 % 7 --->3
    // 20 % 4 ---> 0 20 % 7 --->6
    // 30 % 4 ---> 2 30 % 7 --->2
    // 40 % 4 ---> 0 40 % 7 --->4
    // 50 % 4 ---> 2 50 % 7 --->1
    複製代碼
  6. 浮點型的哈希函數設計

    1. 將浮點型的數據轉化爲一個整數的索引,

    2. 在計算機中都 32 位或者 64 位的二進制表示,只不過計算機解析成了浮點數,

    3. 若是鍵是浮點型的話,那麼就可使用浮點型所存儲的這個空間,

    4. 把它看成是整型來進行處理,

    5. 也就是把這個浮點型所佔用的 32 位空間或 64 位空間使用整數的方式來解析,

    6. 那麼這篇空間一樣能夠能夠表示一個整數,

    7. 以後就能夠將一個大的整數轉成整數相應的方式,也就是取模的方式,

    8. 這樣就解決了浮點型的哈希函數的設計的問題

      // // 單精度
      // 8-bit 23-bit
      // 0 | 0 1 1 1 1 1 0 0 | 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
      // 31 23 0
      
      // //雙進度
      // 11-bit 52-bit
      // 0|011111111100|0100000000000000000000000000000000000000000000000000
      // 63 52 0
      複製代碼
  7. 字符串的哈希函數設計

    1. 字符串相對浮點型來講更加特殊一些,
    2. 浮點型依然是佔 32 位或 64 位這樣的空間,
    3. 而字符串能夠有若干個字符來組合,它所佔的空間數量是不固定的,
    4. 儘管如此,對於字符串的哈希函數設計,依然能夠將它轉成大整型處理,
    5. 例如一個整數能夠轉換成每一位數字的十進制表示法,
    6. 166 = 1 * 10^2 + 6 * 10^1 + 6 * 10^0
    7. 這樣就至關於把一個整數看做是一個字符串,每個字符就是一個數字,
    8. 按照這種方式,就能夠把字符串中每個字符拆分出來,
    9. 若是是英文就能夠把它做爲 26 進制的整數表示法,
    10. code = c * 26^3 + o * 26^2 + d * 26^1 + e * 26^0
    11. c 在 26 進制中對應的是 3,其它的相似,
    12. 這樣一來就能夠把一個字符串看做是 26 進制的整型,
    13. 之因此用 26,這是由於一共有 26 個小寫字母,這個進制是能夠選的,
    14. 例如字符串中大小寫字母都有,那麼就是 52 進制,若是還有各類標點符號,
    15. 那麼就可使 256 進制等等,因爲這個進制能夠選,那麼就可使用一個標記來代替,
    16. 如大 B,也就是 basics(基本)的意思,
    17. 那麼表達式是code = c * B^3 + o * B^2 + d * B^1 + e * B^0
    18. 最後的哈希函數就是
    19. hash(code) = (c * B^3 + o * B^2 + d * B^1 + e * B^0) % M
    20. 這個 M 對應的取模的方式中那個素數,
    21. 這個 M 也表示了哈希表的那個數組中一共有多少個空間,
    22. 對於這種表示的樣子,這個 code 一共有四個字符,因此最高位的 c 字符乘以 B 的三次方,
    23. 若是這個字符串有一百個字符,那麼最高位的 c 字符就要乘以 B 的 99 次方,
    24. 不少時候計算 B 的 k 次方,這個 k 比較大的話,這個計算過程也是比較慢的,
    25. 因此對於這個式子一個常見的轉化形式就是
    26. hash(code) = ((((c * B) + o) * B + d) * B + e) % M
    27. 將字符串轉換成大整型的一個括號轉換成了四個,
    28. 在每個括號裏面作的事情都是拿到一個字符乘以 B 獲得的結果再加上下一個字符,
    29. 再乘以 B 獲得的結果在加上下一個字符,
    30. 再乘以 B 獲得的結果直到加到最後一個字符爲止,
    31. 這樣套四個括號以後,這個式子和那個套一個括號的式子實際上是等價的,
    32. 就是一種簡單的變形,這樣就不須要先算 B^99 而後再算 B^98 等等這麼複雜了,
    33. 每一次都須要乘以一個 B 再加上下一個字符再乘以 B 依此類推就好,
    34. 那麼使用程序實現的時候計算這個哈希函數相應的速度就會快一些,
    35. 這是一個很通用的數學技巧,是數學中的多項式就是這樣的,
    36. 可是這麼加可能會致使整型的溢出,
    37. 那麼就能夠將這個取模的過程分別放入每一個括號裏面,
    38. 這樣就能夠轉化成這種形式
    39. hash(code) = ((((c % M) * B + o) % M * B + d) % M * B + e) % M
    40. 這樣一來,每一次都計算出了比 M 更小的數,因此根本就不用擔憂整型溢出的問題,
    41. 這就是數論中的模運算的一個很重要的性質。
    //hash(code) = ((((c % M) * B + o) % M * B + d) % M * B + e) % M
    
    // 上面的公式中 ((((c % M) * B + o) % M * B + d) % M * B + e) % M
    // 對應下面的代碼,只須要一重for循環便可,最終的到的就是整個字符串的哈希值
    let s = 'code';
    let hash = 0;
    for (let i = 0; i < s.length; i++) hash = (hash * B + s.charAt(i)) % M;
    複製代碼
  8. 複合類型的哈希函數設計

    1. 好比一個學生類,裏面包括了他的年級、班級、姓名等等信息,
    2. 或者一個日期類,裏面包含了年、月、日、時、分、秒、毫秒等等信息,
    3. 依然是轉換成整型來處理,處理方式和字符串是同樣的,
    4. 也是hash(code) = ((((c % M) * B + o) % M * B + d) % M * B + e) % M
    5. 徹底套用這個公式,只不過是這樣套用的,
    6. 日期格式是這樣的,Date:year,month,day,
    7. hash(date) = ((((date.year%M) * B + date.month) % M * B + date.day) % M * B + e) % M
    8. 根據你複合類的不一樣,
    9. 可能須要對 B 的值也就是進制進行一下設計從而選取一個更合理的數值,
    10. 整個思路是一致的。
  9. 哈希函數設計通常來講對任何數據類型都是將它轉換成整型來處理。

    1. 轉換成整型並非哈希函數設計的惟一方法,
    2. 只不過這是一個比較普通比較經常使用比較通用的一種方法,
    3. 在不少特殊的領域有不少相關的論文去講更多的哈希函數設計的方法。

哈希函數的設計,一般要遵循三個原則

  1. 一致性:若是 a==b,則 hash(a)==hash(b)。
    1. 若是兩個鍵相等,那麼扔進哈希函數以後獲得的值也必定要相等,
    2. 可是對於哈希函數來講反過來是不必定成立的,
    3. 一樣的一個哈希值頗有可能對應了兩個不一樣的數據或者不一樣的鍵,
    4. 這就是所謂的哈希衝突的狀況。
  2. 高效性:計算高效簡便。
    1. 使用哈希表就是爲了可以高效的存儲,
    2. 那麼在使用哈希函數計算的時候耗費太多的性能那麼就太得不償失了。
  3. 均勻性:哈希值均勻分佈。
    1. 使用哈希函數以後獲得的索引值就應該儘可能的均勻,
    2. 對於通常的整型能夠經過模一個素數來讓它儘可能的均勻,
    3. 這個條件雖然看起來很簡單,可是真正要知足這個條件,
    4. 探究這個條件背後的數學性質仍是很複雜的一個問題。

js 中 自定義 hashCode 方法

  1. 在 js 中自定義數據類型

    1. 對於本身定義的複合類型,如學生類、日期類型,
    2. 你能夠經過寫 hashCode 方法,
    3. 而後本身實現一下這個方法從新生成 hash 值。
  2. Student

    // Student
    class Student {
       constructor(grade, classId, studentName, studentScore) {
          this.name = studentName;
          this.score = studentScore;
          this.grade = grade;
          this.classId = classId;
       }
    
       //@Override hashCode 2018-11-25-jwl
       hashCode() {
          // 選擇進制
          const B = 31;
    
          // 計算hash值
          let hash = 0;
          hash = hash * B + this.getCode(this.name.toLowerCase());
          hash = hash * B + this.getCode(this.score);
          hash = hash * B + this.getCode(this.grade);
          hash = hash * B + this.getCode(this.classId);
    
          // 返回hash值
          return hash;
       }
    
       //@Override equals 2018-11-25-jwl
       equals(obj) {
          // 三重判斷
          if (!obj) return false;
          if (this === obj) return true;
          if (this.valueOf() !== obj.valueOf()) return false;
    
          // 對屬性進行判斷
          return (
             this.name === obj.name &&
             this.score === obj.score &&
             this.grade === obj.grade &&
             this.classId === obj.classId
          );
       }
    
       // 拆分字符生成數字 -
       getCode(s) {
          s = s + '';
          let result = 0;
          // 遍歷字符 計算結果
          for (const c of s) result += c.charCodeAt(0);
    
          // 返回結果
          return result;
       }
    
       //@Override toString 2018-10-19-jwl
       toString() {
          let studentInfo = `Student(name: ${this.name}, score: ${this.score})`;
          return studentInfo;
       }
    }
    複製代碼
  3. Main

    // main 函數
    class Main {
       constructor() {
          // var s = "leetcode";
          // this.show(new Solution().firstUniqChar(s) + " =====> 返回 0.");
    
          // var s = "loveleetcode";
          // this.show(new Solution().firstUniqChar(s) + " =====> 返回 2.");
    
          const jwl = new Student(10, 4, 'jwl', 99);
          this.show(jwl.hashCode());
          console.log(jwl.hashCode());
    
          const jwl2 = new Student(10, 4, 'jwl', 99);
          this.show(jwl2.hashCode());
          console.log(jwl2.hashCode());
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

哈希衝突的處理-鏈地址法(Seperate Chaining)

  1. 哈希表的本質就是一個數組
    1. 對於一個哈希表來講,對於一個整數求它的 hash 值的時候會對一個素數取模,
    2. 這個素數就是這個數組的空間大小,也能夠把它稱之爲 M,
  2. 在 強類型語言 中獲取到的 hash 值多是一個負數,因此就須要進行處理一下
    1. 最簡單的,直接獲取這個 hash 值的絕對值就能夠了,
    2. 可是不少源碼中,是這樣的一個表示 (hashCode(k1) & 0x7fffffff) % M
    3. 也就是讓 hash 值和一個十六進制的數字進行一個按位與,
    4. 按位與以後再對 M 進行一個取模操做,這和直接獲取這個 hash 值的正負號去掉是同樣的,
    5. 在十六進制中,每一位表示的是四個 bit,那麼 f 表示的就是二進制中的1111
    6. 七個 f 表示的是二進制中的 28 個 1,7 表示的是二進制中的111
    7. 那麼0x7fffffff表示的二進制就是 31 個 1,hash 值對 31 個 1 進行一下按位與,
    8. 在計算機中整型的表示是用的 32 位,其中最高位就是符號位,若是和 31 個 1 作按位與,
    9. 那麼相應的最高爲實際上是 0,這樣操做的結果其實就是最高位的結果,確定是 0,
    10. 而這個 hash 值對應的二進制表示的那 31 位
    11. 再和 31 個 1 進行按位與以後任然保持原來的樣子,
    12. 也就是這個操做作的事情實際上就是把 hash 值整型對應的二進制表示的最高位的 1 給抹去,
    13. 給抹成了 0,若是它原來是 0 的,那麼任然是 0,
    14. 這是由於在計算機中對整型的表示最高位是符號位,若是最高位是 1 表示它是一個負數,
    15. 若是最高位是 0 表示它是一個正數,那麼抹去 1 就至關於把負號去掉了。
    16. 在 js 中這樣作效果很差,因此須要本身根據實際狀況來寫一塊兒算法,如經過時間戳來進行這種操做。
  3. 鏈地址法
    1. 根據元素的哈希值計算出索引後,根據索引來哈希表中的數組裏存儲數據,
    2. 若是索引相同的話,那麼就以鏈表的方式將新元素掛到數組對應的位置中,
    3. 這樣就很好的解決了哈希衝突的問題了,由於每個位置都對應了一個鏈,
    4. 它的本質就是一個查找表,查找表的本質不必定是使用鏈表,
    5. 它的底層其實還可使用樹結構如平衡樹結構,
    6. 對於哈希表的數組中每個位置存的不是一個鏈表而是一個 Map,
    7. 經過哈希值計算出索引後,根據索引找到數組中對應的位置以後,
    8. 就能夠把你要存儲的元素插入該位置的 紅黑樹 裏便可,
    9. 那麼這個 Map 本質就是一個 紅黑樹 Map 數組,這是映射的形式,
    10. 若是你真正要實現的是一個集合,那麼也可使用 紅黑樹 Set 數組,
    11. 哈希表的數組中每個位置存的都是一個查找表,
    12. 只要這個數據結構適合做爲查找表就能夠了,它是能夠有不一樣的底層實現,
    13. 哈希表的數組中每個位置也能夠對應的是一個鏈表,
    14. 當數據規模比較小的時候,其實鏈表要比紅黑樹要快的,
    15. 數據規模比較小的時候使用紅黑樹可能更加耗費性能,如各類旋轉操做,
    16. 由於它要知足紅黑樹的性能,因此反而會慢一些。

實現本身的哈希表

  1. 以前實現的樹結構中都須要進行比較
    1. 其中的鍵都須要實現 compare 這個用來比較兩個元素的方法,
    2. 由於須要經過鍵來進行比較,
    3. 對於哈希表來講沒有這個要求,
    4. 這個 key 不須要實現這個方法。
  2. 在哈希表中存儲的元素都須要實現能夠用來獲取 hashCode 的方法。
  3. 對於哈希表來講相應的開多少空間是很是重要的
    1. 開的空間越合適,那麼相應的哈希衝突就越少,
    2. 空間大小能夠參考http://planetmath.org/goodhashtableprimes
    3. 根據存儲數據的多少來開闢合適的空間,可是不少時候並不知道要開多少的空間,
    4. 此時使用哈希表並不能合理的估計一個 M 值,因此須要進行優化。

代碼示例

  1. MyHashTable

    // 自定義的hash生成類。
    class MyHash {
       constructor() {
          this.store = new Map();
       }
    
       // 生成hash
       hashCode(key) {
          let hash = this.store.get(key);
          if (hash !== undefined) return hash;
          else {
             // 若是 這個hash沒有進行保存 就生成,而且記錄
             let hash = this.calcHashTwo(key);
    
             // 記錄
             this.store.set(key, hash);
    
             // 返回hash
             return hash;
          }
       }
    
       // 獲得的數字比較小 六七位數 如下 輔助函數:生成hash -
       calcHashOne(key) {
          // 生成hash 隨機小數 * 當前日期毫秒 * 隨機小數
          let hash = Math.random() * Date.now() * Math.random();
    
          // hash 取小數部分的字符串
          hash = hash.toString().replace(/^\d*\.\d*?([1-9]+)$/, '$1');
    
          hash = parseInt(hash); // 取整
    
          return hash;
       }
    
       // 獲得的數字很大 十幾位數 左右 輔助函數:生成hash -
       calcHashTwo(key) {
          // 生成hash 隨機小數 * 當前日期毫秒 * 隨機小數
          let hash = Math.random() * Date.now() * Math.random();
    
          // hash 向下取整
          hash = Math.floor(hash);
          return hash;
       }
    }
    
    class MyHashTableBySystem {
       constructor(M = 97) {
          this.M = M; // 空間大小
          this.size = 0; // 實際元素個數
          this.hashTable = new Array(M); // 哈希表
          this.hashCalc = new MyHash(); // 哈希值計算
    
          // 初始化哈希表
          for (var i = 0; i < M; i++) {
             // this.hashTable[i] = new MyAVLTree();
             this.hashTable[i] = new Map();
          }
       }
    
       // 根據key生成 哈希表索引
       hash(key) {
          // 獲取哈希值
          let hash = this.hashCalc.hashCode(key);
          // 對哈希值轉換爲32位的整數 再進行取模運算
          return (hash & 0x7fffffff) % this.M;
       }
    
       // 獲取實際存儲的元素個數
       getSize() {
          return this.size;
       }
    
       // 添加元素
       add(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          // 若是存在就覆蓋
          if (map.has(key)) map.set(key, value);
          else {
             // 不存在就添加
             map.set(key, value);
             this.size++;
          }
       }
    
       // 刪除元素
       remove(key) {
          const map = this.hashTable[this.hash(key)];
    
          let value = null;
          // 存在就刪除
          if (map.has(key)) {
             value = map.delete(key);
             this.size--;
          }
    
          return value;
       }
    
       // 修改操做
       set(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          if (!map.has(key)) throw new Error(key + " doesn't exist!");
    
          map.set(key, value);
       }
    
       // 查找是否存在
       contains(key) {
          return this.hashTable[this.hash(key)].has(key);
       }
    
       // 查找操做
       get(key) {
          return this.hashTable[this.hash(key)].get(key);
       }
    }
    
    // 自定義的哈希表 HashTable 基於使系統的Map 底層是哈希表+紅黑樹
    // 自定義的哈希表 HashTable 基於本身的AVL樹
    class MyHashTableByAVLTree {
       constructor(M = 97) {
          this.M = M; // 空間大小
          this.size = 0; // 實際元素個數
          this.hashTable = new Array(M); // 哈希表
          this.hashCalc = new MyHash(); // 哈希值計算
    
          // 初始化哈希表
          for (var i = 0; i < M; i++) {
             // this.hashTable[i] = new MyAVLTree();
             this.hashTable[i] = new MyAVLTreeMap();
          }
       }
    
       // 根據key生成 哈希表索引
       hash(key) {
          // 獲取哈希值
          let hash = this.hashCalc.hashCode(key);
          // 對哈希值轉換爲32位的整數 再進行取模運算
          return (hash & 0x7fffffff) % this.M;
       }
    
       // 獲取實際存儲的元素個數
       getSize() {
          return this.size;
       }
    
       // 添加元素
       add(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          // 若是存在就覆蓋
          if (map.contains(key)) map.set(key, value);
          else {
             // 不存在就添加
             map.add(key, value);
             this.size++;
          }
       }
    
       // 刪除元素
       remove(key) {
          const map = this.hashTable[this.hash(key)];
    
          let value = null;
          // 存在就刪除
          if (map.contains(key)) {
             value = map.remove(key);
             this.size--;
          }
    
          return value;
       }
    
       // 修改操做
       set(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          if (!map.contains(key)) throw new Error(key + " doesn't exist!");
    
          map.set(key, value);
       }
    
       // 查找是否存在
       contains(key) {
          return this.hashTable[this.hash(key)].contains(key);
       }
    
       // 查找操做
       get(key) {
          return this.hashTable[this.hash(key)].get(key);
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('HashTable Comparison Area');
          const n = 2000000;
    
          const random = Math.random;
          let arrNumber = new Array(n);
    
          // 循環添加隨機數的值
          for (let i = 0; i < n; i++) arrNumber[i] = Math.floor(n * random());
    
          const hashTable = new MyHashTableByAVLTree(1572869);
          const hashTable1 = new MyHashTableBySystem(1572869);
          const performanceTest1 = new PerformanceTest();
    
          const that = this;
          const hashTableInfo = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arrNumber)
                hashTable.add(word, String.fromCharCode(word));
    
             that.show('size : ' + hashTable.getSize());
             console.log('size : ' + hashTable.getSize());
    
             // 刪除
             for (const word of arrNumber) hashTable.remove(word);
    
             // 查找
             for (const word of arrNumber)
                if (hashTable.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:
          console.log(hashTableInfo);
          console.log(hashTable);
          this.show(hashTableInfo);
    
          const hashTableInfo1 = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arrNumber)
                hashTable1.add(word, String.fromCharCode(word));
    
             that.show('size : ' + hashTable1.getSize());
             console.log('size : ' + hashTable1.getSize());
    
             // 刪除
             for (const word of arrNumber) hashTable1.remove(word);
    
             // 查找
             for (const word of arrNumber)
                if (hashTable1.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:
          console.log(hashTableInfo1);
          console.log(hashTable1);
          this.show(hashTableInfo1);
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

哈希表的動態空間處理與複雜度分析

哈希表的時間複雜度

  1. 對於鏈地址法來講
    1. 總共有 M 個地址,若是放入 N 個元素,那麼每個地址就有 N/M 個元素,
    2. 也就是說有 N/M 個元素的哈希值是衝突的,
    3. 若是每一個地址裏面是一個鏈表,那麼平均的時間複雜度就是O(N/M)級別,
    4. 若是每個地址裏面是一個平衡樹,那麼平均的時間複雜度是O(log(N/M))級別,
    5. 這兩個時間複雜度都是平均來看的,並非最壞的狀況,
    6. 哈希表的優點在於,可以讓時間複雜度變成O(1)級別的,
    7. 只要讓這個 M 不是固定的,是動態的,那麼就可以讓時間複雜度變成O(1)級別。
  2. 正常狀況下不會出現最壞的狀況,
    1. 可是在信息安全領域有一種攻擊方法叫作哈希碰撞攻擊,
    2. 也就是當你知道這個哈希計算方式以後,你就會精心設計一套數據,
    3. 當這套數據插入到哈希表中以後,這套數據所有產生哈希衝突,
    4. 這就使得系統的哈希表的時間複雜度變成了最壞的狀況,
    5. 這樣就大大的拖慢整個系統的運行速度,
    6. 也會在哈希表查找的過程當中大大的消耗系統的資源。

哈希表的動態空間處理

  1. 哈希表的本質就是一個數組
    1. 若是這個數組是靜態的話,那麼哈希衝突的機會會不少,
    2. 若是這個數組是動態的話,那麼哈希衝突的機會會不多,
    3. 由於你存儲的元素接近無窮大的話,
    4. 靜態的數組確定是沒法讓相應的時間複雜度接近O(1)級別。
  2. 哈希表的中數組的空間要隨着元素個數的改變進行必定的自適應
    1. 因爲靜態數組固定的地址空間是不合理的,
    2. 因此和本身實現的動態數組同樣,須要進行 resize,
    3. 和本身實現的動態數組不同的是,哈希表中的數組不存在全部位置都填滿,
    4. 由於它的存儲方式和動態數組的按照順序一個一個的塞進數組的方式不同。
    5. 相應的解決方案是,
    6. 當平均每一個地址的承載的元素多過必定程度,就去擴容,
    7. 也就是N / M >= upperTolerance的時候,也就是設置一個上界,
    8. 若是 也就是說平均每一個地址存儲的元素超過了多少個,如 upperTolerance 爲 10,
    9. 那麼N / M大於等於 10,那麼就進行擴容操做。
    10. 反之也有縮容,
    11. 當平均每一個地址承載的元素少過必定程度,就去縮容,
    12. 也就是N / M < lowerTolerance的時候,也就是設置一個下限,
    13. 也就是哈希衝突並不嚴重,那麼就不須要開那麼大的空間了,
    14. 如 lowerTolerance 爲 2,那麼N / M小於 2,那麼就進行縮容操做。
    15. 大概的原理和動態數組擴容和縮容的原理是一致的,可是有些細節方面會不同,
    16. 如新的哈希表的根據 key 獲取哈希值後對 M 取模,這個 M 你須要設置爲新的 newM,
    17. 而且你遍歷的空間也是原來那個舊的 M 個空間地址,並非新的 newM 個空間地址,
    18. 因此你須要先將舊的 M 值存一下,而後再將 newM 賦值給 M,這樣邏輯才徹底正確。

代碼示例

  1. MyHashTable

    // 自定義的hash生成類。
    class MyHash {
       constructor() {
          this.store = new Map();
       }
    
       // 生成hash
       hashCode(key) {
          let hash = this.store.get(key);
          if (hash !== undefined) return hash;
          else {
             // 若是 這個hash沒有進行保存 就生成,而且記錄
             let hash = this.calcHashTwo(key);
    
             // 記錄
             this.store.set(key, hash);
    
             // 返回hash
             return hash;
          }
       }
    
       // 獲得的數字比較小 六七位數 如下 輔助函數:生成hash -
       calcHashOne(key) {
          // 生成hash 隨機小數 * 當前日期毫秒 * 隨機小數
          let hash = Math.random() * Date.now() * Math.random();
    
          // hash 取小數部分的字符串
          hash = hash.toString().replace(/^\d*\.\d*?([1-9]+)$/, '$1');
    
          hash = parseInt(hash); // 取整
    
          return hash;
       }
    
       // 獲得的數字很大 十幾位數 左右 輔助函數:生成hash -
       calcHashTwo(key) {
          // 生成hash 隨機小數 * 當前日期毫秒 * 隨機小數
          let hash = Math.random() * Date.now() * Math.random();
    
          // hash 向下取整
          hash = Math.floor(hash);
          return hash;
       }
    }
    
    class MyHashTableBySystem {
       constructor(M = 97) {
          this.M = M; // 空間大小
          this.size = 0; // 實際元素個數
          this.hashTable = new Array(M); // 哈希表
          this.hashCalc = new MyHash(); // 哈希值計算
    
          // 初始化哈希表
          for (var i = 0; i < M; i++) {
             // this.hashTable[i] = new MyAVLTree();
             this.hashTable[i] = new Map();
          }
       }
    
       // 根據key生成 哈希表索引
       hash(key) {
          // 獲取哈希值
          let hash = this.hashCalc.hashCode(key);
          // 對哈希值轉換爲32位的整數 再進行取模運算
          return (hash & 0x7fffffff) % this.M;
       }
    
       // 獲取實際存儲的元素個數
       getSize() {
          return this.size;
       }
    
       // 添加元素
       add(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          // 若是存在就覆蓋
          if (map.has(key)) map.set(key, value);
          else {
             // 不存在就添加
             map.set(key, value);
             this.size++;
          }
       }
    
       // 刪除元素
       remove(key) {
          const map = this.hashTable[this.hash(key)];
    
          let value = null;
          // 存在就刪除
          if (map.has(key)) {
             value = map.delete(key);
             this.size--;
          }
    
          return value;
       }
    
       // 修改操做
       set(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          if (!map.has(key)) throw new Error(key + " doesn't exist!");
    
          map.set(key, value);
       }
    
       // 查找是否存在
       contains(key) {
          return this.hashTable[this.hash(key)].has(key);
       }
    
       // 查找操做
       get(key) {
          return this.hashTable[this.hash(key)].get(key);
       }
    }
    
    // 自定義的哈希表 HashTable
    // 自定義的哈希表 HashTable
    class MyHashTableByAVLTree {
       constructor(M = 97) {
          this.M = M; // 空間大小
          this.size = 0; // 實際元素個數
          this.hashTable = new Array(M); // 哈希表
          this.hashCalc = new MyHash(); // 哈希值計算
    
          // 初始化哈希表
          for (var i = 0; i < M; i++) {
             // this.hashTable[i] = new MyAVLTree();
             this.hashTable[i] = new MyAVLTreeMap();
          }
    
          // 設定擴容的上邊界
          this.upperTolerance = 10;
          // 設定縮容的下邊界
          this.lowerTolerance = 2;
          // 初始容量大小爲 97
          this.initCapcity = 97;
       }
    
       // 根據key生成 哈希表索引
       hash(key) {
          // 獲取哈希值
          let hash = this.hashCalc.hashCode(key);
          // 對哈希值轉換爲32位的整數 再進行取模運算
          return (hash & 0x7fffffff) % this.M;
       }
    
       // 獲取實際存儲的元素個數
       getSize() {
          return this.size;
       }
    
       // 添加元素
       add(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          // 若是存在就覆蓋
          if (map.contains(key)) map.set(key, value);
          else {
             // 不存在就添加
             map.add(key, value);
             this.size++;
    
             // 平均元素個數 大於等於 當前容量的10倍
             // 擴容就翻倍
             if (this.size >= this.upperTolerance * this.M)
                this.resize(2 * this.M);
          }
       }
    
       // 刪除元素
       remove(key) {
          const map = this.hashTable[this.hash(key)];
    
          let value = null;
          // 存在就刪除
          if (map.contains(key)) {
             value = map.remove(key);
             this.size--;
    
             // 平均元素個數 小於容量的2倍 固然不管怎麼縮容,縮容以後都要大於初始容量
             if (
                this.size < this.lowerTolerance * this.M &&
                this.M / 2 > this.initCapcity
             )
                this.resize(Math.floor(this.M / 2));
          }
    
          return value;
       }
    
       // 修改操做
       set(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          if (!map.contains(key)) throw new Error(key + " doesn't exist!");
    
          map.set(key, value);
       }
    
       // 查找是否存在
       contains(key) {
          return this.hashTable[this.hash(key)].contains(key);
       }
    
       // 查找操做
       get(key) {
          return this.hashTable[this.hash(key)].get(key);
       }
    
       // 重置空間大小
       resize(newM) {
          // 初始化新空間
          const newHashTable = new Array(newM);
          for (var i = 0; i < newM; i++) newHashTable[i] = new MyAVLTree();
    
          const oldM = this.M;
          this.M = newM;
    
          // 方式一
          // let map;
          // let keys;
          // for (var i = 0; i < oldM; i++) {
          // // 獲取全部實例
          // map = this.hashTable[i];
          // keys = map.getKeys();
          // // 遍歷每一對鍵值對 實例
          // for(const key of keys)
          // newHashTable[this.hash(key)].add(key, map.get(key));
          // }
    
          // 方式二
          let etities;
          for (var i = 0; i < oldM; i++) {
             etities = this.hashTable[i].getEntitys();
             for (const entity of etities)
                newHashTable[this.hash(entity.key)].add(
                   entity.key,
                   entity.value
                );
          }
    
          // 從新設置當前hashTable
          this.hashTable = newHashTable;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('HashTable Comparison Area');
          const n = 2000000;
    
          const random = Math.random;
          let arrNumber = new Array(n);
    
          // 循環添加隨機數的值
          for (let i = 0; i < n; i++) arrNumber[i] = Math.floor(n * random());
    
          this.alterLine('HashTable Comparison Area');
          const hashTable = new MyHashTableByAVLTree();
          const hashTable1 = new MyHashTableBySystem();
          const performanceTest1 = new PerformanceTest();
    
          const that = this;
          const hashTableInfo = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arrNumber)
                hashTable.add(word, String.fromCharCode(word));
    
             that.show('size : ' + hashTable.getSize());
             console.log('size : ' + hashTable.getSize());
    
             // 刪除
             for (const word of arrNumber) hashTable.remove(word);
    
             // 查找
             for (const word of arrNumber)
                if (hashTable.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:
          console.log('HashTableByAVLTree' + ':' + hashTableInfo);
          console.log(hashTable);
          this.show('HashTableByAVLTree' + ':' + hashTableInfo);
    
          const hashTableInfo1 = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arrNumber)
                hashTable1.add(word, String.fromCharCode(word));
    
             that.show('size : ' + hashTable1.getSize());
             console.log('size : ' + hashTable1.getSize());
    
             // 刪除
             for (const word of arrNumber) hashTable1.remove(word);
    
             // 查找
             for (const word of arrNumber)
                if (hashTable1.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:
          console.log('HashTableBySystem' + ':' + hashTableInfo1);
          console.log(hashTable1);
          this.show('HashTableBySystem' + ':' + hashTableInfo1);
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

哈希表更復雜的動態空間處理方法

哈希表的複雜度分析

  1. 已經爲哈希表添加了動態處理空間大小的機制了
    1. 因此就須要對這個新的哈希表進行一下時間複雜度的分析。
  2. 本身實現的動態數組的均攤複雜度分析
    1. 當數組中的元素個數等於數組的當前的容量的時候,
    2. 就須要進行擴容,擴容的大小是當前容量的兩倍,
    3. 整個擴容的過程要消耗O(n)的複雜度,
    4. 可是這是通過 n 次O(1)級別的操做以後纔有這一次O(n)級別的操做,
    5. 因此就把這個O(n)級別的操做平攤到 n 次O(1)級別的操做中,
    6. 那麼就能夠簡單的理解以前每一次操做都是O(2)級別的操做,
    7. 這個 2 是一個常數,對於複雜度分析來講會忽略一下常數,
    8. 那麼平均時間複雜度就是O(1)級別的。
  3. 本身實現的動態哈希表的複雜度分析
    1. 其實分析的方式和動態數組的分析方式是同樣的道理,
    2. 也就是說,哈希表中元素個數從 N 增長到了 upperTolerance*N 的時候,
    3. 整個哈希表的地址空間纔會進行一個翻倍這樣的擴容,
    4. 也就是說增長 9 倍原來的空間大小以後纔會進行空間地址的翻倍,
    5. 那麼相對與動態數組來講,是添加了更多的元素才進行的翻倍,
    6. 這個操做也是O(n)級別的操做,
    7. 這一次操做也須要平攤到 9*n次操做中去,
    8. 那麼每一次操做平攤到的時間複雜度就會更少,
    9. 正由於如此就算進行了 resize 操做以後,
    10. 哈希表的平均時間複雜度仍是O(1)級別的,
    11. 其實每一個操做是在O(lowerTolerance)~O(upperTolerance)之間
    12. 這兩個數都是自定義的常數,因此這樣的一個複雜度仍是O(1)級別的,
    13. 不管縮容仍是擴容都是如此,因此這就是哈希表這種數據結構的一個巨大優點,
    14. 這個O(1)級別的時間複雜度是均攤獲得的,是平均的時間複雜度。

更復雜的動態空間處理方法

  1. 對於本身實現的哈希表來講
    1. 擴容操做是從 M -> 2*M,就算初始的 M 是一個素數,
    2. 那麼乘以 2 以後必定是一個偶數,再繼續擴容的過程當中,
    3. 就會是 2^k 乘以 M,因此它顯然再也不是一個素數,
    4. 這樣的一個容量,會隨着擴容而致使哈希表索引分佈再也不均勻,
    5. 因此但願這個空間是一個素數,解決的方法很是的簡單。
    6. 在哈希表中不一樣的空間範圍裏合理的素數已經有人總結出來了,
    7. 也就是說對於哈希表的大小已經有不少與數學相關的研究人員給出了一些建議,
    8. 能夠經過這個網址看到一張表格,表格中就是對應的大小區間、對應的素數以及衝突機率,
    9. http://planetmath.org/goodhashtableprimes
  2. 哈希表的擴容的方案就能夠不是原先的簡單乘以 2 或者除以 2
    1. 能夠根據一張區內對應的素數表來進行擴容和縮容,
    2. 好比初始的大小是 53,擴容的時候就到 97,再擴容就到 193,
    3. 若是要縮容了,就到 97,若是要再縮容的就到 53,就這樣。
    4. 對於哈希表來講,這些素數有在儘可能的維持一個二倍的關係,
    5. 使用這些素數值進行擴容更加的合理。
      // lwr upr % err prime
      // 2^5 2^6 10.416667 53
      // 2^6 2^7 1.041667 97
      // 2^7 2^8 0.520833 193
      // 2^8 2^9 1.302083 389
      // 2^9 2^10 0.130208 769
      // 2^10 2^11 0.455729 1543
      // 2^11 2^12 0.227865 3079
      // 2^12 2^13 0.113932 6151
      // 2^13 2^14 0.008138 12289
      // 2^14 2^15 0.069173 24593
      // 2^15 2^16 0.010173 49157
      // 2^16 2^17 0.013224 98317
      // 2^17 2^18 0.002543 196613
      // 2^18 2^19 0.006358 393241
      // 2^19 2^20 0.000127 786433
      // 2^20 2^21 0.000318 1572869
      // 2^21 2^22 0.000350 3145739
      // 2^22 2^23 0.000207 6291469
      // 2^23 2^24 0.000040 12582917
      // 2^24 2^25 0.000075 25165843
      // 2^25 2^26 0.000010 50331653
      // 2^26 2^27 0.000023 100663319
      // 2^27 2^28 0.000009 201326611
      // 2^28 2^29 0.000001 402653189
      // 2^29 2^30 0.000011 805306457
      // 2^30 2^31 0.000000 1610612741
      複製代碼
  3. 對於計算機組成原理
    1. 32 位的整型最大能夠承載的 int 是2.0 * 10^9左右,
    2. 1610612741 是 1.6\*10^9
    3. 它是比較接近 int 型能夠承載的極限的一個素數了。
  4. 擴容和縮容的注意點
    1. 擴容和縮容不要越界,
    2. 擴容和縮容使用那張表格中區間對應的素數。

代碼示例

  1. MyHashTable

    // 自定義的hash生成類。
    class MyHash {
       constructor() {
          this.store = new Map();
       }
    
       // 生成hash
       hashCode(key) {
          let hash = this.store.get(key);
          if (hash !== undefined) return hash;
          else {
             // 若是 這個hash沒有進行保存 就生成,而且記錄
             let hash = this.calcHashTwo(key);
    
             // 記錄
             this.store.set(key, hash);
    
             // 返回hash
             return hash;
          }
       }
    
       // 獲得的數字比較小 六七位數 如下 輔助函數:生成hash -
       calcHashOne(key) {
          // 生成hash 隨機小數 * 當前日期毫秒 * 隨機小數
          let hash = Math.random() * Date.now() * Math.random();
    
          // hash 取小數部分的字符串
          hash = hash.toString().replace(/^\d*\.\d*?([1-9]+)$/, '$1');
    
          hash = parseInt(hash); // 取整
    
          return hash;
       }
    
       // 獲得的數字很大 十幾位數 左右 輔助函數:生成hash -
       calcHashTwo(key) {
          // 生成hash 隨機小數 * 當前日期毫秒 * 隨機小數
          let hash = Math.random() * Date.now() * Math.random();
    
          // hash 向下取整
          hash = Math.floor(hash);
          return hash;
       }
    }
    
    class MyHashTableBySystem {
       constructor(M = 97) {
          this.M = M; // 空間大小
          this.size = 0; // 實際元素個數
          this.hashTable = new Array(M); // 哈希表
          this.hashCalc = new MyHash(); // 哈希值計算
    
          // 初始化哈希表
          for (var i = 0; i < M; i++) {
             // this.hashTable[i] = new MyAVLTree();
             this.hashTable[i] = new Map();
          }
       }
    
       // 根據key生成 哈希表索引
       hash(key) {
          // 獲取哈希值
          let hash = this.hashCalc.hashCode(key);
          // 對哈希值轉換爲32位的整數 再進行取模運算
          return (hash & 0x7fffffff) % this.M;
       }
    
       // 獲取實際存儲的元素個數
       getSize() {
          return this.size;
       }
    
       // 添加元素
       add(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          // 若是存在就覆蓋
          if (map.has(key)) map.set(key, value);
          else {
             // 不存在就添加
             map.set(key, value);
             this.size++;
          }
       }
    
       // 刪除元素
       remove(key) {
          const map = this.hashTable[this.hash(key)];
    
          let value = null;
          // 存在就刪除
          if (map.has(key)) {
             value = map.delete(key);
             this.size--;
          }
    
          return value;
       }
    
       // 修改操做
       set(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          if (!map.has(key)) throw new Error(key + " doesn't exist!");
    
          map.set(key, value);
       }
    
       // 查找是否存在
       contains(key) {
          return this.hashTable[this.hash(key)].has(key);
       }
    
       // 查找操做
       get(key) {
          return this.hashTable[this.hash(key)].get(key);
       }
    }
    
    // 自定義的哈希表 HashTable
    // 基於系統的哈希表,用來測試
    // 自定義的哈希表 HashTable
    // 基於本身實現的AVL樹
    class MyHashTableByAVLTree {
       constructor() {
          // 設定擴容的上邊界
          this.upperTolerance = 10;
          // 設定縮容的下邊界
          this.lowerTolerance = 2;
          // 哈希表合理的素數表
          this.capacity = [
             53,
             97,
             193,
             389,
             769,
             1543,
             3079,
             6151,
             12289,
             24593,
             49157,
             98317,
             196613,
             393241,
             786433,
             1572869,
             3145739,
             6291469,
             12582917,
             25165843,
             50331653,
             100663319,
             201326611,
             402653189,
             805306457,
             1610612741
          ];
          // 初始容量的索引
          this.capacityIndex = 0;
    
          this.M = this.capacity[this.capacityIndex]; // 空間大小
          this.size = 0; // 實際元素個數
          this.hashTable = new Array(this.M); // 哈希表
          this.hashCalc = new MyHash(); // 哈希值計算
    
          // 初始化哈希表
          for (var i = 0; i < this.M; i++) {
             // this.hashTable[i] = new MyAVLTree();
             this.hashTable[i] = new MyAVLTreeMap();
          }
       }
    
       // 根據key生成 哈希表索引
       hash(key) {
          // 獲取哈希值
          let hash = this.hashCalc.hashCode(key);
          // 對哈希值轉換爲32位的整數 再進行取模運算
          return (hash & 0x7fffffff) % this.M;
       }
    
       // 獲取實際存儲的元素個數
       getSize() {
          return this.size;
       }
    
       // 添加元素
       add(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          // 若是存在就覆蓋
          if (map.contains(key)) map.set(key, value);
          else {
             // 不存在就添加
             map.add(key, value);
             this.size++;
    
             // 平均元素個數 大於等於 當前容量的10倍,同時防止索引越界
             // 就以哈希表合理的素數表 爲標準進行 索引的推移
             if (
                this.size >= this.upperTolerance * this.M &&
                this.capacityIndex + 1 < this.capacity.length
             )
                this.resize(this.capacity[++this.capacityIndex]);
          }
       }
    
       // 刪除元素
       remove(key) {
          const map = this.hashTable[this.hash(key)];
    
          let value = null;
          // 存在就刪除
          if (map.contains(key)) {
             value = map.remove(key);
             this.size--;
    
             // 平均元素個數 小於容量的2倍 固然不管怎麼縮容,索引都不能越界
             if (
                this.size < this.lowerTolerance * this.M &&
                this.capacityIndex > 0
             )
                this.resize(this.capacity[--this.capacityIndex]);
          }
    
          return value;
       }
    
       // 修改操做
       set(key, value) {
          const map = this.hashTable[this.hash(key)];
    
          if (!map.contains(key)) throw new Error(key + " doesn't exist!");
    
          map.set(key, value);
       }
    
       // 查找是否存在
       contains(key) {
          return this.hashTable[this.hash(key)].contains(key);
       }
    
       // 查找操做
       get(key) {
          return this.hashTable[this.hash(key)].get(key);
       }
    
       // 重置空間大小
       resize(newM) {
          // 初始化新空間
          const newHashTable = new Array(newM);
          for (var i = 0; i < newM; i++) newHashTable[i] = new MyAVLTree();
    
          const oldM = this.M;
          this.M = newM;
    
          // 方式一
          // let map;
          // let keys;
          // for (var i = 0; i < oldM; i++) {
          // // 獲取全部實例
          // map = this.hashTable[i];
          // keys = map.getKeys();
          // // 遍歷每一對鍵值對 實例
          // for(const key of keys)
          // newHashTable[this.hash(key)].add(key, map.get(key));
          // }
    
          // 方式二
          let etities;
          for (var i = 0; i < oldM; i++) {
             etities = this.hashTable[i].getEntitys();
             for (const entity of etities)
                newHashTable[this.hash(entity.key)].add(
                   entity.key,
                   entity.value
                );
          }
    
          // 從新設置當前hashTable
          this.hashTable = newHashTable;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('HashTable Comparison Area');
          const n = 2000000;
    
          const random = Math.random;
          let arrNumber = new Array(n);
    
          // 循環添加隨機數的值
          for (let i = 0; i < n; i++) arrNumber[i] = Math.floor(n * random());
    
          this.alterLine('HashTable Comparison Area');
          const hashTable = new MyHashTableByAVLTree();
          const hashTable1 = new MyHashTableBySystem();
          const performanceTest1 = new PerformanceTest();
    
          const that = this;
          const hashTableInfo = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arrNumber)
                hashTable.add(word, String.fromCharCode(word));
    
             that.show('size : ' + hashTable.getSize());
             console.log('size : ' + hashTable.getSize());
    
             // 刪除
             for (const word of arrNumber) hashTable.remove(word);
    
             // 查找
             for (const word of arrNumber)
                if (hashTable.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:13249
          console.log('HashTableByAVLTree' + ':' + hashTableInfo);
          console.log(hashTable);
          this.show('HashTableByAVLTree' + ':' + hashTableInfo);
    
          const hashTableInfo1 = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arrNumber)
                hashTable1.add(word, String.fromCharCode(word));
    
             that.show('size : ' + hashTable1.getSize());
             console.log('size : ' + hashTable1.getSize());
    
             // 刪除
             for (const word of arrNumber) hashTable1.remove(word);
    
             // 查找
             for (const word of arrNumber)
                if (hashTable1.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:5032
          console.log('HashTableBySystem' + ':' + hashTableInfo1);
          console.log(hashTable1);
          this.show('HashTableBySystem' + ':' + hashTableInfo1);
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

哈希表的更多話題

  1. 哈希表:均攤複雜度爲O(1)
  2. 哈希表也能夠做爲集合和映射的底層實現
    1. 平衡樹結構能夠做爲集合和映射的底層實現,
    2. 它的時間複雜度是O(logn),而哈希表的時間複雜度是O(1)
    3. 既然如此平衡樹趕不上哈希表,那麼平衡樹爲何存在。
  3. 平衡樹存在的意義是什麼?
    1. 答:順序性,平衡樹具備順序性,
    2. 由於樹結構自己是基於二分搜索樹,因此他維護了存儲的數據相應的順序性。
  4. 哈希表犧牲了什麼才達到了如此的性能?
    1. 答:順序性,哈希表不具備順序性,因爲再也不維護這些順序信息,
    2. 因此它的性能才比樹結構的性能更加優越。
  5. 對於大多數的算法或者數據結構來講
    1. 一般都是有得必有失的,若是一個算法要比另一個算法要好的話,
    2. 一般都是少維護了一些性質多消耗了一些空間等等,
    3. 不少時候依照這樣的思路來分析以前的那些算法與一樣解決相似問題的算法,
    4. 進行比較以後想明白兩種算法它們的區別在哪兒,
    5. 一個算法比一個算法好,那麼它相應的犧牲了什麼失去了什麼,
    6. 這樣去思考就可以對各類算法對各類數據結構有更加深入的認識。
  6. 集合和映射
    1. 集合和映射的底層實現能夠是鏈表、樹、哈希表。
    2. 這兩種數據結構能夠再抽象的細分紅兩種數據結構,
    3. 一種是有序集合、有序映射,在存儲數據的時候還維持的數據的有序性,
    4. 一般這種數據結構底層的實現都是平衡樹,如 AVL 樹、紅黑樹等等,
    5. 在 系統內置的 Map、Set 這兩個類,底層實現是紅黑樹。
    6. 一種是無序集合、無序映射,
    7. 因此也能夠基於哈希表封裝本身的無序集合類和無序映射類。
    8. 一樣的只要你實現了二分搜索樹的與有序相關的方法,
    9. 那麼這些接口就能夠在有序集合類和有序映射類中進行使用,
    10. 從而使你的集合類和映射類都是有序的。

更多哈希衝突的處理方法

  1. 開放地址法
    1. 這是和鏈地址法其名的一種方法,
    2. 可是也是和鏈地址法正好相反的一種方法。
    3. 鏈地址法是封閉的,可是開放地址法是數組中的空間,
    4. 每個元素都有機會進來,公式:hash(x) = x % 10
    5. 如 進來一個元素 25,那麼25 % 10值爲 5,那它就放到數組中索引爲 5 的位置,
    6. 如 再進來一個元素 11,那麼取模 10 後值爲 1,那麼就放到索引爲 1 的位置,
    7. 如 再進來一個元素 31,那麼取模 10 後值爲 1,那麼就放到索引爲 1 的位置,可是,
    8. 這時候索引爲 1 的位置已經滿了,由於每個數組中存放的再也不是一個查找表了,
    9. 因此就看看索引爲 1 的位置的後一位是否爲空,爲空的話就放到索引+1 的位置,也就是 2,
    10. 如 再進來一個元素 51,那麼取模 10 後值爲 1,也是同樣,看看這個位置是否滿了,
    11. 若是滿就裝,滿了就向後挪一位,直到找到空位置就存進去,
    12. 這就是開放地址法的線性探測法,遇到哈希衝突的時候就去找下一個位置,
    13. 以+1 的方式尋找,可是哈希衝突發生的比較多的時候,
    14. 那麼查找位置的時候就可能就是 O(n)的複雜度,因此須要改進。
    15. 改進的方法有 平方探測法,當遇到哈希衝突的時候,
    16. 先嚐試+1,若是+1 的位置被佔了,那麼就嘗試+4,若是+4 的位置被佔了,
    17. 就嘗試+9,加 9 的位置被佔了,那麼就嘗試+16,這個步長的序列叫作平方序列,
    18. 因此就叫作平方探測法,1 4 9 16分別是1^2 2^2 3^2 4^2
    19. 每相鄰兩個數之間的差也就是步長是 x^2 - (x-1)^2 = 2x - 1,x 是1 2 3 4
    20. 因此平方探測法仍是有必定的規律性,還須要改進,那麼就是二次哈希法。
    21. 二次哈希法就是遇到哈希衝突以後,
    22. 就使用另一個哈希函數來計算下一個位置距離當前位置的步長,
    23. 這些方法都叫作開放地址法,只不過計算步長的方式不同。
    24. 開放地址法也有有個擴容或者縮容的操做,
    25. 也就是當哈希表的空間中存儲量達到必定的程度的時候就會進行擴容和縮容,
    26. 對於發放地址法有一個詞叫作負載率,也就是存儲的元素佔存儲空間的百分比,
    27. 一般當負載率達到百分之 50 的時候就會進行擴容,從而保證哈希表各個操做的高效性,
    28. 對於開放地址法來講,其背後的數學分析也很是複雜,
    29. 結論都是 只要去擴容的這個負載率的值選擇的合適,那麼它的時間複雜度也是O(1)
  2. 開放地址法中哈希表的數組空間中每個位置都有一個元素,
    1. 它對每個元素都是開放的,它的每個位置沒有查找表,
    2. 而不像鏈地址法那樣只對根據 hash 值計算出相同索引的這些元素開放,
    3. 它的每個位置都有一個查找表。
  3. 更多的哈希衝突的處理方法
    1. 除了鏈地址法、開放地址法以外還有其它的哈希衝突處理法,
    2. 如 再哈希法(Rehashing):
    3. 當你使用的一個哈希函數獲取到的索引產生的哈希衝突了,
    4. 那麼就使用另一個 hash 函數來獲取索引。
    5. 還有更難理解更抽象的方法,
    6. 叫作 Coalesced Hashing(合併地址法),這種解決哈希衝突的方法綜合了
    7. Seperate Chaining 和 Open Addressing,
    8. 也就是將鏈地址法(封閉地址法)和開放地址法進行了一個巧妙地融合。
相關文章
相關標籤/搜索