日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

【从蛋壳到满天飞】JS 数据结构解析和算法实现-哈希表

發布時間:2025/3/21 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【从蛋壳到满天飞】JS 数据结构解析和算法实现-哈希表 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

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

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

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

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

哈希表

  • 哈希表相對于之前實現的那些數據結構來說

  • 哈希表是一個相對比較簡單的數據結構,
  • 對于哈希表來說也有許多相對比較復雜的研究,
  • 不過對于這些研究大多數都是比較偏數學的,
  • 對于普通的軟件工程軟件開發來講,
  • 使用哈希表了解哈希表的底層實現,并不需要知道那么多的復雜深奧的內容,
  • 通過 leetcode 上的題目來看哈希表

  • leetcode 上第 387 號問題,在解決這個問題的時候,
  • 開辟的一個 26 個空間的數組就是哈希表,
  • 實際上真正想做是每一個字符和一個數字之間進行一個映射的關系,
  • 這個數字是這個字符在字符串中出現的頻率,
  • 使用一個數組就可以解決這個問題,
  • 那是因為將每一個字符都和一個索引進行了對應,
  • 之后直接用這個索引去數組中尋找相應的對應信息,也就是映射的內容,
  • 二十六的字符對應的索引就是數組中的索引下標,
  • 當每一個字符與索引對應了,
  • 那么對這個字符所對應的對應的內容增刪改查都是 O(1)級別的,
  • 那么這就是哈希表這種數據結構的巨大優勢,
  • 它的本質其實就是將你真正關心的內容轉換成一個索引,
  • 如字符對應的內容轉換成一個索引,然后直接使用數組來存儲相應的內容,
  • 由于數組本身是支持隨機訪問的,
  • 所以可以使用 O(1)的時間復雜度來完成各項操作,
  • 這就是哈希表。
  • // 答題 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);} } 復制代碼
  • 哈希表是對于你所關注的內容將它轉化成索引

  • 如上面的題目中,
  • 你關注的是字符它所對應的頻率,
  • 那么對于每一個字符來說必須先把它轉化成一個索引,
  • 更一般的在一個哈希表中是可以存儲各種數據類型的,
  • 對于每種數據類型都需要一個方法把它轉化成一個索引,
  • 那么相應的關心的這個類型轉換成索引的這個函數就稱之為是哈希函數,
  • 在上面的題目中,哈希函數可以寫成fn(char1) = char1 -'a',
  • 這 fn 就是函數,char1 就是給定的字符,
  • 通過這個函數 fn 就把 char1 轉化成一個索引,
  • 這個轉化的方法體就是char1 -'a',
  • 有了哈希函數將字符轉化為索引之后,之后就只需要在哈希表中操作即可,
  • 在上面的題目中只是簡單的將鍵轉化為索引,所以非常的容易,
  • 還有如一個班里有 30 名學生,從 1-30 給這個學生直接編號即可,
  • 然后在數組中去存取這個學生的信息時直接用編號-1
  • 作為數組的索引這么簡單,通過-1 就將鍵轉化為了索引,太容易了。
  • 在大多數情況下處理的數據是非常復雜的,
  • 如一個城市的居民的信息,那么就會使用居民的身份證號來與之對應,
  • 但是居民的身份證號有 18 位數,那么就不能直接用它作為數組的索引,
  • 復雜的還有字符串,如何將一個字符串轉換為哈希表中的一個索引,
  • 還有浮點數,或者是一個復合類型比如日期年月日時分秒,
  • 那么這些類型就需要先將它們轉化為一個索引才可以使用,
  • 相應的就需要合理的設計一個哈希函數,
  • 那么多的數據類型,所以很難做到每一個鍵通過哈希函數
  • 都能轉化成不同的索引從而實現一一對應,
  • 而且這個索引的值它要非常適合作為數組所對應的索引。
  • 這種情況下很多時候就不得不處理一個在哈希表中非常關鍵的問題

  • 兩個不同的鍵通過哈希函數它能對應同樣一個索引,
  • 這就是哈希沖突,
  • 所以在哈希表上的操作也就是在解決這種哈希沖突,
  • 如果設計的哈希函數非常好都是一一對應的,
  • 那么對哈希表的操作也會非常的簡單,
  • 不過對于更一般的情況,在哈希表上的操作主要考慮怎么解決哈希沖突問題。
  • 哈希表充分的體現了算法設計領域的經典思想

  • 使用空間來換取時間。
  • 很多算法問題很多經典算法在本質上就是使用空間來換取時間,
  • 很多時候多存儲一些東西或者預處理一些東西緩存一些東西,
  • 那么在實際執行算法任務的時候完成這個任務得到這個結果就會快很多,
  • 對于哈希表就非常完美的體現了這一點,
  • 例如鍵對應了身份證號,假如可以開辟無限大的空間,
  • 這個空間大小有 18 個 9 那么大,并且它還是一個數組,
  • 那么完全就可以使用O(1)的時間完成各項操作,
  • 但是很難開辟一個這么大的空間,就算空間中每一個位置只存儲 32 位的整型,
  • 一個字節八個位,就是 4 個字節,4byte 乘以 18 個九,
  • 也就是接近 37 萬 TB 的空間,太大了。
  • 相反,如果空間的大小只有 1 這么大,
  • 那么就代表了存儲的所有內容都會產生哈希沖突,
  • 把所有的內容都堆在唯一的數組空間中,
  • 假設以鏈表的方式來組織整體的數據,
  • 那么相應的各項操作完成的時間復雜度就會是O(n)級別。
  • 以上就是設計哈希表的極端情況,
  • 如果有無限的空間,各項操作都能在O(1)的時間完成,
  • 如果只有 1 的空間,各項操作只能在O(n)的時間完成。
  • 哈希表整體就是在這二者之間產生一個平衡,
  • 哈希表是時間和空間之間的平衡。
  • 對哈希表整體來說這個數組能開多大空間是非常重要的

  • 雖然如此,哈希表整體,哈希函數的設計依然是非常重要的,
  • 很多數據類型本身并不能非常自然的和一個整型索引相對應,
  • 所以必須想辦法讓諸如字符串、浮點數、復合類型日期
  • 能夠跟一個整型把它當作索引來對應。
  • 就算你能開無限的空間,但是把身份證號作為索引,
  • 但是 18 位以下及 18 位以上的空間全部都是浪費掉的,
  • 所以對于哈希表來說,還希望,
  • 對于每一個鍵通過哈希函數得到索引后,
  • 這個索引的分布越均勻越好。
  • 哈希函數的設計

  • 哈希表這種數據結構

  • 其實就是把所關心的鍵通過哈希函數轉化成一個索引,
  • 然后直接把內容存到一個數組中就好了。
  • 對于哈希表來說,關心的主要有兩部分內容

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

  • 鍵通過哈希函數得到的索引分布越均勻越好。
  • 雖然很好理解,但是想要達到這樣的條件是非常難的,
  • 對于數據的存儲的數據類型是五花八門,
  • 所以對于一些特殊領域,有特殊領域的哈希函數設計方式,
  • 甚至有專門的論文來討論如何設計哈希函數,
  • 也就說明哈希函數的設計其實是非常復雜的。
  • 最一般的哈希函數設計原則

  • 將所有類型的數據相應的哈希函數的設計都轉化成是
  • 對于整型進行一個哈希函數的過程。
  • 小范圍的正整數直接使用它來作為索引,
  • 如 26 個字母的 ascll 碼或者一個班級的學生編號。
  • 小范圍的負整數進行偏移,對于數組來說索引都是自然數,
  • 也就是大于等于 0 的數字,做一個簡單的偏移即可,
  • 將它們都變完成自然數,如-100~100,讓它們都加 100,
  • 變成0~200就可以了,非常容易。
  • 大整數如身份證號轉化為索引,通常做法是取模運算,
  • 比如取這個大整數的后四位,等同于mod 10000,
  • 但是這樣就存在陷阱,這個哈希表的數組最大只有一萬空間,
  • 對于哈希表來說空間越大,就越難發生哈希沖突,
  • 那么你可以取這個大整數的后六位,等同于mod 1000000,
  • 但是對于身份證后四位來說,
  • 這四位前面的八位其實是一個人的生日,
  • 如 110108198512166666,取模后六位就是 166666,
  • 這個 16 其實是日期,數值只在 1-31 之間,永遠不可能取 99,
  • 并且只取模后六位,并沒有利用身份證上所有的信息,
  • 所以就會造成分布不均勻的情況。
  • 取模的數字選擇很重要,

  • 所以才會對哈希函數的設計,不同的領域有不同的做法,
  • 就算對身份證號的哈希函數設計的時候都要具體問題具體分析,
  • 哈希函數設計在很多時候很難找到通用的一般設計原則,
  • 具體問題具體分析在特殊的領域是非常重要的,
  • 像身份證號,有一個簡單的解決方案可以解決分布不均勻的問題,
  • 模一個素數,通常情況模一個素數都能更好的解決分布均勻的問題,
  • 所以就可以更有效的利用這個大整數中的信息,
  • 之所以模一個素數可以更有效的解決這個問題,
  • 這是由于它背后有一定的數學理論做支撐,它本身屬于數論領域,
  • 如下圖所示,模 4 就導致了分布不均勻、哈希沖突,
  • 但是模 7 就不一樣了,分布更加均勻減少了哈希沖突,
  • 所以需要看你存儲的數據是否有規律,
  • 通常情況下模一個素數得到的結果會更好,
  • http://planetmath.org/goodhashtableprimes,
  • 可以從這個網站中看到,根據你的數據規模,你取模多大一個素數是合適的,
  • 例如你存儲的數據在 2^5 至 2^6 時,你可以取模 53,哈希沖突的概率是 10.41667,
  • 例如你存儲的數據在 2^23 至 2^24 你可以取模 12582917,沖突概率是 0.000040,
  • 這些都有人研究的,所以你可以從這個網站中去看。
  • 不用去深究,只要了解這個大的基本原則即可。
  • // 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 復制代碼
  • 浮點型的哈希函數設計

  • 將浮點型的數據轉化為一個整數的索引,

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

  • 如果鍵是浮點型的話,那么就可以使用浮點型所存儲的這個空間,

  • 把它當作是整型來進行處理,

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

  • 那么這篇空間同樣可以可以表示一個整數,

  • 之后就可以將一個大的整數轉成整數相應的方式,也就是取模的方式,

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

    // // 單精度 // 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 復制代碼
  • 字符串的哈希函數設計

  • 字符串相對浮點型來說更加特殊一些,
  • 浮點型依然是占 32 位或 64 位這樣的空間,
  • 而字符串可以有若干個字符來組合,它所占的空間數量是不固定的,
  • 盡管如此,對于字符串的哈希函數設計,依然可以將它轉成大整型處理,
  • 例如一個整數可以轉換成每一位數字的十進制表示法,
  • 如166 = 1 * 10^2 + 6 * 10^1 + 6 * 10^0,
  • 這樣就相當于把一個整數看作是一個字符串,每一個字符就是一個數字,
  • 按照這種方式,就可以把字符串中每一個字符拆分出來,
  • 如果是英文就可以把它作為 26 進制的整數表示法,
  • 如code = c * 26^3 + o * 26^2 + d * 26^1 + e * 26^0,
  • c 在 26 進制中對應的是 3,其它的類似,
  • 這樣一來就可以把一個字符串看作是 26 進制的整型,
  • 之所以用 26,這是因為一共有 26 個小寫字母,這個進制是可以選的,
  • 例如字符串中大小寫字母都有,那么就是 52 進制,如果還有各種標點符號,
  • 那么就可以使 256 進制等等,由于這個進制可以選,那么就可以使用一個標記來代替,
  • 如大 B,也就是 basics(基本)的意思,
  • 那么表達式是code = c * B^3 + o * B^2 + d * B^1 + e * B^0,
  • 最后的哈希函數就是
  • hash(code) = (c * B^3 + o * B^2 + d * B^1 + e * B^0) % M,
  • 這個 M 對應的取模的方式中那個素數,
  • 這個 M 也表示了哈希表的那個數組中一共有多少個空間,
  • 對于這種表示的樣子,這個 code 一共有四個字符,所以最高位的 c 字符乘以 B 的三次方,
  • 如果這個字符串有一百個字符,那么最高位的 c 字符就要乘以 B 的 99 次方,
  • 很多時候計算 B 的 k 次方,這個 k 比較大的話,這個計算過程也是比較慢的,
  • 所以對于這個式子一個常見的轉化形式就是
  • hash(code) = ((((c * B) + o) * B + d) * B + e) % M,
  • 將字符串轉換成大整型的一個括號轉換成了四個,
  • 在每一個括號里面做的事情都是拿到一個字符乘以 B 得到的結果再加上下一個字符,
  • 再乘以 B 得到的結果在加上下一個字符,
  • 再乘以 B 得到的結果直到加到最后一個字符為止,
  • 這樣套四個括號之后,這個式子和那個套一個括號的式子其實是等價的,
  • 就是一種簡單的變形,這樣就不需要先算 B^99 然后再算 B^98 等等這么復雜了,
  • 每一次都需要乘以一個 B 再加上下一個字符再乘以 B 依此類推就好,
  • 那么使用程序實現的時候計算這個哈希函數相應的速度就會快一些,
  • 這是一個很通用的數學技巧,是數學中的多項式就是這樣的,
  • 但是這么加可能會導致整型的溢出,
  • 那么就可以將這個取模的過程分別放入每個括號里面,
  • 這樣就可以轉化成這種形式
  • hash(code) = ((((c % M) * B + o) % M * B + d) % M * B + e) % M,
  • 這樣一來,每一次都計算出了比 M 更小的數,所以根本就不用擔心整型溢出的問題,
  • 這就是數論中的模運算的一個很重要的性質。
  • //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; 復制代碼
  • 復合類型的哈希函數設計

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

  • 轉換成整型并不是哈希函數設計的唯一方法,
  • 只不過這是一個比較普通比較常用比較通用的一種方法,
  • 在很多特殊的領域有很多相關的論文去講更多的哈希函數設計的方法。
  • 哈希函數的設計,通常要遵循三個原則

  • 一致性:如果 a==b,則 hash(a)==hash(b)。
  • 如果兩個鍵相等,那么扔進哈希函數之后得到的值也一定要相等,
  • 但是對于哈希函數來說反過來是不一定成立的,
  • 同樣的一個哈希值很有可能對應了兩個不同的數據或者不同的鍵,
  • 這就是所謂的哈希沖突的情況。
  • 高效性:計算高效簡便。
  • 使用哈希表就是為了能夠高效的存儲,
  • 那么在使用哈希函數計算的時候耗費太多的性能那么就太得不償失了。
  • 均勻性:哈希值均勻分布。
  • 使用哈希函數之后得到的索引值就應該盡量的均勻,
  • 對于一般的整型可以通過模一個素數來讓它盡量的均勻,
  • 這個條件雖然看起來很簡單,但是真正要滿足這個條件,
  • 探究這個條件背后的數學性質還是很復雜的一個問題。
  • js 中 自定義 hashCode 方法

  • 在 js 中自定義數據類型

  • 對于自己定義的復合類型,如學生類、日期類型,
  • 你可以通過寫 hashCode 方法,
  • 然后自己實現一下這個方法重新生成 hash 值。
  • 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-jwlhashCode() {// 選擇進制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-jwlequals(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-jwltoString() {let studentInfo = `Student(name: ${this.name}, score: ${this.score})`;return studentInfo;} } 復制代碼
  • 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)

  • 哈希表的本質就是一個數組
  • 對于一個哈希表來說,對于一個整數求它的 hash 值的時候會對一個素數取模,
  • 這個素數就是這個數組的空間大小,也可以把它稱之為 M,
  • 在 強類型語言 中獲取到的 hash 值可能是一個負數,所以就需要進行處理一下
  • 最簡單的,直接獲取這個 hash 值的絕對值就可以了,
  • 但是很多源碼中,是這樣的一個表示 (hashCode(k1) & 0x7fffffff) % M,
  • 也就是讓 hash 值和一個十六進制的數字進行一個按位與,
  • 按位與之后再對 M 進行一個取模操作,這和直接獲取這個 hash 值的正負號去掉是一樣的,
  • 在十六進制中,每一位表示的是四個 bit,那么 f 表示的就是二進制中的1111,
  • 七個 f 表示的是二進制中的 28 個 1,7 表示的是二進制中的111,
  • 那么0x7fffffff表示的二進制就是 31 個 1,hash 值對 31 個 1 進行一下按位與,
  • 在計算機中整型的表示是用的 32 位,其中最高位就是符號位,如果和 31 個 1 做按位與,
  • 那么相應的最高為其實是 0,這樣操作的結果其實就是最高位的結果,肯定是 0,
  • 而這個 hash 值對應的二進制表示的那 31 位
  • 再和 31 個 1 進行按位與之后任然保持原來的樣子,
  • 也就是這個操作做的事情實際上就是把 hash 值整型對應的二進制表示的最高位的 1 給抹去,
  • 給抹成了 0,如果它原來是 0 的,那么任然是 0,
  • 這是因為在計算機中對整型的表示最高位是符號位,如果最高位是 1 表示它是一個負數,
  • 如果最高位是 0 表示它是一個正數,那么抹去 1 就相當于把負號去掉了。
  • 在 js 中這樣做效果不好,所以需要自己根據實際情況來寫一起算法,如通過時間戳來進行這種操作。
  • 鏈地址法
  • 根據元素的哈希值計算出索引后,根據索引來哈希表中的數組里存儲數據,
  • 如果索引相同的話,那么就以鏈表的方式將新元素掛到數組對應的位置中,
  • 這樣就很好的解決了哈希沖突的問題了,因為每一個位置都對應了一個鏈,
  • 它的本質就是一個查找表,查找表的本質不一定是使用鏈表,
  • 它的底層其實還可以使用樹結構如平衡樹結構,
  • 對于哈希表的數組中每一個位置存的不是一個鏈表而是一個 Map,
  • 通過哈希值計算出索引后,根據索引找到數組中對應的位置之后,
  • 就可以把你要存儲的元素插入該位置的 紅黑樹 里即可,
  • 那么這個 Map 本質就是一個 紅黑樹 Map 數組,這是映射的形式,
  • 如果你真正要實現的是一個集合,那么也可以使用 紅黑樹 Set 數組,
  • 哈希表的數組中每一個位置存的都是一個查找表,
  • 只要這個數據結構適合作為查找表就可以了,它是可以有不同的底層實現,
  • 哈希表的數組中每一個位置也可以對應的是一個鏈表,
  • 當數據規模比較小的時候,其實鏈表要比紅黑樹要快的,
  • 數據規模比較小的時候使用紅黑樹可能更加耗費性能,如各種旋轉操作,
  • 因為它要滿足紅黑樹的性能,所以反而會慢一些。
  • 實現自己的哈希表

  • 之前實現的樹結構中都需要進行比較
  • 其中的鍵都需要實現 compare 這個用來比較兩個元素的方法,
  • 因為需要通過鍵來進行比較,
  • 對于哈希表來說沒有這個要求,
  • 這個 key 不需要實現這個方法。
  • 在哈希表中存儲的元素都需要實現可以用來獲取 hashCode 的方法。
  • 對于哈希表來說相應的開多少空間是非常重要的
  • 開的空間越合適,那么相應的哈希沖突就越少,
  • 空間大小可以參考http://planetmath.org/goodhashtableprimes,
  • 根據存儲數據的多少來開辟合適的空間,但是很多時候并不知道要開多少的空間,
  • 此時使用哈希表并不能合理的估計一個 M 值,所以需要進行優化。
  • 代碼示例

  • MyHashTable

    // 自定義的hash生成類。 class MyHash {constructor() {this.store = new Map();}// 生成hashhashCode(key) {let hash = this.store.get(key);if (hash !== undefined) return hash;else {// 如果 這個hash沒有進行保存 就生成,并且記錄let hash = this.calcHashTwo(key);// 記錄this.store.set(key, hash);// 返回hashreturn 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);} } 復制代碼
  • 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(); }; 復制代碼
  • 哈希表的動態空間處理與復雜度分析

    哈希表的時間復雜度

  • 對于鏈地址法來說
  • 總共有 M 個地址,如果放入 N 個元素,那么每一個地址就有 N/M 個元素,
  • 也就是說有 N/M 個元素的哈希值是沖突的,
  • 如果每個地址里面是一個鏈表,那么平均的時間復雜度就是O(N/M)級別,
  • 如果每一個地址里面是一個平衡樹,那么平均的時間復雜度是O(log(N/M))級別,
  • 這兩個時間復雜度都是平均來看的,并不是最壞的情況,
  • 哈希表的優勢在于,能夠讓時間復雜度變成O(1)級別的,
  • 只要讓這個 M 不是固定的,是動態的,那么就能夠讓時間復雜度變成O(1)級別。
  • 正常情況下不會出現最壞的情況,
  • 但是在信息安全領域有一種攻擊方法叫做哈希碰撞攻擊,
  • 也就是當你知道這個哈希計算方式之后,你就會精心設計一套數據,
  • 當這套數據插入到哈希表中之后,這套數據全部產生哈希沖突,
  • 這就使得系統的哈希表的時間復雜度變成了最壞的情況,
  • 這樣就大大的拖慢整個系統的運行速度,
  • 也會在哈希表查找的過程中大大的消耗系統的資源。
  • 哈希表的動態空間處理

  • 哈希表的本質就是一個數組
  • 如果這個數組是靜態的話,那么哈希沖突的機會會很多,
  • 如果這個數組是動態的話,那么哈希沖突的機會會很少,
  • 因為你存儲的元素接近無窮大的話,
  • 靜態的數組肯定是無法讓相應的時間復雜度接近O(1)級別。
  • 哈希表的中數組的空間要隨著元素個數的改變進行一定的自適應
  • 由于靜態數組固定的地址空間是不合理的,
  • 所以和自己實現的動態數組一樣,需要進行 resize,
  • 和自己實現的動態數組不一樣的是,哈希表中的數組不存在所有位置都填滿,
  • 因為它的存儲方式和動態數組的按照順序一個一個的塞進數組的方式不一樣。
  • 相應的解決方案是,
  • 當平均每個地址的承載的元素多過一定程度,就去擴容,
  • 也就是N / M >= upperTolerance的時候,也就是設置一個上界,
  • 如果 也就是說平均每個地址存儲的元素超過了多少個,如 upperTolerance 為 10,
  • 那么N / M大于等于 10,那么就進行擴容操作。
  • 反之也有縮容,
  • 當平均每個地址承載的元素少過一定程度,就去縮容,
  • 也就是N / M < lowerTolerance的時候,也就是設置一個下限,
  • 也就是哈希沖突并不嚴重,那么就不需要開那么大的空間了,
  • 如 lowerTolerance 為 2,那么N / M小于 2,那么就進行縮容操作。
  • 大概的原理和動態數組擴容和縮容的原理是一致的,但是有些細節方面會不一樣,
  • 如新的哈希表的根據 key 獲取哈希值后對 M 取模,這個 M 你需要設置為新的 newM,
  • 并且你遍歷的空間也是原來那個舊的 M 個空間地址,并不是新的 newM 個空間地址,
  • 所以你需要先將舊的 M 值存一下,然后再將 newM 賦值給 M,這樣邏輯才完全正確。
  • 代碼示例

  • MyHashTable

    // 自定義的hash生成類。 class MyHash {constructor() {this.store = new Map();}// 生成hashhashCode(key) {let hash = this.store.get(key);if (hash !== undefined) return hash;else {// 如果 這個hash沒有進行保存 就生成,并且記錄let hash = this.calcHashTwo(key);// 記錄this.store.set(key, hash);// 返回hashreturn 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;// 初始容量大小為 97this.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);}// 重新設置當前hashTablethis.hashTable = newHashTable;} } 復制代碼
  • 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(); }; 復制代碼
  • 哈希表更復雜的動態空間處理方法

    哈希表的復雜度分析

  • 已經為哈希表添加了動態處理空間大小的機制了
  • 所以就需要對這個新的哈希表進行一下時間復雜度的分析。
  • 自己實現的動態數組的均攤復雜度分析
  • 當數組中的元素個數等于數組的當前的容量的時候,
  • 就需要進行擴容,擴容的大小是當前容量的兩倍,
  • 整個擴容的過程要消耗O(n)的復雜度,
  • 但是這是經過 n 次O(1)級別的操作之后才有這一次O(n)級別的操作,
  • 所以就把這個O(n)級別的操作平攤到 n 次O(1)級別的操作中,
  • 那么就可以簡單的理解之前每一次操作都是O(2)級別的操作,
  • 這個 2 是一個常數,對于復雜度分析來說會忽略一下常數,
  • 那么平均時間復雜度就是O(1)級別的。
  • 自己實現的動態哈希表的復雜度分析
  • 其實分析的方式和動態數組的分析方式是一樣的道理,
  • 也就是說,哈希表中元素個數從 N 增加到了 upperTolerance*N 的時候,
  • 整個哈希表的地址空間才會進行一個翻倍這樣的擴容,
  • 也就是說增加 9 倍原來的空間大小之后才會進行空間地址的翻倍,
  • 那么相對與動態數組來說,是添加了更多的元素才進行的翻倍,
  • 這個操作也是O(n)級別的操作,
  • 這一次操作也需要平攤到 9*n次操作中去,
  • 那么每一次操作平攤到的時間復雜度就會更少,
  • 正因為如此就算進行了 resize 操作之后,
  • 哈希表的平均時間復雜度還是O(1)級別的,
  • 其實每個操作是在O(lowerTolerance)~O(upperTolerance)之間,
  • 這兩個數都是自定義的常數,所以這樣的一個復雜度還是O(1)級別的,
  • 無論縮容還是擴容都是如此,所以這就是哈希表這種數據結構的一個巨大優勢,
  • 這個O(1)級別的時間復雜度是均攤得到的,是平均的時間復雜度。
  • 更復雜的動態空間處理方法

  • 對于自己實現的哈希表來說
  • 擴容操作是從 M -> 2*M,就算初始的 M 是一個素數,
  • 那么乘以 2 之后一定是一個偶數,再繼續擴容的過程中,
  • 就會是 2^k 乘以 M,所以它顯然不再是一個素數,
  • 這樣的一個容量,會隨著擴容而導致哈希表索引分布不再均勻,
  • 所以希望這個空間是一個素數,解決的方法非常的簡單。
  • 在哈希表中不同的空間范圍里合理的素數已經有人總結出來了,
  • 也就是說對于哈希表的大小已經有很多與數學相關的研究人員給出了一些建議,
  • 可以通過這個網址看到一張表格,表格中就是對應的大小區間、對應的素數以及沖突概率,
  • http://planetmath.org/goodhashtableprimes,
  • 哈希表的擴容的方案就可以不是原先的簡單乘以 2 或者除以 2
  • 可以根據一張區內對應的素數表來進行擴容和縮容,
  • 比如初始的大小是 53,擴容的時候就到 97,再擴容就到 193,
  • 如果要縮容了,就到 97,如果要再縮容的就到 53,就這樣。
  • 對于哈希表來說,這些素數有在盡量的維持一個二倍的關系,
  • 使用這些素數值進行擴容更加的合理。// 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 復制代碼
  • 對于計算機組成原理
  • 32 位的整型最大可以承載的 int 是2.0 * 10^9左右,
  • 1610612741 是 1.6\*10^9,
  • 它是比較接近 int 型可以承載的極限的一個素數了。
  • 擴容和縮容的注意點
  • 擴容和縮容不要越界,
  • 擴容和縮容使用那張表格中區間對應的素數。
  • 代碼示例

  • MyHashTable

    // 自定義的hash生成類。 class MyHash {constructor() {this.store = new Map();}// 生成hashhashCode(key) {let hash = this.store.get(key);if (hash !== undefined) return hash;else {// 如果 這個hash沒有進行保存 就生成,并且記錄let hash = this.calcHashTwo(key);// 記錄this.store.set(key, hash);// 返回hashreturn 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);}// 重新設置當前hashTablethis.hashTable = newHashTable;} } 復制代碼
  • 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.");});// 總毫秒數:13249console.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.");});// 總毫秒數:5032console.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(); }; 復制代碼
  • 哈希表的更多話題

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

  • 開放地址法
  • 這是和鏈地址法其名的一種方法,
  • 但是也是和鏈地址法正好相反的一種方法。
  • 鏈地址法是封閉的,但是開放地址法是數組中的空間,
  • 每一個元素都有機會進來,公式:hash(x) = x % 10,
  • 如 進來一個元素 25,那么25 % 10值為 5,那它就放到數組中索引為 5 的位置,
  • 如 再進來一個元素 11,那么取模 10 后值為 1,那么就放到索引為 1 的位置,
  • 如 再進來一個元素 31,那么取模 10 后值為 1,那么就放到索引為 1 的位置,但是,
  • 這時候索引為 1 的位置已經滿了,因為每一個數組中存放的不再是一個查找表了,
  • 所以就看看索引為 1 的位置的后一位是否為空,為空的話就放到索引+1 的位置,也就是 2,
  • 如 再進來一個元素 51,那么取模 10 后值為 1,也是一樣,看看這個位置是否滿了,
  • 如果滿就裝,滿了就向后挪一位,直到找到空位置就存進去,
  • 這就是開放地址法的線性探測法,遇到哈希沖突的時候就去找下一個位置,
  • 以+1 的方式尋找,但是哈希沖突發生的比較多的時候,
  • 那么查找位置的時候就可能就是 O(n)的復雜度,所以需要改進。
  • 改進的方法有 平方探測法,當遇到哈希沖突的時候,
  • 先嘗試+1,如果+1 的位置被占了,那么就嘗試+4,如果+4 的位置被占了,
  • 就嘗試+9,加 9 的位置被占了,那么就嘗試+16,這個步長的序列叫做平方序列,
  • 所以就叫做平方探測法,1 4 9 16分別是1^2 2^2 3^2 4^2,
  • 每相鄰兩個數之間的差也就是步長是 x^2 - (x-1)^2 = 2x - 1,x 是1 2 3 4,
  • 所以平方探測法還是有一定的規律性,還需要改進,那么就是二次哈希法。
  • 二次哈希法就是遇到哈希沖突之后,
  • 就使用另外一個哈希函數來計算下一個位置距離當前位置的步長,
  • 這些方法都叫做開放地址法,只不過計算步長的方式不一樣。
  • 開放地址法也有有個擴容或者縮容的操作,
  • 也就是當哈希表的空間中存儲量達到一定的程度的時候就會進行擴容和縮容,
  • 對于發放地址法有一個詞叫做負載率,也就是存儲的元素占存儲空間的百分比,
  • 通常當負載率達到百分之 50 的時候就會進行擴容,從而保證哈希表各個操作的高效性,
  • 對于開放地址法來說,其背后的數學分析也非常復雜,
  • 結論都是 只要去擴容的這個負載率的值選擇的合適,那么它的時間復雜度也是O(1)。
  • 開放地址法中哈希表的數組空間中每一個位置都有一個元素,
  • 它對每一個元素都是開放的,它的每一個位置沒有查找表,
  • 而不像鏈地址法那樣只對根據 hash 值計算出相同索引的這些元素開放,
  • 它的每一個位置都有一個查找表。
  • 更多的哈希沖突的處理方法
  • 除了鏈地址法、開放地址法之外還有其它的哈希沖突處理法,
  • 如 再哈希法(Rehashing):
  • 當你使用的一個哈希函數獲取到的索引產生的哈希沖突了,
  • 那么就使用另外一個 hash 函數來獲取索引。
  • 還有更難理解更抽象的方法,
  • 叫做 Coalesced Hashing(合并地址法),這種解決哈希沖突的方法綜合了
  • Seperate Chaining 和 Open Addressing,
  • 也就是將鏈地址法(封閉地址法)和開放地址法進行了一個巧妙地融合。
  • 總結

    以上是生活随笔為你收集整理的【从蛋壳到满天飞】JS 数据结构解析和算法实现-哈希表的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。