Data Structure_樹

weixin_34007291發表於2018-12-08

線段樹Segment Tree

對於有一類問題,時常關注的是一個區間或者是一個線段,那麼就可以使用線段樹來解決。比較經典的問題,就是區間染色問題:有一面牆,長度為n,每次選擇一段牆來染色,一開始4-6繪製成黃色,然後1-10繪製藍色,2-7繪製紅色,若干次繪色之後能看見多少種顏色,或者是在區間「i,j」區間裡面可以看到多少種顏色。所以主要有兩個操作,染色操作和查詢操作。使用陣列操作其實是可以的,染色就只需要把對應下標的內容,修改就好了;查詢只需要遍歷,這樣複雜度就都是,這樣很明顯效率是不夠的。
實質的應用就是區間查詢,比如統計2017年的消費最高的使用者,或者是某一個太空區間天體的總量。使用線段樹的之後:

使用陣列實現 使用線段樹實現
更新
查詢

10624272-a9d8c7de7ea48a9e.png

線段樹大致的樣子。可以看到線段樹不是完全二叉樹,因為如果是十個元素,是不一定都集中在左邊的,這時候就不一定是完全二叉樹了,但是一定是平衡二叉樹。平衡二叉樹的定義是,最短深度和最大深度的差只能為1,也就是不能超過1。雖然不是完全二叉樹,但是依然可以用陣列來表示,將下面的空節點全部補全,這樣這棵樹就變成滿二叉樹了。如果區間有n個元素,而
,那麼就需要2n個空間來儲存。如果不是,那麼就需要4n個,因為多出的那一個需要一整行取填補它。

Create

建立線段樹其實很簡單,可以看到上面是對半分開。

    private int leftChild(int index) {
        return 2 * index + 1;
    }

    private int rightChild(int index) {
        return 2 * index + 2;
    }

求左右子樹的下標。

public class SegmentTree<E> {
    private E[] data;
    private E[] tree;
    private Merger<E> merger;

    public SegmentTree(E[] arr, Merger<E> merger) {
        this.merger = merger;
        data = (E[]) new Object[arr.length];
        for (int i = 0; i < arr.length; i++) {
            data[i] = arr[i];
        }

        tree = (E[]) new Object[4 * arr.length];
        buildSegmentTree(0, 0, data.length - 1);
    }

data是傳進來的資料,tree是樹的資料merger是操作。還是遞迴,因為樹這種資料結構用起遞迴是天然的方便。引數要有兩個主要的引數,左邊和右邊的邊界,其實按照上面的圖就是中分。當l >= r的時候就是遞迴的最終條件,這個時候直接相等即可,否則就遞迴構建。

    private void buildSegmentTree(int treeIndex, int l, int r) {
        if (l >= r) {
            tree[treeIndex] = data[l];
            return;
        } else {
            int leftTreeIndex = leftChild(treeIndex);
            int rightTreeIndex = rightChild(treeIndex);
            int mid = l + (r - l) / 2;
            buildSegmentTree(leftTreeIndex, l, mid);
            buildSegmentTree(rightTreeIndex, mid + 1, r);
            tree[treeIndex] = merger.merger(tree[leftTreeIndex], tree[rightTreeIndex]);
        }
    }

merger是一個介面,這是因為如果把這個功能寫死了,那麼線段樹的功能就死了。比如求和,如果寫死了那麼這個樹就只能求和。而如果加上了介面,最小值最大值也是可以的。線段樹其實也是一種空間換時間的做法。

    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        SegmentTree<Integer> segmentTree = new SegmentTree<Integer>(arr, (a, b) -> a > b?a:b);
        segmentTree.show();
    }

查詢最大值。Merger就是就是一個介面,具體實現是看業務需求。

Query

線段樹是二分的,那麼這個時候如果要查詢的區間並不是是剛剛好二分的一邊那就需要一個區間找一點一個區間找一點這樣的來組合了。比如找A[2,5],那麼[0,3]找一邊,[4,7]找一邊就好了。
同樣是使用遞迴實現,首先引數要包含的就是樹的邊界和要求的邊界,如果邊界是完全一樣的,直接把當前樹對應的值返回即可,如果邊界是在左邊,那就遞迴左邊找,右邊就右邊找,兩邊都包含還是遞迴相加返回即可。

