前言
二叉搜索树的复杂度
对于 图1 的二叉搜索树,添加、删除、查询的复杂度为:O(h)
,h
为二叉树高度,也等于 O(logn)
,n
为二叉树的节点个数。效率比较高。此时查找元素 10,需要 3 次。
但是上图中元素如果是从小到大添加的节点,这个时候二叉树会退化成链表 如图2,此时同样查找元素 10,则需要 7 次。
当节点 n
比较大的时候,上面两种情况的效率差别非常大。
二叉搜索树的复杂度和其高度相关,要保证高效率,需要降低二叉搜索树的高度。
为了更高效的使用二叉搜索书,防止退化成链表,让其复杂度维持在 O(logn)
,可以使用平衡二叉搜索树(AVL)。
平衡:当节点数量固定时,左右子树的高度越接近,这棵二叉树就越平衡(高度越低)。
最理想的平衡,满二叉树,高度最小(高度差最小)。
平衡二叉搜索树
平衡二叉搜索树,简称平衡二叉树,英文名称 AVL树。
平衡因子:某结点的左右子树的高度差。
平衡二叉树的特点:
- 每个节点的平衡因子只可能是 1、0、-1(绝对值 ≤ 1,如果超过 1,称之为“失衡”)
- 每个节点的左右子树高度差不超过 1
- 搜索、添加、删除的时间复杂度是 O(logn)
添加导致的失衡
下图左边为一棵 AVL树,往它添加元素 11 之后,导致节点 6 的平衡因子变成 -2,此时变为非平衡二叉树,即树失衡。
添加元素可能导致AVL树失衡,在最坏的情况下,可能会导致所有添加节点祖先节点(不包括添加节点的父节点)都失衡。
添加节点的父节点、非祖先节点,都不可能失衡。
当添加元素导致 AVL树 失衡时,我们需要进行适当旋转操作,使其恢复平衡。
最小失衡子树:在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。如上图(右边非AVL树)中,以节点 6 为父节点的那棵树就称为最小失衡子树。
当添加节点导致平衡二叉树出现多棵子树失衡时,只要调整最小的不平衡子树,即可使其恢复为平衡的树。
当插入新结点导致不平衡时, 我们需要找到距离新节点最近的不平衡结点为轴来转动 AVL树来达到平衡。
LL – 右旋转
对于 LL 型,需要执行右旋转操作
LL 是指导致祖父节点失衡的节点是祖父节点的左子树的左子树(left -> left)。
右旋操作思路如下:
1)新插入的节点往上查找,找打第一个失衡的节点(姑且称为 g),以该节点为根节点的子树为最小失衡子树;
2)将失衡节点的左孩子的右子树变成该节点的左子树(g.left = g.left.right);
3)将失衡节点作为左孩子节点的右子树(g.left.right = g);
4)失衡节点的左孩子作为最小失衡子树的根节点(失衡节点root = g.left)。
还需要注意维护的内容:相关节点的 parent 属性 和 高度。
如下图,前面的 AVL树 在添加元素 4 之后导致,祖父节点(此处为根节点)8 失衡,而导致 8 失衡的节点是 8 的左子树的左子树(即 LL
型)
对于 LL
型需要执行右旋转即可使其恢复平衡,如下图:
RR – 左旋转
对于 RR
型,需要执行左旋转即可使其恢复平衡。
RR 是指导致祖父节点失衡的节点是祖父节点的右子树的由子树(right -> right)。
左旋操作思路如下:
1)新插入的节点往上查找,找打第一个失衡的节点(姑且称为 g),以该节点为根节点的子树为最小失衡子树;
2)将失衡节点的右孩子的左子树变成该节点的右子树(g.right = g.right.left);
3)将失衡节点作为右孩子节点的左子树(g.right.left = g);
4)失衡节点的右孩子作为最小失衡子树的根节点(失衡节点root = g.right)。
如下图,前面的 AVL树 在添加元素 9 之后导致,祖父节点(此处为根节点)5 失衡,而导致 5 失衡的节点是 5 的右子树的右子树(即 RR
型)。
左旋转使其恢复平衡:
LR – 左旋转、右旋转
LR 是指导致祖父节点失衡的节点是祖父节点的左子树的右子树(left -> right)。
对于 LR
型,需要先后执行左旋和右旋操作
操作思路如下:
1)新插入的节点往上查找,找打第一个失衡的节点(姑且称为 g);
2)对失衡节点的左孩子进行左旋操作,此时转化为 RR – 左旋转;
3)再对失衡节点做右旋操作,此时转化为 LL – 右旋转。
如下图,往平衡二叉树添加节点 7 后,祖父节点 8 失衡,即为:LR
型(添加的新节点为失衡节点的左子树的右子树)。
先对失衡节点 8 的左孩子 5 进行左旋操作,然后再对失衡节点 8 进行右旋操作,其过程如下图所示:
RL – 右旋转、左旋转
对于 RL
型,需要执行左旋转操作
RL 是指导致祖父节点失衡的节点是祖父节点的左子树的右子树(right -> left)。
对于 RL 型,需要先后执行右旋和左旋操作。
操作思路如下:
1)新插入的节点往上查找,找打第一个失衡的节点(姑且称为 g);
2)对失衡节点的右孩子进行右旋操作,此时转化为 LL – 右旋转;
3)再对失衡节点做左旋操作,此时转化为 RR – 左旋转。
如下图,往平衡二叉树添加节点 6 后,祖父节点 5 失衡,即为:RL
型(添加的新节点为失衡节点的右子树的左子树)。
先对失衡节点 5 的右孩子 8 进行右旋操作,然后再对失衡节点 5 进行左旋操作,其过程如下图所示:
代码实现
AVL树节点结构
/**
* AVL 树节点类
* @param <E> 泛型
*/
private static class AVLNode<E> {
E element; //节点数据
AVLNode<E> left; //左节点
AVLNode<E> right; //右节点
AVLNode<E> parent; //父节点
int height; //节点高度
public AVLNode(E element, AVLNode<E> parent) {
this.element = element;
this.parent = parent;
}
public boolean hasTwoChildren() {
return left != null && right != null;
}
/**
* 是否是左子节点
*/
public boolean isLeftChild() {
return (parent != null && this == parent.left);
}
/**
* 是否是右子节点
*/
public boolean isRightChild() {
return (parent != null && this == parent.right);
}
/**
* 获取节点的平衡因子
*/
public int balanceFactor() {
int leftHeight = left == null ? 0 : left.height;
int rightHeight = right == null ? 0 : right.height;
return leftHeight - rightHeight;
}
/**
* 更新节点高度
*/
public void updateHeight() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
height = Math.max(leftHeight, rightHeight) + 1;
}
public AVLNode<E> tallerChildNode() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
if (leftHeight > rightHeight) return left;
if (leftHeight < rightHeight) return right;
return left;
}
}
复制代码
AVL树 同二叉搜索树设计结构类似
public class AVLTree<E> {
private int size; // 树元素大小
private AVLNode<E> root; // 根节点
private Comparator<E> comparator; // 比较器,可由外部控制比较器规则
// 外部不传入比较器规则,则使用默认的存入节点的对象实现的比较规则
public AVLTree() {
this(null);
}
// 外部传入比较器规则,则使用传入的比较规则
public AVLTree(Comparator<E> comparator) {
this.comparator = comparator;
}
// 元素的数量
public int size() {
return size;
}
// 是否为空
public boolean isEmpty() {
return size == 0;
}
}
复制代码
元素添加
/**
* 添加元素
* 对比二叉搜索树,则在添加节点之后,需要恢复树的平衡
*/
public void add(E element) {
elementNotNullCheck(element);
// 添加根节点
if (root == null) {
root = new AVLNode<>(element, null);
size++;
// 添加元素之后需要恢复平衡
balanceAfterAdd(root);
return;
}
// 添加的不是第一个节点
// 1.找到父节点
AVLNode<E> parent = root;
AVLNode<E> node = root;
int cmp = 0;
while (node != null) {
// 保存父节点,如果此时不保存的话,在 while 循环发现 node == null 的时候,跳出循环
parent = node;
cmp = compare(element, node.element);
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
} else { // 相等
node.element = element;
return;
}
}
// 2.看看插入到父节点的哪个位置
AVLNode<E> newNode = new AVLNode<>(element, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
// 添加元素之后需要恢复平衡
balanceAfterAdd(newNode);
}
/**
* 添加元素之后,恢复平衡
*/
private void balanceAfterAdd(AVLNode<E> node) {
while ((node = node.parent) != null) {
// 节点是否平衡
if (isBalance(node)) {
// 更新节点高度
node.updateHeight();
} else {
// 节点不平衡,则需要回复平衡
rebalance(node);
// 最小失衡树回复平衡,则整棵树恢复平衡跳出循环
break;
}
}
}
/**
* 恢复平衡
*/
private void rebalance(AVLNode<E> node) {
AVLNode<E> subNode = node.tallerChildNode();
AVLNode<E> subSubNode = subNode.tallerChildNode();
if (subNode.isLeftChild()) { // L
if (subSubNode.isLeftChild()) { // LL - 左旋转
rotateRight(node);
} else { // LR - 子节点左旋转,然后失衡节点右旋转
rotateLeft(subNode);
rotateRight(node);
}
} else { // R
if (subSubNode.isLeftChild()) { // RL
rotateRight(subNode);
rotateLeft(node);
} else { // RR
rotateLeft(node);
}
}
}
/**
* 左旋转 - RR 型
*/
private void rotateLeft(AVLNode<E> node) {
AVLNode<E> subNode = node.right;
AVLNode<E> subSubNode = subNode.left;
node.right = subSubNode;
subNode.left = node;
// 让subNode成为最小失衡子树的根节点
subNode.parent = node.parent;
if (node.isLeftChild()) {
node.parent.left = subNode;
} else if (node.isRightChild()) {
node.parent.right = subNode;
} else {
root = subNode;
}
// 更新 subSubNode 的 父节点
if (subSubNode != null) {
subSubNode.parent = node;
}
// 更新 node 的父节点
node.parent = subNode;
// 更新节点的高度
node.updateHeight();
subNode.updateHeight();
}
/**
* 右旋转 - LL 型
*/
private void rotateRight(AVLNode<E> node) {
AVLNode<E> subNode = node.left;
AVLNode<E> subSubNode = subNode.right;
node.left = subSubNode;
subNode.right = node;
// 让subNode成为最小失衡子树的根节点
subNode.parent = node.parent;
if (node.isLeftChild()) {
node.parent.left = subNode;
} else if (node.isRightChild()) {
node.parent.right = subNode;
} else {
root = subNode;
}
// 更新 subSubNode 的 父节点
if (subSubNode != null) {
subSubNode.parent = node;
}
// 更新 node 的父节点
node.parent = subNode;
// 更新节点的高度
node.updateHeight();
subNode.updateHeight();
}
/**
* 节点是否平衡
*/
private boolean isBalance(AVLNode<E> node) {
return Math.abs(node.balanceFactor()) <= 1;
}
/******************** 我是分割线(下面是辅助的相关方法)********************/
/**
* 元素空(null)检查
* @param element 元素
*/
private void elementNotNullCheck(E element) {
if (element == null) {
throw new IllegalArgumentException("element must not be null");
}
}
/**
* 节点值比较
* @param e1
* @param e2
* @return 返回值等于0,代表e1 == e2;返回值大于0,代表e1 > e2;返回值小于0,代表e1 < e2;
*/
private int compare(E e1, E e2) {
// 外部传入了可比较器则使用传入的可比较器
if (this.comparator != null) {
return comparator.compare(e1, e2);
}
// 外部未可比较器则使用e1对应类实现的Comparable的比较器,因为二叉搜索树的节点必须存放可比较的对象元素
return ((Comparable<E>)e1).compareTo(e2);
}
复制代码
元素删除
AVL 树在删除节点可能会导致删除节点的上面的一个父节点(或父节点的父节点…)失衡,因此需要在删除节点后重新检查平衡性并使其恢复平衡。
只可能会导致父节点或祖父节点失衡(但只有一个节点失衡),让父节点恢复平衡后,可能会导致更高层的祖先节点失衡【最多需要 O(logn) 次调整】。
/**
* 删除元素
*/
public void remove(E element) {
// 通过元素值查找节点
AVLNode<E> node = node(element);
if (node == null) return;
size--;
if (node.hasTwoChildren()) { // 度为2的节点
// 找到后继结点
AVLNode<E> s = successor(node);
// 用后继结点的值覆盖读为2的节点的值
node.element = s.element;
// 删除后继结点,这里只需要将node指向s,后面代码统一删除 node 即可
node = s;
}
// 删除 node 节点(此时 node 节点的度必然是 0 或者 1,因为node的度未2的情况,上面已经特殊处理了)
AVLNode<E> replaceNode = node.left != null ? node.left : node.right;
if (replaceNode != null) { // node 是度为1的节点,使用子节点替代replaceNode替换node的位置
// 更改 parent
replaceNode.parent = node.parent;
if (node.parent == null) { // 删除的是根节点
root = replaceNode;
}
// 更改parent的left、right指向
if (node == node.parent.left) {
node.parent.left = replaceNode;
} else if (node == node.parent.right) {
node.parent.right = replaceNode;
}
// 删除节点之后恢复平衡
balanceAfterRemove(node);
} else if(node.parent == null) { // node是叶子节点且根节点
root = null;
// 删除节点之后恢复平衡
balanceAfterRemove(node);
} else { // node 是叶子节点但不是根节点
if (node == node.parent.right) {
node.parent.right = null;
} else {
node.parent.left = null;
}
// 删除节点之后恢复平衡
balanceAfterRemove(node);
}
}
/**
* 删除节点后恢复平衡
* 注意:删除节点后需要恢复所有非平衡节点
*/
private void balanceAfterRemove(AVLNode<E> node) {
while ((node = node.parent) != null) {
if (isBalance(node)) {
node.updateHeight();
} else {
// 需要恢复所有失衡节点,这里对比添加没有 break
rebalance(node);
}
}
}
/**
* 通过值找到节点
*/
private AVLNode<E> node(E element) {
AVLNode<E> node = root;
while (node != null) {
int cmp = compare(element, node.element);
// 相等(即找到)直接返回
if (cmp == 0) return node;
// 查询元素比节点值大,则继续查找节点的右子树
if (cmp > 0) {
node = node.right;
} else {
node = node.left;
}
}
// while 循环结束,未查到对应节点
return null;
}
/**
* 获取一个节点的后继结点
*/
private AVLNode<E> successor(AVLNode<E> node) {
if (node == null) return null;
// 右子树不为空,前驱结点在右子树中(right.left.left...)
if (node.right != null) {
AVLNode<E> p = node.right;
while (p.left != null) {
p = p.left;
}
return p;
}
// 右子树为空,从父节点、祖父节点..中寻找前驱结点
while (node.parent != null && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
复制代码
至此,AVL 树的相关接口已经基本实现。