前端學數據結構之字典和散列表

前面的話

  集合、字典和散列表能夠存儲不重複的值。在集合中,咱們感興趣的是每一個值自己,並把它看成主要元素。在字典中,咱們用[鍵,值]的形式來存儲數據。在散列表中也是同樣(也是以[鍵,值]對的形式來存儲數據)。可是兩種數據結構的實現方式略有不一樣,本文將詳細介紹字典和散列表這兩種數據結構html

 

字典

  集合表示一組互不相同的元素(不重複的元素)。在字典中,存儲的是[鍵,值]對,其中鍵名是用來查詢特定元素的。字典和集合很類似,集合以[值,值]的形式存儲元素,字典則是以[鍵,值]的形式來存儲元素。字典也稱做映射算法

【建立字典】編程

  與Set類類似,ECMAScript 6一樣包含了一個Map類的實現,即咱們所說的字典數組

  下面將要實現的類就是以ECMAScript 6中Map類的實現爲基礎的。它和Set類很類似(但不一樣於存儲[值,值]對的形式,咱們將要存儲的是[鍵,值]對)瀏覽器

  這是咱們的Dictionary類的骨架:數據結構

function Dictionary() {
    var items = {};
}

  與Set類相似,咱們將在一個Object的實例而不是數組中存儲元素。 而後,咱們須要聲明一些映射/字典所能使用的方法app

set(key,value):向字典中添加新元素。
remove(key):經過使用鍵值來從字典中移除鍵值對應的數據值。
has(key):若是某個鍵值存在於這個字典中,則返回true,反之則返回false。
get(key):經過鍵值查找特定的數值並返回。
clear():將這個字典中的全部元素所有刪除。
size():返回字典所包含元素的數量。與數組的length屬性相似。
keys():將字典所包含的全部鍵名以數組形式返回。
values():將字典所包含的全部數值以數組形式返回。

【has】編程語言

  首先來實現has(key)方法。之因此要先實現這個方法,是由於它會被set和remove等其餘方法調用。這個方法的實現和以前在Set類中的實現是同樣的。使用JavaScript中的in操做符來驗證一個key是不是items對象的一個屬性。能夠經過以下代碼來實現:函數

this.has = function(key) { 
  return key in items;
}

【set】性能

  該方法接受一個key和一個value做爲參數。咱們直接將value設爲items對象的key屬性的值。它能夠用來給字典添加一個新的值,或者用來更新一個已有的值

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

【remove】

  它和Set類中的remove方法很類似,惟一的不一樣點在於咱們將先搜索key(而不是value),而後咱們可使用JavaScript的delete操做符來從items對象中移除key屬性

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

【get】

  get方法首先會驗證咱們想要檢索的值是否存在(經過查找key值),若是存在,將返回該值, 反之將返回一個undefined值

this.get = function(key) {
  return this.has(key) ? items[key] : undefined;
};

【values】

  首先,咱們遍歷items對象的全部屬性值(行{1})。爲了肯定值存在,咱們使用has函數來驗證key確實存在,而後將它的值加入values數組(行{2})。最後,咱們就能返回全部找到的值。這個方法以數組的形式返回字典中全部values實例的值:

this.values = function() { 
  var values = {};
  for (var k in items) { //{1} 
    if (this.has(k)) {
      values.push(items[k]); //{2}
    }
  }
  return values;
};

【clear】

    this.clear = function(){
        items = {};
    };

【size】

    this.size = function(){
        return Object.keys(items).length;
    };

【keys】

  keys方法返回在Dictionary類中全部用於標識值的鍵名。要取出一個JavaScript對象中全部的鍵名,能夠把這個對象做爲參數傳入Object類的keys方法,以下:

this.keys = function() {
 return Object.keys(items);
}; 

【items】

  下面來驗證items屬性的輸出值。咱們能夠實現一個返回items變量的方法,叫做getItems:

