上一篇寫了如何實現簡單的Map結構,因為東西太少了不讓上首頁。好吧。。。
這一篇文章說一下散列表hashMap的實現。那么為什么要使用hashMap?hashMap又有什么優勢呢?hashMap是如何檢索數據的?我們一點一點的來解答。
在我們學習一門編程語言的時候,最開始學習的部分就是循環遍歷。那么為什么要遍歷呢?因為我們需要拿到具體的值,數組中我們要遍歷數組獲取所有的元素才能定位到我們想要的元素。對象也是一樣,我們同樣要遍歷所有的對象元素來獲取我們想要的指定的元素。那么無論是array也好,object也好,棧還是隊列還是列表或者集合(我們前面學過的所有數據結構)都需要遍歷。不然我們根本拿不到我們想要操作的具體的元素。但是這樣就有一個問題,那就是效率。如果我們的數據有成百萬上千萬的數據。我們每一次循環遍歷都會消耗大量的時間,用戶體驗可以說幾乎沒有。(當然,前端幾乎不會遇到這種情況,因為大數據量的情況都通過分頁來轉化了)。
那么,有沒有一種快速有效的定位我們想要的元素的數據結構呢?答案就是hashMap。當然,應該也有其它更高效的數據處理方式,但是我暫時不知道啊。。。。
那么hashMap是如何存取元素的呢?首先,hashMap在存儲元素的時候,會通過lose lose散列函數來設置key,這樣我們就無需遍歷整個數據結構,就可以快速的定位到該元素的具體位置,從而獲取到具體的值。
什么是lose lose散列函數呢?其實lose lose散列函數就是簡單的把每個key中的所有字母的ASCII碼值相加,生成一個數字,作為散列表的key。當然,這種方法並不是很好,會生成很多相同的散列值。下面會具體的講解如何解決,以及一種更好的散列函數djb2。
那么我們開始實現我們的hashMap:
// 這里我們沒在重復的去寫clear,size等其他的方法,因為跟前面實在是沒啥區別。 function HashMap() { // 我們使用數組來存儲元素 var list = []; //轉換散列值得loselose散列函數。 var loseloseHashCode = function (key) { var hash = 0; // 遍歷字符串key的長度,注意,字符串也是可以通過length來獲取每一個字節的。 for(var i = 0; i < key.length; i++) { hash += key.charCodeAt(i) } //對hash取余,這是為了得到一個比較小的hash值, //但是這里取余的對象又不能太大,要注意 return hash % 37; } //通過loselose散列函數直接在計算出來的位置放入對應的值。 this.put = function (key,value) { var position = loseloseHashCode(key); console.log(position + "-" + key); list[position] = value; } //同樣的,我們想要得到一個值,只要通過散列函數計算出位置就可以直接拿到,無需循環 this.get = function (key) { return list[loseloseHashCode(key)]; } //這里要注意一下,我們的散列表是松散結構,也就是說散列表內的元素並不是每一個下標index都一定是有值, //比如我存儲兩個元素,一個計算出散列值是14,一個是20,那么其余的位置仍舊是存在的,我們不能刪除它,因為一旦刪除,我們存儲元素的位置也會改變。 //所以這里要移除一個元素,只要為其賦值為undefined就可以了。 this.remove = function (key) { list[loseloseHashCode(key)] = undefined; } this.print = function () { for(var i = 0; i < list.length; i++) { // 大家可以把這里的判斷去掉,看看到底是不是松散的數組結構。 if(list[i] !== undefined) { console.log(i + ":" + list[i]); } } } } //那么我們來測試一下我們的hashMap var hash = new HashMap(); hash.put("Gandalf",'www.gandalf.com'); hash.put("John",'www.john.com'); hash.put("Tyrion",'www.tyrion.com'); //因為我們在put代碼中加了一個console以便我們更好的理解代碼,我們看一下輸出 // 19-Gandalf // 29-John // 16-Tyrion console.log(hash.get('John'));//www.john.com console.log(hash.get("Zaking"));//undefined //那么我們來移除一個元素John hash.remove("John"); console.log(hash.get("John"));//undefined
那么我們就實現並且簡單測試了一下我們自定義的hashMap,發現還不錯哦。但是元素太少,沒有代表性。我們再多測試幾個數據看看會如何?
var conflictHash = new HashMap(); conflictHash.put("Gandalf",'www.Gandalf.com');//19-Gandalf conflictHash.put("John",'www.John.com');//29-John conflictHash.put("Tyrion",'www.Tyrion.com');//16-Tyrion conflictHash.put("Aaron",'www.Aaron.com');//16-Aaron conflictHash.put("Donnie",'www.Donnie.com');//13-Donnie conflictHash.put("Ana",'www.Ana.com');//13-Ana conflictHash.put("Jonathan",'www.Jonathan.com');//5-Jonathan conflictHash.put("Jamie",'www.Jamie.com');//5-Jamie conflictHash.put("Sue",'www.Sue.com');//5-Sue conflictHash.put("Mindy",'www.Mindy.com');//32-Mindy conflictHash.put("Paul",'www.Paul.com');//32-Paul conflictHash.put("Nathan",'www.Nathan.com');//10-Nathan conflictHash.print(); /* 5:www.Sue.com 10:www.Nathan.com 13:www.Ana.com 16:www.Aaron.com 19:www.Gandalf.com 29:www.John.com 32:www.Paul.com */
我們發現后來的把前面相同散列值得元素給替換了。那么之前的元素也就隨之丟失了,這絕不是我們想要看到的樣子。這才十幾個元素就有這么多相同的,如果數據量極大那還了得。。。這啥用沒有啊。。。所以,我們需要解決這樣的問題,我們這里介紹兩種解決這種沖突的方法。分離鏈接和線性探查。
1、分離鏈接
分離鏈接,其實核心就是為散列表的每一個位置創建一個鏈表,並將元素存儲在里面。它可以說是解決沖突的最簡單的方法,但是,它占用了額外的存儲空間。之前的例子,如果用分離鏈接來解決沖突的話,那么看起來就是這個樣子。
那么我們就需要重寫hashMap,我們來看看分離鏈接下的hashMap是如何實現的。由於我們要重寫hashMap類中的方法,所以我們重新構建一個新的類:SeparateHashMap。
function LinkedList() {//...鏈表方法} // 創建分離鏈接法下的hashMap。 function SeparateHashMap () { var list = []; //loselose散列函數。 var loseloseHashCode = function (key) { var hash = 0; for(var i = 0; i < key.length; i++) { hash += key.charCodeAt(i) } return hash % 37; } //這里為什么要創建一個新的用來存儲鍵值對的構造函數? //首先我們要知道的一點是,在分離鏈接下,我們元素所存儲的位置實際上是在鏈表里面。 //而一旦在該散列位置下的鏈表中有多個值,我們仍舊需要通過key去找鏈表中所對應的元素。 //換句話說,分離鏈接下的存儲方式是,首先通過key來計算散列值,然后把對應的key和value也就是ValuePair存入linkedList。 //這就是valuePair的作用了。 var ValuePair = function (key,value) { this.key = key; this.value = value; this.toString = function () { return "[" + this.key + "-" + this.value + "]"; } } //同樣的,我們通過loselose散列函數計算出對應key的散列值。 this.put = function (key,value) { var position = loseloseHashCode(key); //這里如果該位置為undefined,說明這個位置沒有鏈表,那么我們就新建一個鏈表。 if(list[position] == undefined) { list[position] = new LinkedList(); } //新建之后呢,我們就通過linkedList類的append方法把valuePair加入進去。 //那么如果上面的判斷是false,也就是有了鏈表,直接跳過上面的判斷執行加入操作就好了。 list[position].append(new ValuePair(key,value)); } this.get = function (key) { var position = loseloseHashCode(key); //鏈表的操作前面相應的鏈表文章已經寫的很清楚了。這里就盡量簡單說清 //如果這個位置不是undefined,那么說明存在鏈表 if(list[position] !== undefined) { //我們要拿到current,也就是鏈表中的第一個元素進行鏈表中的遍歷。 var current = list[position].getHead(); //如果current.next不為null說明還有下一個 while(current.next) { //如果要查找的key是當前鏈表元素的key,就返回該鏈表節點的value。 //這里要注意一下。current.element = ValuePair噢! if(current.element.key === key) { return current.element.value; } current = current.next; } //那么這里剛開始讓我有些疑惑。為啥還要單獨判斷一下? //我們回頭看一下,我們while循環的條件是current.next。沒current什么事啊...對了。 //所以,這里我們還要單獨判斷一下是不是current。 //總結一下,這段get方法的代碼運行方式是從第一個元素的下一個開始遍歷,如果到最后還沒找到,就看看是不是第一個,如果第一個也不是,那就返回undefined。沒找到想要得到元素。 if(current.element.key === key) { return current.element.value; } } return undefined; } //這個remove方法就不說了。跟get方法一模一樣,get方法是在找到對應的值的時候返回該值的value,而remove方法是在找到該值的時候,重新賦值為undefined,從而移除它。 this.remove = function (key) { var position = loseloseHashCode(key); if(list[position] !== undefined) { var current = list[position].getHead(); while(current.next) { if(current.element.key === key) { list[position].remove(current.element); if(list[position].isEmpty()) { list[position] = undefined; } return true; } current = current.next; } if(current.element.key === key) { list[position].remove(current.element); if(list[position].isEmpty()) { list[position] = undefined; } return true; } } return false; }; this.print = function () { for(var i = 0; i < list.length; i++) { // 大家可以把這里的判斷去掉,看看到底是不是松散的數組結構。 if(list[i] !== undefined) { console.log(i + ":" + list[i]); } } } } var separateHash = new SeparateHashMap(); separateHash.put("Gandalf",'www.Gandalf.com');//19-Gandalf separateHash.put("John",'www.John.com');//29-John separateHash.put("Tyrion",'www.Tyrion.com');//16-Tyrion separateHash.put("Aaron",'www.Aaron.com');//16-Aaron separateHash.put("Donnie",'www.Donnie.com');//13-Donnie separateHash.put("Ana",'www.Ana.com');//13-Ana separateHash.put("Jonathan",'www.Jonathan.com');//5-Jonathan separateHash.put("Jamie",'www.Jamie.com');//5-Jamie separateHash.put("Sue",'www.Sue.com');//5-Sue separateHash.put("Mindy",'www.Mindy.com');//32-Mindy separateHash.put("Paul",'www.Paul.com');//32-Paul separateHash.put("Nathan",'www.Nathan.com');//10-Nathan separateHash.print(); /* 5:[Jonathan-www.Jonathan.com]n[Jamie-www.Jamie.com]n[Sue-www.Sue.com] 10:[Nathan-www.Nathan.com] 13:[Donnie-www.Donnie.com]n[Ana-www.Ana.com] 16:[Tyrion-www.Tyrion.com]n[Aaron-www.Aaron.com] 19:[Gandalf-www.Gandalf.com] 29:[John-www.John.com] 32:[Mindy-www.Mindy.com]n[Paul-www.Paul.com] */ console.log(separateHash.get("Paul")); /* www.Paul.com */ console.log(separateHash.remove("Jonathan"));//true separateHash.print(); /* 5:[Jamie-www.Jamie.com]n[Sue-www.Sue.com] 10:[Nathan-www.Nathan.com] 13:[Donnie-www.Donnie.com]n[Ana-www.Ana.com] 16:[Tyrion-www.Tyrion.com]n[Aaron-www.Aaron.com] 19:[Gandalf-www.Gandalf.com] 29:[John-www.John.com] 32:[Mindy-www.Mindy.com]n[Paul-www.Paul.com] */
其實,分離鏈接法,是在每一個散列值對應的位置上新建了一個鏈表以供重復的值可以存儲,我們需要通過key分別在hashMap和linkedList中查找值,而linkedList中的查找仍舊是遍歷。如果數據量很大,其實仍舊會耗費一些時間。但是當然,肯定要比數組等這樣需要遍歷整個數據結構的方式要效率的多。
下面我們來看看線性探查法。
2、線性探查
什么是線性探查呢?其實就是在hashMap中發生沖突的時候,將散列函數計算出的散列值+1,如果+1還是有沖突那么就+2。直到沒有沖突為止。
其實分離鏈接和線性探查兩種方法,多少有點時間換空間的味道。
我們還是來看代碼。
function LinearHashMap () { var list = []; var loseloseHashCode = function (key) { var hash = 0; for(var i = 0; i < key.length; i++) { hash += key.charCodeAt(i) } return hash % 37; } var ValuePair = function (key,value) { this.key = key; this.value = value; this.toString = function () { return "[" + this.key + "-" + this.value + "]"; } } this.put = function (key,value) { var position = loseloseHashCode(key); //同樣的,若是沒有值。就把該值存入 if(list[position] == undefined) { list[position] = new ValuePair(key,value); } else { // 如果有值,那么久循環到沒有值為止。 var index = ++position; while(list[index] != undefined) { index++ } list[index] = new ValuePair(key,value); } } this.get = function (key) { var position = loseloseHashCode(key); if(list[position] !== undefined) { if(list[position].key === key) { return list[position].value; } else { var index = ++position; while(list[index] === undefined || list[index].key !== key) { index ++; } if(list[index] .key === key) { return list[index].value } } } return undefined; } this.remove = function (key) { var position = loseloseHashCode(key); if(list[position] !== undefined) { if(list[position].key === key) { list[index] = undefined; } else { var index = ++position; while(list[index] === undefined || list[index].key !== key) { index ++; } if(list[index] .key === key) { list[index] = undefined; } } } return undefined; }; this.print = function () { for(var i = 0; i < list.length; i++) { // 大家可以把這里的判斷去掉,看看到底是不是松散的數組結構。 if(list[i] !== undefined) { console.log(i + ":" + list[i]); } } } } var linearHash = new LinearHashMap(); linearHash.put("Gandalf",'www.Gandalf.com');//19-Gandalf linearHash.put("John",'www.John.com');//29-John linearHash.put("Tyrion",'www.Tyrion.com');//16-Tyrion linearHash.put("Aaron",'www.Aaron.com');//16-Aaron linearHash.put("Donnie",'www.Donnie.com');//13-Donnie linearHash.put("Ana",'www.Ana.com');//13-Ana linearHash.put("Jonathan",'www.Jonathan.com');//5-Jonathan linearHash.put("Jamie",'www.Jamie.com');//5-Jamie linearHash.put("Sue",'www.Sue.com');//5-Sue linearHash.put("Mindy",'www.Mindy.com');//32-Mindy linearHash.put("Paul",'www.Paul.com');//32-Paul linearHash.put("Nathan",'www.Nathan.com');//10-Nathan linearHash.print(); console.log(linearHash.get("Paul")); console.log(linearHash.remove("Mindy")); linearHash.print();
LinearHashMap與SeparateHashMap在方法上有着相似的實現。這里就不再浪費篇幅的去解釋了,但是大家仍舊要注意其中的細節。比如說在位置的判斷上的不同之處。
那么HashMap對於沖突的解決方法這里就介紹這兩種。其實還有很多方法可以解決沖突,但是我覺得最好的辦法就是讓沖突的可能性變小。當然,無論是使用什么方法,沖突都是有可能存在的。
那么如何讓沖突的可能性變小呢?很簡單,就是讓計算出的散列值盡可能的不重復。下面介紹一種比loselose散列函數更好一些的散列函數djb2。
var djb2HashCode = function(key) { var hash = 5831; for(var i = 0; i < key.length; i++) { hash = hash * 33 + key.charCodeAt(i); } return hash % 1013; }
大家可以把最開始實現的HashMap的loselose散列函數換成djb2。再去添加元素測試一下是否沖突的可能性變小了。
djb2散列函數中,首先用一個hash變量存儲一個質數(只能被1和自身整除的數)。將hash與33相乘並加上當前迭代道德ASCII碼值相加。最后對1013取余。就得到了我們想要的散列值。
到這里,hashMap就介紹完了。希望大家可以認真的去閱讀查看。
最后,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!