一:javascript資料結構與演算法--雜湊
一:什麼是雜湊表?
雜湊表也叫雜湊表,是根據關鍵碼值(key,value)而直接進行訪問的資料結構,它是通過鍵碼值對映到表中一個位置來訪問記錄的,雜湊表後的資料可以快速的插入和使用,雜湊使用的資料結構叫做雜湊表。
雜湊表的優點及缺點:
優點:在雜湊表上插入,刪除和取用資料都非常快。
缺點:對於查詢來說效率低下,比如查詢一組資料中的最大值與最小值時候,這個時候我們可以使用二叉樹查詢了。
雜湊表實現的具體原理?
- 雜湊函式的選擇依賴於鍵值的資料型別,如果鍵是整型,那麼雜湊函式就是以陣列的長度對鍵取餘。取餘結果就當作陣列的下標,將值儲存在以該數字為下標的陣列空間裡。
- 如果鍵值是字串型別,那麼就將字串中的每個字元的ASCLL碼值相加起來,再對陣列的長度取餘。取餘的結果當作陣列的下標,將值儲存在以該餘數為下標的陣列空間裡面。
一般情況下,雜湊函式會將每個鍵值對映為一個唯一的陣列索引。然而,鍵的數量是無限的,陣列的長度是有限的(在javascript上是這樣的),那麼我們的目標是想讓雜湊函式儘量均勻地對映到陣列中。使用雜湊函式時候,仍然會存在兩個鍵(key)會對映到同一個值的可能。這種現象我們稱為 ”碰撞”,為了避免 ”碰撞”,首先要確保雜湊表中用來儲存資料的陣列大小是個質數,因為這和計算雜湊值時使用的取餘運算有關,並且希望陣列的長度在100以上的質數,這是為了讓資料在雜湊表中能均勻的分佈,所以我們下面的陣列長度先定義為137.
雜湊表的取資料方法:
當儲存記錄時,通過雜湊函式計算出記錄的雜湊地址,當取記錄時候,我們通過同樣的雜湊函式計算記錄的雜湊地址,並按此雜湊地址訪問該記錄。
雜湊基本含義如下圖:
比如我們現在如果想要取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])); }
效果如下: