愛恨交織的紅黑樹

BST

虐你千萬遍,還要待她如初戀的紅黑樹,是否對她既歡喜又畏懼。別擔憂,經過本文講解,但願你能有史無前例的感動。java

紅黑樹也是二叉查找樹,但比普通的二叉查找樹多一些特性條件限制,每一個結點上都存儲有紅色或黑色的標記。由於是二叉查找樹,因此他擁有二叉查找樹的全部特性。紅黑樹是一種自平衡二叉查找樹,在極端數據條件插入時(正序或倒敘)不會退化成類鏈狀數據,能夠更高效的在O(log(n))時間內完成查找,插入,刪除操做。node

準備

在閱讀本文以前,建議先閱讀我上篇文章《二叉查找樹的解讀和實現》,能夠更好的幫助你理解紅黑樹。this

特性

  1. 結點是紅色或黑色
  2. 根結點必須爲黑色
  3. 葉子結點(約定爲null)必定爲黑色
  4. 任一結點到葉子結點的每條路徑上黑色結點數量都相等
  5. 不容許連續兩個結點都爲紅色,也就是說父結點和子結點不能都爲紅色

查找

紅黑樹的查找方式和上篇文章所講述的原理同樣,這裏就不從新講述,以結點[38,20,50,15,27,43,70,60,90]爲例,返回一顆紅黑樹。編碼

紅黑樹

普通操做

紅黑樹的插入和刪除,分爲多種狀況,相對來講比較複雜。插入或刪除新結點後的樹,必需要知足上面五點特性的二叉查找樹,因此要經過不一樣手段來調整樹。但普通操做就是和普通二叉查找樹操做同樣。
好比普通插入中,由於每一個結點只能是紅色或黑色,因此咱們定義新添加的非根結點默認顏色爲紅色。將新結點定義爲紅色的緣由是爲了知足特性4(任一結點到葉子結點的每條路徑上黑色結點數量都相等),不然會多出一個黑色結點打破規則。
如今向樹中插入結點10。設計

插入結點10

從圖中能夠看到,父結點15爲黑色結點,插入紅色結點10,不會增長黑色結點的數量,其餘規則也沒有受到影響,因此,當插入結點的父結點爲黑色時,直接插入樹中,不會破壞原紅黑樹的規則。
該種狀況代碼實現:
結點對象code

package com.ytao.rbt;

/**
 * Created by YANGTAO on 2019/11/9 0009.
 */
public class Node {

    public static String RED = "red";
    public static String BLACK = "black";

    public Integer value;

    public String color;

    public Node left;

    public Node right;


    public Node(Integer value, String color, Node left, Node right) {
        this.value = value;
        this.color = color;
        this.left = left;
        this.right = right;
    }

    public Node(int value, String color) {
        this.value = value;
        this.color = color;
    }
}

實現操做對象

public void commonInsert(Node node, Integer newVal){
    if (node == null)
        node = new Node(newVal, Node.BLACK);
    
    while (true){
        if (newVal < node.value){
            if (node.left == null){
                // 若是左樹爲葉子結點而且父結點爲黑色,能夠直接插入紅色新結點
                if (node.color == Node.BLACK){
                    node.left = new Node(newVal, Node.RED);
                    break;
                }
            }
            node = node.left;
        }else if (newVal > node.value){
            if (node.right == null){
                if (node.color == Node.BLACK){
                    node.right = new Node(newVal, Node.RED);
                    break;
                }
                
            }
            node = node.right;
        }
    }
}

看到這段代碼,是否似曾相識的感受,沒錯,這就是上篇文章的插入操做加了個顏色限制。
一樣刪除也是如此,這裏就不在細述。blog

變色

爲了更好分析清楚變色的緣由,咱們將樹中的50結點提取出來做爲根結點,如圖:get

結點50做爲根的樹

向樹中添加結點55,獲得樹如圖:博客

添加結點55

這時55和60都爲紅色結點,不符合紅黑樹的特性(不容許連續兩個結點都爲紅色),這時咱們須要調整,就使用到變色。
將父結點60變爲黑色,又遇到不符合紅黑樹特性(任一結點到葉子結點的每條路徑上黑色結點數量都相等),由於咱們增長了黑色結點60,多出了一個黑色結點。
這時的結點70必定爲黑色,由於本來的父結點60的顏色爲紅色。將結點70變爲紅色,知足告終點70的左子樹,但右子樹受結點70變爲紅色的影響,少了個黑色結點,恰好結點90爲紅色,能夠將其變爲黑色,知足結點70的右子樹要求。
該種特殊狀況較爲簡單處理,只需經過變色就能處理。

變色調整後

這種條件結構的紅黑樹實現:

