1. 程式人生 > 其它 >【Java提高十八】Map介面集合詳解

【Java提高十八】Map介面集合詳解

四、Map介面

Map與List、Set介面不同,它是由一系列鍵值對組成的集合,提供了key到Value的對映。同時它也沒有繼承Collection。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,所以它不能存在相同的key值,當然value值可以相同。實現map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。

4.1、HashMap

以雜湊表資料結構實現,查詢物件時通過雜湊函式計算其位置,它是為快速查詢而設計的,其內部定義了一個hash表陣列(Entry[] table),元素會通過雜湊轉換函式將元素的雜湊地址轉換成陣列中存放的索引,如果有衝突,則使用雜湊連結串列的形式將所有相同雜湊地址的元素串起來,可能通過檢視HashMap.Entry的原始碼它是一個單鏈表結構。


HashMap詳解

HashMap也是我們使用非常多的Collection,它是基於雜湊表的 Map 介面的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統會根據hash演算法來來計算key-value的儲存位置,我們總是可以通過key快速地存、取value。下面就來分析HashMap的存取。

一、定義

HashMap實現了Map介面,繼承AbstractMap。其中Map介面定義了鍵對映到值的規則,而AbstractMap類提供 Map 介面的骨幹實現,以最大限度地減少實現此介面所需的工作,其實AbstractMap類已經實現了Map,這裡標註Map LZ覺得應該是更加清晰吧!

二、建構函式

HashMap提供了三個建構函式:

HashMap():構造一個具有預設初始容量 (16) 和預設載入因子 (0.75) 的空 HashMap。

HashMap(int initialCapacity):構造一個帶指定初始容量和預設載入因子 (0.75) 的空 HashMap。

HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和載入因子的空 HashMap。

在這裡提到了兩個引數:初始容量,載入因子。這兩個引數是影響HashMap效能的重要引數,其中容量表示雜湊表中桶的數量,初始容量是建立雜湊表時的容量,載入因子是雜湊表在其容量自動增加之前可以達到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用連結串列法的散列表來說,查詢一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查詢效率的降低;如果負載因子太小,那麼散列表的資料將過於稀疏,對空間造成嚴重浪費。系統預設負載因子為0.75,一般情況下我們是無需修改的。

HashMap是一種支援快速存取的資料結構,要了解它的效能必須要了解它的資料結構。

三、資料結構

我們知道在Java中最常用的兩種結構是陣列和模擬指標(引用),幾乎所有的資料結構都可以利用這兩種來組合實現,HashMap也是如此。實際上HashMap是一個“連結串列雜湊”,如下是它資料結構:

從上圖我們可以看出HashMap底層實現還是陣列,只是陣列的每一項都是一條鏈。其中引數initialCapacity就代表了該陣列的長度。下面為HashMap建構函式的原始碼:

從原始碼中可以看出,每次新建一個HashMap時,都會初始化一個table陣列。table陣列的元素為Entry節點。

其中Entry為HashMap的內部類,它包含了鍵key、值value、下一個節點next,以及hash值,這是非常重要的,正是由於Entry才構成了table陣列的項為連結串列。

上面簡單分析了HashMap的資料結構,下面將探討HashMap是如何實現快速存取的。

四、儲存實現:put(key,vlaue)

首先我們先看原始碼

通過原始碼我們可以清晰看到HashMap儲存資料的過程為:首先判斷key是否為null,若為null,則直接呼叫putForNullKey方法。若不為空則先計算key的hash值,然後根據hash值搜尋在table陣列中的索引位置,如果table陣列在該位置處有元素,則通過比較是否存在相同的key,若存在則覆蓋原來key的value,否則將該元素儲存在鏈頭(最先儲存的元素放在鏈尾)。若table在該處沒有元素,則直接儲存。這個過程看似比較簡單,其實深有內幕。有如下幾點:

1、 先看迭代處。此處迭代原因就是為了防止存在相同的key值,若發現兩個hash值(key)相同時,HashMap的處理方式是用新value替換舊value,這裡並沒有處理key,這就解釋了HashMap中沒有兩個相同的key。

2、 在看(1)、(2)處。這裡是HashMap的精華所在。首先是hash方法,該方法為一個純粹的數學計算,就是計算h的hash值。

我們知道對於HashMap的table而言,資料分佈需要均勻(最好每項都只有一個元素,這樣就可以直接找到),不能太緊也不能太鬆,太緊會導致查詢速度慢,太鬆則浪費空間。計算hash值後,怎麼才能保證table元素分佈均與呢?我們會想到取模,但是由於取模的消耗較大,HashMap是這樣處理的:呼叫indexFor方法。

HashMap的底層陣列長度總是2的n次方,在建構函式中存在:capacity <<= 1;這樣做總是能夠保證HashMap的底層陣列長度為2的n次方。當length為2的n次方時,h&(length - 1)就相當於對length取模,而且速度比直接取模快得多,這是HashMap在速度上的一個優化。至於為什麼是2的n次方下面解釋。