ry(int queryL, int queryR) {
        if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR) {
            throw new IllegalArgumentException("permater is illgel!");
        }
        return query(0, 0, data.length - 1, queryL, queryR);
    }

    private E query(int treeIndex, int l, int r, int queryL, int queryR) {
        if (l == queryL && r == queryR) {
            return tree[treeIndex];
        }
        int mid = l + (r - l) / 2;
        int left = leftChild(treeIndex);
        int right = rightChild(treeIndex);
        if (queryL >= mid + 1) {
            return query(right, mid + 1, r, queryL, queryR);
        } else if (queryR <= mid) {
            return query(left, l, mid, queryL, queryR);
        } else {
            return merger.merger(query(left, l, mid, queryL, mid), query(right, mid + 1, r, mid + 1, queryR));
        }
    }

Leecode303

10624272-dde8b7a13d7299ff.png

這道題目其實就可以用剛剛的線段樹解決,使用上面的線段樹就可以直接解決。但其實這並不是最好的解決方法,因為這個題目在前面是要求有一個建構函式的,聯想到在ACM這些題目中有一種方法就是打表法,打表時間是不計入時間的,使用這個時候就可以在建構函式的時候計算迭代元素的和即可,比如存到了第4個元素,那麼第四個元素這個空就不存第四個元素,而是存第四個元素前面的和,包括了第四個元素,也就是[0,4]的和,這樣計算的時候只需要一次減法即可。但是這裡學到了SegmentTree就直接用了

public class NumArray {
    private SegementTree segementTree;

    public NumArray(int[] nums) {
        segementTree = new SegementTree(nums);
    }

    public int sumRange(int i, int j) {
        if (segementTree == null){
            return 0;
        }
        return segementTree.query(i, j);
    }

    public static void main(String[] args) {
        int[] nums = {-2, 0, 3, -5, 2, -1};
        NumArray numArray = new NumArray(nums);
       // System.out.println(numArray.sumRange(0, 2));
    }
}

Update

再來看一道題目:

10624272-babc283ef4d861d9.png

這個題目和上一個題目是有相似之處的,但是不同的是它是要求可變的,也就是涉及到了線段樹的可變操作。
線段樹的更新其實最簡單的。首先引數肯定是更新的索引和變數,遞迴尋找索引所存在的樹,找到之後不論左右子樹有沒有改變,全部重新相加即可。更新還是查詢複雜度都會是

  
    public void set(int index, E e) {
        if (index < 0 || index >= data.length) {
            throw new IllegalArgumentException("index is illgel!!!");
        }
        data[index] = e;
        set(0, 0, data.length - 1, index, e);
    }

    private void set(int treeIndex, int l, int r, int index, E e) {
        if (l == r){
            tree[treeIndex] = e;
            return;
        }
        int mid = l + (r - l)/2;
        int left = leftChild(index);
        int right = rightChild(index);
        if (index <= mid){
            set(left, l, mid, index, e);
        }else if (index > mid){
            set(right, mid+1, right, index, e);
        }
        tree[treeIndex] = merger.merger(tree[left], tree[right]);
    }

上面那個題目也是一樣的,只需要在update裡面新增set函式即可。

二維線段樹

這裡介紹其實是一維的線段樹,還存在有二維的線段樹。一維的意思就是我們處理的就是一個一維的資料,也就是一條線上。同樣把這個思想擴充到二維空間:
10624272-374bfdfa5fb83888.png

線段樹只是一種設計思想,三維就分成八個,也是一樣的。另外,這裡的線段樹區分是使用平均操作,但是有時候在某一個區間訪問很少,再某一個區間很多,這樣就可以不均分,也叫動態線段樹。

字典樹Trie

之前所討論的樹都是二叉樹,無論是搜尋時還是線段樹,或者是後面要講的紅黑樹都是二叉樹,而字典樹是多叉樹。通常字典樹是用來處理字串。比如有20萬個字元,要進行查詢,如果是用樹結構基本就是,但是如果用字典樹,這複雜度和數量沒有關係了,只和你查詢的這個字串的長度有關。

10624272-52379e5d6ee64a4c.png

當遍歷到葉子,那麼就遍歷出了一個單詞。在設計節點的時候,按照常規操作,還是要儲存節點內容,和指向下一個節點的指標。但是要注意,在查詢的時候,我們是直接知道了下一個字元,也就是在查詢之前就已經知道下一個字母了,和查字典一樣,所以是並不需要當前節點的內容。如果一個單詞是另一個單詞的結尾,那麼有可能就不是葉子節點了,所以還需要一個標識來標識這個是不是單詞結尾。

Create

建立字典樹就比較簡單了,之前提到,需要標識和指向下一個節點,由於這個節點數量是不知道的,所以用對映來替代。

public class Trie {
    private class Node {
        public boolean isWord;
        public TreeMap<Character, Node> next;