this.getItems = function() {
 return items;
} 

【完整代碼】

  Dictionary類的完整代碼以下所示

function Dictionary(){

    var items = {};

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

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

    this.has = function(key){
        return items.hasOwnProperty(key);
        //return value in items;
    };

    this.get = function(key) {
        return this.has(key) ? items[key] : undefined;
    };

    this.clear = function(){
        items = {};
    };

    this.size = function(){
        return Object.keys(items).length;
    };

    this.keys = function(){
        return Object.keys(items);
    };

    this.values = function(){
        var values = [];
        for (var k in items) {
            if (this.has(k)) {
                values.push(items[k]);
            }
        }
        return values;
    };

    this.each = function(fn) {
        for (var k in items) {
            if (this.has(k)) {
                fn(k, items[k]);
            }
        }
    };

    this.getItems = function(){
        return items;
    }
}

【使用Dictionary類】

  首先,咱們來建立一個Dictionary類的實例,而後給它添加三條電子郵件地址。咱們將會使用這個dictionary實例來實現一個電子郵件地址簿。使用咱們建立的類來執行以下代碼:

var dictionary = new Dictionary(); 
dictionary.set('Gandalf', 'gandalf@email.com'); 
dictionary.set('John', 'johnsnow@email.com'); 
dictionary.set('Tyrion', 'tyrion@email.com');

  若是執行了以下代碼,輸出結果將會是true:

console.log(dictionary.has('Gandalf'));

  下面的代碼將會輸出3,由於咱們向字典實例中添加了三個元素:

console.log(dictionary.size());

  如今,執行下面的幾行代碼:

console.log(dictionary.keys()); 
console.log(dictionary.values()); 
console.log(dictionary.get('Tyrion'));

  輸出結果分別以下所示:

["Gandalf", "John", "Tyrion"]
["gandalf@email.com", "johnsnow@email.com", "tyrion@email.com"] 
tyrion@email.com

  最後,再執行幾行代碼:

dictionary.remove('John');

  再執行下面的代碼:

console.log(dictionary.keys()); 
console.log(dictionary.values()); 
console.log(dictionary.getItems());

  輸出結果以下所示:

["Gandalf", "Tyrion"] 
["gandalf@email.com", "tyrion@email.com"]
Object {Gandalf: "gandalf@email.com", Tyrion: "tyrion@email.com"}

  移除了一個元素後,如今的dictionary實例中只包含兩個元素了

 

散列表

  下面將詳細介紹HashTable類,也叫HashMap類,是Dictionary類的一種散列表實現方式

  散列算法的做用是儘量快地在數據結構中找到一個值。若是要在數據結構中得到一個值(使用get方法),須要遍歷整個數據結構來找到它。若是使用散列函數,就知道值的具體位置,所以可以快速檢索到該值。散列函數的做用是給定一個鍵值,而後返回值在表中的地址

  舉個例子,咱們繼續使用在前面使用的電子郵件地址簿。咱們將要使用最多見的散列函數——「lose lose」散列函數,方法是簡單地將每一個鍵值中的每一個字母的ASCII值相加

hashtable1

【建立散列表】

  咱們將使用數組來表示咱們的數據結構,從搭建類的骨架開始:

function HashTable(){
  var table = [];
}

  而後,給類添加一些方法。咱們給每一個類實現三個基礎的方法

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

  在實現這三個方法以前,要實現的第一個方法是散列函數,它是HashTable類中的一個私有方法:

var loseloseHashCode = function (key) {
 var hash = 0; //{1}
 for (var i = 0; i < key.length; i++) { //{2}
     hash += key.charCodeAt(i); //{3}
 }
 return hash % 37; //{4}
};

  給定一個key參數,就能根據組成key的每一個字符的ASCII碼值的和獲得一個數字。因此,首先須要一個變量來存儲這個總和(行{1})。而後,遍歷key(行{2})並將從ASCII表中查到的每一個字符對應的ASCII值加到hash變量中(可使用JavaScript的String類中的charCodeAt方法——行{3})。最後,返回hash值。爲了獲得比較小的數值,咱們會使用hash值和一個任意數作除法的餘數(mod)

