二叉查找树(BST),平衡二叉查找树(AVL),红黑树(RBT),B~/B+树(B-tree)。这四种树都具备下面几个优势:
(1) 都是动态结构。在删除,插入操作的时候,都不需要彻底重建原始的索引树。最多就是执行一定量的旋转,变色操作来有限的改变树的形态。而这些操作所付出的代价都远远小于重建一棵树。这一优势在《查找结构专题(1):静态查找结构概论 》中讲到过。
(2) 查找的时间复杂度大体维持在O(log(N))数量级上。可能有些结构在最差的情况下效率将会下降很快,比如二叉树
1.二叉查找树(Binary Search Tree)
下面的图就是两棵二叉查找树,我们可以总结一下他的特点:
- (1) 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
- (2) 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
- (3) 它的左、右子树也分别为二叉查找树
BST 的操作代价分析:
- (1) 查找代价: 任何一个数据的查找过程都需要从根结点出发,沿某一个路径朝叶子结点前进。因此查找中数据比较次数与树的形态密切相关。当树中每个结点左右子树高度大致相同时,树高为logN。则平均查找长度与logN成正比,查找的平均时间复杂度在O(logN)数量级上。当先后插入的关键字有序时,BST退化成单支树结构。此时树高n。平均查找长度为(n+1)/2,查找的平均时间复杂度在O(N)数量级上。
- (2) 插入代价: 新结点插入到树的叶子上,完全不需要改变树中原有结点的组织结构。插入一个结点的代价与查找一个不存在的数据的代价完全相同。
- (3) 删除代价: 当删除一个结点P,首先需要定位到这个结点P,这个过程需要一个查找的代价。然后稍微改变一下树的形态。如果被删除结点的左、右子树只有一个存在,则改变形态的代价仅为O(1)。如果被删除结点的左、右子树均存在,只需要将当P的左孩子的右孩子的右孩子的...的右叶子结点与P互换,在改变一些左右子树即可。因此删除操作的时间复杂度最大不会超过O(logN)。
package com.asteele.clrs.data_structures; import java.util.function.Consumer; public class BinarySearchTree<K extends Comparable<? super K>> { private BSTNode<K> root; private int size; protected static class BSTNode<E extends Comparable<? super E>> { private E data; private BSTNode<E> right; private BSTNode<E> left; private BSTNode<E> parent; protected BSTNode(E data) { this.data = data; } protected BSTNode<E> right() { return this.right; } protected BSTNode<E> left() { return this.left; } protected BSTNode<E> parent() { return this.parent; } } /** * Constructs an empty BinarySearchTree. */ public BinarySearchTree() { this.size = 0; this.root = null; } /** * Adds the given element to the tree. * Returns true if the element is successfully added. * * @param element to be added */ public boolean add(K element) { return addNode(new BSTNode<K>(element)); } protected boolean addNode(BSTNode<K> addedNode) { BSTNode<K> chaser = null; BSTNode<K> current = this.root; while (current != null) { chaser = current; if (addedNode.data.compareTo(current.data) < 0) { current = current.left; } else { current = current.right; // Equal nodes are always placed right. } } addedNode.parent = chaser; if (chaser == null) { this.root = addedNode; } else if (addedNode.data.compareTo(chaser.data) < 0) { chaser.left = addedNode; } else { chaser.right = addedNode; } this.size++; return addedNode != null; } /** * Remove the given element from the tree. If the element is successfully * removed, returns true. If the element does not exist in the tree, returns * false. * * @param element * @return true if found & removed, false otherwise */ public boolean remove(K element) { return removeNode(search(element)); } protected boolean removeNode(BSTNode<K> nodeToRemove) { if (nodeToRemove == null) return false; // The element was not found. if (nodeToRemove.left == null) { transplant(nodeToRemove, nodeToRemove.right); // Transplant accepts null arguments. This works properly when both children are null. } else if (nodeToRemove.right == null) { transplant(nodeToRemove, nodeToRemove.left); } else { BSTNode<K> successor = findTreeMinimum(nodeToRemove.right); if (successor.parent != nodeToRemove) { transplant(successor, successor.right); successor.right = nodeToRemove.right; successor.right.parent = successor; } transplant(nodeToRemove, successor); successor.left = nodeToRemove.left; successor.left.parent = successor; } this.size--; return true; } /** * Finds the minimum node of the tree with the given element as its root. */ private BSTNode<K> findTreeMinimum(BSTNode<K> parent) { BSTNode<K> chaser = null; BSTNode<K> current = parent; while (current != null) { chaser = parent; current = current.left; } return chaser; } /** * Replaces the first given node and its tree with the second given node and * its tree. This method does not change nodeToReplaceWith's left and right * subtrees. Doing so is the responsibility of the caller. */ private void transplant(BSTNode<K> nodeToBeReplaced, BSTNode<K> nodeToReplaceWith) { if (nodeToBeReplaced.parent == null) { this.root = nodeToReplaceWith; } else if (nodeToBeReplaced == nodeToBeReplaced.parent.left) { nodeToBeReplaced.parent.left = nodeToReplaceWith; } else { nodeToBeReplaced.parent.right = nodeToReplaceWith; } if (nodeToReplaceWith != null) { nodeToReplaceWith.parent = nodeToBeReplaced.parent; } } /** * Returns true if the given element exists in the tree, false otherwise. */ public boolean contains(K element) { return search(element) != null; } /** * Find the given element and return its corresponding node. * Returns Null if the element is not found. */ protected BSTNode<K> search(K element) { BSTNode<K> current = this.root; while (current != null) { int comparison = current.data.compareTo(element); if (comparison < 0) { current = current.right; } else if (comparison > 0){ current = current.left; } else { return current; } } return null; } /** * Rotate the tree beginning at the entered node left such that * the entered node becomes the left child of its current right child node. */ protected void rotateLeft(BSTNode<K> topNode) { if (topNode.right != null) { BSTNode<K> rightNode = topNode.right; topNode.right = rightNode.left; if (rightNode.left != null) { rightNode.left.parent = topNode; } rightNode.parent = topNode.parent; if (topNode.parent == null) { this.root = rightNode; } else if (topNode == topNode.parent.left) { topNode.parent.left = rightNode; } else { topNode.parent.right = rightNode; } rightNode.left = topNode; topNode.parent = rightNode; } } /** * Rotate the tree beginning at the entered node right such that * the entered node becomes the right child of its current left child node. */ protected void rotateRight(BSTNode<K> topNode) { if (topNode.left != null) { BSTNode<K> leftNode = topNode.left; topNode.left = leftNode.right; if (leftNode.right != null) { leftNode.right.parent = topNode; } leftNode.parent = topNode.parent; if (topNode.parent == null) { this.root = leftNode; } else if (topNode == topNode.parent.left) { topNode.parent.left = leftNode; } else { topNode.parent.right = leftNode; } leftNode.right = topNode; topNode.parent = leftNode; } } /** * Returns the root node of the tree. * * This protected method is needed for convenience * during the addition of elements to a Red Black Tree. */ protected BSTNode<K> root() { return this.root; } /** * Performs a pre-order traversal of the elements in the tree. For each element the * given consumer action is performed. * * Note that this method does not and cannot alter the data of the tree. * * @param action * action to be performed on each element */ public void preOrderTraversal(java.util.function.Consumer<K> action) { preOrderTraversal(this.root, action); } private static <T extends Comparable<? super T>> void preOrderTraversal( BSTNode<T> root, java.util.function.Consumer<T> action) { if (root != null) { action.accept(root.data); preOrderTraversal(root.left(), action); preOrderTraversal(root.right(), action); } } /** * Performs an in-order traversal of the elements in the tree, that is, one * in which the elements are accessed in sorted order. For each element the * given consumer action is performed. * * Note that this method does not and cannot alter the data of the tree. * * @param action * action to be performed on each element */ public void inOrderTraversal(java.util.function.Consumer<K> action) { inOrderTraversal(this.root, action); } private static <T extends Comparable<? super T>> void inOrderTraversal( BSTNode<T> root, Consumer<T> action) { if (root != null) { inOrderTraversal(root.left(), action); action.accept(root.data); inOrderTraversal(root.right(), action); } } /** * Performs a post-order traversal of the elements in the tree. For each element the * given consumer action is performed. * * Note that this method does not and cannot alter the data of the tree. * * @param action * action to be performed on each element */ public void postOrderTraversal(java.util.function.Consumer<K> action) { postOrderTraversal(this.root, action); } private static <T extends Comparable<? super T>> void postOrderTraversal( BSTNode<T> root, java.util.function.Consumer<T> action) { if (root != null) { postOrderTraversal(root.left(), action); postOrderTraversal(root.right(), action); action.accept(root.data); } } /** * Returns the size of this tree. */ public int size() { return this.size; } }
2.平衡二叉树(Balanced Binary Search Tree)
平衡二叉查找树,又称 AVL树。 它除了具备二叉查找树的基本特征之外,还具有一个非常重要的特点:它 的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值(平衡因子 ) 不超过1。 也就是说AVL树每个节点的平衡因子只可能是-1、0和1。
AVL 的操作代价分析:
- 查找代价: AVL是严格平衡的BST(平衡因子不超过1)。那么查找过程与BST一样,只是AVL不会出现最差情况的BST(单支树)。因此查找效率最好,最坏情况都是O(logN)数量级的。
- 插入代价: AVL必须要保证严格平衡(|bf|<=1),那么每一次插入数据使得AVL中某些结点的平衡因子超过1就必须进行旋转操作。事实上,AVL的每一次插入结点操作最多只需要旋转1次(单旋转或双旋转)。因此,总体上插入操作的代价仍然在O(logN)级别上(插入结点需要首先查找插入的位置)。
- 删除代价:AVL删除结点的算法可以参见BST的删除结点,但是删除之后必须检查从删除结点开始到根结点路径上的所有结点的平衡因子。因此删除的代价稍微要大一些。每一次删除操作最多需要O(logN)次旋转。因此,删除操作的时间复杂度为O(logN)+O(logN)=O(2logN)
package com.asteele.clrs.data_structures; public class AVLTree<K extends Comparable<? super K>> extends BinarySearchTree<K>{ private static class AVLNode<E extends Comparable<? super E>> extends BSTNode<E> { private int height; private AVLNode(E data) { super(data); this.height = 1; } private int balanceFactor() { return leftHeight() - rightHeight(); } private void updateHeight() { this.height = Math.max(leftHeight(), rightHeight()) + 1; } private int leftHeight() { return this.left() == null ? 0 : ((AVLNode<E>)this.left()).height; } private int rightHeight() { return this.right() == null ? 0 : ((AVLNode<E>)this.right()).height; } } /** * {@inheritDoc} */ @Override public boolean add(K element) { AVLNode<K> addedNode = new AVLNode<K>(element); boolean wasSuccessful = super.addNode(addedNode); updateTree(addedNode); // Check for balance starting at chaser. Update heights of nodes. return wasSuccessful; } /** * Walks up the tree beginning at startingNode, checking for balance and updating heights. * If the tree is unbalanced at any node on the path from the startingNode * to the root, the tree is re-balanced. */ private void updateTree(AVLNode<K> startingNode) { while (startingNode != null) { startingNode.updateHeight(); rebalanceTree(startingNode); // Ensure AVL conditions are maintained. startingNode = (AVLNode<K>) startingNode.parent(); } } /** * Re-balances the tree with the given node as its root. If the tree is * already balanced, does nothing. */ private void rebalanceTree(AVLNode<K> root) { int balanceFactor = root.balanceFactor(); if (balanceFactor < -1 || balanceFactor > 1) { AVLNode<K> problemChild; // Which child node's side is too deep? // Is left side too deep? if (balanceFactor > 1) { problemChild = (AVLNode<K>) root.left(); if (problemChild.balanceFactor() == -1) { // Root-Left-Right misalignment super.rotateLeft(problemChild); problemChild = (AVLNode<K>) root.left(); ((AVLNode<K>) problemChild.left()).updateHeight(); } // Root-Left-left misalignment super.rotateRight(root); // Else is right side too deep? } else { problemChild = (AVLNode<K>) root.right(); if (problemChild.balanceFactor() == 1) { // Root-Right-Left misalignment super.rotateRight(problemChild); problemChild = (AVLNode<K>) root.right(); ((AVLNode<K>) problemChild.right()).updateHeight(); } // Root-Right-Right misalignment super.rotateLeft(root); } root.updateHeight(); // Root is now lower than problemChild. Update first. problemChild.updateHeight(); // This is called twice, once here and once in updateTree(). } } /** * {@inheritDoc} */ @Override public boolean remove(K element) { AVLNode<K> nodeToRemove = (AVLNode<K>) search(element); if (super.removeNode(nodeToRemove)) { updateTree((AVLNode<K>) nodeToRemove.parent()); // TODO: SHOULD START AT SUCCESSOR.PARENT. return true; } return false; } }
3.红黑树(Red-BlackTree)
二叉平衡树的严格平衡策略以牺牲建立查找结构(插入,删除操作)的代价,换来了稳定的O(logN) 的查找时间复杂度。但是这样做是否值得呢?
能不能找一种折中策略,即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢? 答案就是:红黑树。
红黑树(red-black tree) 是一棵满足下述性质的二叉查找树:
- 每一个结点要么是红色,要么是黑色。
- 根结点是黑色的。
- 所有叶子结点都是黑色的(实际上都是Null指针,下图用NIL表示)。叶子结点不包含任何关键字信息,所有查询关键字都在非终结点上。
- 每个红色结点的两个子节点必须是黑色的。换句话说:从每个叶子到根的所有路径上不能有两个连续的红色结点
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点
RBT 的操作代价分析:
- 查找代价:由于红黑树的性质(最长路径长度不超过最短路径长度的2倍),可以说明红黑树虽然不像AVL一样是严格平衡的,但平衡性能还是要比BST要好。其查找代价基本维持在O(logN)左右,但在最差情况下(最长路径是最短路径的2倍少1),比AVL要略逊色一点。
- 插入代价:RBT插入结点时,需要旋转操作和变色操作。但由于只需要保证RBT基本平衡就可以了。因此插入结点最多只需要2次旋转,这一点和AVL的插入操作一样。虽然变色操作需要O(logN),但是变色操作十分简单,代价很小。
- 删除代价:RBT的删除操作代价要比AVL要好的多,删除一个结点最多只需要3次旋转操作。
import java.util.ArrayList; import java.util.List; import java.util.Random; //Binary Search Tree public class RedBlackTree extends BinarySearchTree { public final static String RED = "R"; public final static String BLACK = "B"; public final static BinarySearchTreeNode NIL = new RedBlackTreeNode(); static { NIL.setColor(BLACK); NIL.setKey(-10); } protected static void treeInsert(BinarySearchTree T, int key) { BinarySearchTreeNode a = new RedBlackTreeNode(); a.setKey(key); a.setParent(NIL); a.setLeft(NIL); a.setRight(NIL); a.setColor(""); treeInsert(T, a); } public static void treeInsert(BinarySearchTree T, BinarySearchTreeNode target) { // System.out.println("rb treeinsert"); if (target == null) { return; } BinarySearchTreeNode parent = NIL; BinarySearchTreeNode current = T.getRoot(); while (current != NIL && current != null) { parent = current; if (target.getKey() < current.getKey()) { // 小于在左边 current = current.getLeft(); } else if (target.getKey() > current.getKey()) { // 大于在右边 current = current.getRight(); } else { // 等于返回,因为左旋右旋容易出事 return; } } target.setParent(parent); if (parent == NIL) { // Tree root was empty T.setRoot(target); } else { if (target.getKey() < parent.getKey()) { parent.setLeft(target); } else { parent.setRight(target); } } target.setLeft(NIL); target.setRight(NIL); target.setColor(RED); // int spaceLength = 4; // String character = "^"; // printTreeByBFS(T.getRoot(), spaceLength, character); // printTreeByBFS(T.getRoot(), 7, "="); rbInsertFixup(T, target); // printTreeByBFS(T.getRoot(), 7, "*"); } private static void rbInsertFixup(BinarySearchTree T, BinarySearchTreeNode z) { BinarySearchTreeNode uncle = null; // System.out.println(z.getKey()); while (z.getParent().getColor() == RED) { if (z.getParent() == z.getParent().getParent().getLeft()) { uncle = z.getParent().getParent().getRight(); if (uncle.getColor() == RED) { z.getParent().setColor(BLACK); uncle.setColor(BLACK); z.getParent().getParent().setColor(RED); z = z.getParent().getParent(); } else { if (z == z.getParent().getRight()) { z = z.getParent(); leftRotate(T, z); } z.getParent().setColor(BLACK); z.getParent().getParent().setColor(RED); rightRotate(T, z.getParent().getParent()); } } else { uncle = z.getParent().getParent().getLeft(); if (uncle.getColor() == RED) { z.getParent().setColor(BLACK); uncle.setColor(BLACK); z.getParent().getParent().setColor(RED); z = z.getParent().getParent(); } else { if (z == z.getParent().getLeft()) { z = z.getParent(); rightRotate(T, z); } z.getParent().setColor(BLACK); z.getParent().getParent().setColor(RED); leftRotate(T, z.getParent().getParent()); } } } T.getRoot().setColor(BLACK); } public static void treeDelete_Suc(BinarySearchTree T, int key) { BinarySearchTreeNode target = treeSearch_Iterative(T.getRoot(), key); treeDelete_Suc(T, target); } public static BinarySearchTreeNode treeMinimum(BinarySearchTreeNode t) { while (t.getLeft() != null && t.getLeft() != NIL) { t = t.getLeft(); } return t; } public static BinarySearchTreeNode treeMaximum(BinarySearchTreeNode t) { while (t.getRight() != null && t.getRight() != NIL) { t = t.getRight(); } return t; } public static BinarySearchTreeNode treeSuccessor(BinarySearchTreeNode t) { if (t == null) { return t; } if (t.getRight() != null && t.getRight() != NIL) { return treeMinimum(t.getRight()); } BinarySearchTreeNode successor = t.getParent(); while (successor != null && successor != NIL && t == successor.getRight()) { t = successor; successor = successor.getParent(); } return successor; } public static BinarySearchTreeNode treePredecessor(BinarySearchTreeNode t) { if (t == null) { return t; } if (t.getLeft() != null && t.getLeft() != NIL) { return treeMaximum(t.getLeft()); } BinarySearchTreeNode predecessor = t.getParent(); while (predecessor != null && predecessor != NIL && t == predecessor.getLeft()) { t = predecessor; predecessor = predecessor.getParent(); } return predecessor; } public static void treeDelete_Suc(BinarySearchTree T, BinarySearchTreeNode target) { // System.out.println("rb treeDelete_Suc"); BinarySearchTreeNode candidate = null; BinarySearchTreeNode child = null; if (target == null || target == NIL) { return; } if (target.getLeft() == NIL || target.getRight() == NIL) { candidate = target; } else { candidate = treeSuccessor(target); } if (candidate.getLeft() != NIL) { child = candidate.getLeft(); } else { child = candidate.getRight(); } child.setParent(candidate.getParent()); if (candidate.getParent() == null || candidate.getParent() == NIL) { T.setRoot(child); } else { if (candidate == candidate.getParent().getLeft()) { candidate.getParent().setLeft(child); } else { candidate.getParent().setRight(child); } } if (candidate != target) { target.setKey(candidate.getKey()); } // printTreeByBFS(T.getRoot(), 7, "="); if (candidate.getColor() == BLACK) { rbDeleteFixup(T, child); } // printTreeByBFS(T.getRoot(), 7, "*"); } private static void rbDeleteFixup(BinarySearchTree T, BinarySearchTreeNode current) { BinarySearchTreeNode sibling = null; while (current != T.getRoot() && current.getColor() == BLACK) { if (current == current.getParent().getLeft()) { sibling = current.getParent().getRight(); if (sibling.getColor() == RED) { sibling.setColor(BLACK); current.getParent().setColor(RED); leftRotate(T, current.getParent()); sibling = current.getParent().getRight(); } if (sibling.getLeft().getColor() == BLACK && sibling.getRight().getColor() == BLACK) { sibling.setColor(RED); current = current.getParent(); } else { if (sibling.getRight().getColor() == BLACK) { // sibling.getRight().getColor() == RED && // sibling.getRight().getColor() == BLACK sibling.getLeft().setColor(BLACK); sibling.setColor(RED); rightRotate(T, sibling); sibling = current.getParent().getRight(); } // sibling.getRight().getColor() == RED sibling.setColor(current.getParent().getColor()); current.getParent().setColor(BLACK); sibling.getRight().setColor(BLACK); leftRotate(T, current.getParent()); current = T.getRoot(); } } else { sibling = current.getParent().getLeft(); if (sibling.getColor() == RED) { sibling.setColor(BLACK); current.getParent().setColor(RED); rightRotate(T, current.getParent()); sibling = current.getParent().getLeft(); } if (sibling.getRight().getColor() == BLACK && sibling.getLeft().getColor() == BLACK) { sibling.setColor(RED); current = current.getParent(); } else { if (sibling.getLeft().getColor() == BLACK) { sibling.getRight().setColor(BLACK); sibling.setColor(RED); leftRotate(T, sibling); sibling = current.getParent().getLeft(); } sibling.setColor(current.getParent().getColor()); current.getParent().setColor(BLACK); sibling.getLeft().setColor(BLACK); rightRotate(T, current.getParent()); current = T.getRoot(); } } } current.setColor(BLACK); } // FROM WIKI private static void rbDeleteFixup2(BinarySearchTree T, BinarySearchTreeNode child) { if (child.getColor() == RED) { child.setColor(BLACK); } else { deleteCase1(T, child); } } private static void deleteCase1(BinarySearchTree T, BinarySearchTreeNode child) { if (child.getParent() != null) { deleteCase2(T, child); } } private static void deleteCase2(BinarySearchTree T, BinarySearchTreeNode child) { BinarySearchTreeNode s = child.getSibling(); if (s.getColor() == RED) { child.getParent().setColor(RED); s.setColor(BLACK); if (child == child.getParent().getLeft()) { leftRotate(T, child.getParent()); } else { rightRotate(T, child.getParent()); } } deleteCase3(T, child); } private static void deleteCase3(BinarySearchTree T, BinarySearchTreeNode child) { BinarySearchTreeNode s = child.getSibling(); if (child.getParent().getColor() == BLACK && s.getColor() == BLACK && s.getLeft().getColor() == BLACK && s.getRight().getColor() == BLACK) { s.setColor(RED); deleteCase1(T, child.getParent()); } else { deleteCase4(T, child); } } private static void deleteCase4(BinarySearchTree T, BinarySearchTreeNode child) { BinarySearchTreeNode s = child.getSibling(); if (child.getParent().getColor() == RED && s.getColor() == BLACK && s.getLeft().getColor() == BLACK && s.getRight().getColor() == BLACK) { s.setColor(RED); child.getParent().setColor(BLACK); } else { deleteCase5(T, child); } } private static void deleteCase5(BinarySearchTree T, BinarySearchTreeNode child) { BinarySearchTreeNode s = child.getSibling(); if (s.getColor() == BLACK) { if (child == child.getParent().getLeft()) { if (s.getLeft().getColor() == RED && s.getRight().getColor() == BLACK) { s.setColor(RED); s.getLeft().setColor(BLACK); rightRotate(T, s); } } else { if (s.getLeft().getColor() == BLACK && s.getRight().getColor() == RED) { s.setColor(RED); s.getRight().setColor(BLACK); leftRotate(T, s); } } } deleteCase6(T, child); } private static void deleteCase6(BinarySearchTree T, BinarySearchTreeNode child) { BinarySearchTreeNode s = child.getSibling(); s.setColor(child.getParent().getColor()); child.getParent().setColor(BLACK); if (child == child.getParent().getLeft()) { s.getRight().setColor(BLACK); leftRotate(T, child.getParent()); } else { s.getLeft().setColor(BLACK); rightRotate(T, child.getParent()); } } public static BinarySearchTreeNode leftRotate(BinarySearchTree T, BinarySearchTreeNode target) { // System.out.println("leftRotate"); // printTreeByBFS(target, 3, "|"); if (target == null) { return null; } BinarySearchTreeNode parent = target.getRight(); target.setRight(parent.getLeft()); if (parent.getLeft() != RedBlackTree.NIL) { parent.getLeft().setParent(target); } parent.setParent(target.getParent()); if (target.getParent() == RedBlackTree.NIL) { T.setRoot(parent); } else { if (target == target.getParent().getLeft()) { target.getParent().setLeft(parent); } else { target.getParent().setRight(parent); } } parent.setLeft(target); target.setParent(parent); return parent; } public static BinarySearchTreeNode rightRotate(BinarySearchTree T, BinarySearchTreeNode target) { if (target == null) { return null; } BinarySearchTreeNode parent = target.getLeft(); target.setLeft(parent.getRight()); if (parent.getRight() != RedBlackTree.NIL) { parent.getRight().setParent(target); } parent.setParent(target.getParent()); if (target.getParent() == RedBlackTree.NIL) { T.setRoot(parent); } else { if (target == target.getParent().getRight()) { target.getParent().setRight(parent); } else { target.getParent().setLeft(parent); } } parent.setRight(target); target.setParent(parent); return parent; } public static BinarySearchTree initTree(int[] a) { BinarySearchTree T = new RedBlackTree(); int length = a.length; for (int i = 0; i < length; i++) { int v = a[i]; treeInsert(T, v); // int spaceLength = 3; // String character = "*"; // printTreeByBFS(T, spaceLength, character); } return T; } public static void main(String[] args) { int key = 85; int treeLength = 20; int spaceLength = 4; String character = " "; int[] a = getRandomArray(treeLength, key); BinarySearchTree tree = (BinarySearchTree) initTree(a); inOrderTreeWalk(tree.getRoot()); System.out.println(); printTreeByBFS(tree.getRoot(), spaceLength, character); } }
在前面专题中讲的BST、AVL、RBT都是典型的二叉查找树结构,其查找的时间复杂度与树高相关。那么降低树高自然对查找效率是有所帮助的。另外还有一个比较实际的问题:就是大量数据存储中,实现查询这样一个实际背景下,平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁,进而导致效率低下。那么如何减少树的深度(当然不能减少查询数据量),一个基本的想法就是:
1. 每个节点存储多个元素 (但元素数量不能无限多,否则查找就退化成了节点内部的线性查找了)。
2. 摒弃二叉树结构,采用多叉树 (由于节点内元素数量不能无限多,自然子树的数量也就不会无限多了)。
这样我们就提出来了一个新的查找树结构 ——多路查找树。 根据AVL给我们的启发,一颗平衡多路查找树(B~树)自然可以使得数据的查找效率保证在O(logN)这样的对数级别上。
4.多路查找树[2-4]树
树是一棵典型的平衡多路查找树。性质如下:
1. 大小性质:每个结点最多4个子结点。
2. 深度性质:所有外部结点的深度相同。
其实是一棵迷你型的B树,其主要应用并不是为了将大数据量存储在外存上,而是通过减少树高来降低二叉查找树的查找代价
5.B~树(平衡多路二叉树)
B~树,又叫平衡多路查找树。一棵m阶的B~树 (m叉树)的特性如下:
1) 树中每个结点至多有m个孩子;
2) 除根结点和叶子结点外,其它每个结点至少有[m/2]个孩子;
3) 若根结点不是叶子结点,则至少有2个孩子;
4) 所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null);
5) 每个非终端结点中包含有n个关键字信息: (n,A0,K1,A1,K2,A2,......,Kn,An)。其中,
a) Ki (i=1...n)为关键字,且关键字按顺序排序Ki < K(i-1)。
b) Ai为指向子树根的接点,且指针A(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。
c) 关键字的个数n必须满足: [m/2]-1 <= n <= m-1
6.B+树
B+树:是应文件系统所需而产生的一种B~树的变形树。 一棵m阶的B+树和m阶的B-树的差异在于:
1) 有n棵子树的结点中含有n个关键字; (B~树是n棵子树有n+1个关键字)
2) 所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (B~树的叶子节点并没有包括全部需要查找的信息)
3) 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (B~树的非终节点也包含需要查找的有效信息)
a、B+树的磁盘读写代价更低
我们都知道磁盘时可以块存储的,也就是同一个磁道上同一盘块中的所有数据都可以一次全部读取(详见《 外部存储器—磁盘 》 )。而B+树的内部结点并没有指向关键字具体信息的指针(比如文件内容的具体地址 , 比如说不包含B~树结点中的FileHardAddress[filenum]部分) 。因此其内部结点相对B~树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。这样,一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B~树(一个结点最多8个关键字)的内部结点需要2个盘快。而B+树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B~树就比B+数多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
b、B+树的查询效率更加稳定。
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。