我們回到indexFor方法,該方法僅有一條語句:h&(length - 1),這句話除了上面的取模運算外還有一個非常重要的責任:均勻分佈table資料和充分利用空間。

這裡我們假設length為16(2^n)和15,h為5、6、7。

當n=15時,6和7的結果一樣,這樣表示他們在table儲存的位置是相同的,也就是產生了碰撞,6、7就會在一個位置形成連結串列,這樣就會導致查詢速度降低。誠然這裡只分析三個數字不是很多,那麼我們就看0-15。

從上面的圖表中我們看到總共發生了8此碰撞,同時發現浪費的空間非常大,有1、3、5、7、9、11、13、15處沒有記錄,也就是沒有存放資料。這是因為他們在與14進行&運算時,得到的結果最後一位永遠都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置處是不可能儲存資料的,空間減少,進一步增加碰撞機率,這樣就會導致查詢速度慢。而當length = 16時,length – 1 = 15 即1111,那麼進行低位&運算時,值總是與原來hash值相同,而進行高位運算時,其值等於其低位值。所以說當length = 2^n時,不同的hash值發生碰撞的概率比較小,這樣就會使得資料在table陣列中分佈較均勻,查詢速度也較快。

這裡我們再來複習put的流程:當我們想一個HashMap中新增一對key-value時,系統首先會計算key的hash值,然後根據hash值確認在table中儲存的位置。若該位置沒有元素,則直接插入。否則迭代該處元素連結串列並依此比較其key的hash值。如果兩個hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的Entry的value覆蓋原來節點的value。如果兩個hash值相等但key值不等 ,則將該節點插入該連結串列的鏈頭。具體的實現過程見addEntry方法,如下:

這個方法中有兩點需要注意:

一、鏈的產生。

這是一個非常優雅的設計。系統總是將新的Entry物件新增到bucketIndex處。如果bucketIndex處已經有了物件,那麼新新增的Entry物件將指向原有的Entry物件,形成一條Entry鏈,但是若bucketIndex處沒有Entry物件,也就是e==null,那麼新新增的Entry物件指向null,也就不會產生Entry鏈了。

二、擴容問題。

隨著HashMap中元素的數量越來越多,發生碰撞的概率就越來越大,所產生的連結串列長度就會越來越長,這樣勢必會影響HashMap的速度,為了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table陣列長度*載入因子。但是擴容是一個非常耗時的過程,因為它需要重新計算這些資料在新table陣列中的位置並進行復制處理。所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。

五、讀取實現:get(key)

相對於HashMap的存而言,取就顯得比較簡單了。通過key的hash值找到在table陣列中的索引處的Entry,然後返回該key對應的value即可。

在這裡能夠根據key快速的取到value除了和HashMap的資料結構密不可分外,還和Entry有莫大的關係,在前面就提到過,HashMap在儲存過程中並沒有將key,value分開來儲存,而是當做一個整體key-value來處理的,這個整體就是Entry物件。同時value也只相當於key的附屬而已。在儲存的過程中,系統根據key的hashcode來決定Entry在table陣列中的儲存位置,在取的過程中同樣根據key的hashcode取出相對應的Entry物件。


4.2、TreeMap

鍵以某種排序規則排序,內部以red-black(紅-黑)樹資料結構實現,實現了SortedMap介面

TreeMap的實現是紅黑樹演算法的實現,所以要了解TreeMap就必須對紅黑樹有一定的瞭解,其實這篇博文的名字叫做:根據紅黑樹的演算法來分析TreeMap的實現,但是為了與Java提高篇系列博文保持一致還是叫做TreeMap比較好。通過這篇博文你可以獲得如下知識點:

1、紅黑樹的基本概念。

2、紅黑樹增加節點、刪除節點的實現過程。

3、紅黑樹左旋轉、右旋轉的複雜過程。

4、Java 中TreeMap是如何通過put、deleteEntry兩個來實現紅黑樹增加、刪除節點的。

我想通過這篇博文你對TreeMap一定有了更深的認識。好了,下面先簡單普及紅黑樹知識。


TeeMap詳解

一、紅黑樹簡介

紅黑樹又稱紅-黑二叉樹,它首先是一顆二叉樹,它具體二叉樹所有的特性。同時紅黑樹更是一顆自平衡的排序二叉樹。

我們知道一顆基本的二叉樹他們都需要滿足一個基本性質--即樹中的任何節點的值大於它的左子節點,且小於它的右子節點。按照這個基本性質使得樹的檢索效率大大提高。我們知道在生成二叉樹的過程是非常容易失衡的,最壞的情況就是一邊倒(只有右/左子樹),這樣勢必會導致二叉樹的檢索效率大大降低(O(n)),所以為了維持二叉樹的平衡,大牛們提出了各種實現的演算法,如:AVL,SBT,伸展樹,TREAP ,紅黑樹等等。

平衡二叉樹必須具備如下特性:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。也就是說該二叉樹的任何一個等等子節點,其左右子樹的高度都相近。

