散列及碰撞處理
散列是一種常用的數據存儲技術。散列使用的數據結構叫做散列表。在散列表上插入、刪除、取用數據都非常快,但是對於查找來說效率很低。
散列表的長度是預先設定的,存儲數據時,通過一個散列函數將鍵映射為一個數字,這個數字的長度為0到散列表的長度。編寫散列函數前需要先確定散列表數組的長度,我們對數大小常見的限制是:數組長度應該是一個質數。數組長度的確定策略都是基於處理碰撞問題的。
1、散列函數
我們采取的散列方式為除留余數法,基於這個方法的散列函數可以更加均勻的分布隨機的整數鍵。對於字符串類型的鍵,散列值可以設為每個字符的ASCII碼值的和除以數組長度的余數。散列函數定義如下:
function simpleHash(data) {var total = 0; for ( var i = 0; i < data.length; ++i) { total += data.charCodeAt(i); } return total % this.table.length; }
於是,HashTable類的構造函數如下:
function HashTable() { this.table = new Array(137);this.simpleHash = simpleHash; this.showDistro = showDistro;this.put = put; //this.get=get; } function simpleHash(data) { var total = 0; for ( var i = 0; i < data.length; ++i) { total += data.charCodeAt(i); } return total % this.table.length; } function put(data) { var pos = this.simpleHash(data);this.table[pos] = data; } function showDistro() { for ( var i = 0; i < this.table.length; ++i) { if (this.table[i] != undefined) { document.write(i + ": " + this.table[i]); document.write("<br />"); } } }
simpleHash()散列函數通過JavaScript的charCodeAt()函數,返回每個字符的ASCII碼值,相加得到散列值,put()方法通過調用simpleHash()得到數組的索引,並將數據存儲到該索引對應的位置。通過實驗可以發現,這個散列函數散列的數據並不是均勻分布的,向數組的兩端集中,並且更嚴重的問題是,容易引發碰撞!所以需要優化一下散列函數來避免碰撞。
為了避免碰撞:
1、首先要保證散列表中用來存儲數據的數組大小是個質數。這個計算散列值時采取的取余運算有關。
2、有了合適的散列表大小後,接下來就需要有一個更好的散列函數。霍納算法很好的解決了這個問題。新的散列函數在每次求值前都先乘以一個質數,這裏建議使用一個較小的質數。
使用霍納算法的散列函數:
function betterHash(data) { const H = 37; var total = 0; for ( var i = 0; i < data.length; ++i) { total += total*H + data.charCodeAt(i); } total = total % this.table.length;
return parseInt(total);
}
2、從散列表中存取數據
使用散列表來存儲數據,我們需要重新定義put() 方法,使其可以同時接受鍵和數據作為參數,對鍵值散列後,將數據存儲到散列表中:
function put(key, data) { var pos = this.betterHash(key); this.table[pos] = data; }
get() 方法同樣需要對鍵值進行散列化,得到數據在散列表中存儲的位置:
function get(key) { return this.table[this.betterHash(key)]; }
3、碰撞處理
1)開鏈法
開鏈法是指,實現散列表的底層數組中,每個數組元素又是一個新的數組結構。
實現開鏈法的方法:在創建存儲散列過的鍵值的數組時,通過調用一個函數創建一個空的新數組,將該數組賦值給散列表裏的每個數組元素,創建一個二維數組。我們稱這個數組為鏈。函數buildChains() 定義如下:
function buildChains() { for ( var i = 0; i < this.table.length; ++i) { this.table[i] = new Array(); } }
使用了開鏈法後,我們需要重新定義put()和get()方法。
put()方法先將鍵值散列,散列後的值對應數組中的一個位置,先查看該位置上數組的第一個單元格是否有值,如果有值則會搜索下一個位置,直到找到可以存放數據的單元格,並將數組存儲進去,新的put()方法實現如下:
function put(key, data) { var pos = this.betterHash(key); var index = 0; if (this.table[pos][index] == undefined) { this.table[pos][index] = key; this.table[pos][index + 1] = data; } else { while (this.table[pos][index] != undefined) { ++index; } this.table[pos][index] = key; this.table[pos][index + 1] = data; } }
新的put()方法不同於之前的方法,之前的方法僅存放數據,而新的put()方法使用鏈中兩個相同的單元格同時存放了散列後的鍵值和數據,第一個單元格存放鍵值,第二個單元格存放數據。這樣便於在碰撞出現時,鏈中存放多條數據時,從散列表中取值。
get()方法先對鍵值散列,根據散列後的值找到散列表中相應的位置,查找該位置的鏈中是否存在這個鍵值,如果有,則把緊跟在鍵值後面的單元格的數據返回,沒找到就返回undefined。新的get()方法實現如下:
function get(key) { var index = 0; var pos = this.betterHash(key); if(this.table[pos][index] == key) { console.log(this.table[pos][index + 1]); return; } else { while (this.table[pos][index] != key) { index += 2; } console.log(this.table[pos][index + 1]); return; } document.write(‘undefined‘); return; }
散列表現在使用的是多維數組存儲數據,為了更好的顯示使用了開鏈法後鍵值的分布,則需要對showDistro()方法進行如下修改:
function showDistro() { for ( var i = 0; i < this.table.length; ++i) { if (this.table[i][0] != undefined) { document.write(i + ": " + this.table[i]); document.write("<br />"); } } }
2)線性探測法
線性探測法隸屬於一種更一般化的散列技術:開放尋址散列。當發生碰撞時,線性探測法會檢查散列表中下一個位置是否為空,如果為空,則將數據存儲在該位置,如果不為空,則繼續檢查下一個位置,直到找到空位置為止。當存儲數據的數組特別大時,選擇線性探測法要比開鏈法好,有一個公式可以幫助我們來判斷使用那種碰撞處理方法:如果數組的大小是待存儲數據的1.5倍,使用開鏈法;如果數組的大小是待存儲數據的兩倍或兩倍以上,則選擇線性探測法。
下面用代碼說明線性探測法的工作原理,重新定義put()和get()方法,在構造函數中新增加一個數組
this.values = [];
數組value和table並行工作,table用來存儲鍵值,value用來存儲對應的數據。put()方法定義如下:
function put(key, data) { var pos = this.betterHash(key); if (this.table[pos] == undefined) { this.table[pos] = key; this.values[pos] = data; } else { while (this.table[pos] != undefined) { pos++; } this.table[pos] = key; this.values[pos] = data; } }
get()方法先搜索散列後的鍵值在散列表中的位置,如果在table中找到key,則返回values中對應位置上的數據,如果沒找到則循環搜索,直到找到了table中對應的鍵,或者table中對應的值為undefined時,表示鍵值和數據沒有存儲到散列表。get()方法如下:
function get(key) { var hash= -1; var hash= this.betterHash(key); if(hash > -1) { for (var i=hash; this.table[i] != undefined; i++) { if (this.table[i] == key) { return this.values[i]; } } } return undefined; }
散列及碰撞處理