前言
【從蛋殼到滿天飛】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-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; } } 複製程式碼
-
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(); } // 生成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); } } 複製程式碼
-
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(); } // 生成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; } } 複製程式碼
-
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 型可以承載的極限的一個素數了。
- 32 位的整型最大可以承載的 int 是
- 擴容和縮容的注意點
- 擴容和縮容不要越界,
- 擴容和縮容使用那張表格中區間對應的素數。
程式碼示例
-
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; } } 複製程式碼
-
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(); }; 複製程式碼
雜湊表的更多話題
- 雜湊表:均攤複雜度為
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,
- 也就是將鏈地址法(封閉地址法)和開放地址法進行了一個巧妙地融合。