紅黑樹顧名思義就是節點是紅色或者黑色的平衡二叉樹,它通過顏色的約束來維持著二叉樹的平衡。對於一棵有效的紅黑樹二叉樹而言我們必須增加如下規則:

1、每個節點都只能是紅色或者黑色

2、根節點是黑色

3、每個葉節點(NIL節點,空節點)是黑色的。

4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

這些約束強制了紅黑樹的關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這棵樹大致上是平衡的。因為操作比如插入、刪除和查詢某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查詢樹。所以紅黑樹它是複雜而高效的,其檢索效率O(log n)。下圖為一顆典型的紅黑二叉樹。

對於紅黑二叉樹而言它主要包括三大基本操作:左旋、右旋、著色。

左旋 右旋

注:由於本文主要是講解Java中TreeMap,所以並沒有對紅黑樹進行非常深入的瞭解和研究,如果諸位想對其進行更加深入的研究Lz提供幾篇較好的博文:

1、紅黑樹系列集錦

2、紅黑樹資料結構剖析

3、紅黑樹

二、TreeMap資料結構

TreeMap的定義如下:

TreeMap繼承AbstractMap,實現NavigableMap、Cloneable、Serializable三個介面。其中AbstractMap表明TreeMap為一個Map即支援key-value的集合, NavigableMap(更多)則意味著它支援一系列的導航方法,具備針對給定搜尋目標返回最接近匹配項的導航方法 。

TreeMap中同時也包含了如下幾個重要的屬性:

對於葉子節點Entry是TreeMap的內部類,它有幾個重要的屬性:

注:前面只是開胃菜,下面是本篇博文的重中之重,在下面兩節我將重點講解treeMap的put()、delete()方法。通過這兩個方法我們會了解紅黑樹增加、刪除節點的核心演算法。

三、TreeMap put()方法

在瞭解TreeMap的put()方法之前,我們先了解紅黑樹增加節點的演算法。

紅黑樹增加節點

紅黑樹在新增節點過程中比較複雜,複雜歸複雜它同樣必須要依據上面提到的五點規範,同時由於規則1、2、3基本都會滿足,下面我們主要討論規則4、5。假設我們這裡有一棵最簡單的樹,我們規定新增的節點為N、它的父節點為P、P的兄弟節點為U、P的父節點為G。

對於新節點的插入有如下三個關鍵地方:

1、插入新節點總是紅色節點 。

2、如果插入節點的父節點是黑色, 能維持性質 。

3、如果插入節點的父節點是紅色, 破壞了性質. 故插入演算法就是通過重新著色或旋轉, 來維持性質 。

為了保證下面的闡述更加清晰和根據便於參考,我這裡將紅黑樹的五點規定再貼一遍:

1、每個節點都只能是紅色或者黑色

2、根節點是黑色

3、每個葉節點(NIL節點,空節點)是黑色的。

4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

一、為跟節點

若新插入的節點N沒有父節點,則直接當做根據節點插入即可,同時將顏色設定為黑色。(如圖一(1))

二、父節點為黑色

這種情況新節點N同樣是直接插入,同時顏色為紅色,由於根據規則四它會存在兩個黑色的葉子節點,值為null。同時由於新增節點N為紅色,所以通過它的子節點的路徑依然會儲存著相同的黑色節點數,同樣滿足規則5。(如圖一(2))

(圖一)

三、若父節點P和P的兄弟節點U都為紅色

對於這種情況若直接插入肯定會出現不平衡現象。怎麼處理?P、U節點變黑、G節點變紅。這時由於經過節點P、U的路徑都必須經過G所以在這些路徑上面的黑節點數目還是相同的。但是經過上面的處理,可能G節點的父節點也是紅色,這個時候我們需要將G節點當做新增節點遞迴處理。

四、若父節點P為紅色,叔父節點U為黑色或者缺少,且新增節點N為P節點的右孩子

對於這種情況我們對新增節點N、P進行一次左旋轉。這裡所產生的結果其實並沒有完成,還不是平衡的(違反了規則四),這是我們需要進行情況5的操作。

五、父節點P為紅色,叔父節點U為黑色或者缺少,新增節點N為父節點P左孩子

這種情況有可能是由於情況四而產生的,也有可能不是。對於這種情況先已P節點為中心進行右旋轉,在旋轉後產生的樹中,節點P是節點N、G的父節點。但是這棵樹並不規範,它違反了規則4,所以我們將P、G節點的顏色進行交換,使之其滿足規範。開始時所有的路徑都需要經過G其他們的黑色節點數一樣,但是現在所有的路徑改為經過P,且P為整棵樹的唯一黑色節點,所以調整後的樹同樣滿足規範5。

上面展示了紅黑樹新增節點的五種情況,這五種情況涵蓋了所有的新增可能,不管這棵紅黑樹多麼複雜,都可以根據這五種情況來進行生成。下面就來分析Java中的TreeMap是如何來實現紅黑樹的。