【put】

  如今,有了散列函數,咱們就能夠實現put方法了:

this.put = function(key, value) {
 var position = loseloseHashCode(key); //{5}
 console.log(position + ' - ' + key); //{6}
 table[position] = value; //{7}
}; 

  首先,根據給定的key和所建立的散列函數計算出它在表中的位置(行{5})。爲了便於展現信息,咱們將計算出的位置輸出至控制檯(行{6})。因爲它不是必需的,咱們也能夠將這行代碼移除。而後要作的,是將value參數添加到用散列函數計算出的對應的位置上(行{7})

【get】

  從HashTable實例中查找一個值也很簡單。爲此,將會實現一個get方法。首先,咱們會使用所建立的散列函數來求出給定key所對應的位置。這個函數會返回值的位置,所以咱們所要作的就是根據這個位置從數組table中得到這個值。

this.get = function (key) {
 return table[loseloseHashCode(key)];
}; 

【remove】

  要從HashTable實例中移除一個元素,只須要求出元素的位置(可使用散列函數來獲取)並賦值爲undefined。

  對於HashTable類來講,咱們不須要像ArrayList類同樣從table數組中將位置也移除。因爲元素分佈於整個數組範圍內,一些位置會沒有任何元素佔據,並默認爲undefined值。咱們也不能將位置自己從數組中移除(這會改變其餘元素的位置),不然,當下次須要得到或移除一個元素的時候,這個元素會不在咱們用散列函數求出的位置上

this.remove = function(key) {
 table[loseloseHashCode(key)] = undefined;
}; 

【完整代碼】

  HashTable類的完整代碼以下所示 

function HashTable() {

    var table = [];

    var loseloseHashCode = function (key) {
        var hash = 0;
        for (var i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i);
        }
        return hash % 37;
    };

    var djb2HashCode = function (key) {
        var hash = 5381;
        for (var i = 0; i < key.length; i++) {
            hash = hash * 33 + key.charCodeAt(i);
        }
        return hash % 1013;
    };

    var hashCode = function (key) {
        return loseloseHashCode(key);
    };

    this.put = function (key, value) {
        var position = hashCode(key);
        console.log(position + ' - ' + key);
        table[position] = value;
    };

    this.get = function (key) {
        return table[hashCode(key)];
    };

    this.remove = function(key){
        table[hashCode(key)] = undefined;
    };

    this.print = function () {
        for (var i = 0; i < table.length; ++i) {
            if (table[i] !== undefined) {
                console.log(i + ": " + table[i]);
            }
        }
    };
}

【使用HashTable類】

  下面執行一些代碼來測試HashTable類:

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 

  下面的圖表展示了包含這三個元素的HashTable數據結構:

hashtable2

  如今來測試get方法:

console.log(hash.get('Gandalf'));
console.log(hash.get('Loiane')); 

  得到以下的輸出:

gandalf@email.com
undefined 

  因爲Gandalf是一個在散列表中存在的鍵,get方法將會返回它的值。而因爲Loiane是一個不存在的鍵,當咱們試圖在數組中根據位置獲取值的時候(一個由散列函數生成的位置),返回值將會是undefined(即不存在)

  而後,咱們試試從散列表中移除Gandalf:

hash.remove('Gandalf');
console.log(hash.get('Gandalf'));

  因爲Gandalf再也不存在於表中,hash.get('Gandalf')方法將會在控制檯上給出undefined的輸出結果

【散列集合】

  在一些編程語言中,還有一種叫做散列集合的實現。散列集合由一個集合構成,可是插入、移除或獲取元素時,使用的是散列函數。咱們能夠重用本章中實現的全部代碼來實現散列集合,不一樣之處在於,再也不添加鍵值對,而是隻插入值而沒有鍵。例如,可使用散列集合來存儲全部的英語單詞(不包括它們的定義)。和集合類似,散列集合只存儲惟一的不重複的值

 

