紅黑樹其實很簡單
1、背景
在開發過程中免不了需要維護一組資料,並且要能夠快速地進行增刪改查。如果資料量很大並且需要持久化,那麼就選擇資料庫。但如果資料量相對少一些不需要持久化並且對響應時間要求很高,那麼直接儲存在本地記憶體中就最合適了。
2、連結串列
將資料儲存在本地記憶體也不是隨便存的,需要按不同的場景選擇合適的資料結構。我們先來看下面這一組資料,54、24、74、12、42、62、83、9、17、33、49、58、65、79、90、5、20、29、37、46、69、86、95,需要選用一個數據結構儲存它們,並且要能支援快速的增刪改查操作。
說起資料結構,一般人第一反應就是最簡單的線性結構,比如說陣列、連結串列、棧或者佇列等。如果有某一個場景可以使用下標隨機訪問資料,那麼毫無疑問陣列是最合適的,畢竟是直接通過記憶體地址(指標)訪問資料,效能槓槓的。但實際開發中絕大多數場景都不可能提供下標訪問資料,所以這裡我們就先使用一個雙向連結串列來看一下:
雙向連結串列實現起來比較簡單,這裡就直接上程式碼了:
public class DoublyLinked<V> implements Iterable<E> { /*連結串列的頭和尾結點,不儲存資料只作為標記*/ private final Node first, last; /*連結串列長度*/ private int size; public DoublyLinked() { this.first = new Node(null, null, new Node()); (this.last = first.next).prev = first; } /*連結串列中節點*/ class Node { V value; Node prev, next; public Node() {} public Node(V value, Node prev, Node next) { this.value = value; this.prev = prev; this.next = next; } }/*獲取連結串列長度*/ public int size() { return size; } /*獲取連結串列頭結點節點*/ public V peekFirst() { Node node = first.next; return last == node ? null : node.value; } /*獲取連結串列尾結點節點*/ public V peekLast() { Node node = last.prev; return first == node ? null : node.value; } /*向連結串列頭部插入節點*/ public void addFirst(V value) { Node first = this.first; first.next = first.next.prev = new Node(value, first, first.next); ++size; } /*向連結串列尾部插入節點*/ public void addLast(V value) { Node last = this.last; last.prev = last.prev.next = new Node(value, last.prev, last); ++size; } /*刪除連結串列頭結點節點*/ public V removeFirst() { Node first = this.first, node = first.next; if (last != node) { (first.next = node.next).prev = first; node.prev = node.next = null; --size; } return node.value; } /*刪除連結串列尾結點節點*/ public V removeLast() { Node last = this.last, node = last.prev; if (first != node) { (last.prev = node.prev).next = last; node.prev = node.next = null; --size; } return node.value; } /*查詢連結串列中的節點*/ public boolean contains(V value) { for (Node node = first.next, last = this.last; last != node; node = node.next) { if (Objects.equals(value, node.value)) { return true; } } return false; } /*替換連結串列中的節點*/ public boolean replace(V source, V target) { for (Node node = first.next, last = this.last; last != node; node = node.next) { if (Objects.equals(source, node.value)) { node.value = target; return true; } } return false; } /*替換連結串列中的節點*/ public void replaceAll(V source, V target) { for (Node node = first.next, last = this.last; last != node; node = node.next) { if (Objects.equals(source, node.value)) { node.value = target; } } } /*刪除連結串列中的節點*/ public boolean remove(V value) { for (Node prev = first, node = prev.next, last = this.last; last != node; node = (prev = node).next) { if (Objects.equals(value, node.value)) { (prev.next = node.next).prev = prev; node.prev = node.next = null; --size; return true; } } return false; } /*刪除連結串列中的節點*/ public void removeAll(V value) { for (Node prev = first, node = prev.next, last = this.last; last != node; node = (prev = node).next) { if (Objects.equals(value, node.value)) { (prev.next = node.next).prev = prev; node.prev = node.next = null; node = prev; --size; } } } /*建立迭代器*/ @Override public Iterator<E> iterator() { return new Iterator<E>() { private Node current = DoublyLinked.this.first; @Override public boolean hasNext() { return DoublyLinked.this.last != current.next; } @Override public E next() { return (current = current.next).value; } }; } }
上面的連結串列中資料的是無序且可重複的,其中有關首尾節點操作的方法時間複雜度都為O(1),但同時這些操作能適用的範圍也很窄。工作中大多數場景都是隻知道具體的值然後操作連結串列,這時候更多的是使用contains、replace、remove等需要遍歷連結串列中節點的方法,如果維護的是一個值有序或者值去重的連結串列,則插入節點的方法也需要遍歷連結串列。如果每次遍歷查詢節點都是最壞情況也就是每次查詢的節點都在連結串列尾端,這時候時間複雜度就為O(n),其中n為連結串列的長度。在資料量少的場景下是沒什麼問題的,但是如果資料量很大連結串列很長,又遇到了高併發高吞吐量的場景,效能可能就不夠看了。
3、二叉查詢(排序)樹
連結串列的操作弊端很明顯,需要頻繁遍歷節點,但是遍歷這個工作又是省不了的,那麼可以想辦法把需要遍歷的路徑變短,這時二叉查詢(排序)樹就很符合這個需求了。我們先來看一下二叉查詢樹的特點,首先其肯定是一棵二叉樹,其次樹中任意節點的關鍵字值一定大於其左子節點關鍵字值並且小於其右子節點關鍵字值。
3.1、查詢
看圖2,這是一棵二叉查詢數,我們知道一個關鍵字值“37”,需要在樹中找到這個節點。首先與根節點的關鍵字值進行比較,“37”比“54”小所以繼續與根節點左子節點的關鍵字值“24”進行比較,很明顯比24大所以繼續與右子節點的關鍵字值“42”進行比較,以此類推在比較到第5個節點時就找到了關鍵字值“37”這個節點。
這樣查詢資料不需要再遍歷樹中的所有節點,只需要沿著每個節點的一個子節點往下遍歷就可以,大大縮短了查詢路徑,理想情況下時間複雜度變為了O(log n),其時間增長曲線為一個對數函式。
那麼基於這樣的邏輯,查詢最大值和最小值是不是也很簡單,這裡就不多說了。
3.2、插入
上面講解了怎麼查詢節點,這裡繼續來看一下如何往樹中插入一個節點,這裡注意空樹也是一個二叉查詢樹。
插入節點也需要從根結點開始沿著某一個路徑進行遍歷,如果在遍歷的過程中遇到了關鍵字值與需要插入的值相等的節點,則直接做修改操作,否則就繼續遍歷。直到找到最後一個節點,然後將需要插入的值包裝成節點插入到該節點下。
還是看上圖2,需要往樹中插入一個關鍵字值為“52”的節點,先將52與根節點關鍵字值“54”進行比較。52小於54,然後與左子節點關鍵字值“24”進行比較。52比24大,再繼續與42進行比較。52比42大,再繼續與49進行比較。52比49大,本來需要再繼續與關鍵字值為“49”節點的右子節點進行比較的,但是關鍵字值為“49”的節點沒有右子節點,所以這裡就直接把52這個值包裝成節點作為關鍵字值為“49”的右子節點插入。
根據上面的邏輯,如果需要分別插入關鍵字值為“44”和“17”的節點,那麼邏輯應該是什麼樣的呢?大家可以自己試驗一下。
3.3、刪除
刪除節點就比較麻煩一下,這裡大概可以分為兩種情況。一種是刪除節點有兩個子節點,一種是刪除節點最多隻有一個子節點。比如在圖2中分別需要刪除關鍵字值為“65”、“29”、“24”三個節點,其中“24”這個節點的刪除就比較麻煩。
我們先看刪除“65”這個節點,我們需要先找到關鍵值為“65”這個節點,怎麼根據一個值在樹中找到相應的節點,這個不用在過多說明了吧。由於“65”這個節點有一個右子節點,並且“65”是其父節點“62”的右子節點,所以移除“65”這個節點之後,需要將右子節點“69”作為其父節點“62”的右子節點,這樣刪除就完成了。至於刪除關鍵字值為“29”的節點就更簡單了,“29”這個節點是個光棍沒有子節點,將其移除之後不需要再做其他操作。再來看刪除關鍵字值為“24”這個節點的操作,由於24這個節點有兩個子節點,如果將其移除之後兩個子節點再與其父節點建立新的父子關係,那麼就沒有辦法繼續維持二叉查詢樹的結構了。這時就需要使用前繼節點或者後繼結點替換刪除節點,作為新的刪除節點被刪除。
關鍵字值為“24”的節點的前繼節點為“20”後繼節點為“29”,大家看一下這三個節點之間都有什麼關係?從值的大小來看,20是樹中所有關鍵字值中比24小的最大值,29是比24大的最小值。從樹的結構上來看,在水平衡軸上,關鍵字值為“20”和“29”的兩個節點是最靠近“24”節點的,也就意味著“24”的前繼節點不可能有右子節點,後繼結點不可能有左子節點。那麼我們轉換一下思路,假如將“24”這個節點移除之後,是否可以使用“20”或者“29”這兩個節點的其中一個節點填充到“24”的位置上呢。最終刪除“24”被轉換為了刪除“20”或者“29”節點了,問題是不是就解決了。至於如何查詢前繼節點和後繼結點,這裡就不說了,大家可以自己思考一下。
3.4、程式碼示例
public class BSTree<K extends Comparable<K>, V> { private Node root; private int size; /*獲取樹中節點個數*/ public int size() { return size; } /*獲取根節點*/ public Entry<K, V> getRoot() { return null == root ? null : new Entry<>(root.key, root.value); } /*判斷以給定節點為根節點的樹是否為一棵二叉查詢樹*/ public boolean isBSTree(Node node) { // 如果當前節點為空,則從根節點開始遞迴校驗 node = null == node ? root : node; if (null == node) { // 空樹也是一棵二叉查詢樹 return true; } // 獲取當前節點的父節點、左子節點、右子節點 Node parent = node.parent, nodeLeft = node.left, nodeRight = node.right; if (null != parent && parent.left != node && parent.right != node) { // 父節不為空,但是父節點的左右子節點引用都沒有指向當前節點,所以不是一棵二叉樹 return false; } if (null != nodeLeft) { // 當前節點的左子節點不為空 if (nodeLeft.parent != node || nodeLeft.compareTo(node) >= 0) { // 左子節點父節點引用沒有指向當前節點,不滿足是一棵二叉樹。 // 或者左子節點的key值大於等於當前節點的key值,不滿足是一棵二叉查詢樹(當前節點的key值大於左子節點小於右子節點) return false; } if (!isBSTree(nodeLeft)) { // 遞迴左子節點,判斷子樹是否滿足紅黑樹特性 return false; } } if (null != nodeRight) { // 當前節點的右子節點不為空 if (nodeRight.parent != node || nodeRight.compareTo(node) <= 0) { return false; } if (!isBSTree(nodeRight)) { return false; } } return true; } /*根據key查詢value*/ public V find(K key) { if (null == key) { throw new NullPointerException(); } Node node = findNode(key, root); return null == node ? null : node.value; } /*查詢Key值最小的節點*/ public Entry<K, V> findMin() { Node node = root; while (null != node && null != node.left) { node = node.left; } return null == node ? null : new Entry<>(node.key, node.value); } /*查詢Key值最大的節點*/ public Entry<K, V> findMax() { Node node = root; while (null != node && null != node.right) { node = node.right; } return null == node ? null : new Entry<>(node.key, node.value); } /*插入一個節點*/ public V put(K key, V value) { if (null == key) { throw new NullPointerException(); } Node current = root, parent = null; // 獲取節點要插入的位置的父節點 int compare = 0; while (null != current && 0 != (compare = key.compareTo(current.key))) { current = compare > 0 ? (parent = current).right : (parent = current).left; } if (null != current) { // 要插入的key已存在 V ov = current.value; current.value = value; return ov; } if (null == parent) { // 要插入的樹為空樹 this.root = new Node(key, value, null); } else { // 插入新節點 if (compare < 0) { parent.left = new Node(key, value, parent); } else { parent.right = new Node(key, value, parent); } } ++size; return null; } /*刪除節點*/ public V remove(K key) { if (null == key) { throw new NullPointerException(); } Node remove, replace; if (null == (remove = findNode(key, root))) { // 需要刪除的節點在樹中找不到 return null; } V value = remove.value; if (null != remove.left && null != (replace = remove.right)) { // 刪除節點的左右子節點都不為空節點,將刪除節點和後繼節點替換(也可以使用前繼節點替換刪除節點) while (null != replace.left) { replace = replace.left; } remove.key = replace.key; remove.value = replace.value; remove = replace; } // 此時最多隻有一個子節點 Node parent = remove.parent, child = null == (child = remove.left) ? remove.right : child; // 獲取父節點和子節點 if (null != parent && null != child) { // 父節點不為空,並且有一個不為空的子節點 (parent.left == remove ? parent.left = child : (parent.right = child)).parent = parent; remove.parent = remove.left = null; } else if (null != parent) { // 父節點不為空,並且子節點都為空 remove.parent = parent.right == remove ? parent.right = null : (parent.left = null); } else if (null != child) { // 父節點為空,但是子節點不為空 root = child; remove.left = remove.right = child.parent = null; } else { // 父節點和子節點都為空 root = null; } --size; return value; } /*採用中序遍歷二叉樹,保證資料從小到大遍歷*/ public void forEach(Consumer<Entry<K, V>> action) { Deque<Node> deque = new LinkedList<>(); for (Node node = root; node != null || !deque.isEmpty(); ) { for (; node != null; node = node.left) { deque.push(node); } Node pop = deque.pop(); action.accept(new Entry<>(pop.key, pop.value)); node = pop.right; } } /*通用查詢節點*/ private Node findNode(K key, Node current) { Node found = null; for (int compare; null != current && null == found; ) { if ((compare = key.compareTo(current.key)) > 0) { current = current.right; } else if (compare < 0) { current = current.left; } else { found = current; } } return found; } class Node implements Comparable<Node> { K key; V value; Node parent, left, right; public Node(K key, V value, Node parent) { this.key = key; this.value = value; this.parent = parent; } @Override public int compareTo(Node node) { return key.compareTo(node.key); } } public static class Entry<K, V> { private final K key; private final V value; public Entry(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } @Override public String toString() { return "Entry{key=" + key + ", value=" + value + '}'; } } }
通過上面的分析,我們明白了二叉查詢樹的原理,其查詢遍歷節點路徑的長度取決於樹的高/深度。二叉樹中結點左右子樹的高度差為節點的平衡因子,平衡二叉樹所有節點平衡因子為只會為-1、0、1,平衡因子越小樹就越平衡,查詢遍歷節點所能體現出來的效能也就越好。試想一下,如果圖2中的資料事先就已經排好序了,然後依次插入二叉查詢樹中會有什麼結果呢?假如資料是升序排列,那麼插入樹中的值會越來越大,結果就是資料只會一直往右子樹中插入,最終樹就會退化為一個連結串列,平衡因子就為樹中節點的數量。刪除節點的時候也可能造成平衡因子不可預測的增長。
4、紅黑樹
由於二叉查詢樹在插入或者刪除節點的時候,有可能平衡因子會變的很大,甚至會直接退化為連結串列。最典型的就是插入有序資料的時候問題會很嚴重,但是在實際生產過程中,我們並不能避免插入的資料是有序的,但這樣又會破壞樹的平衡性,所以就得想辦法在插入或者刪除節點之後對樹的平衡進行調整。能滿足這種特性的二叉樹我們稱其為自平橫二叉樹,目前行業內已經存在很多很成熟的自平橫二叉查詢樹。
現在常用到的自平橫二叉查詢樹大概有6中,分別為堆樹(Treap)、大小平衡樹(SBT)、伸展樹(Splay)、AVL樹(平衡二叉查詢樹)、紅黑樹(Red-Black)、替罪羊樹(Scapegoat)等。他們有的是通過節點旋轉,有的是通過區域性節點重組等操作進行自平橫。今天我們要介紹的當然就是這其中最亮的那個仔平衡樹中的扛把子紅黑樹。
我們來看一下紅黑樹的特性,首先其肯定是一棵二叉查詢樹,然後為了保持平衡其主要有以下5個特點:
- 每個節點的顏色是紅色或者黑色;
- 根節點顏色是黑色;
- 每個葉子結點(Nil)的顏色都是黑色(這裡的葉子節點是指不存在的空節點);
- 紅色節點的子節點都必須是黑色的(所有路徑中都不允許出現連續的兩個紅色節點);
- 任意節點到其葉子結點的所有路徑上黑色節點的個數是相等的;
紅黑樹和AVL樹不一樣,並沒有顯示地宣告其平衡因子的範圍,但是通過其5個特性卻隱式地保證了樹中任意節點的左右子樹高度差絕對不會超過一倍,所以紅黑樹是一棵近似平衡的二叉查詢樹。那麼說道這一點,紅黑樹只是近似平衡,和AVL樹這種絕對平衡(任意節點左右子樹高度差絕對值不大於1)的樹相比,它憑什麼更有優勢呢?答案就在自平衡的操作上。基於上面的5點特性,紅黑樹在自平衡的時候能儘可能少的處理更多的節點,並且其時間複雜度相對更加穩定。所以目前各領域中紅黑樹應用的相對較多,比如jdk中的HashMap(jdk1.8及以上版本)、TreeMap,STL中的map、set,Linux系統中的epoll等。
上面講解了連結串列、二叉查詢樹等,其實都只是作為拋磚引玉的作用,現在我們就要開始真正進入主題,詳細分析一下紅黑樹了。
4.1、節點旋轉
紅黑樹是通過節點的左右旋轉或者改變節點顏色進行自平橫的。修改節點顏色很好理解,在程式碼裡面無非就是一個欄位不同的值而已,這裡就重點說一下旋轉。節點旋轉分為左旋轉和又旋轉:
- 左旋轉:以旋轉結點作為支點,旋轉節點的右子結點變為旋轉結點的父結點,右子結點的左子結點變為旋轉結點的右子結點,旋轉節點的左子結點保持不變。如圖4:
- 右旋轉:以旋轉結點作為支點,旋轉結點的左子結點變為旋轉結點的父結點,左子結點的右子結點變為旋轉結點的左子結點,旋轉結點的右子結點保持不變。如圖5:
大家可以看一下,樹被旋轉前後的變化。可以發現其依然能夠維持是一棵二叉查詢樹的結構,並且又能夠調節部分節點的結構和左右子樹的高度。所以通過旋轉節點和改變節點的顏色,就能夠將一棵不平衡的紅黑樹(不滿足紅黑樹5個特性的樹)調整成為一棵標準的紅黑樹。下面就開始分析在插入和刪除節點之後,如何讓破壞了平衡性的紅黑樹進行自平橫。
節點說明:
4.2、插入操作
前面講解紅黑樹特性的地方已經說道紅黑樹也是一棵二叉查詢樹,只不過其需要在插入節點之後需要做自平橫操作(節點旋轉或者顏色改變)。所以由此可知,在插入結點的時候紅黑樹和二叉查的操作是一樣的,也就是先找到節點應該插入到什麼位置(父節點的位置),如果在查詢遍歷過程中發現值已存在就做修改,如果不存在就插入。節點插入之後可能就會破壞樹的平衡(不符合紅黑樹5大特性),下面我們就做自平橫操作的分析。
首先,大家要記住一點,在你準備插入一個節點之前,紅黑樹一定是滿足上面那5大特性的,但是由於往樹中插入了一個節點而導致了樹有可能不滿足那5個特性。
一般初始節點都預設設定為紅色節點,為了後面方便描述,在這裡我們統一稱插入節點為“當前節點”,在一棵標準的紅黑樹中插入一個節點之後,則會產生如下4個需要自平橫的場景:
- 場景1:當前結點的父節點為空節點
平衡操作為,直接將當前節點顏色設定為黑色,然後結束平衡操作。
- 場景2:當前節點的父節點為黑色節點
由於前節點為初始節點且父節點是黑色節點,所以其左右子節點為葉子節點,其兄弟節點既可以是紅色節點也可以是葉子節點。如下圖:
注:其中下劃線標註的節點為當前節點。
由於向黑色節點下插入一個紅色子節點不會破壞紅黑樹的平衡,當前場景不需要再做旋轉或者改變節點顏色等操作。
注:在後面的【場景4】中,當前節點不是插入節點時,還會衍生出另一種情況。當前節點的左右子節點為黑色節點,兄弟節點既可以是紅色節點也可以是黑色節。但不論兄弟節點為什麼節點,這兩種情況都可以適配成【場景2】。
- 場景3:當前結點的父節點是紅色節點,並且其叔叔節點不是紅色節點
由於當前節點的父節點是紅色節點,所以其祖父節點為黑色節點。由於當前節點為插入結點且叔叔節點不為紅色節點,所以其左右子節點為、叔叔節點都只能是葉子節點。如下圖:
注:其中下劃線標註的節點為當前節點。
我們以上圖中第一顆樹為例,進行平衡操作。首先將當前節點父節點的顏色置黑並且將祖父節點的顏色置紅,再然後以祖父節點為支點進行右旋轉,然後結束平衡操作。如下圖:
我們再來看上圖中第二棵樹的平衡操作。首先以當前節點的父節點為支點進行左旋轉,然後將當前節點的左子節點設為當前節點。如下圖:
那麼是不是就變成了和第一棵樹一樣的結構了,其他的就不用再多說了吧?
注:在後面的【場景4】中,當前節點不是插入節點時,還會衍生出另一種情況。當前節點的左右子節點、兄弟節點都為黑色節點。但不論如何,這兩種情況都可以適配成【場景3】。
- 場景4:當前節點的父節點是紅色節點,並且其叔叔節點也是紅色節點
由於當前節點的父節點和其叔叔節點為紅色節點,所以其祖父節點為黑色節點。由於當前節點是插入節點且叔叔節點為紅色節點,所以其左右子節點、兄弟節點、叔叔節點的左右子節點都為葉子節點。如下圖:
注:其中下劃線標註的節點為當前節點
我們以上圖中第一樹為例,進行平衡操作。首先將當前節點的父節點和叔叔節點顏色置黑,然後將祖父節點的顏色置紅,再然後將祖父節點設定為當前節點。如下圖:
注:在下面的衍生場景中,當前節點不是插入節點時,還會衍生出另一種情況。當前節點的左右子節點、兄弟節點、叔叔節點的左右子節點都為黑色節點。但是不論如何,這兩種情況都可以適配成【場景4】。
大家不要以為這樣就結束了,我們還得繼續看當前節點的父節點的型別。由此又可以衍生出下面4中情況:
- 當前節點的祖父節點為空節點,可適配【場景1】
- 當前節點的父節點為黑色節點,可適配【場景2】。如下圖:
注:其中下劃線標註的節點為當前節點。
- 當前節點的父節點是紅色節點,並且叔叔節點是黑色節點,可適配【場景3】。如下圖:
注:其中下劃線標註的節點為當前節點。
- 當前節點的父節點和叔叔節點都為紅色節點,並且兄弟節點和叔叔節點的左右子節點都為黑色節點,可適配【場景4】。如下圖:
注:其中下劃線標註的節點為當前節點。
插入總結:平衡的過程是一個迴圈,如果在當前節點和其父節點上處理過後還無法使樹平衡,就需要將祖父節點設為當前節點並進入下一個迴圈處理。我們看【場景1】中當前節點的父節點為根節點、【場景2】中當前節點的父節點為黑色節點,都不需要再旋轉或者改變節點顏色,樹已經是平衡狀態了。而【場景3】、【場景4】中經過多次迴圈旋轉和變色處理將樹平衡過後,我們發現當前節點的父節點要麼是根節點要麼就是黑色節點。所以我們總結,當父節點為根節點或者父節點為黑色節點時,樹就已經平衡了。所以大家就明白為什麼初始插入幾節點預設是紅色的了吧。
可能大家單看上面的場景分析還是不甚明白。但是沒關係,說了這麼多,最終的目的是要在程式碼層面去實現的,大家結合上面的場景分析然後去閱讀後面的程式碼示例才是最終目的。
4.3、刪除操作
相對於上面的插入操作,紅黑樹的刪除操作相對來說會複雜一些,其實也就是需要自平橫操作的場景會稍多一些。和插入一樣,紅黑樹的節點刪除操作也是和二叉查詢樹一樣。首先要找到需要刪除的節點,如果找不到就結束,如果找到了就看子節點情況。如果有兩個子非葉子節點的子節點則使用前/後繼結點替換刪除節點,如果只有一個非葉子結點的子節點(注意紅黑樹中任意節點如果只有一個非葉子結點的子節點時,則該子節點一定是隻有兩個葉子結點)則使用該子節點作為前/後繼結點替換刪除節點,那麼最終刪除節點就只有兩個葉子結點。
這時要注意,我們還不能直接就把需要刪除的節點刪除,因為刪除節點需要參與紅黑樹刪除之前的平衡操作,平衡之後才能將節點刪除。這裡要說明一點,刪除操作之前進行自平橫平的目的是要讓刪除節點變成樹中多餘的節點,也就是說剔除了刪除節點紅黑樹還能保持平衡,反之如果沒有剔除樹就不平衡了。
為了後面方便描述,在這裡我們統一稱刪除節點為“當前節點”,在刪除節點之前我們有4個需要平衡的場景:
- 場景1:當前節點的父節點為空節點
直接結束平衡操作,然後將需要刪除的節點刪除。
- 場景2:當前是紅色節點
由於當前節點是紅色節點,所以當前節點的父節點為黑色節點。由於當前節點為刪除節點,所以其左右子節點為葉子節點,其兄弟節點既可以是紅色節點也可以是葉子節點。如下圖:
注:其中下劃線標註的節點為當前節點。
我們以上圖中第一顆樹為例,進行平衡操作。在這裡當前節點為紅色節點,可以直接將當前節點顏色置黑,然後結束平衡操作,最後再將需要好三處的節點刪除即可。如下圖:
可能有人覺得這裡當前節點本來就是刪除節點,樹既然已經是平衡的了,那麼直接將刪除節點刪除不就行了,還將當前節點的顏色置黑是不是就是多餘的操作呢?這一步操作並不多餘,而且很重要,大家可以繼續往後面看。
注:在後面的【場景5】中,當前節點不是刪除節點時,還會衍生出另一種情況。當前節點的左右子節點為黑色節點,其兄弟節點既可以是紅色節點也可以是黑色節點。但不論如何,這兩種情況都可以適配成【場景2】。
- 場景3:當前節點和其兄弟節點都是黑色節點,並且其兄弟節點至少有一個紅色子節點
由於當前節點和其兄弟節點都是黑色節點,所以當前節點的父節點既可以是紅色節點也可以是黑色節點。由於前節點為刪除節點並且其兄弟節點至少有一個紅色節點,所以其左右子節點為葉子節點,其兄弟節點要麼有一個紅色子節點和一個葉子節點,要麼有兩個紅色子節點。如下圖:
注:其中下劃線標註的節點為當前節點。
我們以上圖中第一顆樹為例,進行平衡操作。首先將兄弟節點顏色變為父節點的顏色,然後將父節點和兄弟節點的右子節點的顏色置黑,再然後以父節點為支點進行左旋轉。如下圖:
我們發現樹已經是平衡的,這裡做完操作之後就可以結束平衡,然後將需要刪除的節點刪除就可以了。
我們再來看上圖中第二棵樹的平衡操作。首先將兄弟節點顏色置紅,並且將兄弟節點的左子節點顏色置黑,然後以兄弟節點為支點進行右旋轉。如下圖:
那麼是不是就變成了和第一棵樹一樣的結構了,其他的就不用再多說了吧?
注:在後面的【場景5】中,當前節點不是刪除節點時,還會衍生出另一種情況。當前節點的左右子節點不為葉子節點,其兄弟節點要麼有一個紅色節點一個黑色節點,要麼有兩個紅色節點。但不論如何,這兩種情況都可以適配成【場景3】。
- 場景4:當前節點為黑色節點,其兄弟節點為紅色節點
由於當前節點的兄弟節點為紅色節點,所以其父節點為黑色節點。由於當前節點為刪節點並且是黑色節點,所以其左右子節點為葉子節點,其兄弟節點的左右子節點為黑色節點。如下圖:
注:其中下劃線標註的節點為當前節點。
我們以上圖中第一顆樹為例,進行平衡操作。首先將兄弟節點顏色置黑,同時將父節點顏色置紅,然後以父節點為支點進行左旋轉。如下圖:
大家看上圖,這麼處理之後其實樹還沒有平衡,需要刪除的節點還不能刪除。但是當前場景處理到這裡就已經結束了,剩下的平衡工作需要交給【場景5】進行處理。
注:在後面的【場景5】中,當前節點不是刪除節點時,還會衍生出另一種情況。當前節點的左右子節點為非葉子節點。但不論如何,這兩種情況都可以適配成【場景3】。
- 場景5:當前節點和其兄弟節點都為黑色節點,並且其兄弟節點沒有紅色子節點
由於當前節點和其兄弟節點都是黑色節點,所以當前節點的父節點既可以是紅色節點也可以是黑色節點。由於前節點為刪除節點且其兄弟節點沒有子節點,所以當前節點和其兄弟節點的左右子節點都為葉子節點。如下圖:
注:其中下劃線標註的節點為當前節點。
我們以上圖中第一顆樹為例,進行平衡操作。首先將兄弟節點顏色置紅,然後將父節點設為當前節點。如下圖:
注:在下面的衍生場景中,當前節點不是插入節點時,還會衍生出另一種情況。當前節點的左右子節點不為葉子節點,兄弟節點的左右子節點都為黑色節點。但是不論如何,這兩種情況都可以適配成【場景5】。
大家不要以為這樣就結束了,我們還得繼續看當前節點的兄弟節點的型別。由此又可以衍生出下面5中情況:
- 當前節點的父節點為空節點,可適配【場景1】
- 當前是紅色節點,可以適配【場景2】。如下圖:
注:其中下劃線標註的節點為當前節點
- 當前節點和其兄弟節點都是黑色節點,並且其兄弟節點有一個紅色子節點,可適配【場景3】。如下圖:
注:其中下劃線標註的節點為當前節點
- 當前節點為黑色節點,其兄弟節點為紅色節點,可適配【場景4】。如下圖:
注:其中下劃線標註的節點為當前節點
- 當前節點和其兄弟節點都為黑色節點,並且其兄弟節點有兩個黑色子節點,可適配【場景5】。如下圖:
注:其中下劃線標註的節點為當前節點
刪除總結:和插入平衡的過程一樣,刪除平衡也是一個迴圈處理的過程,如果在當前節點和其兄弟節點上處理過後還無法使樹平衡,就需要將其父節點設為當前節點並進入下一個迴圈處理。再看【場景1】中當前節點為根節點、【場景2】中當前節點為紅色節點、【場景3】的平衡處理之後,都不需要再次旋轉或者改變節點顏色,樹已經是平衡狀態了。而【場景4】、【場景5】中經過多次迴圈旋轉和變色處理將樹平衡之後,發現當前節點要麼是根節點要麼就是紅色節點,或者是【場景3】處理之後的結果。所以總結,當前節點為根節點或者為黑色節點再或者是【場景3】處理之後的結果時,樹就已經平衡了。所以大家就明白【場景2】中為什麼要將當前節點設為黑色節點了吧。
如果大家單看上面還是很暈乎,沒關係,我們接下來就直接上程式碼了,大家對著上面的插入和刪除場景去理解程式碼可能就會清晰一些。
4.4、程式碼示例
- java程式碼示例:
public class RBTree<K extends Comparable<K>, V> { private final static int RED = 0, BLACK = 1; private Node root; private int size; /*獲取根節點的key*/ public Entry<K, V> getRoot() { return null == root ? null : new Entry<>(root.key, root.value); } /*獲取樹中節點個數*/ public int size() { return size; } /*判斷以給定節點為根節點的樹是否為一顆紅黑樹*/ public boolean isRBTree(Node node) { // 如果當前節點為空,則從根節點開始遞迴校驗 node = null == node ? root : node; if (null == node) { // 空樹也是一棵紅黑樹 return true; } // 獲取當前節點的父節點、左子節點、右子節點 Node parent = node.parent, nodeLeft = node.left, nodeRight = node.right; if (null != parent && parent.left != node && parent.right != node) { // 父節不為空,但是父節點的左右子節點引用都沒有指向當前節點,所以不是一顆二叉樹 return false; } if (null != nodeLeft) { // 當前節點的左子節點不為空 if (nodeLeft.parent != node || nodeLeft.compareTo(node) >= 0) { // 左子節點父節點引用沒有指向當前節點,不滿足是一顆二叉樹。 // 或者左子節點的key值大於等於當前節點的key值,不滿足是一顆二叉查詢樹(當前節點的key值大於左子節點小於右子節點) return false; } if (RED == nodeLeft.color) { if (RED == node.color) { // 當前節點和當前節點的左子節點都紅色節點,不滿足是一顆紅黑樹(紅黑樹中的任何路徑上都不能出現連續的兩個紅色節點) return false; } } else { if (null == nodeRight) { // 左子節點為黑色節點右子節點為空節點,不滿足是一個紅黑樹(從一個節點到它所能到達的任何葉子結點的任何路徑上黑色結點個數必須相等) return false; } else if (RED == nodeRight.color && (null == nodeRight.left || null == nodeRight.right)) { // 左子節點為黑色,右子節點為紅色並且其子節點有一個為Nil節點,不滿足是一顆紅黑樹(從一個節點到它所能到達的任何葉子結點的任何路徑上黑色結點個數必須相等) return false; } } if (!isRBTree(nodeLeft)) { // 遞迴左子節點,判斷子樹是否滿足紅黑樹特性 return false; } } if (null != nodeRight) { // 當前節點的右子節點不為空 if (nodeRight.parent != node || nodeRight.compareTo(node) <= 0) { return false; } if (RED == nodeRight.color) { if (RED == node.color) { return false; } } else { if (null == nodeLeft) { return false; } else if (RED == nodeLeft.color && (null == nodeLeft.left || null == nodeLeft.right)) { return false; } } if (!isRBTree(nodeRight)) { return false; } } return true; } /*根據key查詢value*/ public V find(K key) { if (null == key) { throw new NullPointerException(); } Node node = findNode(key, root); return null == node ? null : node.value; } /*查詢Key值最小的節點*/ public Entry<K, V> findMin() { Node node = root; while (null != node && null != node.left) { node = node.left; } return null == node ? null : new Entry<>(node.key, node.value); } /*查詢Key值最大的節點*/ public Entry<K, V> findMax() { Node node = root; while (null != node && null != node.right) { node = node.right; } return null == node ? null : new Entry<>(node.key, node.value); } /*向紅黑樹中插入節點*/ public V put(K key, V value) { if (null == key) { throw new NullPointerException(); } Node current = root, parent = null; // 獲取節點要插入的位置的父節點 int compare = 0; while (null != current && 0 != (compare = key.compareTo(current.key))) { current = compare > 0 ? (parent = current).right : (parent = current).left; } if (null != current) { // 要插入的key已存在 V ov = current.value; current.value = value; return ov; } if (null == parent) { // 要插入的樹為空樹 root = new Node(key, value, BLACK, null); } else { // 插入新節點 Node insert = new Node(key, value, RED, parent); current = compare < 0 ? parent.left = insert : (parent.right = insert); fixAfterPut(current); // 重新平衡插入節點後的樹 } ++size; return null; } /*從紅黑樹中刪除節點*/ public V remove(K key) { if (null == key) { throw new NullPointerException(); } Node remove, parent, replace; if (null == (remove = findNode(key, root))) { // 需要刪除的節點在樹中找不到 return null; } V value = remove.value; if (null != remove.left && null != (replace = remove.right)) { // 刪除節點的左右子節點都不為空節點,將刪除節點和後繼節點替換 while (null != replace.left) { replace = replace.left; } remove.key = replace.key; remove.value = replace.value; remove = replace; } // 此時最多隻有一個非葉子節點 if (null != (null == (replace = remove.left) ? replace = remove.right : replace)) { // 刪除節點的左右子節點有一個不為空,將刪除節點和子節點替換 remove.key = replace.key; remove.value = replace.value; remove = replace; } // 此時全部為葉子節點 if (null == (parent = remove.parent)) { // 刪除節點為根節點 root = null; } else { fixBeforeRemove(remove); // 刪除節點之前需要重新將樹平衡 remove.parent = parent.right == remove ? parent.right = null : (parent.left = null); // 最後刪除節點 } --size; return value; } /*採用中序遍歷二叉樹,保證資料從小到大遍歷*/ public void forEach(Consumer<Entry<K, V>> action) { Deque<Node> deque = new LinkedList<>(); for (Node node = root; node != null || !deque.isEmpty(); ) { for (; node != null; node = node.left) { deque.push(node); } Node pop = deque.pop(); action.accept(new Entry<>(pop.key, pop.value)); node = pop.right; } } /*查詢節點*/ private Node findNode(K key, Node current) { Node found = null; for (int compare; null != current && null == found; ) { if ((compare = key.compareTo(current.key)) > 0) { current = current.right; } else if (compare < 0) { current = current.left; } else { found = current; } } return found; } /*左旋轉節點*/ private void rotateLeft(Node rotate) { // 獲取旋轉節點的右子節點 Node right, parent, broLeft; if (null == rotate || null == (right = rotate.right)) { return; } if (null != (broLeft = rotate.right = right.left)) { // 將旋轉節點的右子節點設定為右子節點的左子節點,並將右子節點的左子節點父節點設定為旋轉節點 broLeft.parent = rotate; } if (null == (parent = right.parent = rotate.parent)) { // 右子節點的父節點設定為旋轉節點的父節點,如果父節點為空則將右子節點設定為根節點,並將顏色設定為黑色 (this.root = right).color = BLACK; } else if (parent.left == rotate) { parent.left = right; } else { parent.right = right; } right.left = rotate; rotate.parent = right; } /*右旋轉節點*/ private void rotateRight(Node rotate) { // 獲取旋轉節點的左子節點 Node left, parent, broRight; if (null == rotate || null == (left = rotate.left)) { return; } if (null != (broRight = rotate.left = left.right)) { // 將旋轉節點的左子節點設定為左子節點的右子節點,並將左子節點的右子節點父節點設定為旋轉節點 broRight.parent = rotate; } if (null == (parent = left.parent = rotate.parent)) { // 將左子節點的父節點設定為旋轉節點的父節點,如果父節點為空則將左子節點設定為根節點,並將顏色置黑 (this.root = left).color = BLACK; } else if (parent.left == rotate) { parent.left = left; } else { parent.right = left; } left.right = rotate; rotate.parent = left; } /*插入資料之後將樹進行平衡*/ private void fixAfterPut(Node current) { for (Node parent, grandfather, graLeft, graRight; ; ) { if (null == (parent = current.parent)) { // TODO: 當前節點父節點是空節點,適配【場景1】 current.color = BLACK; break; } if (BLACK == parent.color || null == (grandfather = parent.parent)) { // TODO: 當前節點的父節點是黑色節點,或者祖父節點是空節點(父節點是根節點),適配【場景2】 break; } if ((graLeft = grandfather.left) == parent) { // 父節點為祖父節點的左子節點 /* * 節點情況分析: * 1、當前節點不為空,並且為紅色節點 * 2、當前節點的父節點不為空,並且為紅色節點 * 3、當前節點的祖父節點不為空,並且為黑色節點 */ if (null != (graRight = grandfather.right) && RED == graRight.color) { // TODO: 當前節點的叔叔節點是紅色節點,適配【場景4】 graRight.color = BLACK; // 將叔叔節點顏色置黑 parent.color = BLACK; // 將父節點顏色置黑 grandfather.color = RED; // 將祖父節點顏色置紅 current = grandfather; // 將祖父節點設為當前節點 } else { // TODO: 當前節點的叔叔節點是葉子節點或者黑色節點,適配【場景3】 if (current == parent.right) { // 當前節點為父節點的右子節點 rotateLeft(current = parent); // 將將父節點設為當前節點並將當前節點左旋轉 grandfather = (parent = current.parent).parent; // 重新為父節點和祖父節點賦值 } parent.color = BLACK; // 將父節點顏色置黑 grandfather.color = RED; // 將祖父節點顏色置紅 rotateRight(grandfather); // 將祖父節點進行右旋轉 } } else { // 父節點為祖父節點的右子節點,這裡就不做註釋了 if (null != graLeft && RED == graLeft.color) { graLeft.color = BLACK; parent.color = BLACK; grandfather.color = RED; current = grandfather; } else { if (current == parent.left) { rotateRight(current = parent); grandfather = (parent = current.parent).parent; } parent.color = BLACK; grandfather.color = RED; rotateLeft(grandfather); } } } } /*刪除節點之前將數平衡*/ private void fixBeforeRemove(Node current) { for (Node parent, left, right; null != current // 當前節點不為空 && null != (parent = current.parent); ) { // TODO: 當前節點的父節點是空節點,適配【場景1】 if (RED == current.color) { // TODO: 當前節點為紅色節點,適配【場景2】 current.color = BLACK; break; } if ((left = parent.left) == current) { // 如果當前節點為父節點的左子節點 /* * 節點情況分析: * 1、當前節點是黑色節點 * 2、當前節點的兄弟節點不是葉子結點 */ if (RED == (right = parent.right).color) { // TODO: 當前節點的兄弟節點為紅色節點,適配【場景4】 /* * 節點情況分析: * 1、父節點為黑色節點; * 2、兄弟節點的左右子節點為黑色節點; */ right.color = BLACK; // 將兄弟節點顏色置黑 parent.color = RED; // 將父節點顏色置紅 rotateLeft(parent); // 將父節點左旋轉(當前節點任然是父節點的左子節點) right = parent.right; // 重新獲取當前節點的兄弟節點 } /* * 節點情況分析: * 1、當前節點的兄弟節點一定為黑色節點 */ Node broLeft = right.left, broRight = right.right; if ((null == broRight || BLACK == broRight.color) && (null == broLeft || BLACK == broLeft.color)) { // TODO: 當前節點兄弟節點的左右子節點不存在紅色節點,適配【場景5】 /* * 節點情況分析: * 情況1:當前節點兄弟節點的左右子節點都為黑色節點 * 情況2:當前節點兄弟節點的左右子節點都為葉子節點 */ right.color = RED; // 將兄弟節點顏色置紅 current = parent; // 將父節點設為當前節點 } else { // TODO: 當前節點的兄弟節點至少有一個紅色子節點,適配【場景3】 if (null == broRight || BLACK == broRight.color) { // 兄弟節點的右子節點為葉子節點或者黑色節點,則兄弟節點的左子節點一定為紅色節點 broLeft.color = BLACK; // 將兄弟節點的左子節點顏色置黑 right.color = RED; // 將兄弟節點顏色置紅 rotateRight(right); // 將兄弟節點右旋轉 right = parent.right; // 重新獲取右子節點 broRight = right.right; } right.color = parent.color; // 將兄弟節點的顏色置為父節點的顏色 broRight.color = BLACK; // 將兄弟節點的右子節點顏色置黑 parent.color = BLACK; // 將父節點顏色置黑 rotateLeft(parent); // 將父節點左旋轉 break; } } else { // 當前節點為右子節點,這裡就不做註釋了 if (RED == left.color) { left.color = BLACK; parent.color = RED; rotateRight(parent); left = parent.left; } Node broLeft = left.left, broRight = left.right; if ((null == broLeft || BLACK == broLeft.color) && (null == broRight || BLACK == broRight.color)) { left.color = RED; current = parent; } else { if (null == broLeft || BLACK == broLeft.color) { broRight.color = BLACK; left.color = RED; rotateLeft(left); left = parent.left; broLeft = left.left; } left.color = parent.color; broLeft.color = BLACK; parent.color = BLACK; rotateRight(parent); break; } } } } class Node implements Comparable<Node> { K key; V value; int color; Node parent, left, right; public Node(K key, V value, int color, Node parent) { this.key = key; this.value = value; this.color = color; this.parent = parent; } @Override public int compareTo(Node node) { return key.compareTo(node.key); } } public static class Entry<K, V> { private final K key; private final V value; public Entry(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } @Override public String toString() { return "Entry{" + "key=" + key + ", value=" + value + '}'; } } }
- java程式碼測試用例:
public class RBTreeTest { @Test public void put() { ThreadLocalRandom random = ThreadLocalRandom.current(); RBTree<Integer, String> tree = new RBTree<>(); for (int i = 0; i < 100; ++i) { int key = (random.nextInt(100000) & Integer.MAX_VALUE) % 100; tree.put(key, String.valueOf(key)); } RBTree.Entry<Integer, String> root = tree.getRoot(); System.out.printf("rootKey=%d, rootValue=%s, size=%d, isRBTree=%b\n", root.getKey(), root.getValue(), tree.size(), tree.isRBTree(null)); } @Test public void remove() { ThreadLocalRandom random = ThreadLocalRandom.current(); RBTree<Integer, String> tree = new RBTree<>(); for (int i = 0; i < 100; ++i) { int key = (random.nextInt(100000) & Integer.MAX_VALUE) % 100; tree.put(key, String.valueOf(key)); } // while (tree.size() > 0) { tree.remove(tree.getRoot().getKey()); System.out.printf("size=%d, isRBTree=%b\n", tree.size(), tree.isRBTree(null)); } } @Test public void find() { ThreadLocalRandom random = ThreadLocalRandom.current(); RBTree<Integer, String> tree = new RBTree<>(); for (int i = 0; i < 100; ++i) { int key = (random.nextInt(100000) & Integer.MAX_VALUE) % 100; tree.put(key, String.valueOf(key)); } System.out.println(tree.find(random.nextInt(100000) % 100)); } @Test public void findMin() { ThreadLocalRandom random = ThreadLocalRandom.current(); RBTree<Integer, String> tree = new RBTree<>(); for (int i = 0; i < 100; ++i) { int key = (random.nextInt(100000) & Integer.MAX_VALUE) % 100; tree.put(key, String.valueOf(key)); } System.out.println(tree.findMin()); } @Test public void findMax() { ThreadLocalRandom random = ThreadLocalRandom.current(); RBTree<Integer, String> tree = new RBTree<>(); for (int i = 0; i < 100; ++i) { int key = (random.nextInt(100000) & Integer.MAX_VALUE) % 100; tree.put(key, String.valueOf(key)); } System.out.println(tree.findMax()); } @Test public void foreach() { ThreadLocalRandom random = ThreadLocalRandom.current(); RBTree<Integer, String> tree = new RBTree<>(); for (int i = 0; i < 100; ++i) { int key = (random.nextInt(100000) & Integer.MAX_VALUE) % 100; tree.put(key, String.valueOf(key)); } RBTree.Entry<Integer, String> root = tree.getRoot(); System.out.printf("rootKey=%d, rootValue=%s, size=%d, isBSTree=%b\n", root.getKey(), root.getValue(), tree.size(), tree.isRBTree(null)); System.out.println(tree.findMin().getValue() + " " + tree.findMax().getValue()); tree.forEach(System.out::println); } }
- C程式碼示例:
由於本人是做java開發的,C的語法和編碼風格並不是很標準,所以大家就將就看一下
#define _CRT_SECURE_NO_WARNINGS #pragma once #include <stdlib.h> #include <stdio.h> #include <string.h> #ifdef __cplusplus extern "C" { #endif // __cplusplus /*紅黑樹節點*/ typedef struct _RBNode { void *p_key; // key可以是任意型別,key值比較器比較key的大小 void *p_value; // value也可以是任意型別 char color; // 0代表紅色,非0代表黑色 struct _RBNode *p_parent, *p_left, *p_right; }RBNode; /*紅黑樹結構體*/ typedef struct _RBTree { int size; // 節點數量 RBNode *p_root; // 根節點指標 // 樹中key值比較器,引數1:須必較的key值指標,引數2:須必交的key值指標,引數3:返回值指標 int(*p_kcmp)(void *, void *, char *); }RBTree; /* * 建立一棵空的紅黑樹; * 引數列表: * p_tree:紅黑樹指標; * Key_Comparator:代表key值比較器; * 返回值: * NULL:代表函式執行失敗;!NULL:代表返回的紅黑樹指標; */ RBTree *create_RBTree(int(*p_kcmp)(void *, void *, char *)); /* * 判斷以給定節點為根節點的樹是否為一顆紅黑樹; * 引數列表: * p_tree:紅黑樹指標; * p_node:需要判斷的起始節點指標,如果為空則取根節點; * p_res:判斷結果,0:不是一顆紅黑樹;1:是一顆紅黑樹; * 返回值: * 0:代表函式執行失敗;!0:代表函式執行成功; */ int is_RBTree(RBTree *p_tree, RBNode *p_node, char *p_res); /* * 在紅黑樹中查詢節點 * 引數列表: * p_key:要查詢的節點的key指標; * p_tree:要查詢的紅黑樹的的指標; * p_res:查詢到的值; * 返回值: * 0:代表函式執行失敗;!0:代表函式執行成功; */ int find_RBTree(void *p_key, RBTree *p_tree, void **p_res); /* * 在紅黑樹中查詢Key值最小的節點 * 引數列表: * p_tree:要查詢的紅黑樹的的指標; * p_res:查詢到的值; * 返回值: * 0:代表函式執行失敗;!0:代表函式執行成功; */ int findMin_RBTree(RBTree *p_tree, RBNode **p_res); /* * 在紅黑樹中查詢Key值最大的節點 * 引數列表: * p_tree:要查詢的紅黑樹的的指標; * p_res:查詢到的值; * 返回值: * 0:代表函式執行失敗;!0:代表函式執行成功; */ int findMax_RBTree(RBTree *p_tree, RBNode **p_res); /* * 向紅黑樹中插入節點 * 引數列表: * p_key:要插入節點的key * p_value:要插入節點的value * p_tree:要插入節點的紅黑樹 * 返回值: * 0:代表函式執行失敗;!0:代表函式執行成功; */ int put_RBTree(void *p_key, void *p_value, RBTree *p_tree); /* * 從紅黑樹中刪除節點 * 引數列表: * p_key:要刪除節點的key * p_tree:要刪除節點的紅黑樹 * p_res:刪除節點的value值指標,如果傳入的是NULL,函式內部會釋放這部分空間 * 返回值: * 0:代表函式執行失敗;!0:代表函式執行成功; */ int remove_RBTree(void *p_key, RBTree *p_tree, void **p_res); /* * 釋放紅黑樹 * 引數列表: * p_tree:需要釋放的紅黑樹 */ void free_RBTree(RBTree *p_tree); #ifdef __cplusplus } #endif // __cplusplus
#include "RBTree.h" int findNode(int(*p_kcmp)(void *, void *, char *), void *p_key, RBNode *p_curr, RBNode **p_res); void rotateLeft(RBTree *p_tree, RBNode *p_rotate); void rotateRight(RBTree *p_tree, RBNode *p_rotate); void fixAfterPut(RBTree *p_tree, RBNode *p_current); void fixBeforeRemove(RBTree *p_tree, RBNode *p_current); RBTree *create_RBTree(int(*p_kcmp)(void *, void *, char *)) { if (!p_kcmp) // 如果key值比較器函式為空,則不能建立紅黑樹 return NULL; // 為紅黑樹開闢空間並初始化 RBTree *p_tree = NULL; size_t st_size = sizeof(RBTree); if (!(p_tree = (RBTree *)malloc(st_size))) return NULL; memset(p_tree, 0, st_size); p_tree->p_kcmp = p_kcmp; return p_tree; } int is_RBTree(RBTree *p_tree, RBNode *p_node, char *p_res) { if (!p_tree) // 要校驗的紅黑樹不能為空 return 0; // 如果傳入的要交驗的節點為空,則從根節點開始校驗,如果根節點也為空,則返回成功(空樹也是一棵紅黑樹) if (!(p_node = !p_node ? p_tree->p_root : p_node)) return *p_res = 1; // 獲取當前節點的父節點、左子節點、右子節點等指標 RBNode *p_parent = p_node->p_parent, *p_nodeL = p_node->p_left, *p_nodeR = p_node->p_right; // 如果父節不為空,但是父節點的左右子節點指標都沒有指向當前節點,所以不是一顆二叉樹 if (p_parent && p_parent->p_left != p_node && p_parent->p_right != p_node) return !(*p_res = 0); char compare = 0; if (p_nodeL) // 當前節點的左子節點不為空 { if (!p_tree->p_kcmp(p_nodeL->p_key, p_node->p_key, &compare)) // 比較左子節點的key和右子節點的key的大小 return 0; // 左子節點父節點引用沒有指向當前節點,不滿足是一顆二叉樹。或者左子節點的key值大於等於當前節點的key值,不滿足是一顆二叉查詢樹 if (p_nodeL->p_parent != p_node || compare >= 0) return !(*p_res = 0); if (!p_nodeL->color) { if (!p_node->color) // 當前節點和當前節點的左子節點都紅色節點,不滿足是一顆紅黑樹 return !(*p_res = 0); } else { if (!p_nodeR) // 左子節點為黑色節點右子節點為空節點,不滿足是一個紅黑樹 return !(*p_res = 0); // 左子節點為黑色,右子節點為紅色並且其子節點有一個為Nil節點,不滿足是一顆紅黑樹 if (!p_nodeR->color && (!p_nodeR->p_left || !p_nodeR->p_right)) return !(*p_res = 0); } // 遞迴左子節點,判斷子樹是否滿足紅黑樹特性 if (!is_RBTree(p_tree, p_nodeL, p_res)) return 0; else if (!*p_res) return 1; } if (p_nodeR) { // 當前節點的右子節點不為空 if (!p_tree->p_kcmp(p_nodeR->p_key, p_node->p_key, &compare)) return 0; if (p_nodeR->p_parent != p_node || compare <= 0) return !(*p_res = 0); if (!p_nodeR->color) { if (!p_node->color) return !(*p_res = 0); } else { if (!p_nodeL) return !(*p_res = 0); if (!p_nodeL->color && (!p_nodeL->p_left || !p_nodeL->p_right)) return !(*p_res = 0); } if (!is_RBTree(p_tree, p_nodeR, p_res)) return 0; else if (!*p_res) return 1; } return *p_res = 1; } int find_RBTree(void *p_key, RBTree *p_tree, void **p_res) { if (!p_key || !p_tree || !p_res) return 0; RBNode *p_node = NULL; if (!findNode(p_tree->p_kcmp, p_key, p_tree->p_root, &p_node)) // 判斷查詢節點函式是否執行失敗 return 0; *p_res = !p_node ? NULL : p_node->p_value; return 1; } int findMin_RBTree(RBTree *p_tree, RBNode **p_res) { if (!p_tree || !p_res) return 0; RBNode *p_node = p_tree->p_root; while (p_node && p_node->p_left) p_node = p_node->p_left; *p_res = p_node; return 1; } int findMax_RBTree(RBTree *p_tree, RBNode **p_res) { if (!p_tree || !p_res) return 0; RBNode *p_node = p_tree->p_root; while (p_node && p_node->p_right) p_node = p_node->p_right; *p_res = p_node; return 1; } int put_RBTree(void *p_key, void *p_value, RBTree *p_tree) { if (!p_key || !p_tree) return 0; RBNode *p_parent = NULL, *p_curr = p_tree->p_root; // 獲取節點要插入的位置的父節點 char compare = 0; while (p_curr) { if (!p_tree->p_kcmp(p_key, p_curr->p_key, &compare)) return 0; if (!compare) break; p_parent = p_curr; p_curr = compare > 0 ? p_curr->p_right : p_curr->p_left; } if (p_curr) // 要插入的key已存在 { if (p_curr->p_key && p_curr->p_key != p_key) free(p_curr->p_key); if (p_curr->p_value && p_curr->p_value != p_value) free(p_curr->p_value); p_curr->p_key = p_key; p_curr->p_value = p_value; return 1; } // 開闢紅黑樹節點空間並初始化 RBNode *p_insert = NULL; size_t st_size = sizeof(RBNode); if (!(p_insert = (RBNode *)malloc(st_size))) return 0; memset(p_insert, 0, st_size); p_insert->p_key = p_key; p_insert->p_value = p_value; if (!(p_insert->p_parent = p_parent)) // 要插入節點的紅黑樹是空樹 (p_tree->p_root = p_insert)->color = 1; else // 要插入節點的紅黑樹不是空樹,直接將節點插入 { if (compare < 0) p_parent->p_left = p_insert; else p_parent->p_right = p_insert; fixAfterPut(p_tree, p_insert); // 重新平衡插入節點後的樹 } ++p_tree->size; return 1; } int remove_RBTree(void *p_key, RBTree *p_tree, void **p_res) { if (!p_key || !p_tree) return 0; RBNode *p_remove = NULL, *p_parent = NULL, *p_replace = NULL; if (!findNode(p_tree->p_kcmp, p_key, p_tree->p_root, &p_remove)) // 判斷查詢節點函式是否執行失敗 return 0; if (!p_remove) // 如果沒有查詢到要刪除的節點就直接返回 return 1; else if (p_res) // 代表返回值的指標不為空,則表示需要將刪除節點的value值傳出去 *p_res = p_remove->p_value; else if (p_remove->p_value) // 直接釋放刪除節點的value值 free(p_remove->p_value); p_remove->p_value = NULL; if (p_remove->p_left && (p_replace = p_remove->p_right)) { // 刪除節點的左右子節點都不為空節點,將刪除節點和後繼節點替換 while (p_replace->p_left) p_replace = p_replace->p_left; void* temp = p_remove->p_key; p_remove->p_key = p_replace->p_key; p_replace->p_key = temp; p_remove->p_value = p_replace->p_value; p_replace->p_value = NULL; p_remove = p_replace; } if ((p_replace = !(p_replace = p_remove->p_left) ? p_remove->p_right : p_replace)) { // 刪除節點的左右子節點有一個不為空,將刪除節點和子節點替換 void* temp = p_remove->p_key; p_remove->p_key = p_replace->p_key; p_replace->p_key = temp; p_remove->p_value = p_replace->p_value; p_replace->p_value = NULL; p_remove = p_replace; } if (!(p_parent = p_remove->p_parent)) // 刪除節點為根節點 p_tree->p_root = NULL; else { fixBeforeRemove(p_tree, p_remove); // 刪除節點之前需要重新將樹平衡 // 最後刪除節點 p_remove->p_parent = NULL; if (p_parent->p_right == p_remove) p_parent->p_right = NULL; else p_parent->p_left = NULL; } // 釋放被刪除節點的空間 if (p_remove->p_key) free(p_remove->p_key); if (p_remove) free(p_remove); p_remove = p_remove->p_key = NULL; --p_tree->size; return 1; } void free_RBTree(RBTree *p_tree) { if (!p_tree) return; for (RBNode *p_node = NULL; p_node = p_tree->p_root; ) remove_RBTree(p_node->p_key, p_tree, NULL); free(p_tree); p_tree = NULL; } static int findNode(int(*p_kcmp)(void *, void *, char *), void *p_key, RBNode *p_curr, RBNode **p_res) { if (!p_kcmp || !p_key || !p_res) return 0; RBNode *p_found = NULL; for (char compare = 0; p_curr && !p_found; ) { if (!p_kcmp(p_key, p_curr->p_key, &compare)) return 0; if (compare > 0) p_curr = p_curr->p_right; else if (compare < 0) p_curr = p_curr->p_left; else *p_res = p_found = p_curr; } return 1; } static void rotateLeft(RBTree *p_tree, RBNode *p_rotate) { RBNode *p_right, *p_parent, *p_broLeft; if (!p_tree || !p_rotate || !(p_right = p_rotate->p_right)) // 如果旋轉節點的右子節點為空節點,則不需要旋轉直接返回 return; // 將旋轉節點的右子節點設定為右子節點的左子節點,並將右子節點的左子節點父節點設定為旋轉節點 if (p_broLeft = p_rotate->p_right = p_right->p_left) p_broLeft->p_parent = p_rotate; if (!(p_parent = p_right->p_parent = p_rotate->p_parent)) // 右子節點的父節點設定為旋轉節點的父節點,如果父節點為空則將右子節點設定為根節點,並將顏色設定為黑色 (p_tree->p_root = p_right)->color = 1; else if (p_parent->p_left == p_rotate) p_parent->p_left = p_right; else p_parent->p_right = p_right; p_right->p_left = p_rotate; p_rotate->p_parent = p_right; } static void rotateRight(RBTree *p_tree, RBNode *p_rotate) { RBNode *p_left, *p_parent, *p_broRight; if (!p_tree || !p_rotate || !(p_left = p_rotate->p_left)) return; if (p_broRight = p_rotate->p_left = p_left->p_right) p_broRight->p_parent = p_rotate; if (!(p_parent = p_left->p_parent = p_rotate->p_parent)) (p_tree->p_root = p_left)->color = 1; else if (p_parent->p_left == p_rotate) p_parent->p_left = p_left; else p_parent->p_right = p_left; p_left->p_right = p_rotate; p_rotate->p_parent = p_left; } static void fixAfterPut(RBTree *p_tree, RBNode *p_current) { for (RBNode *p_parent, *p_gparent, *p_graLeft, *p_graRight; ; ) { if (!(p_parent = p_current->p_parent)) // 父節點為空,則當前節點為根節點 { p_current->color = 1; break; } if (p_parent->color || !(p_gparent = p_parent->p_parent)) // 父節點為黑色節點,或者祖父節點為空(父節點是根節點) break; if ((p_graLeft = p_gparent->p_left) == p_parent) // 父節點為祖父節點的左子節點 { if ((p_graRight = p_gparent->p_right) && !p_graRight->color) // 叔叔節點不為空並且是紅色節點 { p_graRight->color = 1; // 將叔叔節點顏色置黑 p_parent->color = 1; // 將父節點顏色置黑 p_gparent->color = 0; // 將祖父節點顏色置紅 p_current = p_gparent; // 將祖父節點設為當前節點 } else // 叔叔節點為空節點或者為黑色節點 { if (p_current == p_parent->p_right) // 當前節點為父節點的右子節點 { rotateLeft(p_tree, p_current = p_parent); // 將將父節點設為當前節點並將當前節點左旋轉 p_gparent = (p_parent = p_current->p_parent)->p_parent; // 重新為父節點和祖父節點賦值 } p_parent->color = 1; // 將父節點顏色置黑 p_gparent->color = 0; // 將祖父節點顏色置紅 rotateRight(p_tree, p_gparent); // 將祖父節點進行右旋轉 } } else // 父節點為祖父節點的右子節點 { if (p_graLeft && !p_graLeft->color) { p_graLeft->color = 1; p_parent->color = 1; p_gparent->color = 0; p_current = p_gparent; } else { if (p_current == p_parent->p_left) { rotateRight(p_tree, p_current = p_parent); p_gparent = (p_parent = p_current->p_parent)->p_parent; } p_parent->color = 1; p_gparent->color = 0; rotateLeft(p_tree, p_gparent); } } } } static void fixBeforeRemove(RBTree *p_tree, RBNode *p_current) { for (RBNode *p_parent, *p_left, *p_right; p_current && (p_parent = p_current->p_parent); ) { // 當前節點不為空,並且當前節點的父節點也不為空 if (!p_current->color) { /* * 當前節點為紅色節點,則: * 1、當前節點的兄弟節點為空節點; * 2、或者,當前節點分別有一個黑色子節點和紅色子節點,且其中紅色子節點的子節點都為空節點或都為黑色節點; */ p_current->color = 1; break; } /* * 當前節點情況: * 1、當前節點不為空,並且是黑色節點 * 2、當前節點的兄弟節點不為空 */ if ((p_left = p_parent->p_left) == p_current) // 如果當前節點為父節點的左子節點 { if (!(p_right = p_parent->p_right)->color) { /* * 如果當前節點的兄弟節點為紅色節點,則: * 1、父節點就一定為黑色節點; * 2、兄弟節點的左右子節點一定為黑色節點; */ p_right->color = 1; // 將兄弟節點顏色置黑 p_parent->color = 0; // 將父節點顏色置紅 rotateLeft(p_tree, p_parent); // 將父節點左旋轉(當前節點任然是父節點的左子節點) p_right = p_parent->p_right; // 重新獲取當前節點的兄弟節點 } RBNode *p_broLeft = p_right->p_left, *p_broRight = p_right->p_right; if ((!p_broRight || p_broRight->color) && (!p_broLeft || p_broLeft->color)) { // 兄弟節點的左右子節點不存在紅色節點,則兄弟節點的左右子節點都為Nil節點或者都為黑色節點 p_right->color = 0; // 將兄弟節點顏色置紅 p_current = p_parent; // 將父節點設為當前節點 } else // 兄弟節點下一定有一個紅色子節點 { if (!p_broRight || p_broRight->color) { // 如果兄弟節點的右子節點為Nil節點或者黑色節點,則兄弟節點的左子節點一定為紅色節點 p_broLeft->color = 1; // 將兄弟節點的左子節點顏色置黑 p_right->color = 0; // 將兄弟節點顏色置紅 rotateRight(p_tree, p_right); // 將兄弟節點右旋轉 p_right = p_parent->p_right; // 重新獲取右子節點 p_broRight = p_right->p_right; } p_right->color = p_parent->color; // 將兄弟節點的顏色置為父節點的顏色 p_broRight->color = 1; // 將兄弟節點的右子節點顏色置黑 p_parent->color = 1; // 將父節點顏色置黑 rotateLeft(p_tree, p_parent); // 將父節點左旋轉 break; } } else // 當前節點為右子節點 { if (!p_left->color) { p_left->color = 1; p_parent->color = 0; rotateRight(p_tree, p_parent); p_left = p_parent->p_left; } RBNode *p_broLeft = p_left->p_left, *p_broRight = p_left->p_right; if ((!p_broLeft || p_broLeft->color) && (!p_broRight || p_broRight->color)) { p_left->color = 0; p_current = p_parent; } else { if (!p_broLeft || p_broLeft->color) { p_broRight->color = 1; p_left->color = 0; rotateLeft(p_tree, p_left); p_left = p_parent->p_left; p_broLeft = p_left->p_left; } p_left->color = p_parent->color; p_broLeft->color = 1; p_parent->color = 1; rotateRight(p_tree, p_parent); break; } } } }
- C程式碼測試用例:
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <time.h> #include "RBTree.h" #define _VALUE_SIZE 20 static int kcmp(void *p_key1, void *p_key2, char *p_res) { int compare = *((int *)p_key1) - *((int *)p_key2); if (compare > 0) *p_res = 1; else if (compare < 0) *p_res = -1; else *p_res = 0; return 1; } RBTree *init_RBTree(int n_size, int max_key) { RBTree *p_tree = NULL; p_tree = create_RBTree(kcmp); if (!(p_tree = create_RBTree(kcmp))) { printf("建立紅黑樹出現錯誤\n"); return NULL; } int *p_key = NULL; char *p_value = NULL; srand((unsigned int)time(NULL)); for (int i = 0; i < n_size; ++i) { if (!(p_key = (int *)malloc(sizeof(int)))) { printf("開闢key空間失敗\n"); return p_tree; } if (!(p_value = (char *)malloc(_VALUE_SIZE))) { if (!p_key) free(p_key); p_key = NULL; printf("開闢value空間失敗\n"); return p_tree; } _itoa_s((*p_key = rand() % max_key), p_value, _VALUE_SIZE, 16); if (!put_RBTree(p_key, p_value, p_tree)) { printf("向紅黑樹中插入節點失敗\n"); return p_tree; } } return p_tree; } void testFind_RBTree() { RBTree *p_tree = NULL; if (!(p_tree = init_RBTree(100, 1000))) { printf("紅黑樹初始化失敗\n"); return; } RBNode *p_root = p_tree->p_root; printf("初始化紅黑樹成功: root_key=%d, size=%d\n", *((int *)p_root->p_key), p_tree->size); srand((unsigned int)time(NULL)); int key = rand() % 1000; void *p_value = NULL; find_RBTree(&key, p_tree, &p_value); if (p_value) printf("key=%d, value=%s\n", key, (char *)p_value); else printf("key=%d, value=NULL\n", key); free_RBTree(p_tree); // 釋放紅黑樹 } void testPutAndRemove_RBTree() { RBTree *p_tree = NULL; if (!(p_tree = init_RBTree(100, 1000))) { printf("紅黑樹初始化失敗\n"); return; } RBNode *p_root = p_tree->p_root; printf("初始化紅黑樹成功: root_key=%d, size=%d\n", *((int *)p_root->p_key), p_tree->size); char isRBTree = 0; for (int i = 0; i < 20; ++i) { RBNode *p_node = p_tree->p_root; if (p_node) { printf("刪除[root_key=%d]後,", *((int *)p_node->p_key)); remove_RBTree(p_node->p_key, p_tree, NULL); is_RBTree(p_tree, NULL, &isRBTree); printf("p_tree[%s]一顆紅黑樹, size=%d\n", isRBTree ? "還是" : "不是", p_tree->size); } } free_RBTree(p_tree); // 釋放紅黑樹 } int main(void) { testPutAndRemove_RBTree(); //testFind_RBTree(); system("pause"); return EXIT_SUCCESS; }