散列函數
-
正整數
除留余數法,選擇大小為素數M的數組,對於任意正整數k ,計算k除以M的余數。
如果M不是素數,我們可能無法利用鍵中包含的所有信息,這可能導致我們無法均勻地散列散列值 -
浮點數
第一,如果鍵是0-1的實數,我們可以將它乘 M 並四舍五入得到一個0~M-1 之間的索引,有缺陷,高位起的作用更大,最低位對散列值得結果沒影響。
第二,將鍵表示為二進制,然后試用版除留余數法。 -
字符串
基本原理也是除留余數法,Horner方法
int hash = 0;
for(int i = 0 ; i<s.length(); i++){
hash = (R * hash +s.charAt(i)) % M;
}
//R代表進制 -
組合鍵
eg ,年月日
int hash = (((day * R +month) % M) * R +year) % M; -
Java約定
第一,所有數據類型都集成了一個能返回一個32bit整數的hashCode()方法。
第二,每一種數據類型的hashCode()都必須和equals()一致
第三,a.equals(b) 為true a.hashCode()= b.hashCode(); a,b的hashCode相同,並不一樣是一個對象,需要用equal判斷是否為同一對象。 -
hashCode()的返回值轉化為索引值
hashCode()返回32位整數,而我們需要的是 0~M-1索引。轉化方法如下
private int hash(Key x){
return (x.hashCode() & 0x7fffffff) % M;
}
//x.hashCode() & 0xfffffff將符號位屏蔽,然后除留余數法計算。
- hashCode()
public class Transaction{
private final String who;
private final Date when;
private final double amount;
public int hashCode(){
int hash = 17;
hash = 31 * hash + who.hashCode();
hash = 31 * hash + when.hashCode();
hash = 31 * hash + ammount.hashCode();
return hash;
}
}
- 軟緩存
如果散列值得計算很耗時,那么我們或許可以將每個鍵的散列值保存起來,即在每個鍵中使用一個hash變量保存它的hashCode()返回值 - 優秀的散列方法的三個條件
第一,一致性 等價的鍵必然產生相等的散列值
第二,高效性,計算簡便
第三,均勻地散列所有鍵
基於拉鏈法的散列表
定義:碰撞處理是指處理兩個或多個鍵的散列值相同的情況。一種直接的辦法是將大小為M的數組中的每個元素指向一條鏈表,鏈表中每個結點都存儲了
散列值為該元素索引的鍵值對。這種方法稱為拉鏈法。
讓M足夠大,這樣每個鏈表的長度就越短。
查找分兩步:根據散列值找到鏈表,然后沿着鏈表查找相應的鍵。
在一張含有M條鏈表 和N個鍵的散列表中,未命中查找和插入操作所需比較的次數為 ~N/M
基於線性探測法的散列表
另外一種,用大小M的數組保存N個鍵值對,其中 M > N。我們需要依靠數組中的空位解決碰撞沖突。基於這種策略的所有方法統稱為 開放地址散列表
開放地址散列表最簡單的方法叫做 線性探測法
定義:當碰撞發生時,我們直接檢查散列表中的下一個位置。這樣的線性探測有三種結果
第一,命中,該位置的鍵和被查找的鍵相同
第二,未命中,鍵為空(該位置沒有鍵)
第三,繼續查找,該位置的鍵和被查找的鍵不同
基於線性探測法的散列表:
插入思路:
第一:計算key的散列表,找到相應的位置
第二:查看這個位置的值是否為要插入的key。不是的話比較下一個位置 ,直到當前位置的鍵值為空。
第三:如果在遇到空鍵值前遇到相等的鍵,則進行更新操作;如果直到遇到空值也沒遇到相同的鍵,在空鍵值位置插入相應鍵值。
public class LinearProbingHashST<Key, Value> {
private static final int INIT_CAPACITY = 4;
private int n; // number of key-value pairs in the symbol table
private int m; // size of linear probing table
private Key[] keys; // the keys
private Value[] vals; // the values
public LinearProbingHashST() {
this(INIT_CAPACITY);
}
public LinearProbingHashST(int capacity) {
m = capacity;
n = 0;
keys = (Key[]) new Object[m];
vals = (Value[]) new Object[m];
}
private int hash(Key key) {
return (key.hashCode() & 0x7fffffff) % m;
}
// resizes the hash table to the given capacity by re-hashing all of the keys
private void resize(int capacity) {
LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
for (int i = 0; i < m; i++) {
if (keys[i] != null) {
temp.put(keys[i], vals[i]);
}
}
keys = temp.keys;
vals = temp.vals;
m = temp.m;
}
public void put(Key key, Value val) {
if (key == null) throw new IllegalArgumentException("first argument to put() is null");
if (val == null) {
delete(key);
return;
}
// double table size if 50% full
if (n >= m/2) resize(2*m);
int i;
for (i = hash(key); keys[i] != null; i = (i + 1) % m) {
if (keys[i].equals(key)) {
vals[i] = val;
return;
}
}
keys[i] = key;
vals[i] = val;
n++;
}
public Value get(Key key) {
if (key == null) throw new IllegalArgumentException("argument to get() is null");
for (int i = hash(key); keys[i] != null; i = (i + 1) % m)
if (keys[i].equals(key))
return vals[i];
return null;
}
}
刪除操作
鍵簇: 元素在插入數組后集聚而成的一組連續的條目。
我們需要將簇中被刪除鍵的右側的所有鍵重新插入到散列表
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
if (!contains(key)) return;
// find position i of key
int i = hash(key);
while (!key.equals(keys[i])) {
i = (i + 1) % m;
}
// delete key and associated value
keys[i] = null;
vals[i] = null;
// rehash all keys in same cluster
i = (i + 1) % m;
while (keys[i] != null) {
// delete keys[i] an vals[i] and reinsert
Key keyToRehash = keys[i];
Value valToRehash = vals[i];
keys[i] = null;
vals[i] = null;
n--;
put(keyToRehash, valToRehash);
i = (i + 1) % m;
}
n--;
// halves size of array if it's 12.5% full or less
if (n > 0 && n <= m/8) resize(m/2);
assert check();
}
當 散列表快滿的時候查找所需要的探測次數是巨大的,但當使用率 a小於 1/2 是探測的預計次數在1.5~2.5之間。
調整數組的大小
第一,基於線性探測法的數組大小調整
把原表中所有的鍵重新散列並插入到新表
private void resize(int capacity) {
LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
for (int i = 0; i < m; i++) {
if (keys[i] != null) {
temp.put(keys[i], vals[i]);
}
}
keys = temp.keys;
vals = temp.vals;
m = temp.m;
}
第二,拉鏈法的數組大小調整
內存使用
除了存儲鍵和值所需要的空間外,我們實現拉鏈法 SeparateChainingHashST 保存了M個SeparateChainingHashST
對象和它們的引用。每個對象需要16字節,每個引用需要8字節。另外還有N個對象,每個對象需要24字節以及三個引用。
在使用率 1/81/2情況下,線性探測使用4N16N個引用。
方法 N個元素所需的內存
拉鏈法 48N + 32M
線性探測法 32N ~ 128N
二叉查找樹 56N