散列表和散列函數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時結束。
線性探測法可以形象的理解爲將拉鍊法中的鏈表放在數組中,將這種在數組中的“鏈表”稱爲鍵簇,即鍵值的散列值相等的鍵會相鄰,將其稱爲鍵簇。線性探測法的性能和有關,我們將其稱爲使用率,很顯然使用率越大,衝突的機會則會變小,查找時鍵簇變短,查找更快,Kunth在1962年證明了命中所需的比較次數近似爲未命中所需的比較次數近似爲
爲了保證查找的速度一般保證$$之間,當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;
}
}