首先來瞭解一下基本概念
所謂雜湊表(Hash Table,又叫散列表),是儲存索引值對(Key-value)的表,之所以不叫它Map(索引值對一起儲存一般叫做Map),是因為它下面的特性:它能把關鍵碼(key)映射到表中的一個位置來直接存取,這樣訪問速度就非常快。其中的映射函數稱為散列函數(Hash function)。
1) 對於關鍵字key, f(key)是其儲存位置,f則是散列函數
2) 如果key1 != key2 但是 f(key1) == f(key2),這種現象稱為衝突(collison)。衝突不可避免,這是因為key值無限而表容量總是有限(*見篇末思考題*)。我們追求的是對任意關鍵字,散列到表中的地址機率是相等的,這樣的散列函數為均勻散列函數。
散列函數有多種
× 直接定址法:取關鍵字或關鍵字的某個線性函數值為散列地址。即H(key)=key或H(key) = a·key + b,其中a和b為常數(這種散列函數叫做自身函數)
× 數字分析法
× 平方取中法
× 摺疊法
× 隨機數法
× 除留餘數法:取關鍵字被某個不大於散列表表長m的數p除後所得的餘數為散列地址。即 H(key) = key MOD p, p<=m。不僅可以對關鍵字直接模數,也可在摺疊、平方取中等運算之後模數。對p的選擇很重要,一般取素數或m,若p選的不好,容易產生同義字。
可以想像,當表中的資料個數接近表的容量大小時,發生衝突的機率會明顯增大,因此,在“資料個數/表容量”到達某個比例的時侯,需要擴大表的容量,這個比例稱為“裝填因子”(load factor).
解決衝突主要有下面兩類方法:
× 分離連結法,就是對hash到同一地址的不同元素,用鏈表連起來,也叫拉鏈法
× 開放定址法,如果地址有衝突,就在此地址附近找。包括線性探測法,平方探測法,雙散列等
然後來看一下Java的Hashtable實現
java.util.Hashtable的本質是個數組,數組的元素是linked的索引值對(單向鏈表)。
Java代碼 private transient Entry[] table; // Entry數組
private static class Entry<K,V> implements Map.Entry<K,V> { int hash; K key; V value; Entry<K,V> next; // Entry此處表明是個單鏈表 ... }
我們可以使用指定數組大小、裝填因子的建構函式,也可以使用預設建構函式,預設數組的大小是11,裝填因子是0.75.
public Hashtable(int initialCapacity, float loadFactor) { ... } public Hashtable() { this(11, 0.75f); }
當要擴大數組時,大小變為oldCapacity * 2 + 1,當然這無法保證數組的大小總是素數。
來看下其中的元素插入的方法,put方法:
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K, V> e = tab[index]; e != null; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } }
Java中Object類有幾個方法,其中一個是hashCode(), 這說明Java中所有對象都具有這一方法,調用可以得到對象自身的hash碼。對錶的長度取餘得址,並在衝突位置使用鏈表。
HashMap與Hashtable的功能幾乎一樣。但HashMap的的初始數組大小是16而不是11,當要擴大數組時,大小變為原來的2倍,預設的裝填因子也是0.75. 其put方法如下,對hash值和index都有更改:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } /** * Applies a supplemental hash function to a given hashCode, which * defends against poor quality hash functions. This is critical * because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } /** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
再看看其它開源的Java庫中的Hashtable
目前存在多個開源的Java Collection實現,各個目的不同,側重點也不同。以下對開源架構中雜湊表的分析主要從幾個方面入手:預設裝填因子和capacity擴充方式,散列函數以及解決衝突的方法。
1. Trove - Trove庫提供一套高效的基礎集合類。
gnu.trove.set.hash.THashMap的繼承關係:THashMap -> TObjectHash -> THash,其內部的鍵和值使分別用2個數組表示。其解決衝突的方式採用開放定址法,開放定址法對空間要求較高,因此其預設裝填因子load factor是0.5,而不是0.75. 下面看代碼一步步解釋:
預設初始化,裝填因子0.5,數組大小始從素數中取,也就是始終是素數。
/** the load above which rehashing occurs. */ public static final float DEFAULT_LOAD_FACTOR = 0.5f; protected int setUp( int initialCapacity ) { int capacity; capacity = PrimeFinder.nextPrime( initialCapacity ); computeMaxSize( capacity ); computeNextAutoCompactionAmount( initialCapacity ); return capacity; }
然後看其put方法,insertKey(T key)是其散列演算法,hash碼對數組長度取餘後,得到index,首先檢查該位置是否被佔用,如果被佔用,使用雙散列演算法解決衝突,也就是代碼中的insertKeyRehash()方法。
public V put(K key, V value) { // insertKey() inserts the key if a slot if found and returns the index int index = insertKey(key); return doPut(value, index); } protected int insertKey(T key) { consumeFreeSlot = false; if (key == null) return insertKeyForNull(); final int hash = hash(key) & 0x7fffffff; int index = hash % _set.length; Object cur = _set[index]; if (cur == FREE) { consumeFreeSlot = true; _set[index] = key; // insert value return index; // empty, all done } if (cur == key || equals(key, cur)) { return -index - 1; // already stored } return insertKeyRehash(key, index, hash, cur); }
2. Javolution - 對即時、內建、高效能系統提供Java解決方案
Javolution中的雜湊表是jvolution.util.FastMap, 其內部是雙向鏈表,預設初始大小是16,擴充時變為2倍。並沒有顯式定義load factor, 從下面語句可以知道,其值為0.5
if (map._entryCount + map._nullCount > (entries.length >> 1)) { // Table more than half empty. map.resizeTable(_isShared); }
再看下put函數,比較驚人的是其index和slot的取得,完全是用hashkey移位的方式取得的,這樣同時計算了index和避免了碰撞。
private final Object put(Object key, Object value, int keyHash, boolean concurrent, boolean noReplace, boolean returnEntry) { final FastMap map = getSubMap(keyHash); final Entry[] entries = map._entries; // Atomic. final int mask = entries.length - 1; int slot = -1; for (int i = keyHash >> map._keyShift;; i++) { Entry entry = entries[i & mask]; if (entry == null) { slot = slot < 0 ? i & mask : slot; break; } else if (entry == Entry.NULL) { slot = slot < 0 ? i & mask : slot; } else if ((key == entry._key) || ((keyHash == entry._keyHash) && (_isDirectKeyComparator ? key.equals(entry._key) : _keyComparator.areEqual(key, entry._key)))) { if (noReplace) { return returnEntry ? entry : entry._value; } Object prevValue = entry._value; entry._value = value; return returnEntry ? entry : prevValue; } } ... }