TreeMap put()方法實現分析

在TreeMap的put()的實現方法中主要分為兩個步驟,第一:構建排序二叉樹,第二:平衡二叉樹。

對於排序二叉樹的建立,其新增節點的過程如下:

1、以根節點為初始節點進行檢索。

2、與當前節點進行比對,若新增節點值較大,則以當前節點的右子節點作為新的當前節點。否則以當前節點的左子節點作為新的當前節點。

3、迴圈遞迴2步驟知道檢索出合適的葉子節點為止。

4、將新增節點與3步驟中找到的節點進行比對,如果新增節點較大,則新增為右子節點;否則新增為左子節點。

按照這個步驟我們就可以將一個新增節點新增到排序二叉樹中合適的位置。如下:

上面程式碼中do{}程式碼塊是實現排序二叉樹的核心演算法,通過該演算法我們可以確認新增節點在該樹的正確位置。找到正確位置後將插入即可,這樣做了其實還沒有完成,因為我知道TreeMap的底層實現是紅黑樹,紅黑樹是一棵平衡排序二叉樹,普通的排序二叉樹可能會出現失衡的情況,所以下一步就是要進行調整。fixAfterInsertion(e); 調整的過程務必會涉及到紅黑樹的左旋、右旋、著色三個基本操作。程式碼如下:

對這段程式碼的研究我們發現,其處理過程完全符合紅黑樹新增節點的處理過程。所以在看這段程式碼的過程一定要對紅黑樹的新增節點過程有了解。在這個程式碼中還包含幾個重要的操作。左旋(rotateLeft())、右旋(rotateRight())、著色(setColor())。

左旋:rotateLeft()

所謂左旋轉,就是將新增節點(N)當做其父節點(P),將其父節點P當做新增節點(N)的左子節點。即:G.left ---> N ,N.left ---> P。

右旋:rotateRight()

所謂右旋轉即,P.right ---> G、G.parent ---> P。

左旋、右旋的示意圖如下:

(左旋) (右旋)

著色:setColor()

著色就是改變該節點的顏色,在紅黑樹中,它是依靠節點的顏色來維持平衡的。

四、TreeMap delete()方法

紅黑樹刪除節點

針對於紅黑樹的增加節點而言,刪除顯得更加複雜,使原本就複雜的紅黑樹變得更加複雜。同時刪除節點和增加節點一樣,同樣是找到刪除的節點,刪除之後調整紅黑樹。但是這裡的刪除節點並不是直接刪除,而是通過走了“彎路”通過一種捷徑來刪除的:找到被刪除的節點D的子節點C,用C來替代D,不是直接刪除D,因為D被C替代了,直接刪除C即可。所以這裡就將刪除父節點D的事情轉變為了刪除子節點C的事情,這樣處理就將複雜的刪除事件簡單化了。子節點C的規則是:右分支最左邊,或者 左分支最右邊的。

紅-黑二叉樹刪除節點,最大的麻煩是要保持 各分支黑色節點數目相等。 因為是刪除,所以不用擔心存在顏色衝突問題——插入才會引起顏色衝突。

紅黑樹刪除節點同樣會分成幾種情況,這裡是按照待刪除節點有幾個兒子的情況來進行分類:

1、沒有兒子,即為葉結點。直接把父結點的對應兒子指標設為NULL,刪除兒子結點就OK了。

2、只有一個兒子。那麼把父結點的相應兒子指標指向兒子的獨生子,刪除兒子結點也OK了。

3、有兩個兒子。這種情況比較複雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然後刪除子節點C即可。

下面就論各種刪除情況來進行圖例講解,但是在講解之前請允許我再次囉嗦一句,請時刻牢記紅黑樹的5點規定:

1、每個節點都只能是紅色或者黑色

2、根節點是黑色

3、每個葉節點(NIL節點,空節點)是黑色的。

4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

(注:已經講三遍了,再不記住我就懷疑你是否適合搞IT了 O(∩_∩)O~)

誠然,既然刪除節點比較複雜,那麼在這裡我們就約定一下規則:

1、下面要講解的刪除節點一定是實際要刪除節點的後繼節點(N),如前面提到的C。

2、下面提到的刪除節點的樹都是如下結構,該結構所選取的節點是待刪除節點的右樹的最左邊子節點。這裡我們規定真實刪除節點為N、父節點為P、兄弟節點為W兄弟節點的兩個子節點為X1、X2。如下圖(2.1)。

現在我們就上面提到的三種情況進行分析、處理。

情況一、無子節點(紅色節點)

這種情況對該節點直接刪除即可,不會影響樹的結構。因為該節點為葉子節點它不可能存在子節點-----如子節點為黑,則違反黑節點數原則(規定5),為紅,則違反“顏色”原則(規定4)。 如上圖(2.2)。

情況二、有一個子節點

這種情況處理也是非常簡單的,用子節點替代待刪除節點,然後刪除子節點即可。如上圖(2.3)

