1. HashMap的數據結構
http://blog.csdn.net/gaopu12345/article/details/50831631 ??看一下
數據結構中有數組和鏈表來實現對數據的存儲,但這兩者基本上是兩個極端。
數組
數組存儲區間是連續的,占用內存嚴重,故空間復雜的很大。但數組的二分查找時間復雜度小,為O(1);數組的特點是:尋址容易,插入和刪除困難。
鏈表
鏈表存儲區間離散,占用內存比較寬松,故空間復雜度很小,但時間復雜度很大,達O(N)。鏈表的特點是:尋址困難,插入和刪除容易。
哈希表
那么我們能不能綜合兩者的特性,做出一種尋址容易,插入刪除也容易的數據結構?答案是肯定的,這就是我們要提起的哈希表。哈希表((Hash table)既滿足了數據的查找方便,同時不占用太多的內容空間,使用也十分方便。
哈希表有多種不同的實現方法,我接下來解釋的是最常用的一種方法—— 拉鏈法,我們可以理解為“鏈表的數組” ,如圖:


從上圖我們可以發現哈希表是由數組+鏈表組成的,一個長度為16的數組中,每個元素存儲的是一個鏈表的頭結點。那么這些元素是按照什么樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標為12的位置。
HashMap其實也是一個線性的數組實現的,所以可以理解為其存儲數據的容器就是一個線性數組。這可能讓我們很不解,一個線性的數組怎么實現按鍵值對來存取數據呢?這里HashMap有做一些處理。
首先HashMap里面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,我們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map里面的內容都保存在Entry[]里面。
transient Entry[] table;
2. HashMap的存取實現
既然是線性數組,為什么能隨機存取?這里HashMap用了一個小算法,大致是這樣實現:
int hash = key.hashCode(); // 這個hashCode方法這里不詳述,只要理解每個key的hash是一個固定的int值
int index = hash % Entry[].length;
Entry[index] = value;
// 取值時:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];
1)put
這里HashMap里面用到鏈式數據結構的一個概念。上面我們提到過Entry類里面有一個next屬性,作用是指向下一個Entry。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會后又進來一個鍵值對B,通過計算其index也等於0,現在怎么辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那么C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性鏈接在一起。所以疑問不用擔心。也就是說數組中存儲的是最后插入的元素。到這里為止,HashMap的大致實現,我們應該已經清楚了。
}
當然HashMap里面也包含一些優化方面的實現,這里也說一下。比如:Entry[]的長度一定后,隨着map里面數據的越來越長,這樣同一個index的鏈就會很長,會不會影響性能?HashMap里面設置一個因子,隨着map的size越來越大,Entry[]會以一定的規則加長長度。
2)get
3)null key的存取
null key總是存放在Entry[]數組的第一個元素。
4)確定數組index:hashcode % table.length取模
HashMap存取時,都需要計算當前key應該對應Entry[]數組哪個元素,即計算數組下標;算法如下:
5)table初始大小
注意table初始大小並不是構造函數中的initialCapacity!!
而是 >= initialCapacity的2的n次冪!!!!
————為什么這么設計呢?——
3. 解決hash沖突的辦法 (下文詳解)
- 開放定址法(線性探測再散列,二次探測再散列,偽隨機探測再散列)
- 再哈希法
- 鏈地址法
- 建立一個公共溢出區
Java中hashmap的解決辦法就是采用的鏈地址法。
4. 再散列rehash過程
當哈希表的容量超過默認容量時,必須調整table的大小。當容量已經達到最大可能值時,那么該方法就將容量調整到Integer.MAX_VALUE返回,這時,需要創建一張新表,將原表的映射到新表中。
}
}
============================= 華麗的分割線 ==============
hashmap的擴容機制
1、當我們往hashmap中put元素的時候,先根據key的hash值得到這個元素在 數組中的位置(即下標),然后就可以把這個元素放到對應的位置中了。如果這個元素所在的位子上已經存放有其他元素了,那么在同一個位子上的元素將以鏈表的 形式存放,新加入的放在鏈頭,比如a->b->c,新加入的d放到a的位置前面,最先加入的放在鏈尾,也就是c。最后變成d->a->b->c,從hashmap中get元素時,首先計算key的hashcode,找到數組中對應位置的某一元素, 然后通過key的equals方法在對應位置的鏈表中找到需要的元素。
2、
在hashmap中要找到某個元素,需要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過hashmap的數據結構是數組和鏈表的結合,所以我們當然希望這個hashmap里面的元素位置盡量的分布均勻些,盡量使得每個位置上的元素數量只有一個,那么當我們用hash算法求得這個位置 的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表。所以我們首先想到的就是把hashcode對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,能不能找一種更快速,消耗更小的方式那?java中時這樣做的,
Java代碼
staticintindexFor(inth,intlength){
returnh&(length-1);
}
首 先算得key得hashcode值,然后跟數組的長度-1做一次“與”運算(&)。看上去很簡單,其實比較有玄機。比如數組的長度是2的4次方, 那么hashcode就會和2的4次方-1做“與”運算。很多人都有這個疑問,為什么hashmap的數組初始化大小都是2的次方大小時,hashmap 的效率最高,我以2的4次方舉例,來解釋一下為什么數組大小為2的冪時hashmap訪問的性能最高。 看下圖,左邊兩組是數組長度為16(2的4次方),右邊兩組是數組長度為15。兩組的hashcode均為8和9,但是很明顯,當它們和1110“與”的 時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到同一個鏈表上,那么查詢的時候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度為15的時候,hashcode的值會與14(1110)進行“與”,那么 最后一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是 這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的幾率,減慢了查詢的效率!所以說,當數組長度為2的n次冪的時候,不同的key算得得index相同的幾率較小,那么數據在數組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。說到這里,我們再回頭看一下hashmap中默認的數組大小是多少,查看源代碼可以得知是16,為什么是16,而不是15,也不是20呢,看到上面 annegu的解釋之后我們就清楚了吧,顯然是因為16是2的整數次冪的原因,在小數據量的情況下16比15和20更能減少key之間的碰撞,而加快查詢 的效率。
3、
當hashmap中的元素越來越多的時候,碰撞的幾率也就越來越高(因為數組的長度是固定的),所以為了提高查詢的效率,就要對hashmap的數組進行 擴容,數組擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了, 而在hashmap數組擴容之后,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
那么hashmap什么時候進行擴容呢?當hashmap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的 默認值為0.75,也就是說,默認情況下,數組大小為16,那么當hashmap中元素個數超過16*0.75=12的時候,就把數組的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知hashmap中元素的個數,那 么預設元素的個數能夠有效的提高hashmap的性能。
比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經說過,即使是1000,hashmap也自動會將其設置為1024。 但是new HashMap(1024)還不是更合適的,因為0.75*1000 < 1000, 也就是說為了讓0.75 * size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。
======================= 華麗的分割線 =====================================
哈希表及處理hash沖突的方法
看了ConcurrentHashMap的實現, 使用的是拉鏈法.
雖然我們不希望發生沖突,但實際上發生沖突的可能性仍是存在的。當關鍵字值域遠大於哈希表的長度,而且事先並不知道關鍵字的具體取值時。沖突就難免會發生。
另外,當關鍵字的實際取值大於哈希表的長度時,而且表中已裝滿了記錄,如果插入一個新記錄,不僅發生沖突,而且還會發生溢出。
因此,處理沖突和溢出是哈希技術中的兩個重要問題。
哈希法又稱散列法、雜湊法以及關鍵字地址計算法等,相應的表稱為哈希表。這種方法的基本思想是:首先在元素的關鍵字k和元素的存儲位置p之間建立一個對應關系f,使得p=f(k),f稱為哈希函數。創建哈希表時,把關鍵字為k的元素直接存入地址為f(k)的單元;以后當查找關鍵字為k的元素時,再利用哈希函數計算出該元素的存儲位置p=f(k),從而達到按關鍵字直接存取元素的目的。
當關鍵字集合很大時,關鍵字值不同的元素可能會映象到哈希表的同一地址上,即 k1≠k2 ,但 H(k1)=H(k2),這種現象稱為沖突,此時稱k1和k2為同義詞。實際中,沖突是不可避免的,只能通過改進哈希函數的性能來減少沖突。
綜上所述,哈希法主要包括以下兩方面的內容:
1)如何構造哈希函數
2)如何處理沖突。
8.4.1 哈希函數的構造方法
構造哈希函數的原則是:①函數本身便於計算;②計算出來的地址分布均勻,即對任一關鍵字k,f(k) 對應不同地址的概率相等,目的是盡可能減少沖突。
下面介紹構造哈希函數常用的五種方法。
1. 數字分析法
如果事先知道關鍵字集合,並且每個關鍵字的位數比哈希表的地址碼位數多時,可以從關鍵字中選出分布較均勻的若干位,構成哈希地址。例如,有80個記錄,關鍵字為8位十進制整數d1d2d3…d7d8,如哈希表長取100,則哈希表的地址空間為:00~99。假設經過分析,各關鍵字中 d4和d7的取值分布較均勻,則哈希函數為:h(key)=h(d1d2d3…d7d8)=d4d7。例如,h(81346532)=43,h(81301367)=06。相反,假設經過分析,各關鍵字中 d1和d8的取值分布極不均勻, d1 都等於5,d8 都等於2,此時,如果哈希函數為:h(key)=h(d1d2d3…d7d8)=d1d8,則所有關鍵字的地址碼都是52,顯然不可取。
2. 平方取中法
當無法確定關鍵字中哪幾位分布較均勻時,可以先求出關鍵字的平方值,然后按需要取平方值的中間幾位作為哈希地址。這是因為:平方后中間幾位和關鍵字中每一位都相關,故不同關鍵字會以較高的概率產生不同的哈希地址。
例:我們把英文字母在字母表中的位置序號作為該英文字母的內部編碼。例如K的內部編碼為11,E的內部編碼為05,Y的內部編碼為25,A的內部編碼為01, B的內部編碼為02。由此組成關鍵字“KEYA”的內部代碼為11052501,同理我們可以得到關鍵字“KYAB”、“AKEY”、“BKEY”的內部編碼。之后對關鍵字進行平方運算后,取出第7到第9位作為該關鍵字哈希地址,如圖8.23所示。
| 關鍵字 |
內部編碼 |
內部編碼的平方值 |
H(k)關鍵字的哈希地址 |
| KEYA |
11050201 |
122157778355001 |
778 |
| KYAB |
11250102 |
126564795010404 |
795 |
| AKEY |
01110525 |
001233265775625 |
265 |
| BKEY |
02110525 |
004454315775625 |
315 |
圖8.23平方取中法求得的哈希地址
3. 分段疊加法
這種方法是按哈希表地址位數將關鍵字分成位數相等的幾部分(最后一部分可以較短),然后將這幾部分相加,舍棄最高進位后的結果就是該關鍵字的哈希地址。具體方法有折疊法與移位法。移位法是將分割后的每部分低位對齊相加,折疊法是從一端向另一端沿分割界來回折疊(奇數段為正序,偶數段為倒序),然后將各段相加。例如:key=12360324711202065,哈希表長度為1000,則應把關鍵字分成3位一段,在此舍去最低的兩位65,分別進行移位疊加和折疊疊加,求得哈希地址為105和907,如圖8.24所示。
1 2 3 1 2 3
6 0 3 3 0 6
2 4 7 2 4 7
1 1 2 2 1 1
+) 0 2 0 +) 0 2 0
———————— —————————
1 1 0 5 9 0 7
(a)移位疊加 (b) 折疊疊加
圖8.24 由疊加法求哈希地址
4. 除留余數法
假設哈希表長為m,p為小於等於m的最大素數,則哈希函數為
h(k)=k % p ,其中%為模p取余運算。
例如,已知待散列元素為(18,75,60,43,54,90,46),表長m=10,p=7,則有
h(18)=18 % 7=4 h(75)=75 % 7=5 h(60)=60 % 7=4
h(43)=43 % 7=1 h(54)=54 % 7=5 h(90)=90 % 7=6
h(46)=46 % 7=4
此時沖突較多。為減少沖突,可取較大的m值和p值,如m=p=13,結果如下:
h(18)=18 % 13=5 h(75)=75 % 13=10 h(60)=60 % 13=8
h(43)=43 % 13=4 h(54)=54 % 13=2 h(90)=90 % 13=12
h(46)=46 % 13=7
此時沒有沖突,如圖8.25所示。
0 1 2 3 4 5 6 7 8 9 10 11 12
|
|
|
54 |
|
43 |
18 |
|
46 |
60 |
|
75 |
|
90 |
圖8.25 除留余數法求哈希地址
5. 偽隨機數法
采用一個偽隨機函數做哈希函數,即h(key)=random(key)。
在實際應用中,應根據具體情況,靈活采用不同的方法,並用實際數據測試它的性能,以便做出正確判定。通常應考慮以下五個因素 :
l 計算哈希函數所需時間 (簡單)。
l 關鍵字的長度。
l 哈希表大小。
l 關鍵字分布情況。
l 記錄查找頻率
8.4.2 處理沖突的方法
通過構造性能良好的哈希函數,可以減少沖突,但一般不可能完全避免沖突,因此解決沖突是哈希法的另一個關鍵問題。創建哈希表和查找哈希表都會遇到沖突,兩種情況下解決沖突的方法應該一致。下面以創建哈希表為例,說明解決沖突的方法。常用的解決沖突方法有以下四種:
1. 開放定址法
這種方法也稱再散列法,其基本思想是:當關鍵字key的哈希地址p=H(key)出現沖突時,以p為基礎,產生另一個哈希地址p1,如果p1仍然沖突,再以p為基礎,產生另一個哈希地址p2,…,直到找出一個不沖突的哈希地址pi ,將相應元素存入其中。這種方法有一個通用的再散列函數形式:
Hi=(H(key)+di)% m i=1,2,…,n
其中H(key)為哈希函數,m 為表長,di稱為增量序列。增量序列的取值方式不同,相應的再散列方式也不同。主要有以下三種:
l 線性探測再散列
dii=1,2,3,…,m-1
這種方法的特點是:沖突發生時,順序查看表中下一單元,直到找出一個空單元或查遍全表。
l 二次探測再散列
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
這種方法的特點是:沖突發生時,在表的左右進行跳躍式探測,比較靈活。
l 偽隨機探測再散列
di=偽隨機數序列。
具體實現時,應建立一個偽隨機數發生器,(如i=(i+p) % m),並給定一個隨機數做起點。
例如,已知哈希表長度m=11,哈希函數為:H(key)= key % 11,則H(47)=3,H(26)=4,H(60)=5,假設下一個關鍵字為69,則H(69)=3,與47沖突。如果用線性探測再散列處理沖突,下一個哈希地址為H1=(3 + 1)% 11 = 4,仍然沖突,再找下一個哈希地址為H2=(3 + 2)% 11 = 5,還是沖突,繼續找下一個哈希地址為H3=(3 + 3)% 11 = 6,此時不再沖突,將69填入5號單元,參圖8.26 (a)。如果用二次探測再散列處理沖突,下一個哈希地址為H1=(3 + 12)% 11 = 4,仍然沖突,再找下一個哈希地址為H2=(3 - 12)% 11 = 2,此時不再沖突,將69填入2號單元,參圖8.26 (b)。如果用偽隨機探測再散列處理沖突,且偽隨機數序列為:2,5,9,……..,則下一個哈希地址為H1=(3 + 2)% 11 = 5,仍然沖突,再找下一個哈希地址為H2=(3 + 5)% 11 = 8,此時不再沖突,將69填入8號單元,參圖8.26 (c)。
0 1 2 3 4 5 6 7 8 9 10
|
|
|
|
47 |
26 |
60 |
69 |
|
|
|
|
(a) 用線性探測再散列處理沖突
0 1 2 3 4 5 6 7 8 9 10
|
|
|
69 |
47 |
26 |
60 |
|
|
|
|
|
(b) 用二次探測再散列處理沖突
0 1 2 3 4 5 6 7 8 9 10
|
|
|
|
47 |
26 |
60 |
|
|
69 |
|
|
(c) 用偽隨機探測再散列處理沖突
圖8.26開放地址法處理沖突
從上述例子可以看出,線性探測再散列容易產生“二次聚集”,即在處理同義詞的沖突時又導致非同義詞的沖突。例如,當表中i, i+1 ,i+2三個單元已滿時,下一個哈希地址為i, 或i+1 ,或i+2,或i+3的元素,都將填入i+3這同一個單元,而這四個元素並非同義詞。線性探測再散列的優點是:只要哈希表不滿,就一定能找到一個不沖突的哈希地址,而二次探測再散列和偽隨機探測再散列則不一定。
2. 再哈希法
這種方法是同時構造多個不同的哈希函數:
Hi=RH1(key) i=1,2,…,k
當哈希地址Hi=RH1(key)發生沖突時,再計算Hi=RH2(key)……,直到沖突不再產生。這種方法不易產生聚集,但增加了計算時間。
3. 鏈地址法
這種方法的基本思想是將所有哈希地址為i的元素構成一個稱為同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。鏈地址法適用於經常進行插入和刪除的情況。
例如,已知一組關鍵字(32,40,36,53,16,46,71,27,42,24,49,64),哈希表長度為13,哈希函數為:H(key)= key % 13,則用鏈地址法處理沖突的結果如圖8.27所示:
![]() |
圖8.27 鏈地址法處理沖突時的哈希表
本例的平均查找長度 ASL=(1*7+2*4+3*1)=1.5
(2)拉鏈法的優點
與開放定址法相比,拉鏈法有如下幾個優點:
①拉鏈法處理沖突簡單,且無堆積現象,即非同義詞決不會發生沖突,因此平均查找長度較短;
②由於拉鏈法中各鏈表上的結點空間是動態申請的,故它更適合於造表前無法確定表長的情況;
③開放定址法為減少沖突,要求裝填因子α較小,故當結點規模較大時會浪費很多空間。而拉鏈法中可取α≥1,且結點較大時,拉鏈法中增加的指針域可忽略不計,因此節省空間;
④在用拉鏈法構造的散列表中,刪除結點的操作易於實現。只要簡單地刪去鏈表上相應的結點即可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結 點的空間置為空,否則將截斷在它之后填人散列表的同義詞結點的查找路徑。這是因為各種開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。因此在 用開放地址法處理沖突的散列表上執行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點。
(3)拉鏈法的缺點
拉鏈法的缺點是:指針需要額外的空間,故當結點規模較小時,開放定址法較為節省空間,而若將節省的指針空間用來擴大散列表的規模,可使裝填因子變小,這又減少了開放定址法中的沖突,從而提高平均查找速度。??????
4、建立公共溢出區
這種方法的基本思想是:將哈希表分為基本表和溢出表兩部分,凡是和基本表發生沖突的元素,一律填入溢出表

