本文目录
- 一、基本概念
- AVL树的定义
- 查找结点
- 插入节点
- 删除节点
- 二、树的旋转
- 右单旋和右双旋
- 左单旋和左双旋
- 三、递归实现
- 实现代码
- 测试案例
- 四、非递归实现
- 实现代码
- 测试案例
- 五、更简单的实现:不通过逻辑结构的变换删除节点,而是将补位节点的KV或孩子复制到待删除结点中来达到删除的目的
系列目录
一、基本概念
1.AVL树的定义
AVL树:首先是一颗二叉查找树,其次对于树中的任意一个节点,都有该节点的左右孩子的高度差不大于1(-1,0,1)。
AVL树属于自平衡二叉树,通过左右孩子的高度差来保证平衡。
什么是平衡?
对于二叉查找树而言,其保证顺序性,即对于二叉查找树中的任意一个节点,该节点的值大于左孩子小于右孩子。但是在一些场景下,二叉查找树会退化成一颗斜树,或者说会退化成一条链表,从O(logn)的时间复杂度退化成O(n)的时间复杂度。
如下图所示,二叉查找树退化成了斜树,时间复杂度从O(logn)退化成O(n):
如果把这颗斜树转换成AVL树,通过限制左右孩子的高度差来保证平衡(如何旋转让树重新平衡,第二节再详谈),那么其时间复杂度又恢复成了O(logn),如下示例。
a.插入节点1
b.插入节点2
c.插入节点3,此时对于节点1而言,左孩子为null高度-1,右孩子节点2的高度为1,左右孩子的高度差-2,所以需要发生一次向左旋转来让树恢复平衡;
d.插入节点4
e.插入节点5,此时对于节点3而言,左孩子为null高度为-1,右孩子节点4的高度为1,左右孩子的高度差为-2,所以需要发生一次向左旋转让树恢复平衡
f.插入节点6,此时对于节点2而言,左孩子节点1的高度为0,右孩子节点4的高度为2,左右孩子的高度差为-2,也要发生一次向左旋转让树恢复平衡
g.插入节点7,此时对于节点5而言,左孩子为null高度为-1,右孩子节点6高度为1,左右孩子的高度差为-2,所以需要发生一次向左旋转让树恢复平衡
h.插入节点8
此时,整棵树保持平衡,查找节点的时间复杂度为O(logn)。
AVL树需要实现的基本操作有:查询节点、插入节点和删除节点,其中插入节点和删除节点会破坏AVL树的平衡性,所以需要通过旋转来让AVL树重新平衡。
2.查询节点
从根结点出发:
- 如果查找key小于节点key,则向左走;
- 如果查找key大于节点key,则向右走;
- 如果查找key等于节点key,则返回该节点;
- 如果查找到叶子节点还没有找到相等key,那么返回null;
- 查找节点是否为null,是递归实现中递归的终止条件,也是非递归实现中while循环的终止条件;
AVL树的查找操作和二叉查找树一致。
3.插入节点
从根结点出发:
- 如果查找key小于节点key,则向左走;
- 如果查找key大于节点key,则向右走;
- 如果查找key等于节点key,则说明树种已经存在相同key的节点,修改该节点的value并返回;
- 如果查找到叶子节点还没有找到相同的key,则将新节点插入为节点的左孩子或右孩子;
- 查找节点是否为null,是递归实现中递归的终止条件,也是非递归实现中while循环的终止条件;
- 此时新插入节点的高度为0,但是其高度路径上所有父节点的高度都有可能发生变化从而破坏树的平衡,所以需要向上回溯,不断更新父节点的高度值直到根节点,如果某一个节点左右孩子的高度差大于1,则需要发生旋转让树恢复平衡;
AVL树的插入操作相比较二叉查找树,最后还需要向上回溯,不断更新插入节点高度路径上所有节点的高度值,直至根节点,如果发现破坏平衡的节点则需要旋转恢复平衡。
4.删除节点
从根结点出发:
- 如果查找key小于节点key,则向左走;
- 如果查找key大于节点key,则向右走;
- 如果查找key等于节点key,则说明该节点即为待删除结点,此时有两种情况:
- 待删除结点有两个孩子:这种情形,为了保证二叉排序树的顺序性(即大于左孩子小于右孩子),需要从左子树或右子树中找一个最接近待删除节点的节点来替换它,即左子树的最大节点或右子树的最小节点
- 左子树的最大节点,即左子树的最右节点
- 右子树的最小节点,即右子树的最左节点
- 注意:本文后续递归和非递归的实现中,选择右子树的最大节点(即右子树的最左节点)来代替删除节点位置,从而保证二叉查找树的有序性
- 待删除结点最多只有一个孩子:直接让子节点代替删除节点的位置
- 待删除结点有两个孩子:这种情形,为了保证二叉排序树的顺序性(即大于左孩子小于右孩子),需要从左子树或右子树中找一个最接近待删除节点的节点来替换它,即左子树的最大节点或右子树的最小节点
- 待删除节点被删除后,其高度路径上所有父节点的高度都有可能发生变化从而破坏树的平衡,所以需要向上回溯,不断更新父节点的高度值直到根节点,如果某一个节点左右孩子的高度差大于1,则需要发生旋转让树恢复平衡;
AVL树的删除操作相比较二叉查找树,最后还需要向上回溯,不断更新删除节点高度路径上的所有节点的高度值,直至根节点,如果发现破坏平衡的节点则需要旋转恢复平衡。
二、树的旋转
AVL树旋转的目的是为了让破坏AVL平衡条件的节点所在子树恢复平衡。破坏AVL树平衡条件的情况有四种:
1.右单旋和右双旋
如上图1、图2所示,对于目标节点A而言:
- 左孩子的高度值 - 右孩子的高度值 > 1:破坏了AVL树的平衡条件,确定要发生一次向右旋转,但是还不能确定是一次右单旋,还是一次右双旋,还需要做进一步的判断;
- 进一步拿到节点A的左孩子节点B:
- 如果节点B左孩子的高度值大于等于右孩子的高度值:没有发生拐点,形如图1,发生一次右单旋,子树恢复平衡;
- 如果节点B左孩子的高度值小于右孩子的高度值:发生拐点,形如图2,此时需要B节点先发生一次左单旋变成图1,然后A节点再发生一次右单旋,子树恢复平衡;
图1右单旋,伪代码如下:
a.leftChild = b.rightChild;
b.rightChild = a;
1.b节点可能有右孩子,可能没有;
2.但无论b节点有没有右孩子,旋转之后b节点的右孩子只能是a;
3.从下往上回溯的时候发现a节点不平衡,所以子树b本身是平衡的;
4.图1所示b节点的右子树最多有1个节点(如果b的右子树有两个节点,那就发生拐点变成图2),所以旋转之后,a的左孩子最多有一个节点;
图2右双旋,伪代码如下:
a节点左右孩子的高度差大于1,所以要发生一次右旋,又因为发生拐点:
1.所以需要b节点先做一次左旋,变成图1
2.然后a节点再做一次右旋,子树重新平衡
2.左单旋和左双旋
如上图3、图4所示,对于目标节点A而言:
- 左孩子的高度值 - 右孩子的高度值 < -1:破坏了AVL树的平衡条件,确定要发生一次向左旋转,但是还不能确定是一次左单旋,还是一次左双旋,还需要做进一步的判断;
- 进一步拿到节点A的右孩子节点B:
- 如果节点B左孩子的高度值小于等于右孩子的高度值:没有发生拐点,形如图3,发生一次左单旋,子树恢复平衡;
- 如果节点B左孩子的高度值大于右孩子的高度值:发生拐点,形如图4,此时需要B节点先发生一次右单旋变成图3,然后A节点再发生一次左单旋,子树恢复平衡;
图3左单旋,伪代码如下:
a.rightChild = b.leftChild;
b.leftChild = a;
1.b节点可能有左孩子,可能没有;
2.但无论b节点有没有左孩子,旋转之后b节点的左孩子只能是a;
3.从下往上回溯的时候发现a节点不平衡,所以子树b本身是平衡的;
4.图3所示b节点的左子树最多有1个节点(如果b的左子树有两个节点,那就发生拐点变成图4),所以旋转之后,a的右孩子最多有一个节点;
图4左双旋,伪代码如下:
a节点左右孩子的高度差小于-1,所以要发生一次左旋,又因为发生拐点:
1.所以需要b节点先做一次右旋,变成图3
2.然后a节点再做一次左旋,子树重新平衡
三、递归实现
1.实现代码
package cn.wxy.blog;
import java.util.Objects;
import cn.wxy.blog.TraversalTreeTool.TreeNode;
/**
* 递归实现AVL树
* @author 王大锤
* @date 2021年6月13日
*/
public class AVL<K extends Comparable<K>, V> {
static class Node<K extends Comparable<K>, V> implements TreeNode<K, Node<K, V>> {
private K key;
private V value;
private int height; // 节点的高度
private Node<K, V> parent;
private Node<K, V> leftChild;
private Node<K, V> rightChild;
public Node(K key, V value, Node<K, V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public Node<K, V> getParent() {
return parent;
}
public void setParent(Node<K, V> parent) {
this.parent = parent;
}
public Node<K, V> getLeftChild() {
return leftChild;
}
public void setLeftChild(Node<K, V> leftChild) {
this.leftChild = leftChild;
}
public Node<K, V> getRightChild() {
return rightChild;
}
public void setRightChild(Node<K, V> rightChild) {
this.rightChild = rightChild;
}
@Override
public String toString() {
return this.key + ":" + value + " ";
}
}
private Node<K, V> root;
public Node<K, V> getRoot() {
return this.root;
}
public Node<K, V> select(K key) {
if (Objects.nonNull(key)) {
return select(key, this.root);
}
return null;
}
/**
* 递归查询节点,node == null作为递归终止的条件
* @param key
* @param node
* @return
*/
private Node<K, V> select(K key, Node<K, V> node) {
// 递归终止的条件
if (Objects.isNull(node))
return null;
int compare = key.compareTo(node.key);
if (compare < 0)
return select(key, node.leftChild);
else if (compare > 0)
return select(key, node.rightChild);
else
return node;
}
public void insert(K key, V value) {
if (Objects.isNull(key))
throw new NullPointerException();
this.root = insert(key, value, this.root, null);
}
/**
* 递归插入node,node == null作为递归终止的条件
* 插入新节点可能会破坏AVL的平衡性,所以最后需要向上回溯左balance
* @param key
* @param value
* @param node
* @param parent
* @return
*/
private Node<K, V> insert(K key, V value, Node<K, V> node, Node<K, V> parent) {
// 如果递归到最后一个节点,那么创建新节点
if (Objects.isNull(node))
return new Node<K, V>(key, value, parent);
int compare = key.compareTo(node.key);
if (compare < 0)
node.leftChild = insert(key, value, node.leftChild, node);
else if (compare > 0)
node.rightChild = insert(key, value, node.rightChild, node);
else
node.value = value;
return balance(node);
}
public void remove(K key) {
if (Objects.isNull(key