散列表(雜湊表)
阿新 • • 發佈:2021-08-10
散列表
散列表也稱雜湊表,雜湊演算法的作用是儘可能快地在資料結構中找到一個值。
如果要在資料結構中獲得一個值,需要迭代整個資料結構來找到它。如果使用雜湊函式,就知道值的具體位置,因此能夠快速檢索到該值。
雜湊函式的作用是給定一個鍵值,然後返回值在表中的地址。
loselose雜湊函式
下圖為常見的雜湊函式——lose lose雜湊函式,方法是簡單地將每個鍵值中的每個字母的 ASCII 值相加建立雜湊函式
function defaultToString(item){ // 將鍵轉化為字串 if(item === null){ return 'NULL' }else if(item === undefined){ return 'UNDEFINED' }else{ return item.toString() } }
class valuePair{
// 鍵值對 constructor(key, value){ this.key = key this.value = value } toString(){ return `[#${this.key}: ${this.value}]` } } class hashTable{ constructor(toStrFn= defaultToString){ this.toStrFn = toStrFn this.table = {} } loseloseHashCode(key){ // 使用loselose雜湊函式 // 即將每個鍵中的每個字母的ASCII碼相加 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){ // 新增鍵值對 if(key != null && value != null){ const position = this.hashCode(key) this.table[position] = new valuePair(key, value) return true } return false } get(key){ // 通過key查詢值, 找不到返回undefined const valuePair = this.table[this.hashCode(key)] return valuePair } }
處理散列表中的衝突
有時候,一些鍵會有相同的雜湊值。不同的值在散列表中對應相同位置的時候,我們稱其為衝突1.分離連結
分離連結法包括為散列表的每一個位置建立一個連結串列並將元素儲存在裡面。它是解決衝突的最簡單的方法,但是在 HashTable 例項之外還需要額外的儲存空間。如下圖:
實現:
class HashTableSeparateChaining extends hashTable{ constructor(toStrFn = defaultToString){ super(toStrFn) } put(key, value){ // 每個元素為一個連結串列,依次儲存相同hashcode的鍵值對 if(key != null && value != null){ const position = super.hashCode(key) if(this.table[position] == undefined){ this.table[position] = new LinkedList() // LinkedList類詳見連結串列 } this.table[position].push(new valuePair(key, value)) return true } return false } get(key){ // 通過key獲取value const position = super.hashCode(key) const linkedList = this.table[position] if (linkedList != undefined && !linkedList.isEmpty()) { let current = linkedList.getHead() while(current != null){ if(current.element.key === key){ return current.element.value } current = current.next } } return undefined } remove(key){ // 通過key移除對應元素 const position = super.hashCode(key) const linkedList = this.table[position] if(linkedList != undefined && !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 } }
2.線性探查
另一種解決衝突的方法是線性探查。之所以稱作線性,是因為它處理衝突的方法是將元素直接儲存到表中,而不是在單獨的資料結構中。新增元素
當想向表中某個位置新增一個新元素的時候,如果索引為position的位置已經被佔據了,就嘗試position+1的位置。如果position+1的位置也被佔據了,就嘗試position+2的位置,以此類推,直到在散列表中找到一個空閒的位置。 如下圖:刪除元素
在刪除某個元素之後,需要檢驗是否有必要將其後一個或多個元素移動到之前的位置。當搜尋一個鍵的時候,這種方法可以避免找到一個空位置。如果移動元素是必要的,我們就需要在散列表中挪動鍵值對。 下圖展現了這個過程:實現:
class HashTableSeparateChaining extends hashTable{ constructor(toStrFn = defaultToString){ super(toStrFn) } put(key, value){ // 插入元素 // 先判斷位置是否被佔用,插入一個未被佔用的位置 if(key != null && value != null){ const position = super.hashCode(key) if(this.table[position] == undefined){ this.table[position] = new valuePair(key, value) }else{ let index = position + 1 while(this.table[index] != undefined){ index ++ } this.table[index] = new valuePair(key, value) } return true } return false } get(key){ // 獲取元素 const position = super.hashCode(key) if(this.table[position] != undefined){ if(this.table[position].key === key){ return this.table[position].value } let index = position + 1 while(this.table[index] !== undefined && this.table[index].key !== key){ index ++ } if(this.table[index] != undefined && this.table[index].key === key){ return this.table[index].value } } return undefined } remove(key){ const position = super.hashCode(key) if(this.table[position] != undefined){ if(this.table[position].key === key){ delete this.table[position] this.verifyRemoveSideEffect(key, position) // 將刪除空位補上 return true }else{ index = position + 1 while(this.table[index] != undefined && this.table[index].key !== key){ index ++ } if(this.table[index] != undefined && this.table[index].key === key){ delete this.table[index] this.verifyRemoveSideEffect(key, index) return true } } } return undefined } verifyRemoveSideEffect(key, removedPosition){ // 把刪除元素後的且與刪除元素相同雜湊值的元素向上補一位 const Hash = super.hashCode(key) let index = removedPosition + 1 while(this.table[index] != undefined){ // 若在空位處也沒有相同的雜湊值,說明後面都沒有了 const posHash = super.hashCode(this.table[index].key) if(posHash <= Hash || posHash <= removedPosition){
// 只移動小於等於刪除元素的雜湊值 this.table[removedPosition] = this.table[index] delete this.table[index] removedPosition = index } index ++ } } }
建立更好的雜湊函式
我們實現的 lose lose 雜湊函式並不是一個表現良好的雜湊函式,因為它會產生太多的衝突。一個表現良好的雜湊函式是由幾個方面構成的:插入和檢索元素的時間(即效能),以及較低的衝突可能性 另一個可以實現的、比 lose lose 更好的雜湊函式是 djb2,這並不是最好的雜湊函式,但這是最受社群推崇的雜湊函式之一。djb2HashCode(key) { const tableKey = this.toStrFn(key) let hash = 5381 for (let i = 0; i < tableKey.length; i++) { hash = (hash * 33) + tableKey.charCodeAt(i) // 即使loselose雜湊值相同,djb2也幾乎不會相同
} return hash % 1013 }