        public Node(boolean isWord) {
            this.isWord = isWord;
            next = new TreeMap<>();
        }

        public Node() {
            this(false);
        }
    }

    private Node root;
    private int size;

    public Trie() {
        root = new Node();
        this.size = 0;
    }

add

新增元素也很簡單,新增的時候遍歷字串下一個字元,通過這個字元找到下一個節點,如果到了最後一個,那麼就要設定標識,表示這個是一個單詞。但是這裡有一個小陷阱,有可能有插入重複單詞,這個時候就需要判斷標識來維護size了。

    public void add(String word) {
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (cur.next.get(c) == null) {
                cur.next.put(c, new Node());
            }
            cur = cur.next.get(c);
        }
        if (!cur.isWord) {
            cur.isWord = true;
            size++;
        }
    }

Selection

查詢方法其實也很簡單,首先看看當前next的對映裡面有沒有當前要查詢的字元,如果沒有,直接就返回false,如果有就留下。當遍歷到最後一個的時候不能直接返回true,因為如果標識不是true,那麼只能說明是恰好有這個單詞在,而不是有這個單詞,這個單詞可能是剛剛好是某一個單詞的字首而已。

    public boolean contains(String word) {
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (cur.next.get(c) != null){
                cur = cur.next.get(c);
            }else {
                return false;
            }
        }
        return cur.isWord;
    }

字首搜尋

電話本的搜尋如果你打入一個字母,就可以出現前面單詞是這個字母的所有人,這個就是字首搜尋,資料庫也有這樣的搜尋。
其實和之前的搜尋沒有什麼差別:


    public boolean isPrefix(String word){
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (cur.next.get(c) == null){
                return false;
            }
            cur = cur.next.get(c);
        }
        return true;
    }

Leecode208

Leecode裡面有一道這樣的題目,就是實現字典樹:


10624272-67c3f4998f85a793.png

和之前所實現的完全一樣,直接替換一下即可。

Leecode211

10624272-d3615656eabe2422.png

和以往的不同,這個題目新增了類似於正規表示式的匹配,點代表任何字元。其他都差不多,就是搜尋不一樣。遇到點自然就是要遍歷所有的可能了。使用遞迴實現比較簡單,首先是判斷邊界條件,如果要比較的索引已經是最後一個字元了,那就直接返回標識。標識有的才能算有。如果不是,那就要遞迴訪問了,是具體字元,就和之前的查詢一樣,如果是".",遍歷所有下一個節點的字元,只要有一個符合那就可以繼續,如果都沒有或者是空,那麼久可以返回false了。

  
    private boolean match(Node node, String word, int index) {
        if (index == word.length()) {
            return node.isWord;
        }
        char c = word.charAt(index);
        if (c != '.') {
            if (node.next.get(c) == null) {
                return false;
            } else {
                return match(node.next.get(c), word, index + 1);
            }
        } else {
            for (char key : node.next.keySet()) {
                if (match(node.next.get(key), word, index + 1)) {
                    return true;
                }
            }
            return false;
        }
    }

Trie作為map使用

10624272-2c35cec0476e465b.png

這個題目要求計算包含字首的單詞的權值相加是多少。用遞迴很容易可以解答。首先要確認字首在這個字典樹裡面有的,然後再字首的基礎上把後面的單詞全部加起來即可。因為我們關注的是value值而不是單詞,所以不需要Word,也就是標識,有沒有都無所謂,value為0就沒有單詞,也可以替代的。

public int sum(String prefix) {
        Node cur = root;
        for (int i = 0; i < prefix.length(); i++) {
            char c = prefix.charAt(i);
            if (cur.next.get(c) == null){
                return 0;
            }
            cur = cur.next.get(c);
        }
        return sum(cur);
    }

    private int sum(Node cur){
        int res = cur.value;
        for (char key : cur.next.keySet()){
            res += sum(cur.next.get(key));
        }
        return res;
    }
}

最後的遞迴看起來好像沒有判斷遞迴到底的條件,但是實際上已經包含了,當節點的下一個是空的時候,就不經過for迴圈了,就可以直接返回。

AVL樹

對於二叉樹,有時候會存在一些比較極端的情況,如果按照順序插入,就會變成類似連結串列那種情況,這個時候就複雜度就會回到n了,沒有體現出樹的優勢。所以在插入的時候,我們需要維持樹的結構。比較經典的平衡二叉樹之一就是AVL樹了。

平衡二叉樹

