散列表原理及實現
散列表原理
散列表:使用算術操作將鍵轉化爲數組的索引來訪問數組中的鍵值對, 使用散列表,可以實現常數級別的查找和插入.
使用散列的查找算法主要要解決的兩個問題:
- 散列函數的設計(即如何用散列函數將被查找的鍵轉化爲數組的一個索引).
- 處理碰撞衝突的過程(即處理兩個或多個鍵的散列值相同的情況).
PS:處理碰撞衝突的方法主要有拉鍊法和線性探測法.
散列函數的設計
實現散列函數的指導思想 :
設計的散列函數能夠均勻並獨立地將所有的鍵散佈於0 ~ M-1之間.(其中M爲存放鍵值的數組的大小)
優秀的散列方法需要滿足三個條件:
- 一致性 : 等價的鍵必然產生相等的散列值
- 高效性 : 計算簡便
- 均勻性 : 均勻地散列所有的鍵
散列方法舉例—hashCode()方法結合除留餘數法
將默認的hashCode()方法與除留餘數法結合起來產生一個0~M-1的整數,一般會將數組的大小M取爲素數以充分利用原散列值的所有位
private int hash(Key key){
return (key.hashCode()&0x7fffffff % M);
}
處理碰撞衝突
碰撞處理就是處理兩個或多個鍵的散列值相同的情況
拉鍊法
將大小爲M的數組中的每個元素指向一條鏈表,鏈表的每個節點都存儲了散列值爲該元素的索引的鍵值對.
使用拉鍊法查找鍵值的過程 :
- 根據散列值找到對應鏈表
- 沿着對應鏈表查找相對應的鍵
拉鍊法實現
1. 拉鍊法基礎方法
public class SeparateChainingHashST<Key extends Comparator<? super Key>,Value> {
private int N;//鍵值對總數
private int M;//散列表使用的數組大小
private SequentialSearchST<Key,Value>[] st;//存放鏈表對象的數組,實現可參照<算法第四版SequentialSearchST.class>
public SeparateChainingHashST(){
this(997);
}
public SeparateChainingHashST(int M){
/** 創建M條鏈表 */
this.M=M;
st=(SequentialSearchST<Key,Value>[])new SequentialSearchST[M];
for(int i=0;i<M;i++){
st[i]=new SequentialSearchST();
}
}
}
2. 拉鍊法中的hash函數實現
/**
* hash函數
* @param key 要插入的鍵
* @return hash值
*/
private int hash(Key key){
return (key.hashCode()&0x7fffffff % M);
}
3. 拉鍊法的put()和get()方法
public void put(Key key,Value value){
st[hash(key)].put(key,value);
}
public Value get(Key key){
return (Value)st[hash(key)].get(key);
}
線性探測法
開放地址散列表 : 用大小爲M的數組保存N個鍵值對,其中M>N,依靠數組中的空位來解決碰撞衝突(線性探測法是最簡單的開放地址散列表)
線性探測法原理 :
當碰撞發生時(當一個鍵的散列值已經被另外一個不同的鍵佔用),直接檢查散列表的下一個位置(索引值+1)直到遇到相同的鍵或者遇到了空缺的位置
遇到相同的鍵則替換其值,遇到空缺位置則把鍵值寫入即可.
線性探測法實現
1. 線性探測法基礎方法
public class LinearProbingHashST <Key,Value>{
private int N;//存放的鍵值對的總數
private int M;//線性檢測表的大小
private Key[] keys;//鍵
private Value[] values;//值
public LinearProbingHashST(){
keys=(Key[]) new Object[M];
values=(Value[])new Object[M];
}
private LinearProbingHashST(int size){
this.M=size;
keys=(Key[]) new Object[M];
values=(Value[])new Object[M];
}
/**
* resize()方法就是新建一個線性探測表,然後將原表的數據插入
* @param size 線性探測表的大小
*/
private void resize(int size){
LinearProbingHashST newTable=new LinearProbingHashST<Key,Value>(size);
for(int i=0;i<M;i++){
if(keys[i]!=null){
newTable.put(keys[i],values[i]);
}
}
keys=(Key[])newTable.keys;
values=(Value[])newTable.values;
M=newTable.M;
}
}
2. 線性檢測法中的hash函數實現(與拉鍊法相同)
private int hash(Key key){
return (key.hashCode()&0x7fffffff)%M;
}
3. 線性檢測法中的put()和get()方法實現
public void put(Key key,Value value){
if(N>=M){
resize(2*M);
}
int i;
for(i=hash(key);keys[i]!=null;i=(i+1)%M){
if(keys[i].equals(key)){
values[i]=value;
return;
}
}
keys[i]=key;
values[i]=value;
N++;
}
public Value get(Key key){
for(int i=hash(key);keys[i]!=null;i=(i+1)%M){
if(keys[i].equals(key)){
return values[i];
}
}
return null;
}
4. 線性檢測法中的delete()方法實現
public void delete(Key key){
/** 沒有找到key */
if(get(key)==null){
return;
}
/** 找到key對應的數組索引位置,並將其鍵與值刪除 */
int i=hash(key);
while(!keys[i].equals(key)){
i=(i+1)%M;
}
keys[i]=null;
values[i]=null;
/** 將被刪除鍵的右側連續的所有鍵重新插入 */
while(keys[i]!=null){
Key keyToRedo=keys[i];
Value valueToRedo=values[i];
keys[i]=null;
values[i]=null;
N--;
put(keyToRedo,valueToRedo);
i=(i+1)%M;
}
N--;
if(N>0&&N==M/8) resize(M/2);
}
拉鍊法與線性檢測法的性能差距
拉鍊法爲每個鍵值對都分配了一小塊內存,而線性探測法則爲整張表使用了兩個很大的數組,但是兩者的性能差距還是因場景不同而有所變化,最好的方法還是去實踐一下.