算法筆記12:散列表

散列表和散列函數hashcode()

關於hash算法可以閱讀以下文章
https://www.cnblogs.com/xiohao/p/4389672.html
https://blog.csdn.net/u014209205/article/details/80820263

在數組中查找只需要1次查詢就可以得到所需數據,散列表就是希望使用數組提高查詢速度。當有一個大小爲M的數組時,我們根據鍵值分配其在數組中的位置,一般情況下我們使用java實現的hashcode()方法得到一個int值,根據hashcode()%M的值來決定在數組中的位置。
很顯然當鍵的數量大於M時,會出現hash衝突,也就是兩個不同的鍵值的散列值相同,我們可以增大數組M來減少衝突,但是沒法消除衝突,因爲鍵值是由調用散列表的程序的輸入決定的,而且數組的維護需要內存,此時使用拉鍊法和開放地址法解決衝突。

拉鍊法

當hash衝突的時候對散列值相等的部分採用鏈表:首先根據散列值找到鏈表,然後在鏈表中順序查找,拉鍊法的實現主要有兩種:
1 數組中存儲鏈表的結點類型Node,根據散列值找到相應的Node,這個Node就是鏈表的首節點,然後根據鍵值key判斷首節點鍵值是否就是目標值,如果是則返回首節點的value,如果不是則根據Node的next屬性找到下一個節點進行判斷,直到找到對應鍵值的結點或者鏈表查找完畢返回null。插入時根據散列值找到對應的Node,如果爲空則形成新的結點和引用,如果不爲空則判斷鍵值是否相等,如果相等則改變對應Node的value,如果不等則查找下一位,直到遇到null或者鍵值相等的結點。java的HashMap的源碼中就是使用了這種方法,數組tab[]中保存的是鏈表的Node或者紅黑樹的TreeNode
2 數組中存儲的是鏈表,而不是鏈表的結點。根據散列值直接找到鏈表,然後使用鏈表的方法查找、插入和刪除。下面的代碼就是採用這種方式,代碼中LinkedListST訪問超鏈接LinkedListST
可以看到,拉鍊法在查找和插入時比較的次數和鏈表的長度有關,無論鍵如何分佈,鏈表的平均長度肯定是鍵的個數和數組的長度的比值:N/M。所以平均情況下的查找和插入最多需要N/M次比較和1次散列值的計算。

/**
 * 基於順序鏈表查找表實現的拉鍊法的散列表
 */
import second.LinkedListST;
@SuppressWarnings("unchecked")
public class LinkedChainHashST<Key,Value> {
	private LinkedListST<Key, Value>[] tab;//使用鏈表的數組
	private final int M=97;
	
	public LinkedChainHashST(){
		tab=(LinkedListST<Key, Value>[])new LinkedListST[M];
		for (int i = 0; i < tab.length; i++) {
			tab[i]=new LinkedListST<Key, Value>();
		}
	}
	public Value get(Key key){
		return (Value)tab[hash(key)].get(key);		
	}
	public void put(Key key,Value value){
		tab[hash(key)].put(key,value);	
	}
	public void delete(Key key){
		tab[hash(key)].delete(key);
	}
	public int hash(Key key){
		return (key.hashCode() & 0x7fffffff)%M;		
	}
	public void show(){
		for (int i = 0; i < tab.length; i++) {
			tab[i].show();
		}
	}
	public static void main(String[] args) {
		LinkedChainHashST<String, Integer> st=new LinkedChainHashST();
		st.put("lm",3);
		st.put("sgt",4);
		st.put("zx",2);
		st.put("sq",1);
		st.delete("zx");
		st.show();
	}
}

查詢的速度和鏈表的長度有關,在鏈表的長度很長時可以擴大M來減小長度,通常情況下鏈表的長度保持在2-8之間。N>8*M時M可以增加2倍。

開發地址法