首先宣告是平衡二叉樹,首先滿的二叉樹肯定是一棵平衡二叉樹,平衡二叉樹可以使得樹的高度可以達到一個最低的高度。線段樹也是一個平衡二叉樹。在AVL裡面,任意一個節點左右節點高度差不不能超過1。所以需要標註每一個節點的高度,高度可以從底層開始計算,有了高度,那麼就可以計算平衡因子,平衡因子其實就是兩棵左右子樹的高度差,如果高度差沒有超過1那麼就算是平衡了。

新增操作

出現不平橫的時候只有兩種情況,一個就是新增的時候,另一個就是刪除的時候。插入的時候,需要向上維護平衡性。
1.插入的元素是在不平衡節點的左側的左側。也就是一直向左側插入元素。

10624272-4c1a82dd8716774b.png
這也是最簡單的一種情況,插入的時候左孩子比較深,補全:
10624272-f7752be67f49ed88.png

可以看到,這種情況下首先當前節點的平衡因子一定大於0,其次左側孩子的平衡因子是大於等於0。第一個條件很好理解,二叉樹不平衡的條件,第二個條件是因為,既然是左側多新增了一個節點,左邊肯定比右邊高,而平衡因子在這裡計算的是左邊減去右邊,所以肯定是大於等於0的了。事實上應該只有大於0,因為等於0的話,那就意味著左邊子樹單獨拿出來是平衡的,但是如果是平衡又要比右邊子樹多的話,那就意味著要比右邊多數兩個,但是多出一個的時候就已經需要改變的了,所以應該只是大於0,然而,這只是在新增的情況下有,刪除就不一定了,比如上面情況,右邊子樹的有一個右子樹,刪除的時候剛剛高把那個右子樹刪了,而左邊恰好等於0,那麼還是需要換的。另外,還有一個更重要的原因,當左子樹等於0的時候,或者是右子樹等於0的時候,進行的旋轉只能是左旋轉,或者右旋轉,當然對應的前提條件是當前節點是左邊導致的不平衡或者是右邊。**這個時候一個要進行右旋轉。 **

右旋轉
10624272-9010a921b33b8c74.png

5對應的右節點接到7的左節點上,旋轉完之後還需要更改高度,先更改子樹的高度再更改節點高度。

2.插入的元素是右子樹的右子樹,也就是一直向右側插入元素。
這個時候就應該是右側子樹導致的,一直向右側插入元素,那麼右子樹的右邊肯定也多出來了。所以就需要左旋轉。

左旋轉
10624272-bcba4b8188298f19.png

3.如果是往左子樹的右子樹插入了導致不平衡,那麼一次旋轉是不可以的了。這種情況下只能回到我們之前解決過的情況處理,可以通過先把當前節點的左節點進行左旋轉變成情況一,再對當前節點進行右旋轉即可。所以就是先對左子樹進行左旋轉,再對當前節點進行右旋轉。
4.如果是往右子樹的左節點新增導致了不平衡,那麼就需要先右旋轉再進行左旋轉即可。和上面的情況其實是兩兩對應的。

    public Node add(Node node, K key, V value) {
        if (node == null) {
            size++;
            return new Node(key, value);
        }
        if (key.compareTo(node.key) > 0) {
            node.right = add(node.right, key, value);
        } else if (key.compareTo(node.key) < 0) {
            node.left = add(node.left, key, value);
        } else {
            node.value = value;
        }
        node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
        int balanceFactor = getBalanceFactor(node);
        if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
            return rightRotate(node);
        }
        if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
            return leftRotate(node);
        }
        if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
            node.left = leftRotate(node.left);
            return rightRotate(node);
        }
        if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
            node.right = rightRotate(node.right);
            return leftRotate(node);
        }
        return node;
    }

先新增元素,新增完之後再依照上面的情況進行調整。

刪除操作

刪除的時候,維護其實是一樣的。 上面實際上已經列舉了所有的不平衡的情況,所有這裡只需要在刪除的時候新增葛總不平衡的情況即可。

總結

在AVL樹中還可以進行優化,每一次查詢和刪除都是達到了logn的時間複雜度。但是每一次更新都需要維護高度,如果當前節點是沒有改變高度的話,其實是不需要維護上層節點的高度的了。因為從祖先節點來看,子樹高度是沒有變化的。AVL樹還有一種優化結構,就是紅黑樹這種資料結構。

紅黑樹

樹的平衡

