散列算法(也就是哈希)的作用是儘可能快地在數據結構中找到一個值。在之前如果要在數據結構中獲得一個值(使用get方法),需要遍歷整個數據結構來找到它。
所有元素根據和該元素對應的鍵,保存在數組的特定位置,該鍵和字典中的鍵是類似的概念。使用散列表存儲數據時,通過一個散列函數將鍵映射爲一個數字,這個數字的範圍是0 到散列表的長度。
哈希表簡介
哈希表的結構就是數組,但它神奇之處在於對下標值的一種變換,這種變換我們可以稱之爲哈希函數,通過哈希函數可以獲取HashCode。
哈希表最後還是基於數據來實現的,只不過哈希表能夠通過哈希函數把字符串轉化爲對應的下標值,建立字符串和下標值的對應關係。
即使使用一個高效的散列函數,仍然存在將兩個鍵映射成同一個值的可能,這種現象稱爲碰撞(或者說衝突),當碰撞發生時,我們需要有方案去解決。
哈希表特點
- 哈希表可以提供非常快速的插入-刪除-查找操作;
- 無論多少數據,插入和刪除值都只需要非常短的時間,即O(1)的時間級;
- 哈希表的速度比樹還要快,基本可以瞬間查找到想要的元素。但是相對於樹來說編碼要簡單得多。
- 哈希表中的數據是沒有順序的,所以不能以一種固定的方式(比如從小到大 )來遍歷其中的元素。
- 通常情況下,哈希表中的key是不允許重複的,不能放置相同的key,用於保存不同的元素。也就是要避免衝突。
- 對散列表中的數組大小常見的限制是:數組長度應該是一個質數。
- 哈希表中的數據最好能夠均勻排列。
一些哈希表的概念
- 哈希化:將大數字轉化成數組範圍內下標的過程,稱之爲哈希化;
- 哈希函數:我們通常會將單詞轉化成大數字,把大數字進行哈希化的代碼實現放在一個函數中,該函數就稱爲哈希函數;
- 哈希表:對最終數據插入的數組進行整個結構的封裝,得到的就是哈希表。
哈希表的基本方法
- put(key,value):插入或修改操作;
- get(key):獲取哈希表中特定位置的元素;
- remove(key):刪除哈希表中特定位置的元素;
- isEmpty():如果哈希表中不包含任何元素,返回trun,如果哈希表長度大於0則返回false;
- size():返回哈希表包含的元素個數;
- resize(value):對哈希表進行擴容操作——這部分可參閱https://www.cnblogs.com/AhuntSun-blog/p/12636714.html;
- show () : 展示哈希表所有元素
創建簡單的哈希表
爲了避免碰撞,首先要確保散列表中用來存儲數據的數組其大小是個質數。下面創建了簡單的哈希表,當然實際應用中可能會考慮其他的因素,實現起來會比下面寫的要多些判斷,不過這裏對於理解哈希表的邏輯差不多夠用了:
function HashTable() {
// 存儲數據
this.table = new Array(37);
// 哈希函數 —— 字符串轉下標
var loseloseHashCode = function (key) {
var hash = 0; // 存儲位置下標值
for (var i = 0; i < key.length; i++) {
hash += key.charCodeAt(i); // 轉成數字
}
// 爲了得到比較小的數值
// 可以使用hash值和一個任意數做除法的餘數(mod)。
return hash % this.table.length;
};
// 插入數據
HashTable.prototype.put = function(key, value) {
// 字符串轉成下標
var position = loseloseHashCode(key);
this.table[position] = value;
};
// 讀取數據
HashTable.prototype.get = function (key) {
return this.table[loseloseHashCode(key)];
};
// 顯示所有數據
HashTable.prototype.show = function() {
var arr = [];
for(var i = 0;i<this.table.length;i++){
if(this.table[i] != undefined){
// 對象鍵名默認是字符串
// 想用變量得用中括號包起來
arr.push({[i]:this.table[i]})
}
}
return arr
}
// 刪除數據
HashTable.prototype.remove = function(key) {
this.table[loseloseHashCode(key)] = undefined;
};
}
使用霍納算法的哈希函數
爲了避免碰撞,在給散列表一個合適的大小後,接下來要有一個計算散列值的更好方法。霍納算法很好地解決了這個問題。霍納算法的原理如上圖所示,實現方式是求和之前對前數乘以一個質數。如下所示:
function betterHash(string) {
const H = 37; // 質數
var total = 0;
for (var i = 0; i < string.length; ++i) {
// 霍納算法
total += H * total + string.charCodeAt(i);
}
return total % this.table.length;
}
解決衝突
哈希化過後的下標依然可能重複,這種情況稱爲衝突,衝突是不可避免的,我們只能解決衝突。
鏈地址法(二維數組)
開鏈法是指實現散列表的底層數組中,每個數組元素又是一個新的數據結構,比如另一個數組或鏈表,這樣就能存儲多個鍵了。
使用這種技術,即使兩個鍵散列後的值相同,依然被保存在同樣的位置,只不過它們在第二個數組中的位置不一樣罷了。
實現開鏈法的方法是:在創建存儲散列過的鍵值的數組時,通過調用一個函數創建一個新的空數組,然後將該數組賦給散列表裏的每個數組元素。這樣就創建了一個二維數組,我們也稱這個二維數組數組爲鏈。
它是解決衝突的最簡單的方法,但是它在HashTable實例之外還需要額外的存儲空間。
function HashTable() {
// 哈希函數 —— 霍納算法
HashTable.prototype.betterHash = function(string) {
const H = 37; // 質數
var total = 0;
for (var i = 0; i < string.length; ++i) {
// 霍納算法
total += H * total + string.charCodeAt(i);
}
return total % this.table.length;
}
// 存儲數據
this.table = new Array(37);
// 轉爲二維數組
for (var i = 0; i < this.table.length; ++i) {
this.table[i] = new Array();
}
// 插入數據
// 該方法使用鏈中兩個連續的單元格
// 第一個用來保存鍵值,第二個用來保存數據
HashTable.prototype.put = function(key, value) {
// 字符串轉成下標
var pos = this.betterHash(key);
var index = 0;
// 如果單元格已經有數據了,就繼續向下找,直到找到沒數據的位置
while (this.table[pos][index] != undefined) {
index += 2;
}
this.table[pos][index++] = key;
this.table[pos][index] = value;
};
// 讀取數據
HashTable.prototype.get = function (key) {
var index = 0;
var pos = this.betterHash(key);
// 一直找key, 如果一直找不到就返回undefined
while (this.table[pos][index] != key && index <= this.table[pos].length) {
index += 2;
}
return this.table[pos][index++] && this.table[pos][index] || undefined;
};
}
這裏寫個簡單的哈希函數驗證一下效果:
// 創建一個簡單的長度爲5的哈希表 —— 查看鏈式結構效果
function HashTable() {
// 簡單的哈希函數
HashTable.prototype.betterHash = function(num) {
return num % 10;
}
// 存儲數據 —— 長度5
this.table = new Array(5);
}
二維存儲部分也可以存成別的形式,比如鏈表啥的,這裏在示範個對象的吧:
HashTable.prototype.put = function(key, value) {
// 字符串轉成下標
var pos = this.betterHash(key);
var index = 0;
// 如果單元格已經有數據了,就繼續向下找,直到找到沒數據的位置
while (this.table[pos][index] != undefined) {
index++;
}
// 注意下變量名用[] 括起來
this.table[pos][index] = {[key]:value};
};
這裏get方法就不演示了,比數組的寫法還要簡單些。這裏推薦刺蝟書,感覺比學習JavaScript數據結構裏描寫的要詳細。
線性探測法
當發生碰撞時,線性探測法檢查散列表中的下一個位置是否爲空。如果爲空,就將數據存入該位置;如果不爲空,則繼續檢查下一個位置,直到找到一個空的位置爲止。
當存儲數據使用的數組特別大時,選擇線性探測法要比開鏈法好。
這裏有一個公式,可以幫助我們選擇使用哪種碰撞解決辦法:
- 如果數組的大小是待存儲數據個數的1.5 倍,那麼使用開鏈法;
- 如果數組的大小是待存儲數據的兩倍及兩倍以上時,那麼使用線性探測法。
這裏同時存儲key和value信息,存在一個對象裏。
這裏重寫一下線性探測法的put和get方法以助瞭解邏輯:
function HashTable() {
// 哈希函數 —— jiandande
HashTable.prototype.betterHash = function(num) {
return num % this.table.length;
}
// 存儲數據
this.table = new Array(5);
// 插入數據
// 該方法使用鏈中兩個連續的單元格
// 第一個用來保存鍵值,第二個用來保存數據
HashTable.prototype.put = function(key, value) {
// 字符串轉成下標
var pos = this.betterHash(key);
// 元素位置未被佔用就直接插入
if (this.table[pos] == undefined) {
this.table[pos] = {key:key,value:value};
// 元素位置被佔用了就向下查詢
} else {
var index = ++pos;
while (this.table[pos] != undefined){
index++;
}
this.table[index] = {key:key,value:value};
}
};
// 讀取數據
HashTable.prototype.get = function (key) {
var pos = this.betterHash(key);
// 如果存在數據就查詢,不存在就返回 undefined
if (this.table[pos] !== undefined){
// 如果key對了就返回value
if (this.table[pos].key === key) {
return this.table[pos].value;
} else {
var index = ++pos;
// 下面有值並且key不對的時候繼續查詢
while (this.table[index] != undefined
&& this.table[index].key !== key){
index++;
}
// 如果匹配成功就返回值
if (this.table[index].key === key) {
return this.table[index].value;
}
}
}
// 匹配失敗返回 undefined
return undefined; //{14}
};
}
驗證結果如下:
思路是這個思路,實現起來的方案也不唯一。比如說此處的存值是存的是包含key和value信息的對象,也可以換種思路。在哈希表中創建兩個數組,一個數組用來存key,另一個用來存value。匹配到key就取與之對應的value。這裏就不做演示了,可以參考刺蝟圖片的書。