拉鍊法存儲的鍵值數量N小於數組長度M,在散列值重複的地方形成鏈表,開放地址法中是M>N,利用數組中的空位解決hash衝突。開放地址散列表最簡單的方法爲線性探測法:使用兩個數組keys[]和values[]分別保存鍵值key和相應的value。插入時,根據hash值訪問數組中的元素,如果該位置爲空,那麼目前沒有和該鍵衝突的鍵,直接改變keys[]和values[]中的值,如果該位置不爲空則存在衝突,那麼判斷key是否相等,如果相等則是重複,只改變values[]中的值,如果不相等則判斷下一位,直到遇到null或者鍵值相等,當到達數組末尾時轉向從0開始。查找時計算散列值找到元素如果鍵值相等則返回values[]中的值,如果不相等則查詢下一位,遇到相等的鍵值或者null時結束。
線性探測法可以形象的理解爲將拉鍊法中的鏈表放在數組中,將這種在數組中的“鏈表”稱爲鍵簇,即鍵值的散列值相等的鍵會相鄰,將其稱爲鍵簇。線性探測法的性能和α=M/N\alpha =M/N有關,我們將其稱爲使用率,很顯然使用率越大,衝突的機會則會變小,查找時鍵簇變短,查找更快,Kunth在1962年證明了命中所需的比較次數近似爲12(1+11α)\frac{1}{2}\left ( 1+\frac{1}{1-\alpha } \right )未命中所需的比較次數近似爲12(1+1(1α)2)\frac{1}{2}\left ( 1+\frac{1}{(1-\alpha) ^{2} } \right )
爲了保證查找的速度一般保證$$之間,當N<M/8時checksize()縮短一半,N>M/2checksize()增加一倍。線性探測中刪除操作需要處理數組中的後序元素,因爲將該鍵值內容設爲null後原來和改鍵衝突的鍵值將因爲遇到null而不會被訪問到。
線性探測法的實現如下:

/**
 * 基於線性探測法的散列表
 * @author XY
 *
 */
@SuppressWarnings("unchecked")
public class LinearProbingHashST<Key,Value> {
	private Key[] keys;
	private Value[] values;//兩個數組保存鍵和對應的數據
	private int M=97;
	private int N;
	public LinearProbingHashST(){
		keys=(Key[])new Object[M];
		values=(Value[])new Object[M];
	}
	public void put(Key key,Value value){
		int x=hash(key);
		while (keys[x]!=null) {
			if(key.equals(keys[x])){//已經存在
				values[x]=value;
				return;
				}else x=(x+1)%M;
			}
		keys[x]=key;
		values[x]=value;
		N++;
		checksize();
	}
	public Value get(Key key){
		int x=hash(key);
		while(keys[x]!=null){
			if(key.equals(keys[x])) return values[x];//存在
			else {x=(x+1)%M;}
		}
		return null;
	}
	public void delete(Key key){
		for (int i = hash(key);keys[i]!=null; i=(i+1)%M) {
			if(keys[i].equals(key)){
				keys[i]=null;
				values[i]=null;//該鍵值存在,刪除
				rest((i+1)%M);//爲了避免此位置的null對後面的造成影響
				N--;
				checksize();
			}
		}
	}
	private void rest(int x){
		while(keys[x]!=null){//非空值向前移位,直到遇到null
			Key tempk=keys[x];
			Value tempv=values[x];
			keys[x]=null;
			values[x]=null;
			N--;
			put(tempk, tempv);
			x=(x+1)%M;
		}
	}
	private void checksize(){//檢查大小,使得使用率在1/2~1/8之間
		if(N<M/8){
			Key[] skeys=keys;
			Value[] svalues=values;
			this.M=M/2;
			keys=(Key[])new Object[M/2];
			values=(Value[])new Object[M/2];
			for (int i = 0; i < M; i++) {
				if(skeys[i]!=null) put(skeys[i],svalues[i]);
			}
			
		}else if (N>M/2) {
			Key[] skeys=keys;
			Value[] svalues=values;
			this.M=M*2;
			keys=(Key[])new Object[M*2];
			values=(Value[])new Object[M*2];
			for (int i = 0; i < M; i++) {
				if(skeys[i]!=null) put(skeys[i],svalues[i]);
			}
		}
	}
	public int hash(Key key){
		return (key.hashCode() & 0x7fffffff)%M;
	}
}

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