算力 | 手寫紅黑樹

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹對於大多數人來說,似乎都是一場噩夢。當然這裏並不討論紅黑樹在實際開發過程中究竟能否被運用上,但至少,不懂紅黑樹始終是你在數據結構上的一塊短板。大部分情況下,我在interview候選人時,提問並不會太刁鑽,重點是考察候選人分析問題與解決問題的實際能力,當然,如果候選人對算法比較自信時,我便會考察ConcurrentHashMap的相關實現,畢竟ConcurrentHashMap中涉及到的相關數據結構&算法(比如:散列表、鏈表、二叉排序樹、紅黑樹),線程安全(比如:Java8之前的分段鎖,之後的Sync+CAS)等,足以探測出候選人的大致算力。本文,我會","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"側重於紅黑樹的具體實現過程而非概念","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"前世 | 二叉查找樹(BS-Tree)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在正式講解紅黑樹之前,我們有必要首先回顧下二叉查找樹,因爲從本質上來講,","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"紅黑樹演變自二叉查找樹,只是相對而言,前者引入了一些特定的平衡規則,以便於降低時間複雜度","attrs":{}},{"type":"text","text":"。我們都知道,二叉查找樹CRD的平均時間複雜度是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(logn)","attrs":{}}],"attrs":{}},{"type":"text","text":",但在最壞情況下,CRD時間複雜度將會升級爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(n)","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就查找算法而言,性能相對較好的當屬二分查找(時間複雜度爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(log2n)","attrs":{}}],"attrs":{}},{"type":"text","text":"),但二分查找算法在實際使用過程中卻存在一定的侷限,並不太適用於數據頻繁發生變動的場景,因此,在這種情況下,二叉查找樹或許是一個不錯的替代方案。相對於普通的二叉樹而言,二叉查找樹是一種相對特殊的二叉樹,因爲節點會根據值的大小有序分佈在根節點的左/右兩邊,也就是說,小於根節點值的節點會被分佈在左邊,反之右邊,並且根節點的左/右子樹同樣也是一顆二叉查找樹,如圖1所示。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/40/40b7eed679c8fa0f4217ebc903748670.png","alt":null,"title":"圖1 二叉排序樹","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"二叉排序樹的整體實現並不複雜,僅節點的刪除操作略顯繁瑣。接下來,我先從節點的插入操作開始講起。二叉排序樹的特點相信大家已經非常明確了,小於根節點值的節點會被分佈在其左邊,反之右邊;也就是說,當根節點不爲空時,待插入節點首先需要和根節點進行比較,如果小於根節點的值,就同其左子樹按照相同的規則繼續比較,直至最終找到葉子節點後,再將待插入節點作爲新的葉子節點掛靠在原葉子節點的左邊或右邊即可,示例1-1。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void insert(int key, T value) {\n var nn = new Node<>(key, value);\n if (Objects.isNull(root)) {\n root = nn;\n } else {\n insert(root, root.parent, nn);\n }\n}\n\nvoid insert(Node n1, Node n2, Node n3) {\n if (Objects.isNull(n1)) {\n if (n3.key < n2.key) {\n n2.left = n3;\n } else {\n n2.right = n3;\n }\n n3.parent = n2;\n } else {\n insert(n3.key < n1.key ? n1.left : n1.right, n1, n3);\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以圖1中的數據分佈爲基礎,如果新增一個節點11,那麼目標節點將會作爲節點40的左子樹,如圖2所示。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ec/ecf442f56fb64ab4ed9ab8e36bb13209.png","alt":null,"title":"圖2 執行插入操作後的數據分佈","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"節點的遍歷操作,通常情況下有前序、中序,以及後序等3種方式。由於二叉排序樹中的數據分佈相對有序,因此,如果希望數據按照升序的方式執行輸出,則可以採用中序遍歷,示例1-2。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void inOrder() {\n inOrder(root);\n}\n\nvoid inOrder(Node n) {\n if (Objects.nonNull(n)) {\n inOrder(n.left);\n System.out.println(n);\n inOrder(n.right);\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在講解二叉排序樹的刪除操作之前,我們首先需要實現目標節點查找和後繼節點查找等2項查找操作。目標節點的查找,首先需要拿比較值與根節點進行匹配,如果值相等就返回,反之根據二叉排序樹的數據分佈規則繼續與左/右子樹依次匹配,直至最終值相等時才返回目標節點,示例1-3。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"Node getNode(int key) {\n if (Objects.nonNull(root)) {\n return root.key == key ? root : getNode(key, root);\n }\n return null;\n}\n\nNode getNode(int key, Node n) {\n if (Objects.nonNull(n)) {\n if (key == n.key) {\n return n;\n }\n return getNode(key, key < n.key ? n.left : n.right);\n }\n return null;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"所謂後繼節點,實質上指的就是同時存在左/右子樹的待刪除節點的繼承節點","attrs":{}},{"type":"text","text":"。之所以說二叉排序樹的刪除操作略顯繁瑣,是因爲我們需要考慮4種可能存在的刪除情況,如下所示:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"待刪除節點並不存在任何子樹;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"待刪除節點僅存在左子樹;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"待刪除節點僅存在右子樹;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"待刪除節點同時存在左/右子樹。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當待刪除節點並不存在任何子樹時,我們直接刪除目標節點即可;如果待刪除節點僅存在左子樹的情況下,則需要將其parent.left或parent.right指向待刪除節點的左子樹,存在右子樹的情況下執行相反操作;如果待刪除節點同時存在左/右子樹時,就需要先找到後繼節點,然後將待刪除節點的parent.left或parent.right指向後繼節點,並由後繼節點繼承待刪除節點的左/右子樹。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於後繼節點的查找方式,通常有2種,如下所示:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"左子樹的最右節點;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"右子樹的最左節點。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文選擇以“左子樹的最右節點”爲後繼節點的查找方式,示例1-4。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"Node getSucceedNode(int key) {\n var n = getNode(key);\n if (Objects.nonNull(n)) {\n return getSucceedNode(n.left);//如果左子樹不存在任何右節點,後繼節點就是左子樹\n }\n return null;\n}\n\nNode getSucceedNode(Node n) {\n if (Objects.nonNull(n)) {\n var sn = getSucceedNode(n.right);\n return Objects.nonNull(sn) ? sn : n;\n }\n return null;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以圖2爲例,如果待刪除節點爲100,那麼其後繼節點就是60;而如果待刪除節點爲150,其後繼節點就是120,如圖3所示。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4a/4a485b50ce99398c27b6514f6de48c04.png","alt":null,"title":"圖3 尋找後繼節點","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"二叉排序樹的節點刪除操作,示例1-5。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"boolean delete(int key) {\n var n = getNode(key);\n if (Objects.nonNull(n)) {\n delete(n);\n return true;\n }\n return false;\n}\n\nvoid delete(Node n) {\n if (Objects.nonNull(n.left) && Objects.nonNull(n.right)) {\n var sn = getSucceedNode(n.key);\n n.key = sn.key;\n n.value = sn.value;\n n.left.parent = n;\n n.right.parent = n;\n delete(sn);//遞歸刪除後繼節點\n return;\n } else {\n if (Objects.isNull(n.parent)) {\n root = Objects.nonNull(n.left) ? n.left : n.right;\n root.parent = null;\n } else {\n var c = Objects.nonNull(n.left) ? n.left : n.right;\n if (n.parent.left == n) {\n n.parent.left = c;\n } else {\n n.parent.right = c;\n }\n if (Objects.nonNull(c)) {\n c.parent = n.parent;\n }\n }\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,二叉排序樹的基本實現就完成了,關於求最大值、最小值、節點數、樹高等相關實現由於篇幅有限,本文就不再過多進行講解。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"今生 | 紅黑樹(RB-Tree)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在本文的開篇我曾提及過,二叉查找樹CRD的平均時間複雜度是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(logn)","attrs":{}}],"attrs":{}},{"type":"text","text":",但在最壞情況下,CRD時間複雜度會降爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(n)","attrs":{}}],"attrs":{}},{"type":"text","text":"。這是因爲","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"二叉查找樹的時間複雜度與節點的插入順序存在直接關係","attrs":{}},{"type":"text","text":",如果節點的插入順序爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"100、200、300、400、500","attrs":{}}],"attrs":{}},{"type":"text","text":",那麼這顆二叉排序樹就會完全退化成一個鏈表,失去了原本高效的CRD性能,如圖5所示。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4d/4db9edd1b6250b52dfeb87705552a18a.png","alt":null,"title":"圖5 退化成鏈表的二叉排序樹","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"介於鏈表在最壞情況下訪問和搜索的時間複雜度會降爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(n)","attrs":{}}],"attrs":{}},{"type":"text","text":",因此ConcurrentHashMap在鏈表元素個數>=8時,會將其轉換成一顆紅黑樹,以便於提升檢索效率和縮短RT時間,源碼示例1-7。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"if (binCount != 0) {\n if (binCount >= TREEIFY_THRESHOLD) //TREEIFY_THRESHOLD 缺省值爲8\n treeifyBin(tab, i);\n if (!added)\n return val;\n break;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹的性能究竟如何?儘管紅黑樹和二叉查找樹的CRD平均時間複雜度都是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(logn)","attrs":{}}],"attrs":{}},{"type":"text","text":",但在最壞情況下,前者的CRD時間複雜度仍然可以保持在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(logn)","attrs":{}}],"attrs":{}},{"type":"text","text":"水平,而不會下降到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(n)","attrs":{}}],"attrs":{}},{"type":"text","text":",如圖6所示。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a0/a0bc91e4f3a15afb47ff862b1a00da70.png","alt":null,"title":"圖6 常見數據結構的時間複雜度和空間複雜度","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"https://www.bigocheatsheet.com/","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹之所以能夠在最壞的情況下將CRD時間複雜度控制在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"O(logn)","attrs":{}}],"attrs":{}},{"type":"text","text":",與它自身的4項平衡規則密切相關:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"每個節點,只能是紅色或黑色;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"根節點必須是黑色;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"如果節點爲紅色,那麼它的左/右子樹必須是黑色;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"任意路徑的黑高相同。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ab/ab02e492b06b18428185d7c57c32e76b.png","alt":null,"title":"圖7 紅黑樹黑高","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於紅黑樹會在每一次寫入操作完成後對目標節點進行着色、左/右旋轉處理來讓其始終保持平衡,因此,","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"從邏輯上來說(黑高),紅黑樹中任意節點的左/右子樹高度始終是保持一致的,不會因爲節點的不平衡而導致二叉樹退化成鏈表","attrs":{}},{"type":"text","text":",如圖7所示。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"節點插入修正","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹之所以“複雜”,我認爲是大部分人的空間想象力存在短板所導致的。本文,我會盡量用最清晰和路徑最短的示例來進行說明,以便於大家快速理解和上手。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設節點插入成功後,接下來要做的事情就是對目標節點進行重新着色和左/右旋轉處理,單邊(待插入節點的父節點在祖父節點的左邊/右邊)一共存在3種插入修正情況,如下所示:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"插入節點存在uncle節點,且uncle節點爲RED;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"插入節點不存在uncle節點,或uncle節點爲BLACK,且在parent的左邊;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"插入節點不存在uncle節點,或uncle節點爲BLACK,且在parent的右邊。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b0/b0c7940a7ad3732e3f284cd5aaf09605.png","alt":null,"title":"圖8 第1種插入修正處理","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先從第1種情況開始講起。如圖8所示,假設節點200(BLACK)同時擁有左子樹100(RED)和右子樹300(RED),那麼當插入節點50(RED)時(新插入節點缺省都爲RED),根據二叉排序樹的數據分佈規則,節點50會作爲節點100的左子樹,但這卻違背了紅黑樹的規則,RED節點並不能包含相同顏色的子樹。因此,在這種情況下,我們只需要將節點100和300着色爲BLACK,節點200着色爲RED即可(如果是根節點,最終仍然會被修正爲BLACK),示例1-8。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void i_fixup(Node n) {\n if (root == n) {\n root.color = Color.BLACK;//如果是根節點,着色爲BLACK並退出\n return;\n }\n if (Objects.isNull(n) || n.parent.color == Color.BLACK) {\n return;//如果parent.color爲BLACK則退出\n } else {\n var parent = n.parent;\n var gandfather = parent.parent;\n if (gandfather.left == parent) {\n var uncle = gandfather.right;\n if (Objects.nonNull(uncle) && uncle.color == Color.RED) {\n parent.color = Color.BLACK;\n uncle.color = Color.BLACK;\n gandfather.color = Color.RED;\n i_fixup(gandfather);//將gandfather節點進行遞歸修正\n }\n }\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此大家需要注意,","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"如果待插入節點的父節點爲BLACK時,則無需進行任何修正處理,直接插入即可,因爲這並不會破壞紅黑樹的平衡","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/eb/eba555f7f2d72307c7dae809c2a7ec32.png","alt":null,"title":"圖9 第2種插入修正處理","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第2種情況,如圖9所示。節點200(BLACK)僅包含左子樹100(RED),那麼當插入節點50(RED)時,根據二叉排序樹的數據分佈規則,節點50會作爲節點100的左子樹,但這卻違背了紅黑樹的規則,因此我們需要將節點100着色爲BLACK,節點200着色爲RED,然後再對節點200進行右旋後即可恢復這顆紅黑樹的平衡,示例1-9。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"if (Objects.nonNull(uncle) && uncle.color == Color.RED) {\n //省略情況1相關實現邏輯\n} else {\n if (parent.left == n) {//情況2相關實現邏輯\n parent.color = Color.BLACK;\n gandfather.color = Color.RED;\n rightRotate(gandfather);\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對目標節點進行右旋時,如果其左子樹不存在右子樹,那麼當右旋結束後,原父節點會變爲其左子樹的右子樹;如果其左子樹存在右子樹,那麼則由原父節點繼承爲左子樹即可,如圖10所示。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/48/48f719ad4ba036c3b45d617d571d7905.png","alt":null,"title":"圖10 節點右旋示例","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹的節點右旋操作,示例1-10。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void rightRotate(Node n) {\n var left = n.left;\n n.left = left.right;\n if (Objects.nonNull(left.right)) {\n left.right.parent = n;\n }\n left.right = n;\n reset(left, n);//重設引用關係\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左旋操作同理,如果其右子樹不存在左子樹,那麼當左旋結束後,原父節點會變爲其右子樹的左子樹;如果其右子樹存在左子樹,那麼則由原父節點繼承爲右子樹即可。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ae/ae9ca7013c21b4cc58c4242d35ff999e.png","alt":null,"title":"圖10 第3種插入修正情況","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第3種情況,如圖10所示。節點200(BLACK)僅包含左子樹100(RED),當插入節點150(RED)時,根據二叉排序樹的數據分佈規則,由於目標節點>100,因此將會作爲節點100的右子樹,但這同樣也違背了紅黑樹的規則,因此我們需要對其進行修正處理。在此,我們並不必急於對節點200和100着色,而是先將節點100左旋,待旋轉結束後,節點100會作爲節點150的左子樹,然後再將節點100作爲入參遞歸調用修正函數","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"i_fixup()","attrs":{}}],"attrs":{}},{"type":"text","text":"進入到情況2的邏輯處理上,示例1-11。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"if (Objects.nonNull(uncle) && uncle.color == Color.RED) {\n //省略情況1實現邏輯\n} else {\n if (parent.left == n) {\n //省略情況2實現邏輯\n } else {//情況3實現邏輯\n leftRotate(parent);\n i_fixup(parent);\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此大家需要注意,如果待插入節點的父節點在祖父節點的右邊,只需調換順序處理即可,本文就不再過多進行講解了。至此,關於紅黑樹的節點插入修正基本實現就完成了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"節點刪除修正","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到這裏,或許很多同學都會認爲紅黑樹的修正處理太麻煩,確實,左/右旋轉加上着色處理確實容易把人繞暈,但這僅僅只是開始。相對於插入修正而言,紅黑樹的刪除修正單邊(繼承節點在父節點的左邊/右邊)一共存在4種情況,如下所示:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"brother節點爲RED;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"brother節點爲BLACK,且左/右子樹爲null或BLACK;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"brother節點爲BLACK,且存在顏色爲RED的左子樹;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"brother節點爲BLACK,且存在顏色爲RED的右子樹。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d0/d0603bc95833fd343c9349742f85f939.png","alt":null,"title":"圖11 第1種和第2種刪除修正情況","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖11所示。假如我們刪除節點200(BLACK)後,紅黑樹的平衡就一定會被打破,因爲根節點左/右子樹的黑高並不相等,且由於brother節點300爲RED滿足於第1種刪除修正情況,因此,首先需要將其父節點100(BLACK)着色爲RED,brother節點着色爲BLACK,然後再左旋父節點100,待左旋結束後,原父節點100會作爲其右子樹300的左子樹。由於產生左旋後brother節點會被重新指向爲節點250,此時同時也滿足於第2種修正情況,因此我們還需要將節點100着色爲BLACK,其右子樹着色爲RED,示例1-12。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void d_fixup(Node n1, Node n2) {\n Node bro = null;\n while ((Objects.isNull(n1) || n1.color == Color.BLACK) && root != n1) {\n if (n2.left == n1) {//情況1相關實現邏輯\n bro = n2.right;\n if (bro.color == Color.RED) {\n bro.color = Color.BLACK;\n n2.color = Color.RED;\n leftRotate(n2);\n bro = n2.right;\n }\n //情況2相關實現邏輯\n if ((Objects.isNull(bro.left) || bro.left.color == Color.BLACK) && (Objects.isNull(bro.right) || bro.right.color == Color.BLACK)) {\n if (n2.color == Color.RED) {\n n2.color = Color.BLACK;\n bro.color = Color.RED;\n break;\n } else {\n bro.color = Color.RED;\n n1 = n2;\n n2 = n1.parent;\n }\n }\n }\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此大家需要注意,","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"如果被刪除的節點顏色爲RED,則無需進行任何修正處理,直接刪除即可,因爲刪除RED節點並不會對紅黑樹的平衡產生影響","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b2/b22655eeed1e7474270625cfb0e1f331.png","alt":null,"title":"圖12 第3種刪除修正情況","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖12所示。當我們刪除節點100(BLACK)後,紅黑樹的平衡就被打破了,由於brother節點300爲BLACK,且左子樹爲RED,正好滿足於第3種刪除修正情況,因此,我們首先需要將brother着色爲父節點200(BLACK)的顏色,父節點在非BLACK的情況下着色爲BLACK,然後再右旋brother節點,待右旋結束後,brother節點會作爲其原左子樹250的右子樹。此時紅黑樹仍然非平衡,因此我們還需要左旋父節點200,讓這顆紅黑樹恢復平衡,示例1-13。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"if (n2.left == n1) {\n bro = n2.right;\n if (bro.color == Color.RED) {\n //省略情況1相關實現邏輯\n }\n if ((Objects.isNull(bro.left) || bro.left.color == Color.BLACK) && (Objects.isNull(bro.right) || bro.right.color == Color.BLACK)) {\n //省略情況2相關實現邏輯\n } else {\n if (Objects.nonNull(bro.left) && bro.left.color == Color.RED) {//情況3相關實現邏輯\n bro.left.color = n2.color;\n n2.color = Color.BLACK;\n rightRotate(bro);\n leftRotate(n2);\n } \n break;\n }\n} ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2e/2e3bdfbbd41d51c06ae53e80cc0ec35c.png","alt":null,"title":"圖13 第4種刪除修正情況","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖13所示。當我們刪除節點100(BLACK)後,紅黑樹的平衡就被打破了,由於brother節點300爲BLACK,且右子樹爲RED,正好滿足於第4種刪除修正情況,因此,我們首先需要先將brother着色爲父節點200(BLACK)的顏色,父節點在非BLACK的情況下着色爲BLACK,brother節點的右子樹着色爲BLACK,然後再左旋父節點200即可,示例1-14。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"if (n2.left == n1) {\n bro = n2.right;\n if (bro.color == Color.RED) {\n\t\t\t\t//省略情況1相關實現邏輯\n }\n if ((Objects.isNull(bro.left) || bro.left.color == Color.BLACK) && (Objects.isNull(bro.right) || bro.right.color == Color.BLACK)) {\n //省略情況2相關實現邏輯\n } else {\n if (Objects.nonNull(bro.left) && bro.left.color == Color.RED) {\n //省略情況3相關實現邏輯\n } else if (Objects.nonNull(bro.right) && bro.right.color == Color.RED) {//情況4相關實現邏輯\n bro.color = n2.color;\n n2.color = Color.BLACK;\n bro.right.color = Color.BLACK;\n leftRotate(n2);\n }\n break;\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此大家需要注意,如果繼承節點在父節點的右邊,只需調換順序處理即可,本文就不再過多進行講解了。至此,本文內容全部結束。如果在閱讀過程中有任何疑問,歡迎在評論區留言參與討論。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"項目地址","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹實現,","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/gaoxianglong/algorithm","title":""},"content":[{"type":"text","text":"https://github.com/gaoxianglong/algorithm","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦文章:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/d367c19896e4cef6fbb661cf7","title":null},"content":[{"type":"text","text":"硬核系列 | 深入剖析字節碼增強","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/cef6d2931a54f85142d863db7","title":null},"content":[{"type":"text","text":"硬核系列 | 深入剖析 Java 協程","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/61868b3f66de36d32a5f1434f","title":null},"content":[{"type":"text","text":"白玉試毒 | 灰度架構設計方案","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/655943e5f85e6f79ffbd03047","title":null},"content":[{"type":"text","text":"新時代背景下的 Java 語法特性","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/92ba88c7926b5f5c6fbc11830","title":null},"content":[{"type":"text","text":"剖析 Java15 新語法特性","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/4d571787a3280ef3094338f9b","title":null},"content":[{"type":"text","text":"看門狗 | 分佈式鎖架構設計方案 -01","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/545a3accd173d6517ebd0ad59","title":null},"content":[{"type":"text","text":"看門狗 | 分佈式鎖架構設計方案 -02","attrs":{}}]}]}],"attrs":{}}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"碼字不易,歡迎轉發","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章