情況三、有兩個子節點

這種情況可能會稍微有點兒複雜。它需要找到一個替代待刪除節點(N)來替代它,然後刪除N即可。它主要分為四種情況。

1、N的兄弟節點W為紅色

2、N的兄弟w是黑色的,且w的倆個孩子都是黑色的。

3、N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。

4、N的兄弟w是黑色的,且w的右孩子時紅色的。

情況3.1、N的兄弟節點W為紅色

W為紅色,那麼其子節點X1、X2必定全部為黑色,父節點P也為黑色。處理策略是:改變W、P的顏色,然後進行一次左旋轉。這樣處理就可以使得紅黑性質得以繼續保持。N的新兄弟new w是旋轉之前w的某個孩子,為黑色。這樣處理後將情況3.1、轉變為3.2、3.3、3.4中的一種。如下:

情況3.2、N的兄弟w是黑色的,且w的倆個孩子都是黑色的。

這種情況其父節點可紅可黑,由於W為黑色,這樣導致N子樹相對於其兄弟W子樹少一個黑色節點,這時我們可以將W置為紅色。這樣,N子樹與W子樹黑色節點一致,保持了平衡。如下

將W由黑轉變為紅,這樣就會導致新節點new N相對於它的兄弟節點會少一個黑色節點。但是如果new x為紅色,我們直接將new x轉變為黑色,保持整棵樹的平衡。否則情況3.2 會轉變為情況3.1、3.3、3.4中的一種。

情況3.3、N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。

針對這種情況是將節點W和其左子節點進行顏色交換,然後對W進行右旋轉處理。

此時N的新兄弟X1(new w)是一個有紅色右孩子的黑結點,於是將情況3轉化為情況4.

情況3.4、N的兄弟w是黑色的,且w的右孩子時紅色的。

交換W和父節點P的顏色,同時對P進行左旋轉操作。這樣就把左邊缺失的黑色節點給補回來了。同時將W的右子節點X2置黑。這樣左右都達到了平衡。

總結

個人認為這四種情況比較難理解,首先他們都不是單一的某種情況,他們之間是可以進行互轉的。相對於其他的幾種情況,情況3.2比較好理解,僅僅只是一個顏色的轉變,通過減少右子樹的一個黑色節點使之保持平衡,同時將不平衡點上移至N與W的父節點,然後進行下一輪迭代。情況3.1,是將W旋轉將其轉成情況2、3、4情況進行處理。而情況3.3通過轉變後可以化成情況3.4來進行處理,從這裡可以看出情況3.4應該最終結。情況3.4、右子節點為紅色節點,那麼將缺失的黑色節點交由給右子節點,通過旋轉達到平衡。

通過上面的分析,我們已經初步瞭解了紅黑樹的刪除節點情況,相對於增加節點而言它確實是選的較為複雜。下面我將看到在Java TreeMap中是如何實現紅黑樹刪除的。

TreeMap deleteEntry()方法實現分析

通過上面的分析我們確認刪除節點的步驟是:找到一個替代子節點C來替代P,然後直接刪除C,最後調整這棵紅黑樹。下面程式碼是尋找替代節點、刪除替代節點。

(1)除是尋找替代節點replacement,其實現方法為successor()。如下:

(2)處是刪除該節點過程。它主要分為上面提到的三種情況,它與上面的if…else if… else一一對應 。如下:

1、有兩個兒子。這種情況比較複雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然後刪除子節點C即可。

2、沒有兒子,即為葉結點。直接把父結點的對應兒子指標設為NULL,刪除兒子結點就OK了。

3、只有一個兒子。那麼把父結點的相應兒子指標指向兒子的獨生子,刪除兒子結點也OK了。

刪除完節點後,就要根據情況來對紅黑樹進行復雜的調整:fixAfterDeletion()。

這是紅黑樹在刪除節點後,對樹的平衡性進行調整的過程,其實現過程與上面四種複雜的情況一一對應,所以在這個原始碼的時候一定要對著上面提到的四種情況看。


4.3、HashTable

也是以雜湊表資料結構實現的,解決衝突時與HashMap也一樣也是採用了雜湊連結串列的形式,不過效能比HashMap要低


HashTable詳解

有兩個類都提供了一個多種用途的hashTable機制,他們都可以將可以key和value結合起來構成鍵值對通過put(key,value)方法儲存起來,然後通過get(key)方法獲取相對應的value值。一個是前面提到的HashMap,還有一個就是馬上要講解的HashTable。對於HashTable而言,它在很大程度上和HashMap的實現差不多,如果我們對HashMap比較瞭解的話,對HashTable的認知會提高很大的幫助。他們兩者之間只存在幾點的不同,這個後面會闡述。

一、定義

HashTable在Java中的定義如下:

從中可以看出HashTable繼承Dictionary類,實現Map介面。其中Dictionary類是任何可將鍵對映到相應值的類(如 Hashtable)的抽象父類。每個鍵和每個值都是一個物件。在任何一個 Dictionary 物件中,每個鍵至多與一個值相關聯。Map是"key-value鍵值對"介面。

