本系列全部文章:
第一篇文章:學習數據結構與算法之棧與隊列
第二篇文章:學習數據結構與算法之鏈表
第三篇文章:學習數據結構與算法之集合
第四篇文章:學習數據結構與算法之字典和散列表
第五篇文章:學習數據結構與算法之二叉搜索樹javascript
不是新華字典哦,這裏的字典也稱做_映射_,是一種數據結構,跟set集合很類似的一種數據結構,均可以用來存儲無序不重複的數據。不一樣的地方是集合以[值,值]的形式存儲,而字典則是以[鍵,值]或者叫做{key: value}的形式。java
先實現一個構造函數:node
function Dictionary () { var items = {} }
字典須要實現如下方法:git
由於後面的方法都要用到has,因此先實現這個方法github
this.has = function (key) { // 書上用的是in操做符來判斷,可是in對於繼承來的屬性也會返回true,因此我換成了這個 return items.hasOwnProperty(key) }
沒啥好說的,簡單的賦值算法
this.set = function (key, value) { items[key] = value }
首先判斷key是否屬於該字典,而後再刪除segmentfault
this.remove = function (key) { if (this.has(key)) { delete items[key] return true } return false }
返回由數值組成的數組數組
this.values = function () { var values = [] for (var k in items) { if (items.has(k)) { values.push(items[k]) } } return values }
其餘的比較簡單,和集合的方法實現相似,我就直接貼源代碼了數據結構
this.keys = function () { return Object.keys(items) } this.size = function () { return Object.keys(items).length } this.clear = function () { items = {} } this.getItems = function () { return items } this.get = function (key) { return this.has(key) ? items[key] : undefined }
源代碼在此:app
關於散列表的定義,這裏摘抄一下維基百科的解釋:
散列表(Hash table,也叫哈希表),是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。也就是說,它經過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱作散列函數,存放記錄的數組稱作散列表。
能夠理解爲,數據中的鍵經過一個散列函數的計算後返回一個數據存放的地址,所以能夠直接經過地址來訪問它的值,這樣的查找就很是快。
先看這個構造函數,注意:存儲數據用的是數組,由於尋址訪問元素最方便的仍是數組了。
function HashTable () { var table = [] }
須要實現的方法:
把鍵名的每一個字母轉成ASCII碼再相加起來,最後和一個任意的數求餘,獲得數據存儲的地址。
var loseloseHashCode = function (key) { var hash = 0 for (var i = 0; i < key.length; i++) { hash += key.charCodeAt(i) } return hash % 37 }
由於這裏的方法比較簡單,我就直接全貼上來了
this.put = function (key, value) { var position = loseloseHashCode(key) console.log(position + ' - ' + key) table[position] = value } this.remove = function (key) { table[loseloseHashCode(key)] = undefined } this.get = function (key) { return table[loseloseHashCode(key)] } // 用來輸出整個散列表,查看結果用的 this.print = function () { for (var i = 0; i < table.length; i++) { if (table[i] !== undefined) { console.log(i + ': ' + table[i]) } } }
稍等,還沒完呢,如今看似很完美,咱們調用一下剛纔寫的:
var hash = new HashTable() hash.put('Gandalf', 'gandalf@email.com') hash.put('John', 'johnsnow@email.com') hash.put('Tyrion', 'tyrion@email.com') // 輸出結果 // 19 - Gandalf // 29 - John // 16 - Tyrion
好像沒什麼不對,可是仔細想一想:若是在某些狀況下,散列函數根據傳入的鍵計算獲得的地址是同樣的會怎麼樣呢?
看看下面這個例子:
var hash = new HashTable() hash.put('Gandalf', 'gandalf@email.com') hash.put('John', 'john@email.com') hash.put('Tyrion', 'tyrion@email.com') hash.put('Aaron', 'aaron@email.com') hash.put('Donnie', 'donnie@email.com') hash.put('Ana', 'ana@email.com') hash.put('Jonathan', 'jonathan@email.com') hash.put('Jamie', 'jamie@email.com') hash.put('Sue', 'sue@email.com') hash.put('Mindy', 'mindy@email.com') hash.put('Paul', 'paul@email.com') hash.put('Nathan', 'nathan@email.com') // 輸出結果 // 19 - Gandalf // 29 - John // 16 - Tyrion // 16 - Aaron // 13 - Donnie // 13 - Ana // 5 - Jonathan // 5 - Jamie // 5 - Sue // 32 - Mindy // 32 - Paul // 10 - Nathan
這種狀況就比較複雜了:Tyrion和Aaron的值都是16,Donnie和Ana都是13,還有其餘不少重複的值,這時散列表表中是什麼狀況呢
hash.print() // 輸出結果 // 5: sue@email.com // 10: nathan@email.com // 13: ana@email.com // 16: aaron@email.com // 19: gandalf@email.com // 29: john@email.com // 32: paul@email.com
很明顯,後面的元素會覆蓋前面的元素,但這樣是不行的,要想辦法解決衝突
目前主流的方法主要是兩種:分離連接法和線性探查法,這裏就簡單介紹一下分離連接法。
思路很簡單:你不是重複了嗎,那我就在同一個位置裏面放一個鏈表,重複的數據全都放在鏈表裏面,你要找數據就在鏈表裏面找。
若是不理解,能夠參見下圖:
(圖片來源谷歌搜索,侵刪)
根據這個思路,咱們從新實現一下三個方法,不過在這以前,咱們須要一個對象來保存鍵值對
var ValuePair = function (key, value) { this.key = key this.value = value // 重寫toString主要是方便輸出查看 this.toString = function () { return '[' + this.key + ' - ' + this.value + ']' } }
接下來就是重寫方法了
this.put = function (key, value) { var position = loseloseHashCode(key) // 若是發現該位置尚未元素,就先放一個鏈表,再用append加進去 if (table[position] === undefined) { // 由於使用node執行文件,這裏LinkedList是我在頂部用require引入的LinkedList.js table[position] = new LinkedList() } table[position].append(new ValuePair(key, value)) } this.get = function (key) { var position = loseloseHashCode(key) if (table[position] !== undefined) { var current = table[position].getHead() // 遍歷鏈表查找值 while (current.next) { if (current.element.key === key) { return current.element.value } current = current.next } // 檢查元素若是是最後一個的狀況 if (current.element.key === key) { return current.element.value } } return undefined } this.remove = function (key) { var position = loseloseHashCode(key) if (table[position] !== undefined) { var current = table[position].getHead() // 遍歷查找值 while (current.next) { if (current.element.key === key) { // 使用鏈表的remove方法 table[position].remove(current.element) // 當鏈表爲空了,就把散列表該位置設爲undefined if (table[position].isEmpty()) { table[position] = undefined } return true } current = current.next } if (current.element.key === key) { table[position].remove(current.element) if (table[position].isEmpty()) { table[position] = undefined } return true } } return false }
以上就是用分離連接法重寫的哈希表。
第二種辦法思路更粗暴:你不是佔了這個位置嘛,那我就佔下一個,下個位置還被佔了?那就佔再下一個~
具體的實現我就不貼出來了,讀者能夠自行思考並實現,而後對照代碼看看。
這裏先把源代碼放出來了
以上是哈希表的兩個衝突解決辦法,但實際上應用哈希表的時候能避免衝突就儘可能避免衝突,一開始的散列函數不是一個好的函數,由於衝突太多了,這裏就貼書上的一個不錯的散列函數(社區),原理大體是:相加所得的hash數要夠大,且儘可能爲質數,用hash與另外一個大於散列表大小的質數作求餘,這樣獲得的地址也能儘可能不重複。
var djb2HashCode = function (key) { var hash = 5381 for (var i = 0; i < key.length; i++) { hash = hash * 33 + key.charCodeAt(i) } return hash % 1013 }
實現了字典和哈希表感受沒有想象中那麼困難,固然這仍是開始。
不過,這幾天本身不斷地研究數據結構,也讓本身對於JS的理解進一步加深了。繼續努力吧~