javascript資料結構與演算法--雜湊

龍恩0707發表於2015-03-18

一:javascript資料結構與演算法--雜湊

 一:什麼是雜湊表?

      雜湊表也叫雜湊表,是根據關鍵碼值(key,value)而直接進行訪問的資料結構,它是通過鍵碼值對映到表中一個位置來訪問記錄的,雜湊表後的資料可以快速的插入和使用,雜湊使用的資料結構叫做雜湊表。

 雜湊表的優點及缺點:

     優點:在雜湊表上插入,刪除和取用資料都非常快。

     缺點:對於查詢來說效率低下,比如查詢一組資料中的最大值與最小值時候,這個時候我們可以使用二叉樹查詢了。

 雜湊表實現的具體原理?

  1. 雜湊函式的選擇依賴於鍵值的資料型別,如果鍵是整型,那麼雜湊函式就是以陣列的長度對鍵取餘。取餘結果就當作陣列的下標,將值儲存在以該數字為下標的陣列空間裡。
  2. 如果鍵值是字串型別,那麼就將字串中的每個字元的ASCLL碼值相加起來,再對陣列的長度取餘。取餘的結果當作陣列的下標,將值儲存在以該餘數為下標的陣列空間裡面。

    一般情況下,雜湊函式會將每個鍵值對映為一個唯一的陣列索引。然而,鍵的數量是無限的,陣列的長度是有限的(在javascript上是這樣的),那麼我們的目標是想讓雜湊函式儘量均勻地對映到陣列中。使用雜湊函式時候,仍然會存在兩個鍵(key)會對映到同一個值的可能。這種現象我們稱為 ”碰撞”,為了避免 ”碰撞”,首先要確保雜湊表中用來儲存資料的陣列大小是個質數,因為這和計算雜湊值時使用的取餘運算有關,並且希望陣列的長度在100以上的質數,這是為了讓資料在雜湊表中能均勻的分佈,所以我們下面的陣列長度先定義為137.

 雜湊表的取資料方法:

  當儲存記錄時,通過雜湊函式計算出記錄的雜湊地址,當取記錄時候,我們通過同樣的雜湊函式計算記錄的雜湊地址,並按此雜湊地址訪問該記錄。

雜湊基本含義如下圖:

名字 雜湊函式(名字中每個字母的ASCLL碼之和) 雜湊值 雜湊表
Durr 68+117+114+114 413
0  
....  
413 Durr
...  
511 Smith
....  
517 Jones
Smith 81+109+105+116+104 517
Jones 74+111+110+101+115 511

比如我們現在如果想要取Durr值得話,那麼我們就可以取雜湊表中的第413記錄 即可得到值。

二:程式碼如何實現HashTable類;

1.先實現HashTable類,定義一個屬性,雜湊表的長度,如上所說,定義陣列的長度為137.程式碼如下:

function HashTable() {
   this.table = new Array(137);
}

2.實現雜湊函式;

就將字串中的每個字元的ASCLL碼值相加起來,再對陣列的長度取餘。取餘的結果當作陣列的下標,將值儲存在以該餘數為下標的陣列空間裡面。程式碼如下:

function simpleHash (data) {
    var total = 0;
    for(var i = 0; i < data.length; i++) {
        total += data.charCodeAt(i);
    }
    console.log("Hash Value: " +data+ " -> " +total);
    return total % this.table.length;
}

 3. 將資料存入雜湊表。

function put(data){
    var pos = this.simpleHash(data);
    this.table[pos] = data;
}

 4. 顯示雜湊表中的資料

function showDistro (){
    var n = 0;
    for(var i = 0; i < this.table.length; i++) {
        if(this.table[i] != undefined) {
         console.log(i + ":" +this.table[i]);
         }
     }
}

下面是所有的JS程式碼如下:

function HashTable() {
    this.table = new Array(137);
}

HashTable.prototype = {
    simpleHash: function(data) {
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
        console.log("Hash Value: " +data+ " -> " +total);
        return total % this.table.length;
    },
    put: function(data){
        var pos = this.simpleHash(data);
        this.table[pos] = data;
    },
    showDistro: function(){
        var n = 0;
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    }
};

