1. 程式人生 > >高階資料結構---紅黑樹及其插入左旋右旋程式碼java實現

高階資料結構---紅黑樹及其插入左旋右旋程式碼java實現

前面我們說到的二叉查詢樹,可以看到根結點是初始化之後就是固定了的,後續插入的數如果都比它大,或者都比它小,那麼這個時候它就退化成了連結串列了,查詢的時間複雜度就變成了O(n),而不是理想中O(logn),就像這個樣子

 

 

 如果我們有一個平衡機制,讓這棵樹可以動起來,比如將4變成根結點,是不是查詢效率又可以提高了,這就要提到另外一種特殊的二叉樹---紅黑樹(也是一種特殊的二叉查詢樹)。JDK1.8中將HashMap底層實現的資料結構由陣列+連結串列變成了陣列+連結串列+紅黑樹。當連結串列長度超過8就轉換成紅黑樹,明顯紅黑樹的查詢效率是高於連結串列的吧。

 

紅黑樹的特點:

 

 1.每個結點不是紅色就是黑色

 2.不可能有連在一起的紅色結點(黑色的就可以),每個葉子節點都是黑色的空節點(nil),也就是說,葉子節點不儲存資料

 3.根結點都是黑色 root

 4.每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點

 5.新插入的元素都是紅色,根除外

因為紅黑樹要滿足以上特點,所以就有變色機制和旋轉平衡機制來調節樹高度。

變色:

  當前節點紅色,父結點和叔叔結點都是紅色,將父結點和叔叔結點變成黑色,把爺爺結點設定成紅色;只有父結點是紅色,那就將父結點變黑色,爺爺結點變紅色。完成變色之後進行左旋/右旋。

注:下面的當前節點都是變化後的操作結點。

左旋:變完色之後將操作結點變成爺爺結點,以其爺爺結點去旋轉。

  條件:當前結點(爺爺結點)父結點是紅色,叔叔是黑色,且當前結點是右子樹。

   操作結點指向父結點,將當前結點(變色前結點的太爺爺)右孩子的左孩子變成其右孩子,當前結點變成其右孩子的左孩子,其右孩子填補當前結點位置

右旋:

  條件:當前結點父結點是紅色,叔叔是黑色,且當前結點是左子樹。

  父結點變成黑色,爺爺變成紅色(這個變色就是上面的第二種變色),以太爺爺為操作結點右旋。將其左孩子的右子樹變成其左子樹,將當前結點變成其左孩子的右子樹。其做孩子填補當前位置。

左旋右旋動圖:

 

程式碼實現:真心的太抽象了,看起來簡單,程式碼實現起來,各種結點的引用指向太亂了;下面的程式碼幾乎每一行都寫了註釋,尤其是左旋和右旋

 

package com.nijunyang.algorithm.tree;

/**
 * Description: 紅黑樹
 * Created by nijunyang on 2020/4/20 20:23
 *
 * 紅黑樹的性質:
 *         1.每個結點不是紅色就是黑色
 *         2.不可能有連在一起的紅色結點(黑色的就可以),每個葉子節點都是黑色的空節點(nil),也就是說,葉子節點不儲存資料
 *         3.根結點都是黑色 root
 *         4.每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點
 *         5.新插入的元素都是紅色,根除外
 */

public class RedBlackTree {

    private Node root = Node.nil;

    public static void main(String[] args){
        RedBlackTree redBlackTree = new RedBlackTree();
        //19,5,30,1,12,35,7,13,6
        redBlackTree.insert(19);
        redBlackTree.insert(5);
        redBlackTree.insert(30);
        redBlackTree.insert(1);
        redBlackTree.insert(12);
        redBlackTree.insert(35);
        redBlackTree.insert(7);
        redBlackTree.insert(13);
        redBlackTree.insert(6);
        RedBlackTree.inOrderTraversal(redBlackTree);
        System.out.println();
    }

    public <T extends Comparable<T>> void insert(T data){
        Node<T> temp = root;
        Node<T> node = new Node<>(data);
        if (root == Node.nil) {
            root = node;
            node.parent.parent = Node.nil;
        }
        else {
            node.black = false;
            //插入
            while (true) {
                if (temp.data.compareTo(data) < 0) {
                    if (temp.rightChild == Node.nil) {
                        temp.rightChild = node;
                        node.parent = temp;
                        break;
                    } else {
                        temp = temp.rightChild;
                    }
                }
                else if (temp.data.compareTo(data) == 0) {
                    //等於保留原來資料
                    return;
                }
                else {
                    if (temp.leftChild == Node.nil) {
                        temp.leftChild = node;
                        node.parent = temp;
                        break;
                    } else {
                        temp = temp.leftChild;
                    }
                }
            }

            //變色和旋轉
            fixTree(node);

        }
    }