樹的平衡是指樹的每一個節點的左右子節點的數目大致一樣。兩邊都相等是最好的,當然這種情況很少見,一般都是兩邊大致相等。比如在二叉搜尋樹的時候,如果插入的數字是有順序的,那麼就容易退化成極其不平衡的連結串列,搜尋複雜度就會變成了。所以對於插入順序不是平衡的時候,之前所學過的二叉樹就不再是一種好的資料結構了。這個時候就要使用紅黑樹了,紅黑樹其實也是一種二叉樹,只不過是增加了某種特性的二叉樹。如果在插入或刪除的時候如果出現了不平衡的狀態,那麼就要進行調整,保持樹的平衡。

紅黑樹的特徵

首先每一個節點都有顏色,在刪除和新增的過程中是需要保持這些顏色的排列規律。從2-3樹來看。2-3樹是滿足二分搜尋樹的基本性質的。但是2-3樹不是二叉樹,節點可以存放一個元素或者是兩個元素。

10624272-de9c90f532248bec.png
可以看到,如果只有一個元素,那麼只能伸出兩個節點;但是如果存放了2個節點,那麼就可以存放三個節點。那麼如果要滿足二分搜尋樹的性質,如果是一個節點的時候,左小右大,兩個節點的時候就需要左小中右大,中間的要夾在bc中間。
10624272-e83797c2962df413.png
總體的樹結構。二三樹一顆絕對平衡的樹,從根節點到葉子節點經過的節點數是一致的。對於AVL樹,它的平衡條件沒有太嚴格。

那麼2-3樹是如何維持絕對平衡的?

首先新增一個節點42,如果是空節點,那麼就直接新增作為根節點,這個時候只有一個節點的樹是一個平衡樹。再新增一個37節點,按照二叉樹的常規操作,應該和當前節點比較,看看新增到哪裡合適,但是對於2-3樹不是,它永遠不會新增到一個空的節點,會新增到最後一個葉子節點上,現在只有一個根節點,所以就需要和42融合,形成了一個[37,42]的節點。再新增一個12,12比他們都要小,所以應該新增到左子樹去,永遠不會去空的位置,所以還得需要融合,雖然這個融合不符合規矩,但是先進行融合等等處理,這個時候就出現了4節點[12,37,42]。由於不符合規矩,所以可以很容易的就分裂成三個節點。中間節點是37,左右兩邊分別是12和42。再次新增一個18節點,應當新增在左邊,所以自然就新增在了12這個葉子節點上面,滿三個自然就分裂。當新增到第二層的時候再分裂就是這個樣子:
10624272-695dfbc7665badda.png

但是可以看到,其實不是絕對平衡的。如果一個葉子節點本來就是三節點,新增到一個新的節點變成四節點在進行拆解的時候,就需要和它的父親節點融合。
10624272-3e8190f6ed1345e1.png
所以在整一個新增過程中,2-3樹的結構是絕對平衡的。然而還有一種情況:
10624272-d3c2e2668d70cfae.png
按照常規操作,上浮融合,自然根節點也要進行融合分裂:
10624272-5042fadae6b4d989.png

很明顯還是一個平衡的,所以在整一個新增過程中是絕對平衡的。 在葉子節點達到四節點的時候就需要一步一步往上堆,一直到根節點。

紅黑樹的規則

1.每一個節點不是紅色就是黑色。
2.根節點總是黑色。
3.如果節點是紅色,那麼子節點一定是黑色。但是黑色節點下面的子節點可以是紅色也可以是黑色。
4.每一個葉子節點,這裡的葉子節點不是指有數值的葉子節點,而是指最後的空節點叫做葉子節點,也就是null節點是黑色的。
5.從任意一個節點到葉子節點經過的黑色節點是一樣的。

10624272-c823cf9aaf8cb911.png

