1. 程式人生 > >散列及碰撞處理

散列及碰撞處理

鍵值 console 修改 又是 問題 映射 出現 裏的 和數

散列是一種常用的數據存儲技術。散列使用的數據結構叫做散列表。在散列表上插入、刪除、取用數據都非常快,但是對於查找來說效率很低。

散列表的長度是預先設定的,存儲數據時,通過一個散列函數將鍵映射為一個數字,這個數字的長度為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;
    }                            

散列及碰撞處理