【閱讀筆記:雜湊表】Javascript任何物件都是一個雜湊表(hash表)!

愛忘的旺仔發表於2019-07-04

什麼是雜湊表?

  • 雜湊表是Dictionary(字典)的一種雜湊表實現方式,字典傳送門
  • 一個很常見的應用是使用雜湊表來表示物件。Javascript語言內部就是使用雜湊表來表示每個物件。此時,物件的每個屬性和方法(成員)被儲存為key物件型別,每個key指向對應的物件成員。
  • 字典中使用的電子郵件地址簿為例。我們將使用最常見的雜湊函式:lose lose雜湊函式,方法是簡單的將每個鍵值中的每個字元的ASCII值相加,如下圖所示:
    【閱讀筆記:雜湊表】Javascript任何物件都是一個雜湊表(hash表)!

建立雜湊表

class HashTable {
this.table = {};
}
複製程式碼

實現幾個簡單方法

  1. toStrFn() 轉字串 和字典中一樣
toStrFn (key){
    if (key === null) {
    return 'NULL';
  } else if (key === undefined) {
    return 'UNDEFINED';
  } else if (typeof key === 'string' || key instanceof String) {
    return `${key}`;
  }else if ( Object.prototype.toString.call(key)==='[object Object]' ){
    return JSON.stringify(obj)
  }
  return key.toString();
    }
複製程式碼
  1. hashCode(key) 建立雜湊函式
loseloseHashCode(key) {
  if (typeof key === 'number') { // 檢驗key是否是一個數字
  return key; 
  } 
  const tableKey = this.toStrFn(key); // 將 key 轉換為一個字串
  let hash = 0; // 建立一個hash變數
  for (let i = 0; i < tableKey.length; i++) { // 迭代轉為字串後的key
  hash += tableKey.charCodeAt(i); // 從ASCII表中查到的每個字元對應的 ASCII 值加到 hash 變數中
  } 
  return hash % 37; // 返回hash值。為了得到比較小的值,使用hash值和任意數取餘(規避超過最大表示範圍的風險,暫時有坑!!!)
} 

hashCode(key) { //hashCode 方法簡單地呼叫了 loseloseHashCode 方法,將 key 作為引數傳入
  return this.loseloseHashCode(key); 
}
複製程式碼
  1. put(key,value) 將鍵和值加入雜湊表
put(key, value) {
 if (key != null && value != null) { // 檢驗 key 和 value 是否合法,如果不合法就返回 false
  const position = this.hashCode(key); // 根據給出的key,在表中找到一個位置
  this.table[position] = new ValuePair(key, value); // 用 key 和 value 建立一個 ValuePair (此例項和字典中的一樣)例項
  return true; 
  } 
} 
  return false;
複製程式碼
  1. get(key)從雜湊表中獲取一個值
get(key) { 
 const valuePair = this.table[this.hashCode(key)]; 
 return valuePair == null ? undefined : valuePair.value; 
}
複製程式碼
  1. remove(key) 從雜湊表中移除一個值
remove(key) {
 const hash = this.hashCode(key); // 獲取hash
 const valuePair = this.table[hash]; // 獲取值
 if (valuePair != null) { // 如果有值
 delete this.table[hash]; // 刪除它
 return true; // 返回true
 } 
 return false; // 如果沒找到對應的值,返回false
}
複製程式碼

使用 HashTable 類

  • 用一些程式碼來測試一下
const hash = new HashTable(); 
hash.put('Gandalf', 'gandalf@email.com'); 
hash.put('John', 'johnsnow@email.com'); 
hash.put('Tyrion', 'tyrion@email.com'); 

console.log(hash.hashCode('Gandalf') + ' - Gandalf'); // 19 - Gandalf
console.log(hash.hashCode('John') + ' - John'); // 29 - John
console.log(hash.hashCode('Tyrion')+' - Tyrion'); // 16 - Tyrion

console.log(hash.get('Gandalf')); // gandalf@email.com 
console.log(hash.get('Loiane')); // undefined 由於 Loiane 是一個不存在的鍵,所以返回會是 undefined(即不存在)。

hash.remove('Gandalf'); 
console.log(hash.get('Gandalf')); // undefined
複製程式碼

處理雜湊表中的衝突(解決上面的坑)

  • 有時候,一些鍵會有相同的雜湊值。不同的值在雜湊表中對應相同位置的時候,我們稱其為 衝突。來看一下下面程式碼的輸出結果:
const hash = new HashTable();
hash.put('Jonathan', 'jonathan@email.com'); 0
hash.put('Jamie', 'jamie@email.com'); 

