簡介:請允許我當一回標題黨。好了,言歸正傳,本篇主要內容便是介紹HashMap的男二號——TreeNode(男一號還是給Node吧,畢竟是TreeNode的爺爺,而且普通節點一般來說也比TreeNode要多),本篇主要從以下幾個方面介紹:1. ...
請允許我當一回標題黨。 好了,言歸正傳,本篇主要內容便是介紹HashMap的男二號——TreeNode(男一號還是給Node吧,畢竟是TreeNode的爺爺,而且普通節點一般來說也比TreeNode要多),本篇主要從以下幾個方面介紹: 1.紅黑樹介紹 2.TreeNode結構 3.樹化的過程 4.紅黑樹的左旋和右旋 5.TreeNode的左旋和右旋 6.紅黑樹的插入 7.TreeNode的插入 8.紅黑樹的刪除 9.TreeNode的刪除 講解紅黑樹的部分算是理論部分,講解TreeNode的部分則是代碼實踐部分,配合服用效果更加。 保守估計,仔細食用本篇大約需要半小時,請各位細細品嘗。 紅黑樹介紹 什么是紅黑樹?嗯,首先,它是一顆樹,所謂的樹,便是長的像這樣的東西 不像樹?emmmm,你把它想象成一顆倒過來的樹就好了,A~H都是樹的節點,每個節點有零個或者多個子節點,或者說多個孩子,但除了根節點以外,每個節點都只有一個父節點,也稱只有一個父親(老王嘿嘿一笑)。 最上面的A是根節點,最下面的D、H、F、G是葉子節點。每一個非根節點有且只有一個父節點;樹是具有一層一層的層次結構,這里A位於第一層,B、C位於第二層,依次類推。將左邊的B節點部分(包括BDEH)拿出來,則又是一顆樹,稱為樹的子樹。 好了,知道樹是什么東西了,那么紅黑樹是什么樣的呢? 紅黑樹,本質上來說是一顆二叉搜索樹。嗯,還是先說說這個二叉搜索樹吧。二叉代表它的節點最多有兩個子節點,而且左右有順序,不能顛倒,分別叫左孩子和右孩子,這兩個節點互為兄弟節點,嗯,其實叫法根現實里的叫法差不多,以下圖為例,4、9互為兄弟,7是他們的父親,9是2的叔叔,8是2的堂兄弟。,很簡單吧。說完了稱謂,再來說說用途,既然叫做搜索樹表示它的用途是為了更快的搜索和查找而設計的,所以這棵樹本身滿足一定的排序規則,即樹中的任何節點的值大於它的左孩子,且小於它的右孩子。 任意節點的左、右子樹也分別為二叉查找樹。嗯,結合下圖意會一下: 而紅黑樹,就跟它的名字一樣,又紅又黑,紅黑並進,理實交融,節點是非紅即黑的,看起來就像這樣 紅黑樹的主要特性: (1)每個節點要么是黑色,要么是紅色。(節點非黑即紅) (2)根節點是黑色。 (3)每個葉子節點(NIL)是黑色。 (4)如果一個節點是紅色的,則它的子節點必須是黑色的。(也就是說父子節點不能同時為紅色) (5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。(這一點是平衡的關鍵) 說簡單也簡單,其實就是一顆比較平衡的又紅又黑的二叉樹嘛。 TreeNode結構 既然我們已經知道紅黑樹長什么樣了,那么我們再來看看HashMap中的TreeNode代碼里是如何表示的: TreeNode繼承自LinkedHashMap中的內部類——LinkedHashMap.Entry,而這個內部類又繼承自Node,所以算是Node的孫子輩了。我們再來看看它的幾個屬性,parent用來指向它的父節點,left指向左孩子,right指向右孩子,prev則指向前一個節點(原鏈表中的前一個節點),注意,這些字段跟Entry,Node中的字段一樣,是使用默認訪問權限的,所以子類可以直接使用父類的屬性。 樹化的過程 在前幾篇中已經有所介紹,當HashMap桶中的元素個數超過一定數量時,就會樹化,也就是將鏈表轉化為紅黑樹的結構。 從代碼中可以看到,在treeifyBin函數中,先將所有節點替換為TreeNode,然后再將單鏈表轉為雙鏈表,方便之后的遍歷和移動操作。而最終的操作,實際上是調用TreeNode的方法treeify進行的。 final void treeify(Node<K,V>[] tab) { //樹的根節點 TreeNode<K,V> root = null; //x是當前節點,next是后繼 for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; //如果根節點為null,把當前節點設置為根節點 if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; //這里循環遍歷,進行二叉搜索樹的插入 for (TreeNode<K,V> p = root;;) { //p指向遍歷中的當前節點,x為待插入節點,k是x的key,h是x的hash值,ph是p的hash值,dir用來指示x節點與p的比較,-1表示比p小,1表示比p大,不存在相等情況,因為HashMap中是不存在兩個key完全一致的情況。 int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; //如果hash值相等,那么判斷k是否實現了comparable接口,如果實現了comparable接口就使用compareTo進行進行比較,如果仍舊相等或者沒有實現comparable接口,則在tieBreakOrder中比較 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; //進行插入平衡處理 root = balanceInsertion(root, x); break; } } } } //確保給定節點是桶中的第一個元素 moveRootToFront(tab, root); } //這里不是為了整體排序,而是為了在插入中保持一致的順序 static int tieBreakOrder(Object a, Object b) { int d; //用兩者的類名進行比較,如果相同則使用對象默認的hashcode進行比較 if (a == null || b == null || (d = a.getClass().getName(). compareTo(b.getClass().getName())) == 0) d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1); return d; } 這里的邏輯其實不復雜,僅僅是循環遍歷當前樹,然后找到可以該節點可以插入的位置,依次和遍歷節點比較,比它大則跟其右孩子比較,小則與其左孩子比較,依次遍歷,直到找到左孩子或者右孩子為null的位置進行插入。 真正復雜一點的地方在於balanceInsertion函數,這個函數中,將紅黑樹進行插入平衡處理,保證插入節點后仍保持紅黑樹的性質。這個函數稍后在TreeNode的插入中進行介紹,這里先看看moveRootToFront,這個函數是將root節點移動到桶中的第一個元素,也就是鏈表的首節點,這樣做是因為在判斷桶中元素類型的時候會對鏈表進行遍歷,將根節點移動到鏈表前端可以確保類型判斷時不會出現錯誤。 /** * 把給定節點設為桶中的第一個元素 */ static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { int n; if (root != null && tab != null && (n = tab.length) > 0) { int index = (n - 1) & root.hash; //first指向鏈表第一個節點 TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; if (root != first) { //如果root不是第一個節點,則將root與第一個節點位置互換 Node<K,V> rn; tab[index] = root; TreeNode<K,V> rp = root.prev; if ((rn = root.next) != null) ((TreeNode<K,V>)rn).prev = rp; if (rp != null) rp.next = rn; if (first != null) first.prev = root; root.next = first; root.prev = null; } //這里是防御性編程,校驗更改后的結構是否滿足紅黑樹和雙鏈表的特性 //因為HashMap並沒有做並發安全處理,可能在並發場景中意外破壞了結構 assert checkInvariants(root); } } 紅黑樹的左旋和右旋 左旋和右旋,顧名思義嘛,就是將節點以某個節點為中心向左或者向右進行旋轉操作以保持二叉樹的平衡,讓我們看圖說話 圖畫的有點大。將就着看一下吧,左旋和右旋相當於以要旋轉的節點為中心,將子樹(以該節點的父節點為根的子樹)整體向左旋轉,該節點變成子樹的根節點,原來的根節點變成了左孩子,如果該節點原來有左孩子,則將其變為該節點左孩子的右孩子。說起來好像有點繞,可以聯系圖進行形象化的理解,當節點C向左旋轉之后,它的左孩子D可以理解為因為重力作用掉到A的右孩子位置,嗯,就是這樣。右旋也是類似理解即可。 TreeNode的左旋和右旋 了解了左旋和右旋,讓我們看看代碼里是怎樣實現的: /** * 左旋 */ static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { //這里的p即上圖的A節點,r指向右孩子即C,rl指向右孩子的左孩子即D,pp為p的父節點 TreeNode<K,V> r, pp, rl; if (p != null && (r = p.right) != null) { if ((rl = p.right = r.left) != null) rl.parent = p; //將p的父節點的孩子節點指向r if ((pp = r.parent = p.parent) == null) (root = r).red = false; else if (pp.left == p) pp.left = r; else pp.right = r; //將p置為r的左節點 r.left = p; p.parent = r; } return root; } /** * 右旋 */ static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) { //這里的p即上圖的A節點,l指向左孩子即C,lr指向左孩子的右孩子即E,pp為p的父節點 TreeNode<K,V> l, pp, lr; if (p != null && (l = p.left) != null) { if ((lr = p.left = l.right) != null) lr.parent = p; if ((pp = l.parent = p.parent) == null) (root = l).red = false; else if (pp.right == p) pp.right = l; else pp.left = l; l.right = p; p.parent = l; } return root; } 其實,也很簡單嘛。23333 紅黑樹的插入 現在來看看一個比較麻煩一點的操作,紅黑樹的插入,首先找到這個節點要插入的位置,即一層一層比較,大的放右邊,小的放左邊,直到找到為null的節點放入即可,但是如何在插入的過程保持紅黑樹的特性呢,想想好像比較頭疼,但是再仔細想想其實就會發現,其實只有這么幾種情況: 1.插入的為根節點,則直接把顏色改成黑色即可。 2.插入的節點的父節點是黑色節點,則不需要調整,因為插入的節點會初始化為紅色節點,紅色節點是不會影響樹的平衡的。 3.插入的節點的祖父節點為null,即插入的節點的父節點是根節點,直接插入即可(因為根節點肯定是黑色)。 4.插入的節點父節點和祖父節點都存在,並且其父節點是祖父節點的左節點。這種情況稍微麻煩一點,又分兩種子情況: i.插入節點的叔叔節點是紅色,則將父親節點和叔叔節點都改成黑色,然后祖父節點改成紅色即可。 ii.插入節點的叔叔節點是黑色或不存在: a.若插入節點是其父節點的右孩子,則將其父節點左旋, b.若為左孩子,則將其父節點變成黑色節點,將其祖父節點變成紅色節點,然后將其祖父節點右旋。 5.插入的節點父節點和祖父節點都存在,並且其父節點是祖父節點的右節點。這種情況跟上面是類似的,分兩種子情況: i.插入節點的叔叔節點是紅色,則將父親節點和叔叔節點都改成黑色,然后祖父節點改成紅色即可。 ii.插入節點的叔叔節點是黑色或不存在: a.若插入節點是其父節點的左孩子,則將其父節點右旋 b.若為右孩子,則將其父節點變成黑色節點,將其祖父節點變成紅色節點,然后將其祖父節點左旋。 然后重復進行上述操作,直到變成1或2情況時則結束變換。說半天,可能還是雲里霧里,一圖勝千言,讓我們從無到有構建一顆紅黑樹,假設插入的順序為:10,5,9,3,6,7,19,32,24,17(數字是我拍腦袋瞎想的。) 先來插個10,為情景1,直接改成黑色即可,再插入5,為情景2,比10小,放到10的左孩子位置,插入9,比10小,但是比5大,放到5的右孩子位置,此時,為情景4iia,左旋后變成了情景4iib,變色右旋即可完成轉化。插入3后為情景4i,將父節點和叔叔節點同時變色即可,插入6不需要調整,插入7后為情景5i,變色即可。插入19不需要調整,插入32,變成了5iib,左旋變色即可,插入24,變成5iia,右旋后變成5i,變色即可,最后插入17,完美。 看圖說話是不是就簡單明了了,看在我畫圖這么辛苦的份上,點個關注給個贊可好(滑稽)。 TreeNode的插入 了解了紅黑樹的刪除之后,我們再來看下TreeNode中是怎樣用代碼實現的: static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { x.red = true; for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { //情景1:父節點為null if ((xp = x.parent) == null) { x.red = false; return x; } //情景2,3:父節點是黑色節點或者祖父節點為null else if (!xp.red || (xpp = xp.parent) == null) return root; //情景4:插入的節點父節點和祖父節點都存在,並且其父節點是祖父節點的左節點 if (xp == (xppl = xpp.left)) { //情景4i:插入節點的叔叔節點是紅色 if ((xppr = xpp.right) != null && xppr.red) { xppr.red = false; xp.red = false; xpp.red = true; x = xpp; } //情景4ii:插入節點的叔叔節點是黑色或不存在 else { //情景4iia:插入節點是其父節點的右孩子 if (x == xp.right) { root = rotateLeft(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } //情景4iib:插入節點是其父節點的左孩子 if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateRight(root, xpp); } } } } //情景5:插入的節點父節點和祖父節點都存在,並且其父節點是祖父節點的右節點 else { //情景5i:插入節點的叔叔節點是紅色 if (xppl != null && xppl.red) { xppl.red = false; xp.red = false; xpp.red = true; x = xpp; } //情景5ii:插入節點的叔叔節點是黑色或不存在 else {· //情景5iia:插入節點是其父節點的左孩子 if (x == xp.left) { root = rotateRight(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } //情景5iib:插入節點是其父節點的右孩子 if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateLeft(root, xpp); } } } } } } 其實就是一毛一樣的,對號入座即可。 紅黑樹的刪除 講完插入,接下來我們來說說刪除,刪除的話,比刪除還要復雜一點,請各位看官先深呼吸,做好閱讀准備。 之前已經說過,紅黑樹是一顆特殊的二叉搜索樹,所以進行刪除操作時,其實是先進行二叉搜索樹的刪除,然后再進行調整。所以,其實這里分為兩部分內容:1.二叉搜索樹的刪除,2.紅黑樹的刪除調整。 二叉搜索樹的刪除主要有這么幾種情景: 情景1:待刪除的節點無左右孩子。 情景2:待刪除的節點只有左孩子或者右孩子。 情景3:待刪除的節點既有左孩子又有右孩子。 對於情景1,直接刪除即可,情景2,則直接把該節點的父節點指向它的左孩子或者右孩子即可,情景3稍微復雜一點,需要先找到其右子樹的最左孩子(或者左子樹的最右孩子),即左(右)子樹中序遍歷時的第一個節點,然后將其與待刪除的節點互換,最后再刪除該節點(如果有右子樹,則右子樹上位)。總之,就是先找到它的替代者,找到之后替換這個要刪除的節點,然后再把這個節點真正刪除掉。 其實二叉搜索樹的刪除總體來說還是比較簡單的,刪除完之后,如果替代者是紅色節點,則不需要調整,如果是黑色節點,則會導致左子樹和右子樹路徑中黑色節點數量不一致,需要進行紅黑樹的調整,跟上面一樣,替代節點為其父節點的左孩子與右孩子的情況類似,所以這里只說其為左孩子的情景(PS:上一步的尋找替換節點使用的是右子樹的最左節點,所以該節點如果有孩子,只能是右孩子): 情景1:只有右孩子且為紅色,直接用右孩子替換該節點然后變成黑色即可。 (D代表替代節點,即要被刪除的節點,之前在經過二叉搜索樹的刪除后,D節點其實已經被刪除了,這里為了方便理解這個變化過程,所以把這個節點也畫出來了,所以當前的初始狀態是待刪除節點與其替換節點互換位置與顏色之后的狀態) 情景2:只有右孩子且為黑色,那么刪除該節點會導致父節點的左子樹路徑上黑色節點減一,此時只能去借助右子樹,從右子樹中借一個紅色節點過來即可,具體取決於右子樹的情況,這里又分成兩種: i.兄弟節點是紅色,則此時父節點是黑色,且兄弟節點肯定有兩個孩子,且兄弟節點的左右子樹路徑上均有兩個黑色節點,此時只需將兄弟節點與父節點顏色互換,然后將父節點左旋,左旋后,兄弟節點的左子樹SL掛到了父節點p的右孩子位置,這時會導致p的右子樹路徑上的黑色節點比左子樹多一,此時再SL置為紅色即可。 ii.兄弟節點是黑色,那么就只能打它孩子的主意了,這里主要關注遠侄子(兄弟節點的右孩子,即SR)的顏色情況,這里分成兩種情況: a.遠侄子SR是黑色,近侄子任意(白色代表顏色可為任意顏色),則先將S轉為紅色,然后右旋,再將SL換成P節點顏色,P塗成黑色,S也塗成黑色,再進行左旋即可。其實簡單說就是SL上位,替換父節點位置。 b.遠侄子SR為紅色,近侄子任意(該子樹路徑中有且僅有一個黑色節點),則先將兄弟節點與父節點顏色互換,將SR塗成黑色,再將父節點左旋即可。 emmmm...好像也不是很麻煩嘛(逃)。 TreeNode的刪除節點 TreeNode刪除節點其實也是兩步走,先進行二叉搜索樹的刪除,然后再進行紅黑樹的調整,跟之前的情況分析是一致的。 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { ...... //p是待刪除節點,replacement用於后續的紅黑樹調整,指向的是p或者p的繼承者。 //如果p是葉子節點,p==replacement,否則replacement為p的右子樹中最左節點 if (replacement != p) { //若p不是葉子節點,則讓replacement的父節點指向p的父節點 TreeNode<K,V> pp = replacement.parent = p.parent; if (pp == null) root = replacement; else if (p == pp.left) pp.left = replacement; else pp.right = replacement; p.left = p.right = p.parent = null; } //若待刪除的節點p時紅色的,則樹平衡未被破壞,無需進行調整。 //否則刪除節點后需要進行調整 TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement); //p為葉子節點,則直接將p從樹中清除 if (replacement == p) { // detach TreeNode<K,V> pp = p.parent; p.parent = null; if (pp != null) { if (p == pp.left) pp.left = null; else if (p == pp.right) pp.right = null; } }}麻煩的地方就在刪除節點后的調整了,所有邏輯都在balanceDeletion函數里,兩個參數分別表示根節點和刪除節點的繼承者,來看看它的具體實現: static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) { for (TreeNode<K,V> xp, xpl, xpr;;) { //x為空或x為根節點,直接返回 if (x == null || x == root) return root; //x為根節點,染成黑色,直接返回(因為調整過后,root並不一定指向刪除操作過后的根節點,如果之前刪除的是root節點,則x將成為新的根節點) else if ((xp = x.parent) == null) { x.red = false; return x; } //如果x為紅色,則無需調整,返回 else if (x.red) { x.red = false; return root; } //x為其父節點的左孩子 else if ((xpl = xp.left) == x) { //如果它有紅色的兄弟節點xpr,那么它的父親節點xp一定是黑色節點 if ((xpr = xp.right) != null && xpr.red) { xpr.red = false; xp.red = true; //對父節點xp做左旋轉 root = rotateLeft(root, xp); //重新將xp指向x的父節點,xpr指向xp新的右孩子 xpr = (xp = x.parent) == null ? null : xp.right; } //如果xpr為空,則向上繼續調整,將x的父節點xp作為新的x繼續循環 if (xpr == null) x = xp; else { //sl和sr分別為其近侄子和遠侄子 TreeNode<K,V> sl = xpr.left, sr = xpr.right; if ((sr == null || !sr.red) && (sl == null || !sl.red)) { xpr.red = true; //若sl和sr都為黑色或者不存在,即xpr沒有紅色孩子,則將xpr染紅 x = xp; //本輪結束,繼續向上循環 } else { //否則的話,就需要進一步調整 if (sr == null || !sr.red) { if (sl != null) //若左孩子為紅,右孩子不存在或為黑 sl.red = false; //左孩子染黑 xpr.red = true; //將xpr染紅 root = rotateRight(root, xpr); //右旋 xpr = (xp = x.parent) == null ? null : xp.right; //右旋后,xpr指向xp的新右孩子,即上一步中的sl } if (xpr != null) { xpr.red = (xp == null) ? false : xp.red; //xpr染成跟父節點一致的顏色,為后面父節點xp的左旋做准備 if ((sr = xpr.right) != null) sr.red = false; //xpr新的右孩子染黑,防止出現兩個紅色相連 } if (xp != null) { xp.red = false; //將xp染黑,並對其左旋,這樣就能保證被刪除的X所在的路徑又多了一個黑色節點,從而達到恢復平衡的目的 root = rotateLeft(root, xp); } //到此調整已經完畢,進入下一次循環后將直接退出 x = root; } } } //x為其父節點的右孩子,跟上面類似 else { // symmetric if (xpl != null && xpl.red) { xpl.red = false; xp.red = true; root = rotateRight(root, xp); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl == null) x = xp; else { TreeNode<K,V> sl = xpl.left, sr = xpl.right; if ((sl == null || !sl.red) && (sr == null || !sr.red)) { xpl.red = true; x = xp; } else { if (sl == null || !sl.red) { if (sr != null) sr.red = false; xpl.red = true; root = rotateLeft(root, xpl); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl != null) { xpl.red = (xp == null) ? false : xp.red; if ((sl = xpl.left) != null) sl.red = false; } if (xp != null) { xp.red = false; root = rotateRight(root, xp); } x = root; } } } }} 呼。。。終於。。醞釀了好多天的一篇文章總算是寫完了,為了盡量確認轉換的准確性,找了很多資料進行參考,過程中花了不少時間,曾多次准備放棄。。。不過總算是沒有死在娘胎里,也算是完成了一樁心事,開心。 之后還會繼續更新,歡迎大家繼續關注。也歡迎大家前來打臉。 |