    private static void inOrderTraversal(RedBlackTree redBlackTree) {
        TreeUtil.inOrderTraversal(redBlackTree.root);
    }

    /**
     * 變色和旋轉
     * @param node
     * @param <T>
     */
    private <T extends Comparable<T>> void fixTree(Node<T> node) {
        /**
         * 1.變色 條件:父結點及叔叔結點都是紅色,變色過程:把父結點和叔叔結點都變成黑色,把爺爺設定成紅色,指標指向爺爺結點
         * 2.左旋:上一步將指標指向了爺爺結點.條件:當前結點(爺爺結點)父結點是紅色,叔叔是黑色,且當前結點是右子樹。進行左旋:
         * 臨時指標指向父結點,將當前結點(變色前結點的太爺爺)右孩子的左孩子變成其右孩子,當前結點變成其右孩子的左孩子,
         * 其右孩子填補當前結點位置
         *
         * 3.右旋:條件:當前結點父結點是紅色,叔叔是黑色,且當前結點是左子樹。進行右旋:
         * 父結點變成黑色,爺爺變成紅色,以太爺爺為點右旋。將其左孩子的右子樹變成其左子樹,將當前結點變成其左孩子的右子樹。其做孩子填補當前位置
         *
         */
        Node<T> currentNode = node;
        while (!currentNode.parent.black) {
            Node<T> temp;
            if (currentNode.parent == currentNode.parent.parent.leftChild) { //當前父結點是左孩子
                temp = currentNode.parent.parent.rightChild; //叔叔結點
                //變色
                if (temp != Node.nil && !temp.black) { //叔叔也是紅色,將父和叔叔都變黑色
                    currentNode.parent.black = true;
                    temp.black = true;
                    currentNode.parent.parent.black = false; //爺爺變成紅色
                    currentNode = currentNode.parent.parent; //變色完成指向爺爺
                    continue;  //進入下一次迴圈判斷爺爺的位置是否也需要變色,直到不變滿足變色了才開始左旋/右旋
                }
                if (currentNode == currentNode.parent.rightChild) { //當前結點是右子樹
                    currentNode = currentNode.parent; //以其父結點進行左旋
                    //左旋
                    leftRotate(currentNode);
                }
                //右旋
                //父結點變成黑色,爺爺變成紅色,準備右旋
                currentNode.parent.black = true;
                currentNode.parent.parent.black = false;
                //指標指向太爺爺去右旋
                currentNode = currentNode.parent.parent;
                rightRotate(currentNode);
            }
            else { //當前父結點是右孩子
                temp = currentNode.parent.parent.leftChild;
                if (temp != Node.nil && !temp.black) {
                    currentNode.parent.black = true;
                    temp.black = true;
                    currentNode.parent.parent.black = false;
                    currentNode = currentNode.parent.parent;
                    continue;
                }
                if (currentNode == currentNode.parent.leftChild) {
                    currentNode = currentNode.parent;
                    rightRotate(currentNode);
                }
                //父結點變成黑色,爺爺變成紅色,準備左旋
                currentNode.parent.black = true;
                currentNode.parent.parent.black = false;
                //指標指向太爺爺去左旋
                currentNode = currentNode.parent.parent;
                leftRotate(currentNode);
            }
        }
        root.black = true; //根結點始終黑色
    }

    /**
     * 左旋:將其右孩子的左孩子變成其右孩子,當前結點變成其右孩子的左孩子,其右孩子填補當前結點位置
     * @param node
     * @param <T>
     */
    private <T extends Comparable<T>> void leftRotate(Node<T> node) {
        Node <T> currentNode = node;
        if (currentNode.parent != Node.nil) {
            if (currentNode == currentNode.parent.leftChild) { //當前結點是其父的左孩子
                currentNode.parent.leftChild = currentNode.rightChild; // 將其右孩子變成其父的左孩子(右孩子填補當前結點位置)
            }
            else {
                currentNode.parent.rightChild = currentNode.rightChild; //將其右孩子變成其父的右孩子(右孩子填補當前結點位置)
            }

            currentNode.rightChild.parent = currentNode.parent; //修改其右孩子的父指標,移向其父(右孩子填補當前結點位置)
            currentNode.parent = currentNode.rightChild; //當前結點變成其右孩子的孩子
            if (currentNode.rightChild.leftChild != Node.nil) {
                currentNode.rightChild.leftChild.parent = currentNode; //當前結點右孩子的左孩子變成當前結點的孩子,修改父指標
            }
            currentNode.rightChild = currentNode.rightChild.leftChild; //當前結點右孩子的左孩子變成當前結點的右孩子
            currentNode.parent.leftChild = currentNode; //當前結點新的父親(以前它的右孩子)的左孩子指向當前節點
        }
        else { //根就是當前結點
            Node right = root.rightChild;
            root.rightChild = right.leftChild; //將其右孩子的左孩子變成其右孩子
            right.leftChild.parent = root; //修改對應的父指向

            root.parent = right;
            right.leftChild = root; //當前結點變成其右孩子的左孩子
            right.parent = Node.nil;
            root = right;  //右孩子填補當前位置
        }
    }

