數據結構 - 紅黑樹(Red Black Tree)插入詳解與實現(Java)
最終還是決定把紅黑樹的篇章一分為二,插入操作一篇,刪除操作一篇,因為合在一起寫篇幅實在太長了,寫起來都覺得累,何況是閱讀並理解的讀者。
紅黑樹刪除操作請參考 數據結構 - 紅黑樹(Red Black Tree)刪除詳解與實現(Java)
現在網絡上最不缺的就是對某個知識點的講解博文,各種花樣標題百出,更有類似“一文講懂xxx”,“史上最簡單的xxx講解”,“xxx看了還不懂你打我”之類雲雲。其中也不乏有些理論甚至是舉例都雷同的兩篇不同文章,至於作者是不是真的理解自己所寫的內容暫且不說,技術博客這種東西,本來就是提供給大家分享自己學習體會的一個平臺,我也不敢說自己寫的就足夠全面簡潔易懂,只能說有些東西確實不是一兩篇文章就能理解透徹的,只有多讀,多思考,慢慢的就會明了,我也是讀了好幾個人的博文才讀懂的,一些前輩的文章確實很不錯,值得參考和學習。僅希望我所寫這兩篇關於紅黑樹的文章能在眾多的同類博文中給偶然看到的讀者一點點啟示。
正文。
本文要求懂得二叉搜索樹的原理,如果還不理解可以轉閱(理解第一篇便可以):
一、數據結構 - 從二叉搜索樹說到AVL樹(一)之二叉搜索樹的操作與詳解(Java)
二、數據結構 - 從二叉搜索樹說到AVL樹(二)之AVL樹的操作與詳解(Java)
眾所皆知,二叉平衡樹(Binary Balanced Tree)的出現是為了讓一棵二叉搜索樹的查找效率盡可能的最大化,同時為了構造這麽一棵樹,在插入和刪除的時候也要根據一定的規則進行操作,這些操作在一定情況下也會影響到整棵樹的使用效率,所以,我們想有沒有這麽一種樹,我們並不必嚴格要求這棵樹要平衡度很高(比如所有路徑的長度差都必須在一個很小的範圍之內)以提高插入和刪除的效率,同時又不能太影響到查找的效率,已達到一個比較好的使用效果。
在此之前,本文圖例約定如下:
紅黑樹(Red Black Tree - RB Tree)就是這樣一種數據結構,和很多數據結構一樣,紅黑樹也有自己的一套事先規定好的規則,無論在什麽狀態下,一顆紅黑樹都必須滿足以下五個規則(定義), 破壞任何一條規則都不再是一顆紅黑樹。
1. 紅黑樹的節點不是紅色的就是黑色的
2. 紅黑樹的根節點永遠是黑色的
3. 所有葉子節點都是黑色的(註意:紅黑樹的葉子節點是指Nil節點)
4. 同一路徑上不能有相鄰兩個節點都是紅色的
5. 從任一節點到所有葉子節點所經歷的黑色節點個數相同
以上五個定義即使不能背下來,也要十分熟悉。用以上的定義去實現一顆紅黑樹,能使所有搜索路徑長度相差最大不過一倍。
定義紅黑樹節點的數據結構:
public class TreeNode { private int elem; private TreeNode left, right; private TreeNode parent; private NodeColor color; public TreeNode (int elem) { this.elem = elem; color = NodeColor.RED; } }
比普通二叉搜索樹多了一個屬性表示節點顏色,初始化一個節點的時候,節點顏色設置為紅色,因為插入一個紅色節點,只要不違反紅黑樹的規則,插入之後不需要對樹進行調整,但如果直接插入一個黑色節點,那肯定會違反上面所說的第5個規則,勢必要進行調整,所以多一事不如少一事。
在此之前先講一些基本操作,然後再講具體
紅黑樹的基本操作包括染色和旋轉,染色沒有什麽可說的,根據上面所說的第一條定義,染色無非是把一個節點從黑色染成紅色或反之。
旋轉包括右旋和左旋,具體的操作圖例和代碼從我之前寫的一篇文章復制過來就好。
右旋:
做法是以A節點為軸,節點A的左子樹指向其左孩子B的右子樹2,然後節點B的左子樹指向節點A,然後原本節點A的父節點R對應的子樹指向節點B,其他節點不作變化,這邊便完成了左旋操作。
相應的代碼如下:以A點為軸進行右旋
private void rotateRight(TreeNode pivot) { TreeNode leftChild = pivot.getLeft(); TreeNode grandChildRight = leftChild.getRight(); TreeNode parent = pivot.getParent(); if (null == parent) { this.root = leftChild; } else if (pivot == parent.getLeft()) { parent.setLeft(leftChild); } else { parent.setRight(leftChild); } leftChild.setParent(parent); pivot.setLeft(grandChildRight); if (null != grandChildRight) { grandChildRight.setParent(pivot); } leftChild.setRight(pivot); pivot.setParent(leftChild); }右旋操作
左旋:
左旋的操作跟右旋一樣,但是結構是相反的,以節點A為軸,節點A的右子樹指向其有孩子B的左子樹2,然後節點B的左子樹指向節點A,再使原節點A的父節點對應的子樹指向節點B,其他節點不做改變。
相應的代碼如下:以A點為軸進行左旋
private void rotateLeft(TreeNode pivot) { TreeNode rightChild = pivot.getRight(); TreeNode grandChildLeft = rightChild.getLeft(); TreeNode parent = pivot.getParent(); if (null == parent) { // pivot node is root this.root = rightChild; } else if(pivot == parent.getLeft()) { parent.setLeft(rightChild); } else { parent.setRight(rightChild); } rightChild.setParent(parent); pivot.setRight(grandChildLeft); if (null != grandChildLeft) { grandChildLeft.setParent(pivot); } rightChild.setLeft(pivot); pivot.setParent(rightChild); }左旋操作
同樣,請牢牢記住這個旋轉規則,當需要的時候可以信手拈來,不要卡在這種基礎操作上。
上面已經說到初始化一個新的節點N(New)的時候,節點的顏色設置為紅色,然後根據插入的情況可以分為以下兩種:
一、插入節點的父節點P(Parent)是黑色節點
這種情況很舒服,插入一個紅色節點,而父節點又恰好是黑色的,不違反以上某一條定義,插入結束。
二、插入的節點父節點P是紅色節點
這種情況插入時直接違反了上面第四條定義,從這個條件接下去細分,觀察插入節點的叔叔節點U(Uncle)
① 如果節點U是紅色的,做法是把祖父節點GP(Grandparent)染為紅色,並把父節點P和叔叔節點U染為黑色。有人有疑問說那如果祖父節點GP本來就是紅色的怎麽辦,GP節點不可能為紅色,因為如果GP節點為紅色,那插入之前就違反了第四條定義。無論在什麽情況下,請確保插入前的樹是一顆合格的紅黑樹!
如果N為右子樹也同理(註意圖中省略了Nil節點)
② 如果節點U是黑色的(其實就是Nil節點,因為如果如果U不為Nil節點,那N所在的位置本來就應該是一個不為Nil的黑色節點,否則從GP節點下來就會出現兩條黑色節點數不同的路徑,與第五條定義相悖),且節點N為節點U的遠侄子節點,此時的調整做法是把節點P染為黑色,把節點GP染為紅色,並以GP節點根據實際情況做相應的旋轉(若節點U為GP的右子樹,則以GP為軸做右旋操作,若節點U為GP的左子樹,則以GP為軸做左旋操作)。
若此時節點N是節點U的近侄子節點,做法是以節點P為軸做相應的旋轉操作(若N為P的左子樹,則以P為軸做右旋操作,若N為P的右子樹,則以P為軸做左旋操作),旋轉之後轉為上面的情況①,再按照情況①的操作進行調整。
這樣操作之後確保從GP下來的黑色節點數目在調整前後保持不變,如果此時GP節點不是根節點,那如果GP節點的父節點也是紅色的,那此時要把GP當做新插入的節點繼續向上調整,調整規則與上面①②一致,直到遇到黑色節點或者根節點為止(主要針對①情況,因為②情況調整之後當前子樹的根節點就已經是黑色的不會影響整棵樹的結構),每次插入結束後如果根節點不是黑色的,根據第二條定義,把根節點設置為黑色。
所有情況處理好之後,開始寫代碼
寫一個插入新元素的公共方法:
public boolean insert(int elem) { TreeNode node = new TreeNode(elem); boolean inserted = false; if (null == this.root) { this.root = node; inserted = true; } else { inserted = insertNode(this.root, node); } setRootBlack(); //the root must be always black return inserted; }
子方法 private boolean insertNode(TreeNode node, TreeNode newNode) 表示把newNode插入到node的子樹當中,插入成功返回true,元素已經存在則返回false,方法體如下:
private boolean insertNode(TreeNode node, TreeNode newNode) { if (node.getElem() == newNode.getElem()) { return false; // the element already exist } else if (node.getElem() < newNode.getElem()) { if (null == node.getRight()) { node.setRight(newNode); newNode.setParent(node); insertFixUp(newNode); return true; } else { return insertNode(node.getRight(), newNode); } } else { if (null == node.getLeft()) { node.setLeft(newNode); newNode.setParent(node); insertFixUp(newNode); return true; } else { return insertNode(node.getLeft(), newNode); } } }
插入之後就是調整啦,根據上面的調整規則編寫函數 private void insertFixUp(TreeNode node) 表示從node開始向上調整紅黑樹
private void insertFixUp(TreeNode node) { TreeNode parent = node.getParent(); while (null != parent && parent.getColor() == NodeColor.RED) { // parent should not be root for root node must be black boolean uncleInRight = parent.getParent().getLeft() == parent; TreeNode uncle = uncleInRight ? parent.getParent().getRight() : parent.getParent().getLeft(); if (null == uncle) { // uncle is Nil and could not be black node if (uncleInRight) { if (node == parent.getLeft()) { // case 1 parent.setColor(NodeColor.BLACK); parent.getParent().setColor(NodeColor.RED); rotateRight(parent.getParent()); break; } else { // case 2 rotateLeft(parent); node = node.getLeft(); // convert to case 1 } } else { if (node == parent.getRight()) { // case 3 parent.setColor(NodeColor.BLACK); parent.getParent().setColor(NodeColor.RED); rotateLeft(parent.getParent()); break; } else { // case 4 rotateRight(parent); node = node.getRight(); // convert to case 3 } } } else { // uncle node is red parent.setColor(NodeColor.BLACK); uncle.setColor(NodeColor.BLACK); parent.getParent().setColor(NodeColor.RED); node = parent.getParent(); } parent = node.getParent(); } }
至此紅黑樹插入操作結束,步驟也是相對簡單,希望對大家的理解有所幫助。
尊重知識產權,引用轉載請通知作者!
數據結構 - 紅黑樹(Red Black Tree)插入詳解與實現(Java)