HashTable採用"拉鍊法"實現雜湊表,它定義了幾個重要的引數:table、count、threshold、loadFactor、modCount。

table:為一個Entry[]陣列型別,Entry代表了“拉鍊”的節點,每一個Entry代表了一個鍵值對,雜湊表的"key-value鍵值對"都是儲存在Entry陣列中的。

count:HashTable的大小,注意這個大小並不是HashTable的容器大小,而是他所包含Entry鍵值對的數量。

threshold:Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值="容量*載入因子"。

loadFactor:載入因子。

modCount:用來實現“fail-fast”機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操作時,若有其他執行緒對其進行結構性的修改,這時迭代器會立馬感知到,並且立即丟擲ConcurrentModificationException異常,而不是等到迭代完成之後才告訴你(你已經出錯了)。

二、構造方法

在HashTabel中存在5個建構函式。通過這5個建構函式我們構建出一個我想要的HashTable。

預設建構函式,容量為11,載入因子為0.75。

用指定初始容量和預設的載入因子 (0.75) 構造一個新的空雜湊表。

用指定初始容量和指定載入因子構造一個新的空雜湊表。其中initHashSeedAsNeeded方法用於初始化hashSeed引數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。這個hashSeed是一個與例項相關的隨機值,主要用於解決hash衝突。

構造一個與給定的 Map 具有相同對映關係的新雜湊表。

三、主要方法

HashTable的API對外提供了許多方法,這些方法能夠很好幫助我們操作HashTable,但是這裡我只介紹兩個最根本的方法:put、get。

首先我們先看put方法:將指定 key 對映到此雜湊表中的指定 value。注意這裡鍵key和值value都不可為空。

put方法的整個處理流程是:計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。如下:

首先我們假設一個容量為5的table,存在8、10、13、16、17、21。他們在table中位置如下:

然後我們插入一個數:put(16,22),key=16在table的索引位置為1,同時在1索引位置有兩個數,程式對該“連結串列”進行迭代,發現存在一個key=16,這時要做的工作就是用newValue=22替換oldValue16,並將oldValue=16返回。

在put(33,33),key=33所在的索引位置為3,並且在該連結串列中也沒有存在某個key=33的節點,所以就將該節點插入該連結串列的第一個位置。

在HashTabled的put方法中有兩個地方需要注意:

1、HashTable的擴容操作,在put方法中,如果需要向table[]中新增Entry元素,會首先進行容量校驗,如果容量已經達到了閥值,HashTable就會進行擴容處理rehash(),如下:

在這個rehash()方法中我們可以看到容量擴大兩倍+1,同時需要將原來HashTable中的元素一一複製到新的HashTable中,這個過程是比較消耗時間的,同時還需要重新計算hashSeed的,畢竟容量已經變了。這裡對閥值囉嗦一下:比如初始值11、載入因子預設0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操作,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操作,一次類推。

下面是計算key的hash值,這裡hashSeed發揮了作用。

相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table陣列中的索引位置,然後迭代連結串列,匹配直到找到相對應key的value,若沒有找到返回null。

四、HashTable與HashMap的區別

HashTable和HashMap存在很多的相同點,但是他們還是有幾個比較重要的不同點。

第一:我們從他們的定義就可以看出他們的不同,HashTable基於Dictionary類,而HashMap是基於AbstractMap。Dictionary是什麼?它是任何可將鍵對映到相應值的類的抽象父類,而AbstractMap是基於Map介面的骨幹實現,它以最大限度地減少實現此介面所需的工作。

第二:HashMap可以允許存在一個為null的key和任意個為null的value,但是HashTable中的key和value都不允許為null。如下:

當HashMap遇到為null的key時,它會呼叫putForNullKey方法來進行處理。對於value沒有進行任何處理,只要是物件都可以。

而當HashTable遇到null時,他會直接丟擲NullPointerException異常資訊。

第三:Hashtable的方法是同步的,而HashMap的方法不是。所以有人一般都建議如果是涉及到多執行緒同步時採用HashTable,沒有涉及就採用HashMap,但是在Collections類中存在一個靜態方法:synchronizedMap(),該方法建立了一個執行緒安全的Map物件,並把它作為一個封裝的物件來返回,所以通過Collections類的synchronizedMap方法是可以同步訪問潛在的HashMap。

五、Queue

佇列,它主要分為兩大類,一類是阻塞式佇列,佇列滿了以後再插入元素則會丟擲異常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一種佇列則是雙端佇列,支援在頭、尾兩端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。

六、異同點

6.1、Vector和ArrayList