public void changeColor(Node node, int newVal){
    if (node.left == null || node.right == null)
        return;
    // 經過判斷待插入結點的父結點和叔叔結點,是否知足咱們須要的條件
    if (node.left.color == Node.RED && node.right.color == Node.RED){
        // 肯定是更新到左樹仍是右樹中
        Node base = compare(newVal, node.value) > 0 ? node.right : node.left;
        // 和待插入結點的父結點做比較
        if (newVal < base.value && base.left == null){
            base.left = new Node(newVal, Node.RED);
        }else if (newVal > base.value && base.right == null){
            base.right = new Node(newVal, Node.RED);
        }
    }
    node.color = Node.RED;
    // 經過取反獲取插入結點的叔叔結點並將顏色變黑色
    Node uncleNode = compare(newVal, node.value) > 0 ? node.left : node.right;
    uncleNode.color = Node.BLACK;
}

public int compare(int o1, int o2){
    if (o1 == o2)
        return 0;
    return o1 > o2 ? 1 : -1;
}

旋轉

當僅僅經過變色沒法解決咱們須要知足特性時,咱們就要考慮使用紅黑樹的旋轉。
旋轉在插入和刪除中,會頻繁用到該操做,爲了知足咱們的五條特性,經過旋轉能夠生成一顆新的紅黑樹,旋轉分爲左旋轉和右旋轉。

左旋轉

左旋轉爲逆時針的旋轉,相似於把父結點往左邊拉(能夠這麼記憶區分左右旋轉的方向),變換如圖:

左旋轉

右旋轉

右旋轉與左旋轉出方向相反外,其餘都同樣,變換如圖:

右旋轉

從圖中能夠看出,旋轉後的父子結點,關係對調了,同時子結點的子結點給了父結點。
若是是左旋轉,那麼父結點會成爲旋轉結點的左子結點;子結點的左子結點會成爲父結點的右子結點。
若是是右旋轉,那麼父結點會成爲旋轉結點的右子結點;子結點的右子結點會成爲父結點的左子結點。
聽起來比較比較拗口,記住一條規則,左小右大,結合上圖兩個旋轉就比較好理解。
用代碼實現旋轉以下:

/**
 *
 * @param node 兩個旋轉結點中的父結點
 * @param value 兩個旋轉結點中子結點的值,由於在整合旋轉的時候,node能夠遍歷查找出來,value做爲須要旋轉的標記結點
 */
public void rotate(Node node, int value){
    Node nodeChild = compare(value, node.value) > 0 ? node.right : node.left;
    if (nodeChild != null && value == nodeChild.value){
        Node parent = node;
        // 旋轉子結點小於旋轉父結點,執行的是右旋轉,不然爲左旋轉
        if (value < node.value){
            rightRotate(parent);
        }else if (value > node.value){
            leftRotate(parent);
        }
    }
}

/**
 * 左旋轉
 * @param node 旋轉的父結點
 */
public void leftRotate(Node node){
    Node rightNode = node.right;
    // 旋轉結點的左子結點給父結點的右子結點
    node.right = rightNode.left;
    // 父結點做爲子結點的左子結點
    rightNode.left = node;
}

/**
 * 右旋轉
 * @param node
 */
public void rightRotate(Node node){
    Node leftNode = node.left;
    // 旋轉結點的右子結點給父結點的左子結點
    node.left = leftNode.right;
    // 父結點做爲子結點的右子結點
    leftNode.right = node;
}

旋轉變色案例應用

在上面結點爲38的紅黑中插入結點55。應用前面講解到的變色後,紅黑樹結構如圖:

此時出現不知足紅黑樹特性(不容許連續兩個結點都爲紅色),這時須要咱們將結點50和結點38進行左旋轉,獲得以下圖:

根結點50不符合紅黑樹特性(根結點必須爲黑色),因此先將根結點變爲黑色後。

如今獲得的紅黑樹,又出現違背(任一結點到葉子結點的每條路徑上黑色結點數量都相等)特性,左樹比右樹多一個黑色結點,此時將38,20,15,27顏色改變。

這裏通過變色旋轉完成了上面這課樹的操做紅黑樹的調整。

因爲代碼篇幅較大,並無將所有可能狀況都考慮進來。相信理解了紅黑樹的對編碼實現不是太大問題。

總結

紅黑樹的操做是基於普通二叉查找樹上加了紅黑樹的特性,不論是插入仍是刪除操做,也就是在普通紅黑樹上進行旋轉變色調整樹結構,因此在理解紅黑樹的時候,主要把握旋轉,變色,利用旋轉變色來知足紅黑樹的特性,這也是紅黑樹的精華所在。懂得其原理和設計思想的話,應用到實際中解決問題確實是很不錯的設計。固然,紅黑樹在實際的操做過程當中是多變的,複雜的,要徹底掌握仍是要花點時間來研究的。




我的博客: https://ytao.top
個人公衆號 ytao
個人公衆號

相關文章
相關標籤/搜索