通過對每個提到的名字呼叫 hash.hashCode 方法,輸出結果如下。
5 - Jonathan 
5 - Jamie
複製程式碼
  • Jonathan和Jamie有相同的雜湊值5。
  • 由於 Jamie是最後一個被新增的,它將是在 HashTable 例項中佔據位置 5 的元素。
  • 如果呼叫Hash.get(Jonathan)後輸出的是'jonathan@email.com'還是'jamie@email.com'呢?
  • 有兩種處理衝突的方法:分離連結和線性探查

分離連結

  • 分離連結法包括為雜湊表的每一個位置建立一個連結串列並將元素儲存在裡面。它是解決衝突的 最簡單的方法,但是在 HashTable 例項之外還需要額外的儲存空間。
  • 重寫一下三個方法:put、get和remove。
  1. put()
put(key, value) { 
 if (key != null && value != null) {
     const position = this.hashCode(key);
     if (this.table[position] == null) { // 判斷新元素的位置是否已被佔據
         this.table[position] = new LinkedList(); // 初始化一個 LinkedList 類(連結串列類的實現方法見連結串列傳送門)的例項
     } 
     this.table[position].push(new ValuePair(key, value)); // 向連結串列中新增一個ValuePair例項
     return true; 
 } 
 return false; 
}
複製程式碼

連結串列傳送門

  1. get()
get(key) { 
  const position = this.hashCode(key); // 轉化hash值
  const linkedList = this.table[position]; // 獲取hash對應的地址
  if (linkedList != null && !linkedList.isEmpty()) { // 如果連結串列例項存在
      let current = linkedList.getHead(); // 如果有,獲取連結串列頭的引用地址
      while (current != null) { // 迭代到最後
          if (current.element.key === key) { // 找到key值與傳入key相同的
            return current.element.value; // 返回value值
          }
          current = current.next; // 如果key值與傳入key不同,再往下找
      } 
  } 
  return undefined; // 如果連結串列例項不存在,返回undefined
}
複製程式碼
  1. remove()
remove(key) { 
  const position = this.hashCode(key);  // 轉化hash值
  const linkedList = this.table[position];  // 獲取hash對應的地址
  if (linkedList != null && !linkedList.isEmpty()) { // 如果連結串列例項存在 
        let current = linkedList.getHead();  // 如果有,獲取連結串列頭的引用地址
        while (current != null) { // 迭代到最後
            if (current.element.key === key) { // 找到key值與傳入key相同的
              linkedList.remove(current.element); // 使用 remove 方法將其從連結串列中移除
              if (linkedList.isEmpty()) { // 刪除後如果空了
                delete this.table[position]; // 也要在雜湊表中的位置刪除
              }
              return true; // 返回 true 表示該元素已經被移除
            } 
            current = current.next; // 如果key值與傳入key不同,再往下找
        } 
    } 
    return false; // 返回false表示該元素在雜湊表中不存在
}
複製程式碼

線性探查

  • 另一種解決衝突的方法是線性探查。之所以稱作線性,是因為它處理衝突的方法是將元素直 4 接儲存到表中,而不是在單獨的資料結構中。
  • 當想向表中某個位置新增一個新元素的時候,如果索引為 position 的位置已經被佔據了,就嘗試 position+1 的位置。如果 position+1 的位置也被佔據了,就嘗試 position+2 的位 置,以此類推,直到在雜湊表中找到一個空閒的位置。
  • 想象一下,有一個已經包含一些元素的雜湊表,我們想要新增一個新的鍵和值。我們計算這個新鍵的 hash,並檢查雜湊表中對應的位置 是否被佔據。如果沒有,我們就將該值新增到正確的位置。如果被佔據了,我們就迭代雜湊表, 直到找到一個空閒的位置。
  • 同樣的也需要重寫一下三個方法
  1. put()
put(key, value) { 
  if (key != null && value != null) { // 檢驗傳入的key和value是否有效
      const position = this.hashCode(key);  // 獲取hash值
      if (this.table[position] == null) { // 如果這個hash值的位置沒有元素存在
        this.table[position] = new ValuePair(key, value); // 直接等於一個ValuePair例項就好了
      } else {  // 反之,不存在
          let index = position + 1; // 先建立一個變數,等於hash值加一
          while (this.table[index] != null) { // 迭代,直到找到一個空位置
            index++;
          } 
          this.table[index] = new ValuePair(key, value); // 在這個空位置處放入一個ValuePair例項
      } 
      return true; 
  } 
  return false; 
}
複製程式碼
  1. get()
