JavaScript數據結構之 —— 08哈希表

散列算法(也就是哈希)的作用是儘可能快地在數據結構中找到一個值。在之前如果要在數據結構中獲得一個值(使用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。這裏就不做演示了,可以參考刺蝟圖片的書。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章