我們先來做demo來測試下,如上面的程式碼;如下:

var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];

var hTable = new HashTable();

for(var i = 0; i < someNames.length; ++i) {

       hTable.put(someNames[i]);

}

hTable.showDistro(); 

列印如下:

simpleHash方法裡面

列印如下:

上面可以看出,不同的鍵名(key),但是他們的值相同,由此發生了碰撞,所以最後一個值會存入雜湊表中。

二:一個更好的雜湊函式;

    為了避免碰撞,我們要有一個計算雜湊值的更好方法。霍納演算法很好地解決了這個問題,新的雜湊函式仍然先計算字串中各字元的ASCLL碼值,不過求和時每次要乘以一個質數。我們這裡建議是31.程式碼如下:

function betterHash(string) {
    var H = 31;
    var total = 0;
    for(var i = 0; i < string.length; ++i) {
        total += H * total + string.charCodeAt(i);
    }
    total = total % this.table.length;
    console.log("Hash Value: " +string+ " -> " +total);
    if(total < 0) {
        total += this.table.length - 1;
    }
    return parseInt(total);
} 

但是我們的put的方法要改成如下了:

function put(data) {
   var pos = this.betterHash(data);
   this.table[pos] = data;
} 

下面是所有的JS程式碼如下:

function HashTable() {
    this.table = new Array(137);
}

HashTable.prototype = {
    simpleHash: function(data) {
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
        console.log("Hash Value: " +data+ " -> " +total);
        return total % this.table.length;
    },
    
    put: function(data){
        //var pos = this.simpleHash(data);
        var pos = this.betterHash(data);
        this.table[pos] = data;
    },
    
    showDistro: function(){
        var n = 0;
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    },
    betterHash: function(string){
        var H = 31;
        var total = 0;
        for(var i = 0; i < string.length; ++i) {
            total += H * total + string.charCodeAt(i);
        }
        total = total % this.table.length;
        console.log("Hash Value: " +string+ " -> " +total);
        if(total < 0) {
            total += this.table.length - 1;
        }
        return parseInt(total);
    }
};

測試程式碼還是上面的如下:

var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
var hTable = new HashTable();
for(var i = 0; i < someNames.length; ++i) {
    hTable.put(someNames[i]);
}
hTable.showDistro();

如下圖執行所示:

三:雜湊化整型鍵;

   上面我們展示瞭如何雜湊字串型別的鍵,現在我們來看看如何雜湊化整型鍵,使用的資料是學生的成績。我們將隨機產生一個9位數的鍵,用以識別學生身份和一門成績。

   程式碼如下:

function getRandomInt(min,max) {
    return Math.floor(Math.random() * (max - min +1)) + min;
}
function genStuData(arr) {
    for(var i = 0; i < arr.length; ++i) {
        var num = "";
         for(var j = 1; j <= 9; ++j) {
             num += Math.floor(Math.random() * 10);
         }
         console.log(num);
         num += getRandomInt(50,100);
         console.log(num);
         arr[i] = num;
     }
}

使用getRandomInt()函式時,可以指定隨機數的最大值與最小值。拿學生的成績來看,最低分是50,最高分是100;

genStuData()函式生成學生的資料。裡面的迴圈是用來生成學生的ID,緊跟在迴圈後面的程式碼生存一個隨機成績,並把成績連在ID的後面。如下:

主程式會把ID和成績分離。如下所示:

下面就是上面的測試程式碼如下:

var numStudents = 10;
var students = new Array(numStudents);
genStuData(students);
    
console.log("Student data: \n");
for(var i = 0; i < students.length; i++) {
    console.log(students[i].substring(0,8) + " " +students[i].substring(9));
}
console.log("\n\nData distribution:\n");
        
var hTable = new HashTable();
for(var i = 0; i < students.length; i++) {
    hTable.put(students[i]);
}
hTable.showDistro();

從雜湊表取值

   前面講的是雜湊函式,現在我們要學會使用雜湊存取資料操作,現在我們需要修改put方法,使得該方法同時接受鍵和資料作為引數,對鍵值雜湊後,將資料儲存到雜湊表中,如下put程式碼:

function put(key,data) {
    var pos = this.betterHash(key);
    this.table[pos] = data;
}

Put()方法將鍵值雜湊化後,將資料儲存到雜湊化後的鍵值對應的陣列中的位置上。

接下來我們定義get方法,用以讀取儲存在雜湊表中的資料。該方法同樣需要對鍵值進行雜湊化,然後才能知道資料到底儲存在陣列的什麼位置上。程式碼如下:

function get(key) {
    return this.table[this.betterHash(key)];
}

下面是所有的JS程式碼如下:

function HashTable() {
    this.table = new Array(137);
}

HashTable.prototype = {
    simpleHash: function(data) {
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
        console.log("Hash Value: " +data+ " -> " +total);
        return total % this.table.length;
    },
    
    put: function(key,data) {
        var pos = this.betterHash(key);
        this.table[pos] = data;
    },
    get: function(key) {
        return this.table[this.betterHash(key)];
    },
    showDistro: function(){
        var n = 0;
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    },
    betterHash: function(string){
        var H = 31;
        var total = 0;
        for(var i = 0; i < string.length; ++i) {
            total += H * total + string.charCodeAt(i);
        }
        total = total % this.table.length;
        console.log("Hash Value: " +string+ " -> " +total);
        if(total < 0) {
            total += this.table.length - 1;
        }
        return parseInt(total);
    }
};

測試程式碼如下:

var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
var hTable = new HashTable();
for(var i = 0; i < someNames.length; ++i) {
    hTable.put(someNames[i],someNames[i]);
}
for(var i = 0; i < someNames.length; ++i) {
    console.log(hTable.get(someNames[i]));
}

四:碰撞處理

    當雜湊函式對於多個輸入產生同樣的輸出時,就產生了碰撞。當碰撞發生時,我們仍然希望將鍵儲存到通過雜湊演算法產生的索引位置上,但是不可能將多份資料儲存到一個陣列單元中。那麼實現開鏈法的方法是:在建立儲存雜湊過的鍵值的陣列時,通過呼叫一個函式建立一個新的空陣列,然後在該陣列賦值給雜湊表裡的每個陣列元素元素,這樣就建立了一個二維陣列,使用這種技術,即使兩個鍵雜湊後的值相同,依然被儲存在同樣的位置上,但是他們的第二個陣列的位置不一樣。

 1下面我們通過如下方法來建立一個二維陣列,我們也稱這個陣列為鏈。程式碼如下:

function buildChains(){
    for(var i = 0; i < this.table.length; i++) {
        this.table[i] = new Array();
    }
}

  2. 雜湊表裡面使用多維陣列儲存資料,所以showDistro()方法程式碼需要改成如下:

function showDistro() {
  var n = 0;
  for(var i = 0; i < this.table.length; i++) {
    if(this.table[i][0] != undefined) {
        console.log(i + ":" +this.table[i]);
     }
    }
}

  3. 使用了開鏈法後,需要重新對put和get方法進行改造,put()方法實現原理如下:

    put()方法將鍵值雜湊,雜湊後的值對應陣列中的一個位置,先嚐試將資料放在該位置上的陣列中的第一個單元格,如果該單元格里已經有資料了,put()方法會搜尋下一個位置,直到找到能放置資料的單元格,並把資料儲存進去,下面是put實現的程式碼:

function put(key,data) {
    var pos = this.simpleHash(key);
    var index = 0;
    if(this.table[pos][index] == undefined) {
        this.table[pos][index] = data;
    }else {
        while(this.table[pos][index] != undefined) {
            ++index;
        }
        this.table[pos][index] = data;
    }
}

   4.get() 方法先對鍵值雜湊,根據雜湊後的值找到雜湊表相應的位置,然後搜尋該位置上的鏈,直到找到鍵值,如果找到,就將緊跟在鍵值後面的資料返回,如果沒有找到,就返回undefined。程式碼如下:

function get(key) {
    var index = 0;
    var pos = this.simpleHash(key);
    if(this.table[pos][index] == key) {
        return this.table[pos][index];
    }else {
        while(this.table[pos][index] != key) {
            ++index;
        }
        return this.table[pos][index];
    }
    return undefined;
}

