谈起AVL树,就要说起 AVL树的由来 。普通的二叉搜索树到底遇到了什么问题呢?它到底能解决什么问题呢?
传统二叉搜索树存在以下问题
1. 二分查找在大多数情况下很快,但是如果 这个二叉树生成的时候有顺序,就可能 单一的偏左,或者偏右,
成为普通的单链表查询方式。效率变低。
AVL二叉树 提出的解决方案
1.保留基本的搜索二叉树的规则
2.新增 左右子树平衡因子,也就是说任意节点的左右子树的高度差不能超过1.
在插入 过程中可能遇到 打破这种规则的,也就是说 节点插入后,新插入节点到 树根之间的节点存在 高度差超过1.
此时为了保持树的平衡就需要 进行旋转
抽象的说 一共有以下四种旋转的情况
1. 在 节点的 左节点 的左子树插入节点 导致高度差超过1 (右旋)
2. 在 节点 右节点 右子树 插入 节点 导致高度差超过1 ( 左旋)
3. 在节点的 左 节点的右子树 插入节点 导致高度差超过1 ( 左 -右)
4. 在节点的 右 节点的 左子树的 插入节点 导致高度差超过1 (右-左)
具体的旋转和插入的代码如下:
import lombok.Data; public class AVLTree<E extends Comparable<E>> { // 树根结点 private Node<E> root; public Node<E> getRoot(){ return root; } // 向树中插入数据 public Node<E> add(E element) { return root = insert(element, root); } // 删除树中值为element的元素 public Node<E> delete(E element) { return root = remove(element, root); } // 前序遍历树 public void print() { print(root); } /** * 向树中插入数据 * * @param element 数据的值 * @param node 树的根结点 * @return 返回插入数据后的树的根结点 */ private Node<E> insert(E element, Node<E> node) { if (node == null) { return new Node<>(element); } if (element.compareTo(node.element) < 0) { node.left = insert(element, node.left); } else if (element.compareTo(node.element) > 0) { node.right = insert(element, node.right); } calcHeight(node); return balance(node); } /** * 删除树中的元素 * * @param element 要删除的元素值 * @param node 树的根结点 * @return 返回删除后的树根结点 */ private Node<E> remove(E element, Node<E> node) { if (node == null || (node.left == null && node.right == null)) { return null; } if (element.compareTo(node.element) < 0) { node.left = remove(element, node.left); } else if (element.compareTo(node.element) > 0) { node.right = remove(element, node.right); } else { if (node.right == null) {// 右空左不空 node = node.left; } else if (node.left == null) {// 左空右不空 node = node.right; } else {//左右都不空,则取出右子树最小结点,并用来替换根结点 Node<E> rightMin = searchMin(node.right); node.element = rightMin.element; node.right = remove(rightMin.element, node.right); } } calcHeight(node); return balance(node); } /** * 打印以node为树根的树 * * @param node 树根 */ private void print(Node<E> node) { if (node == null) { return; } System.out.println(node.element + " , height = " + node.height); print(node.left); print(node.right); } /** * AVL树的结点类 * * @param <E>结点值的类型 */ @Data class Node<E> { E element; Node<E> left; Node<E> right; int height; public Node(E element) { this.element = element; } } private Node<E> searchMin(Node<E> node) { assert node != null; if (node.left != null) { return searchMin(node.left); } return node; } /** * 计算结点的高度 * * @param node 要计算的节点 * @return 返回节点高度 */ private int height(Node<E> node) { return node == null ? -1 : node.height; } private void calcHeight(Node<E> node) { node.height = Math.max(height(node.left), height(node.right)) + 1; } /** * 左旋 * * @param node 要旋转的子树的根结点 * @return 返回旋转后的子树的根结点 */ private Node<E> leftRotate(Node<E> node) { Node<E> newNode = node.right; node.right = newNode.left; newNode.left = node; calcHeight(node); calcHeight(newNode); return newNode; } /** * 右旋 * * @param node 要旋转的子树的根结点 * @return 返回旋转后的子树的根结点 */ private Node<E> rightRotate(Node<E> node) { Node<E> newNode = node.left; node.left = newNode.right; newNode.right = node; calcHeight(node); calcHeight(newNode); return newNode; } /** * 先左旋再右旋 * * @param node 要旋转的子树的根结点 * @return 返回旋转后的子树的根结点 */ private Node<E> leftAndRightRotate(Node<E> node) { node.left = leftRotate(node.left); return rightRotate(node); } /** * 先右旋再左旋 * * @param node 要旋转的子树的根结点 * @return 返回旋转后的子树的根结点 */ private Node<E> rightAndLeftRotate(Node<E> node) { node.right = rightRotate(node.right); return leftRotate(node); } /** * 让以node为根结点的树恢复平衡 * * @param node 根结点 * @return 返回恢复平衡后的树的根结点 */ private Node<E> balance(Node<E> node) { // assert node != null; if (height(node.left) - height(node.right) == 2) { if (height(node.left.left) > height(node.left.right)) { // 需要进行右旋转 return rightRotate(node); } else {// 需要左旋再右旋 return leftAndRightRotate(node); } } else if (height(node.right) - height(node.left) == 2) { if (height(node.right.right) > height(node.right.left)) { // 需要进行左旋转 return leftRotate(node); } else {// 需要右旋再左旋 return rightAndLeftRotate(node); } } return node; } }
测试代码:
import com.alibaba.fastjson.JSON; /** * * AVL树测试 * * */ public class AVLTest { public static void main(String[] args) { AVLTree<Integer> tree = new AVLTree<>(); tree.add(3); tree.add(2); tree.add(1); tree.add(4); tree.add(5); tree.add(6); tree.add(7); tree.add(10); tree.add(9); tree.add(8); System.out.println(JSON.toJSON(tree.getRoot())); } }
此处只讲原理 ,实际应用中,大多都以红黑树 替代 AVL树。
因为AVL树是对高度差及其敏感的,这样虽然能高度保证树的平衡。使查询效率高。但是也付出了代价,得不断的根据判断高度,调整平衡。 但是在删除的时候,这个导致在 删除元素的时候,最坏的时间复杂度 O(logN)可能导致 雪崩。
因为 造成AVL删除雪崩的真正原因正是因为,他能容忍这个1的高度差。在高度差大量积累后,删除薄弱侧的节点,可能引起需要大量调整才能重新平衡的情况。
红黑树的解决方案是 容忍不平衡 【增大容忍不平衡的情况】
红黑树的思路的核心是增大了可容忍的高度差,从而实现既保证查询效率(O(logN)),也保证了插入和删除后调整平衡的效率(O(1))。
红黑树的查询效率(2 * O(logN))是略低于AVL树(O(logN))的,但是红黑树通过牺牲了少许查询效率,使插入删除后的调整效率达到了常数级别。
红黑树算法中的着色策略、对于父节点、叔节点、祖父节点等等节点的颜色判断、以及相应的调整策略都是经过极度抽象后的结果,因此想要从头到尾彻底理解红黑树的设计思想其实还是有些难度的。
顺便捎带说一句:
这就是 Java 中 HashMap 中使用 红黑树做 数据结构的原因。
有的得有舍,各种算法需要知道优缺点,还有演进路线。