這些規則直接提出來不好理解,來看看對應於2-3樹是怎麼理解的。首先看看紅黑樹顏色在2-3樹中是代表了什麼意義,如果葉子節點是紅色,那麼就代表這個節點和根節點是融合在一起的,黑色就是分開,這個性質很重要。在紅黑樹中根節點一定是黑色,那麼一開始新增的42就是黑色的,然後新增了一個37,這樣這兩個節點就對應了2-3樹的三節點,來了12後紅黑樹的形狀:
10624272-639aaf8ddd24a3d1.png
可以看到,葉子節點是紅色的,按照剛剛的表述,其實是和父節點融合的,分裂後只需要改變顏色就好了,所以把兩個葉子節點變成黑色即可。
10624272-07d6264227a3825f.png
如果這個節點是葉子節點,這個根節點要繼續向上融合,所以這個節點要變成紅色才代表向上融合。
10624272-ee59c5e72a273efd.png
這個就對應了在三節點上新增元素的操作。
這是一種情況,第二種情況就是再新增的時候節點是合併在了葉子節點上:
10624272-8492fdb160dd0a22.png
這個時候就需要右旋轉了,然後再做顏色翻轉即可。但是做了顏色翻轉之後需要以當前紅色的根節點做向上的判斷,因為根節點變成了紅色,可能會出現兩個連續紅色節點的情況,因為紅色節點的葉子節點一定要黑色。這種情況其實對應的就是新增之後節點分裂的情況。上面第一種情況也是需要分裂,但是不一樣的是新增的順序不一樣導致了分裂不一樣。還有一種情況就是插入了左節點的右子樹,這種情況也是融合,但是處理方式有所不一樣。事實上用2-3-4樹來理解也是可以的。
再回到上面的幾個規則,如果一個根節點是黑色,葉子節點一定是紅色,這是因為只有紅色才代表了融合,這個時候就是一個三節點了。最後一個經過同等數量的黑色節點,其實就是經過了2-3樹的節點,因為二三樹是絕對平衡的,所以紅黑樹也有這個性質。至於為什麼每一次新增都是紅色節點,是因為2-3樹永遠不會新增到空節點,會產生融合,所以新增紅色節點代表融合。

新增操作

新增的時候,新增的節點將是紅色的,從2-3樹的角度來理解,因為加入的葉子節點是不能插入到空節點的,所以自然是紅色代表融合了。從紅黑樹本身的增刪功能來看,新增紅色是最保險的方法,因為紅黑樹是看黑色節點的數量來保持平衡的,直接新增黑色一定會導致不平衡,因為會有一條路多了一個黑色,本來是平衡的,加了一個另外的勢必不平衡。所以設定成紅色是影響最小的。
紅黑樹的修正手段也就幾種,首先就是改變顏色了,然後就是執行旋轉操作。

10624272-f39a1dd512421129.png
旋轉其實也很容易理解,左旋轉的時候,beta這個節點接上了x沒有位置放了,所以只能接在x上,右旋轉也是一樣的意思。所以紅黑樹的插入演算法就需要做出改變,插入的時候前面的步驟是一樣的,從根節點向下查詢要插入父節點的位置,插入節點之後,後面就需要新增檢測樹的操作,檢測這個樹是否是紅黑樹了,如果不是,那麼就要進行修正。
新增修正的情況有三種,其實對應的就是上面提到的三個情況了:
①插入的是根節點,那麼直接就可以把當前節點變成黑色了,對照規則一,根節點為黑色。同時在2-3樹中黑色代表單個節點,這是自然的了。
②父節點是黑色,這種情況下是沒有違反任何規則,完美度過。
③當父節點是紅色,叔叔節點也是紅色的時候,就需要處理了,新增的節點本身就是紅色,父親又是紅色,這就違反了性質4。
由於是連續的兩個黑色,那麼只需要把父節點設定成黑色就好了,但是設定黑色會違反性質5,所以是行不通的。
10624272-6228944fce534e44.png
左邊紅色的點就是插入的。這裡要注意的是,祖父節點一定是黑色,紅色不可能,紅色節點葉子節點一定是黑色。當把父親節點設定成黑色之後,問題來了,插入節點這邊多了一個黑色,所以把叔叔節點也設定成黑色,這樣的話這整一個分支就變多了一個黑色節點了。所以把祖父節點變成紅色即可:
10624272-5ea876c14f4eae36.png
其實上面的步驟是可以看做是一個2-3樹的分裂新增過程,一開始可以看到祖父節點的兩個節點是紅色,所以祖父是一個三節點,暫時的三節點,還沒有分裂而已,再新增一個的時候需要分裂,所以分出來的那兩個節點是黑色,2-3樹分裂的時候如果不是根節點是要和上頭合併的,所以祖父節點就是紅色了,插入就常規插入到葉子節點合併。在這裡不一定是看成是2-3樹,看成2-3-4樹也是可以的。
④當父節點是紅色,叔叔節點為黑色,插入的節點是父節點的左孩子。
10624272-0ccd79f66e397029.png
違反了4的規則,如果把左邊的一個節點變成黑色就可以了,但是這樣右違反了5,所以需要祖父的兩個分支增加一個黑色的,祖父節點變成紅色的來處理,可以把父節點變成黑色,祖父節點變成紅色,右旋轉即可。這個情況其實就是剛剛2-3樹模擬的時候的第二種情況了,分裂的時候。
⑤當父節點是紅色色,叔叔節點是黑色,插入為右孩子。
10624272-923e9cea858876cc.png
這個情況和上面的情況很相似,左旋轉就回到了上面的情況,按照上面情況處理即可。
總的來說,就是對應著2-3樹的分裂過程,只不過對應的結構不同可能有所差異。
10624272-71a910094e194853.png

