學習數據結構與算法之字典和散列表

本系列全部文章:
第一篇文章:學習數據結構與算法之棧與隊列
第二篇文章:學習數據結構與算法之鏈表
第三篇文章:學習數據結構與算法之集合
第四篇文章:學習數據結構與算法之字典和散列表
第五篇文章:學習數據結構與算法之二叉搜索樹javascript

字典

不是新華字典哦,這裏的字典也稱做_映射_,是一種數據結構,跟set集合很類似的一種數據結構,均可以用來存儲無序不重複的數據。不一樣的地方是集合以[值,值]的形式存儲,而字典則是以[鍵,值]或者叫做{key: value}的形式。java

用JavaScript實現字典

先實現一個構造函數:node

function Dictionary () {
  var items = {}
}

字典須要實現如下方法:git

  • set(key, value):向字典中添加新元素
  • remove(key):經過使用key來從字典中移除對應元素
  • has(key):經過key來判斷字典中是否有該元素
  • get(key):經過key來找到特定的數值並返回
  • clear():清空字典
  • size():返回字典包含元素的數量
  • keys():返回字典所包含的全部鍵名,以數組形式返回
  • values():同上一個方法同樣,只不過鍵名換成了數值,也是以數組形式返回

實現has

由於後面的方法都要用到has,因此先實現這個方法github

this.has = function (key) {
  // 書上用的是in操做符來判斷,可是in對於繼承來的屬性也會返回true,因此我換成了這個
  return items.hasOwnProperty(key)
}

實現set

沒啥好說的,簡單的賦值算法

this.set = function (key, value) {
  items[key] = value
}

實現remove

首先判斷key是否屬於該字典,而後再刪除segmentfault

this.remove = function (key) {
  if (this.has(key)) {
    delete items[key]
    return true
  }
  return false
}

實現values

返回由數值組成的數組數組

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)而直接訪問在內存存儲位置的數據結構。也就是說,它經過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱作散列函數,存放記錄的數組稱作散列表

能夠理解爲,數據中的鍵經過一個散列函數的計算後返回一個數據存放的地址,所以能夠直接經過地址來訪問它的值,這樣的查找就很是快。

用JavaScript實現散列表

先看這個構造函數,注意:存儲數據用的是數組,由於尋址訪問元素最方便的仍是數組了。

function HashTable () {
    var table = []
}

須要實現的方法:

  • put(key, value):向散列表增長一個新的項
  • remove(key):根據鍵值從散列表中移除值
  • get(key):返回根據鍵值檢索到的特定的值

先實現散列函數

把鍵名的每一個字母轉成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的理解進一步加深了。繼續努力吧~

相關文章
相關標籤/搜索