高階資料結構---紅黑樹及其插入左旋右旋程式碼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