處理衝突

  有時候,一些鍵會有相同的散列值。不一樣的值在散列表中對應相同位置的時候,咱們稱其爲衝突。例如,咱們看看下面的代碼會獲得怎樣的輸出結果:

var hash = new HashTable();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@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),Jonathan、Jamie和Sue有相同的散列值(5),Mindy和Paul也有相同的散列值(32)

  那HashTable實例會怎樣呢?執行以前的代碼後散列表中會有哪些值呢?爲了得到結果,咱們來實現一個叫做print的輔助方法,它會在控制檯上輸出HashTable中的值:

this.print = function() {
 for (var i = 0; i < table.length; ++i) { //{1}
   if (table[i] !== undefined) { //{2}
     console.log(i + ": " + table[i]);//{3}
   }
 }
}; 

  首先,遍歷數組中的全部元素(行{1})。當某個位置上有值的時候(行{2}),會在控制檯上輸出位置和對應的值(行{3})。如今來使用這個方法:

hash.print();

  在控制檯上獲得以下的輸出結果:

5:sue@email.com
10:nathan@email.com
13:ana@email.com
16:aaron@email.com
19:gandalf@email.com
29:johnsnow@email.com
32:paul@email.com

  Jonathan、Jamie和Sue有相同的散列值,也就是5。因爲Sue是最後一個被添加的,Sue將是在HashTable實例中佔據位置5的元素。首先,Jonathan會佔據這個位置,而後Jamie會覆蓋它,而後Sue會再次覆蓋。這對於其餘發生衝突的元素來講也是同樣的。

  使用一個數據結構來保存數據的目的顯然不是去丟失這些數據,而是經過某種方法將它們所有保存起來。所以,當這種狀況發生的時候就要去解決它。處理衝突有幾種方法:分離連接、線性探查和雙散列法

【分離連接】

  分離連接法包括爲散列表的每個位置建立一個鏈表並將元素存儲在裏面。它是解決衝突的最簡單的方法,可是它在HashTable實例以外還須要額外的存儲空間

  例如,咱們在以前的測試代碼中使用分離連接的話,輸出結果將會是這樣:

hashtable3

  在位置5上,將會有包含三個元素的LinkedList實例;在位置1三、16和32上,將會有包含兩個元素的LinkedList實例;在位置十、19和29上,將會有包含單個元素的LinkedList實例

  對於分離連接和線性探查來講,只須要重寫三個方法:put、get和remove。這三個方法在每種技術實現中都是不一樣的

  爲了實現一個使用了分離連接的HashTable實例,咱們須要一個新的輔助類來表示將要加入LinkedList實例的元素。咱們管它叫ValuePair類(在HashTable類內部定義):

var ValuePair = function(key, value){
 this.key = key;
 this.value = value;
 this.toString = function() {
  return '[' + this.key + ' - ' + this.value + ']'; 
  }
};

  這個類只會將key和value存儲在一個Object實例中。咱們也重寫了toString方法,以便以後在瀏覽器控制檯中輸出結果

  咱們來實現第一個方法,put方法,代碼以下:

this.put = function(key, value){
 var position = loseloseHashCode(key);
 if (table[position] == undefined) { //{1}
   table[position] = new LinkedList();
 }
 table[position].append(new ValuePair(key, value)); //{2}
}; 

  在這個方法中,將驗證要加入新元素的位置是否已經被佔據(行{1})。若是這個位置是第一次被加入元素,咱們會在這個位置上初始化一個LinkedList類的實例(你已經在第5章中學習過)。而後,使用append方法向LinkedList實例中添加一個ValuePair實例(鍵和值)(行{2})

  而後,咱們實現用來獲取特定值的get方法:

this.get = function(key) {
 var position = loseloseHashCode(key);
 if (table[position] !== undefined){ //{3}
  //遍歷鏈表來尋找鍵/值
  var current = table[position].getHead(); //{4}
    while(current.next){ //{5}
    if (current.element.key === key){ //{6}
      return current.element.value; //{7}
    }
    current = current.next; //{8}
  }
  //檢查元素在鏈表第一個或最後一個節點的狀況
  if (current.element.key === key){ //{9}
    return current.element.value;
  }
 }
 return undefined; //{10}
}; 

  咱們要作的第一個驗證,是肯定在特定的位置上是否有元素存在(行{3})。若是沒有,則返回一個undefined表示在HashTable實例中沒有找到這個值(行{10})。若是在這個位置上有值存在,咱們知道這是一個LinkedList實例。如今要作的是遍歷這個鏈表來尋找咱們須要的元素。在遍歷以前先要獲取鏈表表頭的引用(行{4}),而後就能夠從鏈表的頭部遍歷到尾部(行{5},current.next將會是null)。

  Node鏈表包含next指針和element屬性。而element屬性又是ValuePair的實例,因此它又有value和key屬性。能夠經過current.element.next來得到Node鏈表的key屬性,並經過比較它來肯定它是否就是咱們要找的鍵(行{6})。(這就是要使用ValuePair這個輔助類來存儲元素的緣由。咱們不能簡單地存儲值自己,這樣就不能肯定哪一個值對應着特定的鍵。)若是key值相同,就返回Node的值(行{7});若是不相同,就繼續遍歷鏈表,訪問下一個節點(行{8})。

  若是要找的元素是鏈表的第一個或最後一個節點,那麼就不會進入while循環的內部。所以,須要在行{9}處理這種特殊的狀況

  使用分離連接法從HashTable實例中移除一個元素和以前在本章實現的remove方法有一些不一樣。如今使用的是鏈表,咱們須要從鏈表中移除一個元素。來看看remove方法的實現:

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){ //{11}
      table[position].remove(current.element); //{12}
      if (table[position].isEmpty()){ //{13}
        table[position] = undefined; //{14}
      }
      return true; //{15}
    }
    current = current.next;
  }
  // 檢查是否爲第一個或最後一個元素
  if (current.element.key === key){ //{16}
    table[position].remove(current.element);
  if (table[position].isEmpty()){
    table[position] = undefined;
  }
  return true;
  }
 }
 return false; //{17}
}; 

  在remove方法中,咱們使用和get方法同樣的步驟找到要找的元素。遍歷LinkedList實例時,若是鏈表中的current元素就是要找的元素(行{11}),使用remove方法將其從鏈表中移除。而後進行一步額外的驗證:若是鏈表爲空了(行{13}——鏈表中再也不有任何元素了),就將散列表這個位置的值設爲undefined(行{14}),這樣搜索一個元素或打印它的內容的時候,就能夠跳過這個位置了。最後,返回true表示這個元素已經被移除(行{15})或者在最後返回false表示這個元素在散列表中不存在(行{17})。一樣,須要和get方法同樣,處理元素在第一個或最後一個的狀況(行{16})

  重寫了這三個方法後,咱們就擁有了一個使用了分離連接法來處理衝突的HashMap實例

  分離連接的HashMap的完整代碼以下所示