1,vector是執行緒同步的,所以它也是執行緒安全的,而arraylist是執行緒非同步的,是不安全的。如果不考慮到執行緒的安全因素,一般用arraylist效率比較高。 2,如果集合中的元素的數目大於目前集合陣列的長度時,vector增長率為目前陣列長度的100%,而arraylist增長率為目前陣列長度的50%.如過在集合中使用資料量比較大的資料,用vector有一定的優勢。 3,如果查詢一個指定位置的資料,vector和arraylist使用的時間是相同的,都是0(1),這個時候使用vector和arraylist都可以。而如果移動一個指定位置的資料花費的時間為0(n-i)n為總長度,這個時候就應該考慮到使用linklist,因為它移動一個指定位置的資料所花費的時間為0(1),而查詢一個指定位置的資料時花費的時間為0(i)。

ArrayList 和Vector是採用陣列方式儲存資料,此陣列元素數大於實際儲存的資料以便增加和插入元素,都允許直接序號索引元素,但是插入資料要設計到陣列元素移動等記憶體操作,所以索引資料快插入資料慢,Vector由於使用了synchronized方法(執行緒安全)所以效能上比ArrayList要差,LinkedList使用雙向連結串列實現儲存,按序號索引資料需要進行向前或向後遍歷,但是插入資料時只需要記錄本項的前後項即可,所以插入數度較快!

6.2、Aarraylist和Linkedlist

1.ArrayList是實現了基於動態陣列的資料結構,LinkedList基於連結串列的資料結構。 2.對於隨機訪問get和set,ArrayList覺得優於LinkedList,因為LinkedList要移動指標。 3.對於新增和刪除操作add和remove,LinedList比較佔優勢,因為ArrayList要移動資料。 這一點要看實際情況的。若只對單條資料插入或刪除,ArrayList的速度反而優於LinkedList。但若是批量隨機的插入刪除資料,LinkedList的速度大大優於ArrayList. 因為ArrayList每插入一條資料,要移動插入點及之後的所有資料。

6.3、HashMap與TreeMap

1、HashMap通過hashcode對其內容進行快速查詢,而TreeMap中所有的元素都保持著某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap(HashMap中元素的排列順序是不固定的)。HashMap中元素的排列順序是不固定的)。

2、 HashMap通過hashcode對其內容進行快速查詢,而TreeMap中所有的元素都保持著某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap(HashMap中元素的排列順序是不固定的)。集合框架”提供兩種常規的Map實現:HashMap和TreeMap (TreeMap實現SortedMap介面)。

3、在Map 中插入、刪除和定位元素,HashMap 是最好的選擇。但如果您要按自然順序或自定義順序遍歷鍵,那麼TreeMap會更好。使用HashMap要求新增的鍵類明確定義了hashCode()和 equals()的實現。 這個TreeMap沒有調優選項,因為該樹總處於平衡狀態。

6.4、hashtable與hashmap

1、歷史原因:Hashtable是基於陳舊的Dictionary類的,HashMap是Java 1.2引進的Map介面的一個實現 。

2、同步性:Hashtable是執行緒安全的,也就是說是同步的,而HashMap是執行緒序不安全的,不是同步的 。

3、值:只有HashMap可以讓你將空值作為一個表的條目的key或value 。

七、對集合的選擇

7.1、對List的選擇

1、對於隨機查詢與迭代遍歷操作,陣列比所有的容器都要快。所以在隨機訪問中一般使用ArrayList

2、LinkedList使用雙向連結串列對元素的增加和刪除提供了非常好的支援,而ArrayList執行增加和刪除元素需要進行元素位移。

3、對於Vector而已,我們一般都是避免使用。

4、將ArrayList當做首選,畢竟對於集合元素而已我們都是進行遍歷,只有當程式的效能因為List的頻繁插入和刪除而降低時,再考慮LinkedList。

7.2、對Set的選擇

1、HashSet由於使用HashCode實現,所以在某種程度上來說它的效能永遠比TreeSet要好,尤其是進行增加和查詢操作。

3、雖然TreeSet沒有HashSet效能好,但是由於它可以維持元素的排序,所以它還是存在用武之地的。

7.3、對Map的選擇

1、HashMap與HashSet同樣,支援快速查詢。雖然HashTable速度的速度也不慢,但是在HashMap面前還是稍微慢了些,所以HashMap在查詢方面可以取代HashTable。

2、由於TreeMap需要維持內部元素的順序,所以它通常要比HashMap和HashTable慢。


Map集合總結

一、Map概述

首先先看Map的結構示意圖

Map:“鍵值”對對映的抽象介面。該對映不包括重複的鍵,一個鍵對應一個值。

SortedMap:有序的鍵值對介面,繼承Map介面。

NavigableMap:繼承SortedMap,具有了針對給定搜尋目標返回最接近匹配項的導航方法的介面。

AbstractMap:實現了Map中的絕大部分函式介面。它減少了“Map的實現類”的重複編碼。

Dictionary:任何可將鍵對映到相應值的類的抽象父類。目前被Map介面取代。

TreeMap:有序散列表,實現SortedMap 介面,底層通過紅黑樹實現。

HashMap:是基於“拉鍊法”實現的散列表。底層採用“陣列+連結串列”實現。

