什麼是雜湊表?
- 雜湊表是Dictionary(字典)的一種雜湊表實現方式,字典傳送門
- 一個很常見的應用是使用雜湊表來表示物件。Javascript語言內部就是使用雜湊表來表示每個物件。此時,物件的每個屬性和方法(成員)被儲存為key物件型別,每個key指向對應的物件成員。
- 以字典中使用的電子郵件地址簿為例。我們將使用最常見的雜湊函式:lose lose雜湊函式,方法是簡單的將每個鍵值中的每個字元的ASCII值相加,如下圖所示:
建立雜湊表
class HashTable {
this.table = {};
}
複製程式碼
實現幾個簡單方法
- 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();
}
複製程式碼
- hashCode(key) 建立雜湊函式
loseloseHashCode(key) {
if (typeof key === 'number') {
return key;
}
const tableKey = this.toStrFn(key);
let hash = 0;
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i);
}
return hash % 37;
}
hashCode(key) {
return this.loseloseHashCode(key);
}
複製程式碼
- put(key,value) 將鍵和值加入雜湊表
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
this.table[position] = new ValuePair(key, value);
return true;
}
}
return false;
複製程式碼
- get(key)從雜湊表中獲取一個值
get(key) {
const valuePair = this.table[this.hashCode(key)];
return valuePair == null ? undefined : valuePair.value;
}
複製程式碼
- remove(key) 從雜湊表中移除一個值
remove(key) {
const hash = this.hashCode(key);
const valuePair = this.table[hash];
if (valuePair != null) {
delete this.table[hash];
return true;
}
return 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');
console.log(hash.hashCode('John') + ' - John');
console.log(hash.hashCode('Tyrion')+' - Tyrion');
console.log(hash.get('Gandalf'));
console.log(hash.get('Loiane'));
hash.remove('Gandalf');
console.log(hash.get('Gandalf'));
複製程式碼
處理雜湊表中的衝突(解決上面的坑)
- 有時候,一些鍵會有相同的雜湊值。不同的值在雜湊表中對應相同位置的時候,我們稱其為
衝突。來看一下下面程式碼的輸出結果:
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。
- put()
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new LinkedList();
}
this.table[position].push(new ValuePair(key, value));
return true;
}
return false;
}
複製程式碼
連結串列傳送門
- get()
get(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.getHead();
while (current != null) {
if (current.element.key === key) {
return current.element.value;
}
current = current.next;
}
}
return undefined;
}
複製程式碼
- remove()
remove(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.getHead();
while (current != null) {
if (current.element.key === key) {
linkedList.remove(current.element);
if (linkedList.isEmpty()) {
delete this.table[position];
}
return true;
}
current = current.next;
}
}
return false;
}
複製程式碼
線性探查
- 另一種解決衝突的方法是線性探查。之所以稱作線性,是因為它處理衝突的方法是將元素直 4
接儲存到表中,而不是在單獨的資料結構中。
- 當想向表中某個位置新增一個新元素的時候,如果索引為 position 的位置已經被佔據了,就嘗試 position+1 的位置。如果 position+1 的位置也被佔據了,就嘗試 position+2 的位
置,以此類推,直到在雜湊表中找到一個空閒的位置。
- 想象一下,有一個已經包含一些元素的雜湊表,我們想要新增一個新的鍵和值。我們計算這個新鍵的 hash,並檢查雜湊表中對應的位置
是否被佔據。如果沒有,我們就將該值新增到正確的位置。如果被佔據了,我們就迭代雜湊表,
直到找到一個空閒的位置。
- 同樣的也需要重寫一下三個方法
- put()
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new ValuePair(key, value);
} else {
let index = position + 1;
while (this.table[index] != null) {
index++;
}
this.table[index] = new ValuePair(key, value);
}
return true;
}
return false;
}
複製程式碼
- get()
get(key) { const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key) {
return this.table[position].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) {
return this.table[position].value;
}
return undefined;
}
}
複製程式碼
- remove() 和get方法基本相同
verifyRemoveSideEffect(key, removedPosition) {
const hash = this.hashCode(key);
let index = removedPosition + 1;
while (this.table[index] != null) {
const posHash = this.hashCode(this.table[index].key);
if (posHash <= hash || posHash <= removedPosition) {
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) {
delete this.table[position];
this.verifyRemoveSideEffect(key, position);
return true;
}
let index = position + 1;
while (this.table[index] != null && this.table[index].key !== key ) {
index++;
}
if (this.table[index] != null && this.table[index].key === key) {
delete this.table[index];
this.verifyRemoveSideEffect(key, index);
return true;
}
}
return false;
}
複製程式碼
建立更好的雜湊函式
- 我們實現的雜湊函式並不是一個表現良好的雜湊函式,因為它會產生太多的衝突。
一個表現良好的雜湊函式是由幾個方面構成的:
- 插入和檢索元素的時間(即效能)
- 較低的衝突可能性。
- 另一個可以實現的更好的雜湊函式:
djb2HashCode(key) {
const tableKey = this.toStrFn(key);
let hash = 5381;
for (let i = 0; i < tableKey.length; i++) {
hash = (hash * 33) + tableKey.charCodeAt(i);
}
return hash % 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'));
console.log(map.size);
console.log(map.keys());
console.log(map.values());
console.log(map.get('Tyrion'));
map.delete('Gandalf');
console.log(map.has('Gandalf'));
複製程式碼
ES2105 WeakMap 類和 WeakSet 類
- 除了 Set 和 Map 這兩種新的資料結構,ES2015還增加了它們的弱化版本,WeakSet 和 WeakMap。
- 建立和使用這兩個類主要是為了效能。WeakSet 和 WeakMap 是弱化的(用物件作為鍵), 沒有強引用的鍵。這使得 JavaScript 的垃圾回收器可以從中清除整個入口。
- 另一個優點是,必須用鍵才可以取出值。這些類沒有 entries、keys 和 values 等迭代器