日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 前端技术 > javascript >内容正文

javascript

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

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

前言

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

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

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

本文章適合 對(duì)數(shù)據(jù)結(jié)構(gòu)想了解并且感興趣的人群,文章風(fēng)格一如既往如此,就覺得手機(jī)上看起來(lái)比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時(shí)間跨度也算將近半年時(shí)間了,希望對(duì)想學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的人或者正在學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的人群有幫助。

哈希表

  • 哈希表相對(duì)于之前實(shí)現(xiàn)的那些數(shù)據(jù)結(jié)構(gòu)來(lái)說(shuō)

  • 哈希表是一個(gè)相對(duì)比較簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu),
  • 對(duì)于哈希表來(lái)說(shuō)也有許多相對(duì)比較復(fù)雜的研究,
  • 不過對(duì)于這些研究大多數(shù)都是比較偏數(shù)學(xué)的,
  • 對(duì)于普通的軟件工程軟件開發(fā)來(lái)講,
  • 使用哈希表了解哈希表的底層實(shí)現(xiàn),并不需要知道那么多的復(fù)雜深?yuàn)W的內(nèi)容,
  • 通過 leetcode 上的題目來(lái)看哈希表

  • leetcode 上第 387 號(hào)問題,在解決這個(gè)問題的時(shí)候,
  • 開辟的一個(gè) 26 個(gè)空間的數(shù)組就是哈希表,
  • 實(shí)際上真正想做是每一個(gè)字符和一個(gè)數(shù)字之間進(jìn)行一個(gè)映射的關(guān)系,
  • 這個(gè)數(shù)字是這個(gè)字符在字符串中出現(xiàn)的頻率,
  • 使用一個(gè)數(shù)組就可以解決這個(gè)問題,
  • 那是因?yàn)閷⒚恳粋€(gè)字符都和一個(gè)索引進(jìn)行了對(duì)應(yīng),
  • 之后直接用這個(gè)索引去數(shù)組中尋找相應(yīng)的對(duì)應(yīng)信息,也就是映射的內(nèi)容,
  • 二十六的字符對(duì)應(yīng)的索引就是數(shù)組中的索引下標(biāo),
  • 當(dāng)每一個(gè)字符與索引對(duì)應(yīng)了,
  • 那么對(duì)這個(gè)字符所對(duì)應(yīng)的對(duì)應(yīng)的內(nèi)容增刪改查都是 O(1)級(jí)別的,
  • 那么這就是哈希表這種數(shù)據(jù)結(jié)構(gòu)的巨大優(yōu)勢(shì),
  • 它的本質(zhì)其實(shí)就是將你真正關(guān)心的內(nèi)容轉(zhuǎn)換成一個(gè)索引,
  • 如字符對(duì)應(yīng)的內(nèi)容轉(zhuǎn)換成一個(gè)索引,然后直接使用數(shù)組來(lái)存儲(chǔ)相應(yīng)的內(nèi)容,
  • 由于數(shù)組本身是支持隨機(jī)訪問的,
  • 所以可以使用 O(1)的時(shí)間復(fù)雜度來(lái)完成各項(xiàng)操作,
  • 這就是哈希表。
  • // 答題 class Solution {// leetcode 387. 字符串中的第一個(gè)唯一字符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);} } 復(fù)制代碼
  • 哈希表是對(duì)于你所關(guān)注的內(nèi)容將它轉(zhuǎn)化成索引

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

  • 兩個(gè)不同的鍵通過哈希函數(shù)它能對(duì)應(yīng)同樣一個(gè)索引,
  • 這就是哈希沖突,
  • 所以在哈希表上的操作也就是在解決這種哈希沖突,
  • 如果設(shè)計(jì)的哈希函數(shù)非常好都是一一對(duì)應(yīng)的,
  • 那么對(duì)哈希表的操作也會(huì)非常的簡(jiǎn)單,
  • 不過對(duì)于更一般的情況,在哈希表上的操作主要考慮怎么解決哈希沖突問題。
  • 哈希表充分的體現(xiàn)了算法設(shè)計(jì)領(lǐng)域的經(jīng)典思想

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

  • 雖然如此,哈希表整體,哈希函數(shù)的設(shè)計(jì)依然是非常重要的,
  • 很多數(shù)據(jù)類型本身并不能非常自然的和一個(gè)整型索引相對(duì)應(yīng),
  • 所以必須想辦法讓諸如字符串、浮點(diǎn)數(shù)、復(fù)合類型日期
  • 能夠跟一個(gè)整型把它當(dāng)作索引來(lái)對(duì)應(yīng)。
  • 就算你能開無(wú)限的空間,但是把身份證號(hào)作為索引,
  • 但是 18 位以下及 18 位以上的空間全部都是浪費(fèi)掉的,
  • 所以對(duì)于哈希表來(lái)說(shuō),還希望,
  • 對(duì)于每一個(gè)鍵通過哈希函數(shù)得到索引后,
  • 這個(gè)索引的分布越均勻越好。
  • 哈希函數(shù)的設(shè)計(jì)

  • 哈希表這種數(shù)據(jù)結(jié)構(gòu)

  • 其實(shí)就是把所關(guān)心的鍵通過哈希函數(shù)轉(zhuǎn)化成一個(gè)索引,
  • 然后直接把內(nèi)容存到一個(gè)數(shù)組中就好了。
  • 對(duì)于哈希表來(lái)說(shuō),關(guān)心的主要有兩部分內(nèi)容

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

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

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

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

  • 將浮點(diǎn)型的數(shù)據(jù)轉(zhuǎn)化為一個(gè)整數(shù)的索引,

  • 在計(jì)算機(jī)中都 32 位或者 64 位的二進(jìn)制表示,只不過計(jì)算機(jī)解析成了浮點(diǎn)數(shù),

  • 如果鍵是浮點(diǎn)型的話,那么就可以使用浮點(diǎn)型所存儲(chǔ)的這個(gè)空間,

  • 把它當(dāng)作是整型來(lái)進(jìn)行處理,

  • 也就是把這個(gè)浮點(diǎn)型所占用的 32 位空間或 64 位空間使用整數(shù)的方式來(lái)解析,

  • 那么這篇空間同樣可以可以表示一個(gè)整數(shù),

  • 之后就可以將一個(gè)大的整數(shù)轉(zhuǎn)成整數(shù)相應(yīng)的方式,也就是取模的方式,

  • 這樣就解決了浮點(diǎn)型的哈希函數(shù)的設(shè)計(jì)的問題

    // // 單精度 // 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// //雙進(jìn)度 // 11-bit 52-bit // 0|011111111100|0100000000000000000000000000000000000000000000000000 // 63 52 0 復(fù)制代碼
  • 字符串的哈希函數(shù)設(shè)計(jì)

  • 字符串相對(duì)浮點(diǎn)型來(lái)說(shuō)更加特殊一些,
  • 浮點(diǎn)型依然是占 32 位或 64 位這樣的空間,
  • 而字符串可以有若干個(gè)字符來(lái)組合,它所占的空間數(shù)量是不固定的,
  • 盡管如此,對(duì)于字符串的哈希函數(shù)設(shè)計(jì),依然可以將它轉(zhuǎn)成大整型處理,
  • 例如一個(gè)整數(shù)可以轉(zhuǎn)換成每一位數(shù)字的十進(jìn)制表示法,
  • 如166 = 1 * 10^2 + 6 * 10^1 + 6 * 10^0,
  • 這樣就相當(dāng)于把一個(gè)整數(shù)看作是一個(gè)字符串,每一個(gè)字符就是一個(gè)數(shù)字,
  • 按照這種方式,就可以把字符串中每一個(gè)字符拆分出來(lái),
  • 如果是英文就可以把它作為 26 進(jìn)制的整數(shù)表示法,
  • 如code = c * 26^3 + o * 26^2 + d * 26^1 + e * 26^0,
  • c 在 26 進(jìn)制中對(duì)應(yīng)的是 3,其它的類似,
  • 這樣一來(lái)就可以把一個(gè)字符串看作是 26 進(jìn)制的整型,
  • 之所以用 26,這是因?yàn)橐还灿?26 個(gè)小寫字母,這個(gè)進(jìn)制是可以選的,
  • 例如字符串中大小寫字母都有,那么就是 52 進(jìn)制,如果還有各種標(biāo)點(diǎn)符號(hào),
  • 那么就可以使 256 進(jìn)制等等,由于這個(gè)進(jìn)制可以選,那么就可以使用一個(gè)標(biāo)記來(lái)代替,
  • 如大 B,也就是 basics(基本)的意思,
  • 那么表達(dá)式是code = c * B^3 + o * B^2 + d * B^1 + e * B^0,
  • 最后的哈希函數(shù)就是
  • hash(code) = (c * B^3 + o * B^2 + d * B^1 + e * B^0) % M,
  • 這個(gè) M 對(duì)應(yīng)的取模的方式中那個(gè)素?cái)?shù),
  • 這個(gè) M 也表示了哈希表的那個(gè)數(shù)組中一共有多少個(gè)空間,
  • 對(duì)于這種表示的樣子,這個(gè) code 一共有四個(gè)字符,所以最高位的 c 字符乘以 B 的三次方,
  • 如果這個(gè)字符串有一百個(gè)字符,那么最高位的 c 字符就要乘以 B 的 99 次方,
  • 很多時(shí)候計(jì)算 B 的 k 次方,這個(gè) k 比較大的話,這個(gè)計(jì)算過程也是比較慢的,
  • 所以對(duì)于這個(gè)式子一個(gè)常見的轉(zhuǎn)化形式就是
  • hash(code) = ((((c * B) + o) * B + d) * B + e) % M,
  • 將字符串轉(zhuǎn)換成大整型的一個(gè)括號(hào)轉(zhuǎn)換成了四個(gè),
  • 在每一個(gè)括號(hào)里面做的事情都是拿到一個(gè)字符乘以 B 得到的結(jié)果再加上下一個(gè)字符,
  • 再乘以 B 得到的結(jié)果在加上下一個(gè)字符,
  • 再乘以 B 得到的結(jié)果直到加到最后一個(gè)字符為止,
  • 這樣套四個(gè)括號(hào)之后,這個(gè)式子和那個(gè)套一個(gè)括號(hào)的式子其實(shí)是等價(jià)的,
  • 就是一種簡(jiǎn)單的變形,這樣就不需要先算 B^99 然后再算 B^98 等等這么復(fù)雜了,
  • 每一次都需要乘以一個(gè) B 再加上下一個(gè)字符再乘以 B 依此類推就好,
  • 那么使用程序?qū)崿F(xiàn)的時(shí)候計(jì)算這個(gè)哈希函數(shù)相應(yīng)的速度就會(huì)快一些,
  • 這是一個(gè)很通用的數(shù)學(xué)技巧,是數(shù)學(xué)中的多項(xiàng)式就是這樣的,
  • 但是這么加可能會(huì)導(dǎo)致整型的溢出,
  • 那么就可以將這個(gè)取模的過程分別放入每個(gè)括號(hào)里面,
  • 這樣就可以轉(zhuǎn)化成這種形式
  • hash(code) = ((((c % M) * B + o) % M * B + d) % M * B + e) % M,
  • 這樣一來(lái),每一次都計(jì)算出了比 M 更小的數(shù),所以根本就不用擔(dān)心整型溢出的問題,
  • 這就是數(shù)論中的模運(yùn)算的一個(gè)很重要的性質(zhì)。
  • //hash(code) = ((((c % M) * B + o) % M * B + d) % M * B + e) % M// 上面的公式中 ((((c % M) * B + o) % M * B + d) % M * B + e) % M // 對(duì)應(yīng)下面的代碼,只需要一重for循環(huán)即可,最終的到的就是整個(gè)字符串的哈希值 let s = 'code'; let hash = 0; for (let i = 0; i < s.length; i++) hash = (hash * B + s.charAt(i)) % M; 復(fù)制代碼
  • 復(fù)合類型的哈希函數(shù)設(shè)計(jì)

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

  • 轉(zhuǎn)換成整型并不是哈希函數(shù)設(shè)計(jì)的唯一方法,
  • 只不過這是一個(gè)比較普通比較常用比較通用的一種方法,
  • 在很多特殊的領(lǐng)域有很多相關(guān)的論文去講更多的哈希函數(shù)設(shè)計(jì)的方法。
  • 哈希函數(shù)的設(shè)計(jì),通常要遵循三個(gè)原則

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

  • 在 js 中自定義數(shù)據(jù)類型

  • 對(duì)于自己定義的復(fù)合類型,如學(xué)生類、日期類型,
  • 你可以通過寫 hashCode 方法,
  • 然后自己實(shí)現(xiàn)一下這個(gè)方法重新生成 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() {// 選擇進(jìn)制const B = 31;// 計(jì)算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;// 對(duì)屬性進(jìn)行判斷return (this.name === obj.name &&this.score === obj.score &&this.grade === obj.grade &&this.classId === obj.classId);}// 拆分字符生成數(shù)字 -getCode(s) {s = s + '';let result = 0;// 遍歷字符 計(jì)算結(jié)果for (const c of s) result += c.charCodeAt(0);// 返回結(jié)果return result;}//@Override toString 2018-10-19-jwltoString() {let studentInfo = `Student(name: ${this.name}, score: ${this.score})`;return studentInfo;} } 復(fù)制代碼
  • Main

    // main 函數(shù) 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());}// 將內(nèi)容顯示在頁(yè)面上show(content) {document.body.innerHTML += `${content}<br /><br />`;}// 展示分割線alterLine(title) {let line = `--------------------${title}----------------------`;console.log(line);document.body.innerHTML += `${line}<br /><br />`;} }// 頁(yè)面加載完畢 window.onload = function() {// 執(zhí)行主函數(shù)new Main(); }; 復(fù)制代碼
  • 哈希沖突的處理-鏈地址法(Seperate Chaining)

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

  • 之前實(shí)現(xiàn)的樹結(jié)構(gòu)中都需要進(jìn)行比較
  • 其中的鍵都需要實(shí)現(xiàn) compare 這個(gè)用來(lái)比較兩個(gè)元素的方法,
  • 因?yàn)樾枰ㄟ^鍵來(lái)進(jìn)行比較,
  • 對(duì)于哈希表來(lái)說(shuō)沒有這個(gè)要求,
  • 這個(gè) key 不需要實(shí)現(xiàn)這個(gè)方法。
  • 在哈希表中存儲(chǔ)的元素都需要實(shí)現(xiàn)可以用來(lái)獲取 hashCode 的方法。
  • 對(duì)于哈希表來(lái)說(shuō)相應(yīng)的開多少空間是非常重要的
  • 開的空間越合適,那么相應(yīng)的哈希沖突就越少,
  • 空間大小可以參考http://planetmath.org/goodhashtableprimes,
  • 根據(jù)存儲(chǔ)數(shù)據(jù)的多少來(lái)開辟合適的空間,但是很多時(shí)候并不知道要開多少的空間,
  • 此時(shí)使用哈希表并不能合理的估計(jì)一個(gè) M 值,所以需要進(jìn)行優(yōu)化。
  • 代碼示例

  • MyHashTable

    // 自定義的hash生成類。 class MyHash {constructor() {this.store = new Map();}// 生成hashhashCode(key) {let hash = this.store.get(key);if (hash !== undefined) return hash;else {// 如果 這個(gè)hash沒有進(jìn)行保存 就生成,并且記錄let hash = this.calcHashTwo(key);// 記錄this.store.set(key, hash);// 返回hashreturn hash;}}// 得到的數(shù)字比較小 六七位數(shù) 以下 輔助函數(shù):生成hash -calcHashOne(key) {// 生成hash 隨機(jī)小數(shù) * 當(dāng)前日期毫秒 * 隨機(jī)小數(shù)let hash = Math.random() * Date.now() * Math.random();// hash 取小數(shù)部分的字符串hash = hash.toString().replace(/^\d*\.\d*?([1-9]+)$/, '$1');hash = parseInt(hash); // 取整return hash;}// 得到的數(shù)字很大 十幾位數(shù) 左右 輔助函數(shù):生成hash -calcHashTwo(key) {// 生成hash 隨機(jī)小數(shù) * 當(dāng)前日期毫秒 * 隨機(jī)小數(shù)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; // 實(shí)際元素個(gè)數(shù)this.hashTable = new Array(M); // 哈希表this.hashCalc = new MyHash(); // 哈希值計(jì)算// 初始化哈希表for (var i = 0; i < M; i++) {// this.hashTable[i] = new MyAVLTree();this.hashTable[i] = new Map();}}// 根據(jù)key生成 哈希表索引hash(key) {// 獲取哈希值let hash = this.hashCalc.hashCode(key);// 對(duì)哈希值轉(zhuǎn)換為32位的整數(shù) 再進(jìn)行取模運(yùn)算return (hash & 0x7fffffff) % this.M;}// 獲取實(shí)際存儲(chǔ)的元素個(gè)數(shù)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 基于使系統(tǒng)的Map 底層是哈希表+紅黑樹 // 自定義的哈希表 HashTable 基于自己的AVL樹 class MyHashTableByAVLTree {constructor(M = 97) {this.M = M; // 空間大小this.size = 0; // 實(shí)際元素個(gè)數(shù)this.hashTable = new Array(M); // 哈希表this.hashCalc = new MyHash(); // 哈希值計(jì)算// 初始化哈希表for (var i = 0; i < M; i++) {// this.hashTable[i] = new MyAVLTree();this.hashTable[i] = new MyAVLTreeMap();}}// 根據(jù)key生成 哈希表索引hash(key) {// 獲取哈希值let hash = this.hashCalc.hashCode(key);// 對(duì)哈希值轉(zhuǎn)換為32位的整數(shù) 再進(jìn)行取模運(yùn)算return (hash & 0x7fffffff) % this.M;}// 獲取實(shí)際存儲(chǔ)的元素個(gè)數(shù)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);} } 復(fù)制代碼
  • Main

    // main 函數(shù) class Main {constructor() {this.alterLine('HashTable Comparison Area');const n = 2000000;const random = Math.random;let arrNumber = new Array(n);// 循環(huán)添加隨機(jī)數(shù)的值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.");});// 總毫秒數(shù):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.");});// 總毫秒數(shù):console.log(hashTableInfo1);console.log(hashTable1);this.show(hashTableInfo1);}// 將內(nèi)容顯示在頁(yè)面上show(content) {document.body.innerHTML += `${content}<br /><br />`;}// 展示分割線alterLine(title) {let line = `--------------------${title}----------------------`;console.log(line);document.body.innerHTML += `${line}<br /><br />`;} }// 頁(yè)面加載完畢 window.onload = function() {// 執(zhí)行主函數(shù)new Main(); }; 復(fù)制代碼
  • 哈希表的動(dòng)態(tài)空間處理與復(fù)雜度分析

    哈希表的時(shí)間復(fù)雜度

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

  • 哈希表的本質(zhì)就是一個(gè)數(shù)組
  • 如果這個(gè)數(shù)組是靜態(tài)的話,那么哈希沖突的機(jī)會(huì)會(huì)很多,
  • 如果這個(gè)數(shù)組是動(dòng)態(tài)的話,那么哈希沖突的機(jī)會(huì)會(huì)很少,
  • 因?yàn)槟愦鎯?chǔ)的元素接近無(wú)窮大的話,
  • 靜態(tài)的數(shù)組肯定是無(wú)法讓相應(yīng)的時(shí)間復(fù)雜度接近O(1)級(jí)別。
  • 哈希表的中數(shù)組的空間要隨著元素個(gè)數(shù)的改變進(jìn)行一定的自適應(yīng)
  • 由于靜態(tài)數(shù)組固定的地址空間是不合理的,
  • 所以和自己實(shí)現(xiàn)的動(dòng)態(tài)數(shù)組一樣,需要進(jìn)行 resize,
  • 和自己實(shí)現(xiàn)的動(dòng)態(tài)數(shù)組不一樣的是,哈希表中的數(shù)組不存在所有位置都填滿,
  • 因?yàn)樗拇鎯?chǔ)方式和動(dòng)態(tài)數(shù)組的按照順序一個(gè)一個(gè)的塞進(jìn)數(shù)組的方式不一樣。
  • 相應(yīng)的解決方案是,
  • 當(dāng)平均每個(gè)地址的承載的元素多過一定程度,就去擴(kuò)容,
  • 也就是N / M >= upperTolerance的時(shí)候,也就是設(shè)置一個(gè)上界,
  • 如果 也就是說(shuō)平均每個(gè)地址存儲(chǔ)的元素超過了多少個(gè),如 upperTolerance 為 10,
  • 那么N / M大于等于 10,那么就進(jìn)行擴(kuò)容操作。
  • 反之也有縮容,
  • 當(dāng)平均每個(gè)地址承載的元素少過一定程度,就去縮容,
  • 也就是N / M < lowerTolerance的時(shí)候,也就是設(shè)置一個(gè)下限,
  • 也就是哈希沖突并不嚴(yán)重,那么就不需要開那么大的空間了,
  • 如 lowerTolerance 為 2,那么N / M小于 2,那么就進(jìn)行縮容操作。
  • 大概的原理和動(dòng)態(tài)數(shù)組擴(kuò)容和縮容的原理是一致的,但是有些細(xì)節(jié)方面會(huì)不一樣,
  • 如新的哈希表的根據(jù) key 獲取哈希值后對(duì) M 取模,這個(gè) M 你需要設(shè)置為新的 newM,
  • 并且你遍歷的空間也是原來(lái)那個(gè)舊的 M 個(gè)空間地址,并不是新的 newM 個(gè)空間地址,
  • 所以你需要先將舊的 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 {// 如果 這個(gè)hash沒有進(jìn)行保存 就生成,并且記錄let hash = this.calcHashTwo(key);// 記錄this.store.set(key, hash);// 返回hashreturn hash;}}// 得到的數(shù)字比較小 六七位數(shù) 以下 輔助函數(shù):生成hash -calcHashOne(key) {// 生成hash 隨機(jī)小數(shù) * 當(dāng)前日期毫秒 * 隨機(jī)小數(shù)let hash = Math.random() * Date.now() * Math.random();// hash 取小數(shù)部分的字符串hash = hash.toString().replace(/^\d*\.\d*?([1-9]+)$/, '$1');hash = parseInt(hash); // 取整return hash;}// 得到的數(shù)字很大 十幾位數(shù) 左右 輔助函數(shù):生成hash -calcHashTwo(key) {// 生成hash 隨機(jī)小數(shù) * 當(dāng)前日期毫秒 * 隨機(jī)小數(shù)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; // 實(shí)際元素個(gè)數(shù)this.hashTable = new Array(M); // 哈希表this.hashCalc = new MyHash(); // 哈希值計(jì)算// 初始化哈希表for (var i = 0; i < M; i++) {// this.hashTable[i] = new MyAVLTree();this.hashTable[i] = new Map();}}// 根據(jù)key生成 哈希表索引hash(key) {// 獲取哈希值let hash = this.hashCalc.hashCode(key);// 對(duì)哈希值轉(zhuǎn)換為32位的整數(shù) 再進(jìn)行取模運(yùn)算return (hash & 0x7fffffff) % this.M;}// 獲取實(shí)際存儲(chǔ)的元素個(gè)數(shù)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; // 實(shí)際元素個(gè)數(shù)this.hashTable = new Array(M); // 哈希表this.hashCalc = new MyHash(); // 哈希值計(jì)算// 初始化哈希表for (var i = 0; i < M; i++) {// this.hashTable[i] = new MyAVLTree();this.hashTable[i] = new MyAVLTreeMap();}// 設(shè)定擴(kuò)容的上邊界this.upperTolerance = 10;// 設(shè)定縮容的下邊界this.lowerTolerance = 2;// 初始容量大小為 97this.initCapcity = 97;}// 根據(jù)key生成 哈希表索引hash(key) {// 獲取哈希值let hash = this.hashCalc.hashCode(key);// 對(duì)哈希值轉(zhuǎn)換為32位的整數(shù) 再進(jìn)行取模運(yùn)算return (hash & 0x7fffffff) % this.M;}// 獲取實(shí)際存儲(chǔ)的元素個(gè)數(shù)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++;// 平均元素個(gè)數(shù) 大于等于 當(dāng)前容量的10倍// 擴(kuò)容就翻倍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--;// 平均元素個(gè)數(shù) 小于容量的2倍 當(dāng)然無(wú)論怎么縮容,縮容之后都要大于初始容量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++) {// // 獲取所有實(shí)例// map = this.hashTable[i];// keys = map.getKeys();// // 遍歷每一對(duì)鍵值對(duì) 實(shí)例// 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);}// 重新設(shè)置當(dāng)前hashTablethis.hashTable = newHashTable;} } 復(fù)制代碼
  • Main

    // main 函數(shù) class Main {constructor() {this.alterLine('HashTable Comparison Area');const n = 2000000;const random = Math.random;let arrNumber = new Array(n);// 循環(huán)添加隨機(jī)數(shù)的值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.");});// 總毫秒數(shù):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.");});// 總毫秒數(shù):console.log('HashTableBySystem' + ':' + hashTableInfo1);console.log(hashTable1);this.show('HashTableBySystem' + ':' + hashTableInfo1);}// 將內(nèi)容顯示在頁(yè)面上show(content) {document.body.innerHTML += `${content}<br /><br />`;}// 展示分割線alterLine(title) {let line = `--------------------${title}----------------------`;console.log(line);document.body.innerHTML += `${line}<br /><br />`;} }// 頁(yè)面加載完畢 window.onload = function() {// 執(zhí)行主函數(shù)new Main(); }; 復(fù)制代碼
  • 哈希表更復(fù)雜的動(dòng)態(tài)空間處理方法

    哈希表的復(fù)雜度分析

  • 已經(jīng)為哈希表添加了動(dòng)態(tài)處理空間大小的機(jī)制了
  • 所以就需要對(duì)這個(gè)新的哈希表進(jìn)行一下時(shí)間復(fù)雜度的分析。
  • 自己實(shí)現(xiàn)的動(dòng)態(tài)數(shù)組的均攤復(fù)雜度分析
  • 當(dāng)數(shù)組中的元素個(gè)數(shù)等于數(shù)組的當(dāng)前的容量的時(shí)候,
  • 就需要進(jìn)行擴(kuò)容,擴(kuò)容的大小是當(dāng)前容量的兩倍,
  • 整個(gè)擴(kuò)容的過程要消耗O(n)的復(fù)雜度,
  • 但是這是經(jīng)過 n 次O(1)級(jí)別的操作之后才有這一次O(n)級(jí)別的操作,
  • 所以就把這個(gè)O(n)級(jí)別的操作平攤到 n 次O(1)級(jí)別的操作中,
  • 那么就可以簡(jiǎn)單的理解之前每一次操作都是O(2)級(jí)別的操作,
  • 這個(gè) 2 是一個(gè)常數(shù),對(duì)于復(fù)雜度分析來(lái)說(shuō)會(huì)忽略一下常數(shù),
  • 那么平均時(shí)間復(fù)雜度就是O(1)級(jí)別的。
  • 自己實(shí)現(xiàn)的動(dòng)態(tài)哈希表的復(fù)雜度分析
  • 其實(shí)分析的方式和動(dòng)態(tài)數(shù)組的分析方式是一樣的道理,
  • 也就是說(shuō),哈希表中元素個(gè)數(shù)從 N 增加到了 upperTolerance*N 的時(shí)候,
  • 整個(gè)哈希表的地址空間才會(huì)進(jìn)行一個(gè)翻倍這樣的擴(kuò)容,
  • 也就是說(shuō)增加 9 倍原來(lái)的空間大小之后才會(huì)進(jìn)行空間地址的翻倍,
  • 那么相對(duì)與動(dòng)態(tài)數(shù)組來(lái)說(shuō),是添加了更多的元素才進(jìn)行的翻倍,
  • 這個(gè)操作也是O(n)級(jí)別的操作,
  • 這一次操作也需要平攤到 9*n次操作中去,
  • 那么每一次操作平攤到的時(shí)間復(fù)雜度就會(huì)更少,
  • 正因?yàn)槿绱司退氵M(jìn)行了 resize 操作之后,
  • 哈希表的平均時(shí)間復(fù)雜度還是O(1)級(jí)別的,
  • 其實(shí)每個(gè)操作是在O(lowerTolerance)~O(upperTolerance)之間,
  • 這兩個(gè)數(shù)都是自定義的常數(shù),所以這樣的一個(gè)復(fù)雜度還是O(1)級(jí)別的,
  • 無(wú)論縮容還是擴(kuò)容都是如此,所以這就是哈希表這種數(shù)據(jù)結(jié)構(gòu)的一個(gè)巨大優(yōu)勢(shì),
  • 這個(gè)O(1)級(jí)別的時(shí)間復(fù)雜度是均攤得到的,是平均的時(shí)間復(fù)雜度。
  • 更復(fù)雜的動(dòng)態(tài)空間處理方法

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

  • MyHashTable

    // 自定義的hash生成類。 class MyHash {constructor() {this.store = new Map();}// 生成hashhashCode(key) {let hash = this.store.get(key);if (hash !== undefined) return hash;else {// 如果 這個(gè)hash沒有進(jìn)行保存 就生成,并且記錄let hash = this.calcHashTwo(key);// 記錄this.store.set(key, hash);// 返回hashreturn hash;}}// 得到的數(shù)字比較小 六七位數(shù) 以下 輔助函數(shù):生成hash -calcHashOne(key) {// 生成hash 隨機(jī)小數(shù) * 當(dāng)前日期毫秒 * 隨機(jī)小數(shù)let hash = Math.random() * Date.now() * Math.random();// hash 取小數(shù)部分的字符串hash = hash.toString().replace(/^\d*\.\d*?([1-9]+)$/, '$1');hash = parseInt(hash); // 取整return hash;}// 得到的數(shù)字很大 十幾位數(shù) 左右 輔助函數(shù):生成hash -calcHashTwo(key) {// 生成hash 隨機(jī)小數(shù) * 當(dāng)前日期毫秒 * 隨機(jī)小數(shù)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; // 實(shí)際元素個(gè)數(shù)this.hashTable = new Array(M); // 哈希表this.hashCalc = new MyHash(); // 哈希值計(jì)算// 初始化哈希表for (var i = 0; i < M; i++) {// this.hashTable[i] = new MyAVLTree();this.hashTable[i] = new Map();}}// 根據(jù)key生成 哈希表索引hash(key) {// 獲取哈希值let hash = this.hashCalc.hashCode(key);// 對(duì)哈希值轉(zhuǎn)換為32位的整數(shù) 再進(jìn)行取模運(yùn)算return (hash & 0x7fffffff) % this.M;}// 獲取實(shí)際存儲(chǔ)的元素個(gè)數(shù)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 // 基于系統(tǒng)的哈希表,用來(lái)測(cè)試 // 自定義的哈希表 HashTable // 基于自己實(shí)現(xiàn)的AVL樹 class MyHashTableByAVLTree {constructor() {// 設(shè)定擴(kuò)容的上邊界this.upperTolerance = 10;// 設(shè)定縮容的下邊界this.lowerTolerance = 2;// 哈希表合理的素?cái)?shù)表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; // 實(shí)際元素個(gè)數(shù)this.hashTable = new Array(this.M); // 哈希表this.hashCalc = new MyHash(); // 哈希值計(jì)算// 初始化哈希表for (var i = 0; i < this.M; i++) {// this.hashTable[i] = new MyAVLTree();this.hashTable[i] = new MyAVLTreeMap();}}// 根據(jù)key生成 哈希表索引hash(key) {// 獲取哈希值let hash = this.hashCalc.hashCode(key);// 對(duì)哈希值轉(zhuǎn)換為32位的整數(shù) 再進(jìn)行取模運(yùn)算return (hash & 0x7fffffff) % this.M;}// 獲取實(shí)際存儲(chǔ)的元素個(gè)數(shù)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++;// 平均元素個(gè)數(shù) 大于等于 當(dāng)前容量的10倍,同時(shí)防止索引越界// 就以哈希表合理的素?cái)?shù)表 為標(biāo)準(zhǔn)進(jìn)行 索引的推移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--;// 平均元素個(gè)數(shù) 小于容量的2倍 當(dāng)然無(wú)論怎么縮容,索引都不能越界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++) {// // 獲取所有實(shí)例// map = this.hashTable[i];// keys = map.getKeys();// // 遍歷每一對(duì)鍵值對(duì) 實(shí)例// 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);}// 重新設(shè)置當(dāng)前hashTablethis.hashTable = newHashTable;} } 復(fù)制代碼
  • Main

    // main 函數(shù) class Main {constructor() {this.alterLine('HashTable Comparison Area');const n = 2000000;const random = Math.random;let arrNumber = new Array(n);// 循環(huán)添加隨機(jī)數(shù)的值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.");});// 總毫秒數(shù):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.");});// 總毫秒數(shù):5032console.log('HashTableBySystem' + ':' + hashTableInfo1);console.log(hashTable1);this.show('HashTableBySystem' + ':' + hashTableInfo1);}// 將內(nèi)容顯示在頁(yè)面上show(content) {document.body.innerHTML += `${content}<br /><br />`;}// 展示分割線alterLine(title) {let line = `--------------------${title}----------------------`;console.log(line);document.body.innerHTML += `${line}<br /><br />`;} }// 頁(yè)面加載完畢 window.onload = function() {// 執(zhí)行主函數(shù)new Main(); }; 復(fù)制代碼
  • 哈希表的更多話題

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

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

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

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。