數據結構 - 紅黑樹(Red Black Tree)插入詳解與實現(Java)

  最終仍是決定把紅黑樹的篇章一分爲二,插入操做一篇,刪除操做一篇,由於合在一塊兒寫篇幅實在太長了,寫起來都以爲累,況且是閱讀並理解的讀者。html

      紅黑樹刪除操做請參考 數據結構 - 紅黑樹(Red Black Tree)刪除詳解與實現(Java)node

  如今網絡上最不缺的就是對某個知識點的講解博文,各類花樣標題百出,更有相似「一文講懂xxx」,「史上最簡單的xxx講解」,「xxx看了還不懂你打我」之類云云。其中也不乏有些理論甚至是舉例都雷同的兩篇不一樣文章,至於做者是否是真的理解本身所寫的內容暫且不說,技術博客這種東西,原本就是提供給你們分享本身學習體會的一個平臺,我也不敢說本身寫的就足夠全面簡潔易懂,只能說有些東西確實不是一兩篇文章就能理解透徹的,只有多讀,多思考,慢慢的就會明瞭,我也是讀了好幾我的的博文才讀懂的,一些前輩的文章確實很不錯,值得參考和學習。僅但願我所寫這兩篇關於紅黑樹的文章能在衆多的同類博文中給偶然看到的讀者一點點啓示。網絡

  正文。數據結構

  本文要求懂得二叉搜索樹的原理,若是還不理解能夠轉閱(理解第一篇即可以):ide

    1、數據結構 - 從二叉搜索樹說到AVL樹(一)之二叉搜索樹的操做與詳解(Java)函數

    2、數據結構 - 從二叉搜索樹說到AVL樹(二)之AVL樹的操做與詳解(Java)學習

  衆所皆知,二叉平衡樹(Binary Balanced Tree)的出現是爲了讓一棵二叉搜索樹的查找效率儘量的最大化,同時爲了構造這麼一棵樹,在插入和刪除的時候也要根據必定的規則進行操做,這些操做在必定狀況下也會影響到整棵樹的使用效率,因此,咱們想有沒有這麼一種樹,咱們並沒必要嚴格要求這棵樹要平衡度很高(好比全部路徑的長度差都必須在一個很小的範圍以內)以提升插入和刪除的效率,同時又不能太影響到查找的效率,已達到一個比較好的使用效果。this

  在此以前,本文圖例約定以下:spa

   

  紅黑樹(Red Black Tree - RB Tree)就是這樣一種數據結構,和不少數據結構同樣,紅黑樹也有本身的一套事先規定好的規則,不管在什麼狀態下,一顆紅黑樹都必須知足如下五個規則(定義), 破壞任何一條規則都再也不是一顆紅黑樹。3d

  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)的時候,節點的顏色設置爲紅色,而後根據插入的狀況能夠分爲如下兩種:

  1、插入節點的父節點P(Parent)是黑色節點

  這種狀況很舒服,插入一個紅色節點,而父節點又剛好是黑色的,不違反以上某一條定義,插入結束。

  2、插入的節點父節點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();
        }
    }

  

  至此紅黑樹插入操做結束,步驟也是相對簡單,但願對你們的理解有所幫助。

 

 

 

 

 

 

 

  尊重知識產權,引用轉載請通知做者!

相關文章
相關標籤/搜索