    /**
     * 右旋:父結點變成黑色,爺爺變成紅色,準備右旋。將其左孩子的右子樹變成其左子樹,將當前結點變成其左孩子的右子樹。其左孩子填補當前位置,
     * 最後當前節點變成其
     * @param node  node
     * @param <T>
     */
    private <T extends Comparable<T>> void rightRotate(Node<T> node) {
        Node <T> currentNode = node;
        if (currentNode.parent != Node.nil) {
            if (currentNode == currentNode.parent.leftChild) {       //判斷當前結點是其父的左/右結點,其左孩子填補當前位置
                currentNode.parent.leftChild = currentNode.leftChild;
            } else {
                currentNode.parent.rightChild = currentNode.leftChild;
            }

            currentNode.leftChild.parent = currentNode.parent; //其左孩子填補當前位置,左孩子父指標指向其父指標
            currentNode.parent = currentNode.leftChild; //當前結點變成其左孩子的子樹
            if (currentNode.leftChild.rightChild != Node.nil) {
                currentNode.leftChild.rightChild.parent = currentNode; //將其左孩子的右子樹變成其左子樹
            }
            currentNode.leftChild = currentNode.leftChild.rightChild; //將其左孩子的右子樹變成其左子樹
            currentNode.parent.rightChild = currentNode; //當前結點新的父親(以前它的左孩子)的右孩子指向當前節點
        } else {  //當前結點是根結點
            Node<T> left = root.leftChild;
            root.leftChild = root.leftChild.rightChild; // 將其左孩子的右子樹變成其左子樹
            left.rightChild.parent = root;
            root.parent = left;
            left.rightChild = root; //將當前結點變成其左孩子的右子樹
            left.parent = Node.nil;
            root = left; //左孩子填補當前位置
        }
    }


    private static class Node<T extends Comparable<T>> extends TreeNode<T> {
        private static final Node nil = new Node<>(null);
        T data;
        Node<T> parent = nil;
        Node<T> leftChild = nil;
        Node<T> rightChild = nil;
        boolean black = true; //預設黑色

        public Node(T data) {
            this.data = data;
        }

        @Override
        public T getData() {
            return data;
        }

        @Override
        public void setData(T data) {
            this.data = data;
        }

        @Override
        public Node<T> getLeftChild() {
            return leftChild;
        }

        public void setLeftChild(Node<T> leftChild) {
            this.leftChild = leftChild;
        }

        @Override
        public Node<T> getRightChild() {
            return rightChild;
        }

        public void setRightChild(Node<T> rightChild) {
            this.rightChild = rightChild;
        }

        @Override
        public String toString() {
            return "data=" + data;

        }
    }
}

 

中序遍歷的程式碼:

    /**
     * 二叉樹中序遍歷 左子樹 根 右子樹
     * @param node   二叉樹節點
     */
    public static<N extends TreeNode<T>, T> void inOrderTraversal(N node){
        if(node == null){
            return;
        }
        //先找左再輸出根,再去找右
        inOrderTraversal(node.getLeftChild());
        if (node.getData() != null) {
            System.out.print(node.getData());
            System.out.print(" ");
        }
        inOrderTraversal(node.getRightChild());
    }

 

TreeNode:

public class TreeNode<T> {
    protected T data;
    protected TreeNode<T> leftChild;
    protected TreeNode<T> rightChild;

    public TreeNode() {
    }
}

在 https://www.cs.usfca.edu/~galles/visualization/RedBlack.html 上面驗證了下插入程式碼的執行結果和這兒的圖解結果是一致的。19,5,30,1,12,35,7,13,6

 

 

 

因為紅黑樹通過變色和左旋/右旋機制使得個子樹的高度儘量平衡,所以他的查詢效率是O(logn)。其插入和刪除也是近似O(logn).

 

&n