刪除操作

這個操作,有有點複雜了。
①刪除的就是本身的根節點,而且根節點左右子樹是空的。直接刪就好了,沒有上面好講的。
②如果刪除的節點是紅色的,那麼它的父親節點就一定是黑色的。因為是紅色的,直接拿孩子節點補充就好了,因為是沒有影響的。
③如果刪除節點是黑色的,而且兄弟節點是紅色的,兄弟節點的孩子節點是黑色,刪除節點的父親節點也是黑色的。

10624272-24f140b8c7668b8f.png
互換兄弟節點和父節點的顏色,然後對父節點做左旋轉。這樣還沒結束,這樣的結果就可以使得變成下面的情況處理了,,這個時候左子樹就可以作為5處理了。
④如果刪除節點是黑色,父節點和兄弟節點以及兄弟節點的孩子都是黑色。
10624272-2d7abb0b452448f1.png
左邊少了一個黑色的節點,那麼就需要右邊也少一個,所以把兄弟節點變成紅色即可。這個時候這整一顆樹就都少一個黑色節點了,問題是,這整一顆樹有可能只是一小部分,所以還需要回到情況1討論,這裡處理之後回到情況1判斷。
⑤刪除的節點為黑色,父親節點為紅色,兄弟節點和兄弟節點的孩子為黑色
10624272-d308a3221ac72066.png
這個情況下就只需要互換顏色即可,左邊少了一個黑色節點,這個時候只需要把父節點和兄弟節點交換即可,因為因為交換之後兩個子樹都會經過這個父節點,而整個子樹黑色高度沒有變化。這個情況就是真正完成的。
⑥刪除的節點是黑色,兄弟節點也是黑色,兄弟孩子的左節點是紅色,兄弟節點的右子樹為黑色,父親節點隨便顏色。
10624272-6d10471de25380fb.png
這個時候就要對兄弟節點做右旋轉,然後對調兄弟節點和兄弟左孩子節點,那麼情況就轉移到情況7處理了。
⑦刪除的節點是黑色,兄弟節點也是黑色,兄弟節點的右孩子為紅色,父親節點和兄弟節點左孩子隨便顏色。
10624272-d7554d47733703cc.png
這種情況下,先互換父節點和兄弟節點的顏色,再對父節點進行左旋轉操作,那麼左邊,也就是刪除了節點的這邊刪除前和刪除後是沒有變化的,有變化的主要是右邊。
10624272-4bb4fc9f6d9c41fb.png
圖錯了,兄弟節點的右子樹是紅色,我懶的改了。
上圖所指示的標籤都是根據原樹結構標註。沒有經過父節點孩子的右兩條路,一條是左子樹-》右,一條是-》右。左子樹那條是兄弟節點-》父節點-》兄弟節點的孩子節點,這是更新之後的,沒有更新之前的是右子樹-》左子樹,也就是父節點-》兄弟節點,-》兄弟·節點孩子節點,可以看到只是換了一個順序而已,既然之前是平衡樹,換個順序肯定黑色節點的數量不會變吧,況且只是交換了顏色而已。
那麼只是有一條路要跟新了,也就是經過兄弟節點的右子樹那條,紅色那條。之前是經過了父節點-》兄弟節點-》兄弟節點的右節點,現在只是經過了兄弟節點和右節點,明顯是少了一個黑色節點,很顯然,該黑色就OK了,所以上圖是最終改完的結果,之所以說錯了只是還沒到說到那一步。
要注意的是,在很多的部落格文章中,刪除情況有所區別,有五種的也有四種的,這裡是五種的,四種的是因為把第四和第五中情況和起來了。四種的話應該是這麼寫,兄弟節點和孩子全是黑色,把兄弟節點變成紅色,當前節點切換到父節點重新向上判斷。這裡只是把父節點分開了。本質是完全一樣的。
10624272-309823bf88ea3289.png

紅黑樹的實現

程式碼不太好,實現起來有點複雜,哎!

    private class Node<T extends Comparable<T>> {
        public boolean color;
        public T key;
        Node<T> left;
        Node<T> right;
        Node<T> parent;

        public Node(T key, boolean color, Node<T> parent, Node<T> left, Node<T> right) {
            this.key = key;
            this.color = color;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }

        public T getKey() {
            return key;
        }

        @Override
        public String toString() {
            String s = "" + key + (this.color == RED ? "R" : "B");
            if (this.left != null) {
                s += ("  " + left.key);
            }
            if (this.right != null) {
                s += ("  " + right.key);
            }
            return s;
        }
    }