function HashTableSeparateChaining(){

    var table = [];

    var ValuePair = function(key, value){
        this.key = key;
        this.value = value;

        this.toString = function() {
            return '[' + this.key + ' - ' + this.value + ']';
        }
    };

    var loseloseHashCode = function (key) {
        var hash = 0;
        for (var i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i);
        }
        return hash % 37;
    };

    var hashCode = function(key){
        return loseloseHashCode(key);
    };

    this.put = function(key, value){
        var position = hashCode(key);
        console.log(position + ' - ' + key);

        if (table[position] == undefined) {
            table[position] = new LinkedList();
        }
        table[position].append(new ValuePair(key, value));
    };

    this.get = function(key) {
        var position = hashCode(key);

        if (table[position] !== undefined  && !table[position].isEmpty()){

            //iterate linked list to find key/value
            var current = table[position].getHead();

            do {
                if (current.element.key === key){
                    return current.element.value;
                }
                current = current.next;
            } while(current);
        }
        return undefined;
    };

    this.remove = function(key){

        var position = hashCode(key);

        if (table[position] !== undefined){

            //iterate linked list to find key/value
            var current = table[position].getHead();

            do {
                if (current.element.key === key){
                    table[position].remove(current.element);
                    if (table[position].isEmpty()){
                        table[position] = undefined;
                    }
                    return true;
                }
                current = current.next;
            } while(current);
        }

        return false;
    };

    this.print = function() {
        for (var i = 0; i < table.length; ++i) {
            if (table[i] !== undefined) {
               console.log(table[i].toString());
            }
        }
    };
}

【線性探查】

  另外一種解決衝突的方法是線性探查。當想向表中某個位置加入一個新元素的時候,若是索引爲index的位置已經被佔據了,就嘗試index+1的位置。若是index+1的位置也被佔據了,就嘗試index+2的位置,以此類推

  繼續實現須要重寫的三個方法。第一個是put方法:

this.put = function(key, value){
 var position = loseloseHashCode(key); // {1}
 if (table[position] == undefined) { // {2}
   table[position] = new ValuePair(key, value); // {3}
 } else {
  var index = ++position; // {4}
  while (table[index] != undefined){ // {5}
    index++; // {6}
  }
  table[index] = new ValuePair(key, value); // {7}
 }
}; 

  和以前同樣,先得到由散列函數生成的位置(行{1}),而後驗證這個位置是否有元素存在(若是這個位置被佔據了,將會經過行{2}的驗證)。若是沒有元素存在,就在這個位置加入新元素(行{3}——一個ValuePair的實例)

  若是這個位置已經被佔據了,須要找到下一個沒有被佔據的位置(position的值是undefined),所以咱們聲明一個index變量並賦值爲position+1(行{4}——在變量名前使用自增運算符++會先遞增變量值而後再將其賦值給index)。而後驗證這個位置是否被佔據(行{5}),若是被佔據了,繼續將index遞增(行{6}),直到找到一個沒有被佔據的位置。而後要作的,就是將值分配到這個位置(行{7})

  若是再次執行前面實例中插入數據的代碼,下圖展現使用了線性探查的散列表的最終結果:

hashtable4

  讓咱們來模擬一下散列表中的插入操做

  一、試着插入Gandalf。它的散列值是19,因爲散列表剛剛被建立,位置19仍是空的——能夠在這裏插入數據

  二、試着在位置29插入John。它也是空的,因此能夠插入這個姓名

  三、試着在位置16插入Tyrion。它是空的,因此能夠插入這個姓名

  四、試着插入Aaron,它的散列值也是16。位置16已經被Tyrion佔據了,因此須要檢查索引值爲position+1的位置(16+1)。位置17是空的,因此能夠在位置17插入Aaron

  五、接着,試着在位置13插入Donnie。它是空的,因此能夠插入這個姓名

  六、想在位置13插入Ana,可是這個位置被佔據了。所以在位置14進行嘗試,它是空的,因此能夠在這裏插入姓名

  七、而後,在位置5插入Jonathan,這個位置是空的,因此能夠插入這個姓名

  八、試着在位置5插入Jamie,可是這個位置被佔了。因此跳至位置6,這個位置是空的,所以能夠在這個位置插入姓名

  九、試着在位置5插入Sue,可是位置被佔據了。因此跳至位置6,但也被佔了。接着跳至位置7,這裏是空的,因此能夠在這裏插入姓名。以此類推

  如今插入了全部的元素,下面實現get方法來獲取它們的值

