轉自:http://blog.csdn.net/kingmicrosoft/article/details/49805339
前言:
數組的特點是:尋址容易,插入和刪除困難;
鏈表的特點是:尋址困難,插入和刪除容易;
我們可以構造一種結合兩種優點的“鏈表散列”的數據結構,可以理解爲鏈表的數組,HashMap就是基於其實現的。
1.哈希表的缺點有和優點
優點:
相對數組可以節省存儲空間;
插入和尋址都很快;
在散列表中,查找一個元素的時間和鏈表中是相同的,都爲O(n),但是在實踐中散列表效率是很高的,查找一個元素的期望的時間爲O(1);
缺點:
它是基於數組的, 數組創建完後擴展比較難, 所以當哈希表被填滿的時候,性能會下降很多;所以,最好是知道表中要存儲多少數據;
2. 理解尋址
在理解Hashmap之前,先理解哈尋址
直接尋址方式:
哈希尋址:
關鍵字是k的元素被散列到槽h(k);
所以現在就剩下幾個問題:
1.如何哈希化
//JDK源碼
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// 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);
}
哈希函數的設計其實很有講究, 目標就是儘量減少衝突,同時把尋址控制在一定範圍內;
具體的理論現在還理解不了, 源碼的分析可以參考:
http://pengranxiang.iteye.com/blog/543893
2.如何解決衝突
指定的數組大小是需要存儲的數據量的兩倍,因此,可能有一半的單元是空的.
當衝突發生,
方法一:找到數組的一個空位,把數據插入,稱爲開放地址法;
方法二:創建一個存放鏈表的數組, 數組內不直接存放數據,這樣當衝突發生,新的數據項直接接到這個數組下標所指的鏈表中;(鏈地址法)
2.1 開放地址法:
一種簡單的就是:當要插入的數據的位置是1234, 如果位置被佔了, 那麼就看看1235, 以此類推,直到找到空位, 這樣的方式叫線性探測;
當然,還有其他更好的改進的探測方法,就不仔細說了;
2.2 鏈地址法:
在鏈地址法中,如果需要在N個單元的數組中存放大於N個數據,因此裝填因子大於1;
裝填因子爲2/3左右的時候,開發地址法的哈希表效率會下降很多, 而鏈地址法當因子爲大於1,且對性能影響不是很大;
當然, 如果鏈表中有許多項, 存儲時間會變長, 因爲存儲特定的數據需要搜索鏈表一半的長度;
2.3 JDK的鏈地址法具體實現
(這部分原文是來自 http://xiaolu123456.iteye.com/blog/1485349)
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;
/*判斷當前確定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那麼新值覆蓋原來的舊值,並返回舊值。
如果存在相同的hashcode,那麼他們確定的索引位置就相同,這時判斷他們的key是否相同,如果不相同,這時就是產生了hash衝突。
Hash衝突後,那麼HashMap的單個bucket裏存儲的不是一個 Entry,而是一個 Entry 鏈。
系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 爲止——如果恰好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),
那系統必須循環到最後才能找到該元素。
*/
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
理解HASHMAP衝突最重要的一句話: 衝突是不可避免的,所以要去解決, 但是要盡最大努力,減少衝突的機會;
個人的理解是:減少衝突一方面是體現在哈希函數的設計上, 另外,作爲使用者也要注意下容量是否合適;
HashMap的API裏面有一句:
通常,默認加載因子 (.75) 在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。
3. 哈希表怎麼擴容
上面提到了默認的加載因子爲0.75, 那麼什麼時候JDK裏面的Hashmap數組會擴容? 擴多大?
在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減少 rehash 操作次數。
如果 初始容量*加載因子<最大數據條目,則會發生擴容操作。
//JDK源碼
- void addEntry(int hash, K key, V value, int bucketIndex) {
- if ((size >= threshold) && (null != table[bucketIndex])) {
- resize(2 * table.length);
- hash = (null != key) ? hash(key) : 0;
- bucketIndex = indexFor(hash, table.length);
- }
- createEntry(hash, key, value, bucketIndex);
- }
- * @param newCapacity the new capacity, MUST be a power of two;
- * must be greater than current capacity unless current
- * capacity is MAXIMUM_CAPACITY (in which case value
- * is irrelevant).
- */
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable, initHashSeedAsNeeded(newCapacity));
- table = newTable;
- threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
- }
每次在原來的基礎上增大1倍(table.lenght*2)
所以在使用的過程中, 合理使用擴容.