WeakHashMap:基於“拉鍊法”實現的散列表。

HashTable:基於“拉鍊法”實現的散列表。

總結如下:

他們之間的區別:

二、內部雜湊: 雜湊對映技術

幾乎所有通用Map都使用雜湊對映技術。對於我們程式設計師來說我們必須要對其有所瞭解。

雜湊對映技術是一種就元素對映到陣列的非常簡單的技術。由於雜湊對映採用的是陣列結果,那麼必然存在一中用於確定任意鍵訪問陣列的索引機制,該機制能夠提供一個小於陣列大小的整數,我們將該機制稱之為雜湊函式。在Java中我們不必為尋找這樣的整數而大傷腦筋,因為每個物件都必定存在一個返回整數值的hashCode方法,而我們需要做的就是將其轉換為整數,然後再將該值除以陣列大小取餘即可。如下:

下面是HashMap、HashTable的:

位置的索引就代表了該節點在陣列中的位置。下圖是雜湊對映的基本原理圖:

在該圖中1-4步驟是找到該元素在陣列中位置,5-8步驟是將該元素插入陣列中。在插入的過程中會遇到一點點小挫折。在眾多肯能存在多個元素他們的hash值是一樣的,這樣就會得到相同的索引位置,也就說多個元素會對映到相同的位置,這個過程我們稱之為“衝突”。解決衝突的辦法就是在索引位置處插入一個連結列表,並簡單地將元素新增到此連結列表。當然也不是簡單的插入,在HashMap中的處理過程如下:獲取索引位置的連結串列,如果該連結串列為null,則將該元素直接插入,否則通過比較是否存在與該key相同的key,若存在則覆蓋原來key的value並返回舊值,否則將該元素儲存在鏈頭(最先儲存的元素放在鏈尾)。下面是HashMap的put方法,該方法詳細展示了計算索引位置,將元素插入到適當的位置的全部過程:

HashMap的put方法展示了雜湊對映的基本思想,其實如果我們檢視其它的Map,發現其原理都差不多!

三、Map優化

首先我們這樣假設,假設雜湊對映的內部陣列的大小隻有1,所有的元素都將對映該位置(0),從而構成一條較長的連結串列。由於我們更新、訪問都要對這條連結串列進行線性搜尋,這樣勢必會降低效率。我們假設,如果存在一個非常大陣列,每個位置連結串列處都只有一個元素,在進行訪問時計算其 index 值就會獲得該物件,這樣做雖然會提高我們搜尋的效率,但是它浪費了控制元件。誠然,雖然這兩種方式都是極端的,但是它給我們提供了一種優化思路:使用一個較大的陣列讓元素能夠均勻分佈。在Map有兩個會影響到其效率,一是容器的初始化大小、二是負載因子。

3.1、調整實現大小

在雜湊對映表中,內部陣列中的每個位置稱作“儲存桶”(bucket),而可用的儲存桶數(即內部陣列的大小)稱作容量 (capacity),我們為了使Map物件能夠有效地處理任意數的元素,將Map設計成可以調整自身的大小。我們知道當Map中的元素達到一定量的時候就會調整容器自身的大小,但是這個調整大小的過程其開銷是非常大的。調整大小需要將原來所有的元素插入到新陣列中。我們知道index = hash(key) % length。這樣可能會導致原先衝突的鍵不在衝突,不衝突的鍵現在衝突的,重新計算、調整、插入的過程開銷是非常大的,效率也比較低下。所以,如果我們開始知道Map的預期大小值,將Map調整的足夠大,則可以大大減少甚至不需要重新調整大小,這很有可能會提高速度。下面是HashMap調整容器大小的過程,通過下面的程式碼我們可以看到其擴容過程的複雜性:

3.2、負載因子

為了確認何時需要調整Map容器,Map使用了一個額外的引數並且粗略計算儲存容器的密度。在Map調整大小之前,使用”負載因子”來指示Map將會承擔的“負載量”,也就是它的負載程度,當容器中元素的數量達到了這個“負載量”,則Map將會進行擴容操作。負載因子、容量、Map大小之間的關係如下:負載因子 * 容量 > map大小 ----->調整Map大小。

例如:如果負載因子大小為0.75(HashMap的預設值),預設容量為11,則 11 * 0.75 = 8.25 = 8,所以當我們容器中插入第八個元素的時候,Map就會調整大小。

負載因子本身就是在控制元件和時間之間的折衷。當我使用較小的負載因子時,雖然降低了衝突的可能性,使得單個連結串列的長度減小了,加快了訪問和更新的速度,但是它佔用了更多的控制元件,使得陣列中的大部分控制元件沒有得到利用,元素分佈比較稀疏,同時由於Map頻繁的調整大小,可能會降低效能。但是如果負載因子過大,會使得元素分佈比較緊湊,導致產生衝突的可能性加大,從而訪問、更新速度較慢。所以我們一般推薦不更改負載因子的值,採用預設值0.75.