this.get = function(key) {
 var position = loseloseHashCode(key);
 if (table[position] !== undefined){ //{8}
  if (table[position].key === key) { //{9}
    return table[position].value; //{10}
  } else {
    var index = ++position;
    while (table[index] === undefined  || table[index].key !== key){ //{11}
      index++;
    }
    if (table[index].key === key) { //{12}
      return table[index].value; //{13}
    }
  }
 }
 return undefined; //{14}
}; 

  要得到一個鍵對應的值,先要肯定這個鍵存在(行{8})。若是這個鍵不存在,說明要查找的值不在散列表中,所以能夠返回undefined(行{14})。若是這個鍵存在,須要檢查咱們要找的值是否就是這個位置上的值(行{9})。若是是,就返回這個值(行{10})。

  若是不是,就在散列表中的下一個位置繼續查找,直到找到一個鍵值與咱們要找的鍵值相同的元素(行{11})。而後,驗證一下當前項就是咱們要找的項(行{12}——只是爲了確認一下)而且將它的值返回(行{13})。

  咱們沒法肯定要找的元素實際上在哪一個位置,這就是使用ValuePair來表示HashTable元素的緣由

  remove方法和get方法基本相同,不一樣之處在於行{10}和{13},它們將會由下面的代碼代替:

table[index]=undefined;

  要移除一個元素,只須要給其賦值爲undefined,來表示這個位置再也不被佔據而且能夠在必要時接受一個新元素

  線性探查的HashTable的完整代碼以下所示

function HashLinearProbing(){

    var table = [];

    var ValuePair = function(key, value){
        this.key = key;
        this.value = value;

        this.toString = function() {
            return '[' + this.key + ' - ' + this.value + ']';
        }
    };

    var loseloseHashCode = function (key) {
        var hash = 0;
        for (var i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i);
        }
        return hash % 37;
    };

    var hashCode = function(key){
        return loseloseHashCode(key);
    };

    this.put = function(key, value){
        var position = hashCode(key);
        console.log(position + ' - ' + key);

        if (table[position] == undefined) {
            table[position] = new ValuePair(key, value);
        } else {
            var index = ++position;
            while (table[index] != undefined){
                index++;
            }
            table[index] = new ValuePair(key, value);
        }
    };

    this.get = function(key) {
        var position = hashCode(key);

        if (table[position] !== undefined){
            if (table[position].key === key) {
                return table[position].value;
            } else {
                var index = ++position;
                while (table[index] !== undefined && (table[index] && table[index].key !== key)){
                    index++;
                }
                if (table[index] && table[index].key === key) {
                    return table[index].value;
                }
            }
        } else { //search for possible deleted value
            var index = ++position;
            while (table[index] == undefined || index == table.length ||
                (table[index] !== undefined && table[index] && table[index].key !== key)){
                index++;
            }
            if (table[index] && table[index].key === key) {
                return table[index].value;
            }
        }
        return undefined;
    };

    this.remove = function(key){
        var position = hashCode(key);

        if (table[position] !== undefined){
            if (table[position].key === key) {
                table[position] = undefined;
            } else {
                var index = ++position;
                while (table[index] === undefined || table[index].key !== key){
                    index++;
                }
                if (table[index].key === key) {
                    table[index] = undefined;
                }
            }
        }
    };

    this.print = function() {
        for (var i = 0; i < table.length; ++i) {
            if (table[i] !== undefined) {
                console.log(i + ' -> ' + table[i].toString());
            }
        }
    };
}

【更好的散列函數】

  「loselose」散列函數並非一個表現良好的散列函數,由於它會產生太多的衝突。若是使用這個函數的話,會產生各類各樣的衝突。一個表現良好的散列函數是由幾個方面構成的:插入和檢索元素的時間(即性能),固然也包括較低的衝突可能性

  另外一個能夠實現的比「loselose」更好的散列函數是djb2:

var djb2HashCode = function (key) {
 var hash = 5381; //{1}
 for (var i = 0; i < key.length; i++) { //{2}
     hash = hash * 33 + key.charCodeAt(i); //{3}
 }
 return hash % 1013; //{4}
}; 

  它包括初始化一個hash變量並賦值爲一個質數(行{1}——大多數實現都使用5381),而後迭代參數key(行{2}),將hash與33相乘(用來看成一個魔力數),並和當前迭代到的字符的ASCII碼值相加(行{3})

  最後,咱們將使用相加的和與另外一個隨機質數(比咱們認爲的散列表的大小要大——在本例中,咱們認爲散列表的大小爲1000)相除的餘數。

  若是再次執行前面實例中插入數據的代碼,這將是使用djb2HashCode代替loseloseHashCode的最終結果:

798-Gandalf
838-John
624-Tyrion
215-Aaron
278-Donnie
925-Ana
288-Jonathan
962-Jamie
502-Sue
804-Mindy
54-Paul
223-Nathan

  沒有衝突!這並非最好的散列函數,但這是最被社區推薦的散列函數之一

 

ES6

  ECMAScript 2015新增了Map類。咱們能夠基於ES6的Map類開發咱們的Dictionary類

  看看原生的Map類怎麼用。仍是用咱們原來測試Dictionary類的例子:

var map = new Map();
map.set('Gandalf', 'gandalf@email.com');
map.set('John', 'johnsnow@email.com');
map.set('Tyrion', 'tyrion@email.com');
console.log(map.has('Gandalf')); //輸出true
console.log(map.size); //輸出3
console.log(map.keys()); //輸出["Gandalf", "John", "Tyrion"]
console.log(map.values()); //輸出["gandalf@email.com",
"johnsnow@email.com", "tyrion@email.com"]
console.log(map.get('Tyrion')); //輸出tyrion@email.com 

  和Dictionary類不一樣,ES6的Map類的values方法和keys方法都返回Iterator,而不是值或鍵構成的數組。另外一個區別是,咱們實現的size方法返回字典中存儲的值的個數,而ES6的Map類則有一個size屬性

  刪除map中的元素能夠用delete方法:

map.delete('John');

  clear方法會重置map數據結構,這跟咱們在Dictionary類裏實現的同樣

  除了Set和Map這兩種新的數據結構,ES6還增長了它們的弱化版本,WeakSet和WeakMap。基本上,Map和Set與其弱化版本之間僅有的區別是:

  一、WeakSet或WeakMap類沒有entries、keys和values等方法;

  二、只能用對象做爲鍵

  建立和使用這兩個類主要是爲了性能。WeakSet和WeakMap是弱化的(用對象做爲鍵),沒有強引用的鍵。這使得JavaScript的垃圾回收器能夠從中清除整個入口。另外一個優勢是,必須用鍵才能夠取出值。這些類沒有entries、keys和values等迭代器方法,所以,除非知道鍵,不然沒有辦法取出值

  使用WeakMap類的例子以下:

var map = new WeakMap();
var ob1 = {name:'Gandalf'}, //{1}
    ob2 = {name:'John'},
    ob3 = {name:'Tyrion'};
map.set(ob1, 'gandalf@email.com'); //{2}
map.set(ob2, 'johnsnow@email.com');
map.set(ob3, 'tyrion@email.com');
console.log(map.has(ob1)); //{3} 輸出true
console.log(map.get(ob3)); //{4} 輸出tyrion@email.com
map.delete(ob2); //{5}

  WeakMap類也能夠用set方法,但不能使用數字、字符串、布爾值等基本數據類型,須要將名字轉換爲對象(行{1}和行{2})。搜索(行{3})、讀取(行{4})和刪除值(行{5}),也要傳入做爲鍵的對象。一樣的邏輯也適用於WeakSet類

相關文章
相關標籤/搜索