get(key) {  const position = this.hashCode(key); 
  if (this.table[position] != null) { // 確定這個鍵存在
      if (this.table[position].key === key) { // 如果這個值沒變動過
        return this.table[position].value; // 直接返回該位置的value值
      } 
      while (this.table[index] != null && this.table[index].key !== key) { // 如果這個值改變了,就從下一個位置繼續迭代,直到找到要找的元素或者空位置
        let index = position + 1;
        index++; 
      } 
      if (this.table[index] != null && this.table[index].key === key) { //當跳出迴圈時,如果該位置不是空並且它的key和傳入的key相同,返回它的value
        return this.table[position].value;
      } 
      return undefined; // {8} 
  } 
}
複製程式碼
  1. remove() 和get方法基本相同

verifyRemoveSideEffect(key, removedPosition) { // 該函式用於在刪除後把新增時移動的值移回原位置,接收兩個值:被刪除的 key 和該 key 被刪除的位置。
  const hash = this.hashCode(key); // 獲取被刪除的 key 的 hash 值
  let index = removedPosition + 1; // 建立一個變數,等於刪除位置+1
  while (this.table[index] != null) { // 迭代 直到找到空位置
  const posHash = this.hashCode(this.table[index].key); // 迭代時當前位置上元素的 hash 值
  if (posHash <= hash || posHash <= removedPosition) { // 如果當前元素的hash值小於等於原始的值或者小於等於刪除key的hash值,就需要把它移動到刪除的位置
  this.table[removedPosition] = this.table[index];
  delete this.table[index]; // 再刪除它當前元素(因為它已經被複制到刪除的位置了)
  removedPosition = index;  // 再把變數更新為新刪除的位置,重複,直到有空位置
  } 
  index++; 
  } 
}

remove(key) { 
  const position = this.hashCode(key); 
  if (this.table[position] != null) {  // 判斷該位置是否有值
      if (this.table[position].key === key) { // 如果該位置的key等於傳入的key
          delete this.table[position]; // 刪除該值
          this.verifyRemoveSideEffect(key, position); // 刪除後把原來屬於該位置的值挪回來
          return true; 
      } 
      let index = position + 1; // 如果該位置的key不等於傳入的key,證明被移動過
      while (this.table[index] != null && this.table[index].key !== key ) { // 迭代
        index++; 
      } 
      if (this.table[index] != null && this.table[index].key === key) {// 如果該位置不為空,並且它的key等於傳入的key
          delete this.table[index]; // 刪除
          this.verifyRemoveSideEffect(key, index); // 挪回來
          return true; 
      } 
  } 
  return false; 
}
複製程式碼

建立更好的雜湊函式

  • 我們實現的雜湊函式並不是一個表現良好的雜湊函式,因為它會產生太多的衝突。 一個表現良好的雜湊函式是由幾個方面構成的:
    • 插入和檢索元素的時間(即效能)
    • 較低的衝突可能性。
  • 另一個可以實現的更好的雜湊函式:
djb2HashCode(key) {
  const tableKey = this.toStrFn(key); // 先將鍵轉化為字串
  let hash = 5381; // 初始化一個hash變數並複製為一個質數(大多數實現都使用 5381)
  for (let i = 0; i < tableKey.length; i++) { // 迭代key
    hash = (hash * 33) + tableKey.charCodeAt(i); // 將hash與33相乘再加上當前迭代到的字元的 ASCII碼
  } 
  return hash % 1013; // 最後,我們將使用相加的和與另一質數(1013)相除後的餘數
}
複製程式碼

ES2015 Map 類

  • 和我們的 Dictionary 類不同,ES2015 的 Map 類的 values 方法和 keys 方法都返回 Iterator,而不是值或鍵構成的陣列。
  • 另一個區別是,我們實現的 size 方法返回字典中儲存的值的個數,而 ES2015 的 Map 類則有一個 size 屬性。
const 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

map.delete('Gandalf');
console.log(map.has('Gandalf')); // false 
複製程式碼

ES2105 WeakMap 類和 WeakSet 類

  • 除了 Set 和 Map 這兩種新的資料結構,ES2015還增加了它們的弱化版本,WeakSet 和 WeakMap。
  • 建立和使用這兩個類主要是為了效能。WeakSet 和 WeakMap 是弱化的(用物件作為鍵), 沒有強引用的鍵。這使得 JavaScript 的垃圾回收器可以從中清除整個入口。
  • 另一個優點是,必須用鍵才可以取出值。這些類沒有 entries、keys 和 values 等迭代器

相關文章