由於帶上了父母節點,用遞迴實現帶父母節點的維護,就有點難度了。
首先新增操作的更新,新增操作使用的是迭代實現,篇幅較大,不貼了。

    private void insertFixUp(Node<T> node) {
        Node<T> parent, grandParent;
        while ((parent = parentOf(node)) != null && isRed(parent)) {
            grandParent = parentOf(parent);
            if (parent == grandParent.left) {
                //third condition,the uncle is red
                Node<T> uncle = grandParent.right;
                if (uncle != null && isRed(uncle)) {
                    setBlack(uncle);
                    setBlack(parent);
                    setRed(grandParent);
                    node = grandParent;
                    continue;
                }
                //fifth condition,the uncle is black and left child of the current node
                if (parent.right == node) {
                    Node<T> temp;
                    leftRotate(parent);
                    temp = parent;
                    parent = node;
                    node = temp;
                }
                //fourth condiction,same as above but the right child of the current node.
                setBlack(parent);
                setRed(grandParent);
                rightRotate(grandParent);
            } else {
                //third condition,the uncle is red.
                Node<T> uncle = grandParent.left;
                if (uncle != null && isRed(uncle)) {
                    setBlack(uncle);
                    setBlack(parent);
                    setRed(grandParent);
                    node = grandParent;
                    continue;
                }
                //fifth condition,the uncle is black,left child
                if (parent.left == node) {
                    Node<T> temp;
                    rightRotate(parent);
                    temp = parent;
                    parent = node;
                    node = temp;
                }
                //fourth condition,the uncle is black,right child
                setBlack(parent);
                setRed(grandParent);
                leftRotate(grandParent);
            }
        }
        setBlack(root);
    }

每一次處理完一種情況之後要記得更新當前節點node的資訊,因為處理完後不一定就結束了,可能是更新到了一種新的情況。
刪除操作的更新,這個有點複雜。

    private void removeFixUp(Node<T> node, Node<T> par) {
        Node<T> uncle;
        Node<T> parent;
        parent = node == null ? par : node.parent;
        while ((node == null || isBlack(node)) && node != root) {
            if (parent.left == node) {
                uncle = parent.right;
                //the uncle is red, condition three
                if (isRed(uncle)) {
                    setBlack(uncle);
                    setRed(parent);
                    leftRotate(parent);
                    uncle = parent.right;
                }
                //the uncle and his child are all black
                if ((uncle.left == null || isBlack(uncle.left)) &&
                        (uncle.right == null || isBlack(uncle.right))) {
                    setRed(uncle);
                    node = parent;
                    parent = parentOf(node);
                } else {
                    //the uncle is black and red of his child on the left
                    if (uncle.right == null || isBlack(uncle.right)) {
                        setBlack(uncle.left);
                        setRed(uncle);
                        rightRotate(uncle);
                        uncle = parent.right;
                    }
                    setColor(uncle, colorOf(parent));
                    setBlack(parent);
                    setBlack(uncle.right);
                    leftRotate(parent);
                    node = this.root;
                    break;
                }
            } else {
                uncle = parent.left;
                if (isRed(uncle)) {
                    setBlack(uncle);
                    setRed(parent);
                    rightRotate(parent);
                    uncle = parent.left;
                }
                if ((uncle.left == null || isBlack(uncle.left)) &&
                        (uncle.right == null || isBlack(uncle.right))) {
                    setRed(uncle);
                    node = parent;
                    parent = parentOf(node);
                } else {
                    if (uncle.left == null || isBlack(uncle.left)) {
                        setBlack(uncle.right);
                        setRed(uncle);
                        leftRotate(uncle);
                        uncle = parent.left;
                    }
                    setColor(uncle, colorOf(parent));
                    setBlack(parent);
                    setBlack(uncle.left);
                    rightRotate(parent);
                    node = this.root;
                    break;
                }
            }
        }
        if (node != null) {
            setBlack(node);
        }
    }

每次處理完後也要記得更新節點的資訊,第一種情況更新完的時候,uncle節點不再是原來的了,所以要進行更新。第二種情況也是,由於處理節點已經切換到了父親節點,於是要對父親節點切換,第三種情況uncle節點也發生了改變,同樣要切換。第四種情況直接結束。

最後附上GitHub及其程式碼:https://github.com/GreenArrow2017/DataStructure_Java/tree/master/out/production/DataStructure_Java/Tree

相關文章