下面是實現開鏈法的所有JS程式碼如下:

function HashTable() {
    this.table = new Array(137);
}

HashTable.prototype = {
    simpleHash: function(data) {
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
        console.log("Hash Value: " +data+ " -> " +total);
        return total % this.table.length;
    },
    put: function(key,data) {
        var pos = this.simpleHash(key);
        var index = 0;
        if(this.table[pos][index] == undefined) {
            this.table[pos][index] = data;
        }else {
            while(this.table[pos][index] != undefined) {
                ++index;
            }
            this.table[pos][index] = data;
        }
    },
    get: function(key) {
        var index = 0;
        var pos = this.simpleHash(key);
        if(this.table[pos][index] == key) {
            return this.table[pos][index];
        }else {
            while(this.table[pos][index] != key) {
                ++index;
            }
            return this.table[pos][index];
        }
        return undefined;
    },
    showDistro: function(){
        var n = 0;
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i][0] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    },
    buildChains: function() {
        for(var i = 0; i < this.table.length; i++) {
            this.table[i] = new Array();
        }
    }
};

測試程式碼如下:

var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
var hTable = new HashTable();
hTable.buildChains();
for(var i = 0; i < someNames.length; ++i) { hTable.put(someNames[i],someNames[i]); } hTable.showDistro(); console.log("--------------------------"); for(var i = 0; i < someNames.length; ++i) { console.log("開鏈法 "+i + " "+hTable.get(someNames[i])); }

效果如下:

呼叫get()方法列印資料如下:

五:線性探測法

 基本原理:

   線性探測法屬於一般的雜湊技術:開放定址雜湊。當發生碰撞時,線性探測法檢查雜湊表中的當前位置是否為空,如果為空,將資料存入該位置,如果不為空,則繼續檢查下一個位置,直到找到一個空的位置為止。

什麼時候使用線性探測法,什麼時候使用開鏈法呢?

如果陣列的大小是待儲存資料的個數是1.5倍,那麼使用開鏈法,如果陣列的大小是待儲存的資料的2倍及2倍以上時,那麼使用線性探測法。

為了實現線性探測法,我們需要增加一個新陣列 values ,用來儲存資料。程式碼如下:

function HashTable() {
     this.table = new Array(137);
     this.values = [];
}

我們知道探測法原理之後,我們就可以寫put方法程式碼了,程式碼如下:

function put(key,data) {
    var pos = this.simpleHash(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;
    }
}

2. get()方法的基本原理是:先搜尋鍵在雜湊表中的位置,如果找到,則返回陣列values中對應位置上的資料。如果沒有找到,則迴圈搜尋,以當前的位置的下一個位置開始迴圈搜尋,如果找到對應的鍵,則返回對應的資料,否則的話 返回undefined;程式碼如下:

function get(key) {
    var pos = this.simpleHash(key);
    if(this.table[pos] == key) {
        return this.values[pos];
    }else {
        for(var i = pos+1; i < this.table.length; i++) {
        if(this.table[i] == key) {
            return this.values[i];
         }
        }
        }
    return undefined;
        
}

下面是所有JS程式碼如下:

function HashTable() {
    this.table = new Array(137);
    this.values = [];
}

HashTable.prototype = {
    simpleHash: function(data) {
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
        console.log("Hash Value: " +data+ " -> " +total);
        return total % this.table.length;
    },
    put: function(key,data) {
        var pos = this.simpleHash(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: function(key) {
        var pos = this.simpleHash(key);
        if(this.table[pos] == key) {
            return this.values[pos];
        }else {
            for(var i = pos+1; i < this.table.length; i++) {
                if(this.table[i] == key) {
                    return this.values[i];
                }
            }
        }
        return undefined;
        
    },
    showDistro: function(){
        var n = 0;
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    }
};

測試程式碼如下:

var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
var hTable = new HashTable();
        
for(var i = 0; i < someNames.length; ++i) {
    hTable.put(someNames[i],someNames[i]);
}
hTable.showDistro();
console.log("--------------------------");
        
for(var i = 0; i < someNames.length; ++i) {
    console.log(hTable.get(someNames[i]));
}

效果如下:

 

相關文章