一 原理部分
本质:avl本质是二叉树型数据结构,树的数据结构在有顺序性后常用于查找,avl的特点是在插入和删除的过程中始终能保持树的平衡(树的所有节点平衡,节点平衡是指左右节点高度相差小于2)
因为平衡的特性使得avl在使用的过程中不会退化,保证查找的时间复杂度稳定在 O(logN),avl命名是发明者名字取首字母的简称
保持平衡-核心原理:
既然要保持平衡,那就需要理解,不平衡的源头、如何发现不平衡,解决不平衡。也只需理解这么多,就够了。
1.源头:高度的变化(插入&删除操作)
从定义看,全部节点平衡则树平衡,而节点要平衡,即左右子节点的高度相差小于2,最终指向了节点的高度
插入&删除操作是唯二的两种影响树高的操作,插入会使得节点的高度+0或者+1,删除同理使得节点的高度-0或者-1
高度的变化会破坏平衡
2.发现:平衡因子
不平衡的特点是树的平衡是指节点的左右节点高度相差大于1,AVL引入平衡因子,来具象化的表示树是否平衡。
平衡因子 = 左子树的高度 - 右子树的高度
平衡因子 = 0 表示左右节点的高度相同,节点平衡
平衡因子 > 0 表示左节点的高于右节点,树左边高
平衡因子 < 0 表示右节点的高于左节点,树右边高
而节点的平衡要求左右子节点高度小于2,因此平衡的节点平衡因子只有三种情况:-1、0、1,即
判断节点是否平衡,只需要计算出节点的平衡因子,判断平衡因子是否大于-2且小于2
判断树是否平衡,只需判断树中所有节点是否平衡
3.解决:旋转
接下来,我们要引入新的概念了,旋转
我自己来下个定义,仅供你参考,旋转是一种对于有顺序的树,在保证顺序的前提下,使得子节点和根节点关系互换的操作
左旋(前提是右子节点不为空,否则无法操作):根节点和右子树互换父子关系,右节点作为树新的根节点,原根节点作为原右子树的最左节点(为了保证有序性,可以想象一下)
操作上来看,以右子树为圆心,使得根节点向左旋转至右节点左子树的位置
右旋(同理左子节点不为空):根节点和左子树互换父子关系,左节点作为树新的根节点,原根节点作为原左子树的最右节点(同理)
操作上来看,以左子树为圆心,使得根节点向右旋转至左节点右子树的位置
总结一下旋转操作:
-
作为圆心的节点被选举成新的根节点,树原来圆心节点方向的高度-1(圆心节点方向:左旋就是树右边,右旋就是树左边)
-
被淘汰的根节点作为新根节点的子节点,树原来非圆心节点方向的高度 = 原根节点非圆心方向子树高度 + 1 + 新根节点原来非圆心方向子树高度(只是描述的有点绕,请务必结合操作理解,后续代码部分会为这块买单的)
-
以上操作不破坏树的顺序性(左子节点比根节点小,右子节点比根节点大)
(如果是新手,建议停下来感受一下,理解清楚了再往下)
好了,明白了旋转操作,我们简单设想一下,旋转的目标如果是一棵不平衡树的根节点,且偏高方向的子节点的另外一个方向节点为空,即高度为0(很绕,对应旋转总结第二点,公式里的 新根节点原来非圆心方向子树高度 部分)
如果是树左边高我们右旋,树右边高我们左旋,带来的结果是
-
树偏高方向的高度-1 & 树偏低的方向高度+1 -> 树的平衡度-2
-
树的整体顺序性不变
-
树的整体高度-1 (注意这个点,联想一下不平衡的源头,后续代码部分会为这块买单的)
(至此,如果你全部理解了,那你已经会原理了,如果设想阶段没懂,没关系,继续往下看)
然后,我们直奔主题了,用旋转来扭转不平衡的局面
为了让你好理解旋转,我们用自底向上的视角分析,看一下不平衡的树都是什么情况
a.单层,只有一个节点,左右子树相同高度相同(左右子树都为空,空节点高度固定看作-1),一定平衡
(空节点的高度我们文章统一看作-1,当然很多地方也会看作0,其实看作什么都行,高度是一个基准,是一个相对的概念,用于比较,单独拿出来没有意义)
b.两层,一个父节点,带两个或者一个子节点,有3种情况,这两种情况都高度相差均<2,一定平衡
c.三层,一个父节点,带子辈&孙子辈节点,有n种情况,其中不平衡的有6种情况,不一定平衡
可以发现三层树不平衡的情况为其中一边的子节点为空
d.四层,一个父节点,带一个或者两个c类节点,有n多种情况,因为子节点不一定平衡,所以其本身也不一定平衡
e.五层,一个父节点,带一个或者两个d类节点,有n多种情况,因为子节点不一定平衡,所以其本身也不一定平衡
...
z.x层,一个父节点,带一个或者两个(z-1)类节点,有n多种情况,因为子节点不一定平衡,所以其本身也不一定平衡
以上我们枚举出了所有的情况,可以发现三层以下,都是平衡的无需解决,三层以上为三层的嵌套组合
三层树结构的解法,按照操作方法来分,有四种情况
LL
RL
RR
LR
以上
- LL和RR类型的情况不难理解
- LR和RL类型的情况,需要理解一下,为什么需要先旋转子树。因为如果不旋转子树,直接进行旋转的话,树的平衡性没有变化,只是从原来的方向偏向了另外一个方向。具体你可以看看前文旋转部分总结里的第二点。
总结一下:
| 失衡类型 | 失衡原因 | 旋转操作 | 旋转后效果 |
|---|---|---|---|
| LL | 左子树的左分支过高 | 对失衡节点右旋转 | 左子树高度降低,平衡因子恢复 |
| RL | 右子树的左分支过高 | 先右旋转子树,再左旋转 | 转化为 RR 型后修复 |
| RR | 右子树的右分支过高 | 对失衡节点左旋转 | 右子树高度降低,平衡因子恢复 |
| LR | 左子树的右分支过高 | 先左旋转子树,再右旋转 | 转化为 LL 型后修复 |
三层以上的解法
最小操作单元就是如上操作,自底向上检查节点的平衡性,如果不平衡做旋转操作,直至树平衡
(只需要学会了平衡的最小操作-3层树的平衡方法,就能实现avl树。而从一棵完全不确定的树转成avl树过程会比较繁琐这里就不展开细说,本质还是三层树结构平衡的操作,加上嵌套的判断实现) (我们自己实现的avl树比较简单,每次插入/删除都保证了平衡性,即每次都是在一棵平衡的树基础上,引入单个节点变量【插入/删除】,基于确定的变化触发平衡操作,相较于从一棵完全不确定的树直接转成平衡的树要效率很多)
二 代码部分
实现思路:
从源头杜绝不平衡,即保证删除和插入操作不影响整体平衡即可
- 插入,BST(AVL也是BST,不理解的看扩展部分)插入新节点都会落在叶子节点,可能会引起高度+1,高度的变化有传递性,会向父节点传递,这就会导致平衡性可能会被破坏,但也只会导致插入过程搜索位置途径链路上节点的平衡。 所以通过递归确定插入位置插入后,沿着递归回溯的方向做平衡检查,如果不平衡做旋转操作,即可使得插入不影响平衡性。 且做了平衡操作后,会恢复插入引起的高度变化(这个结合原理描述的,旋转操作对树的整体高度影响理解一下肯定能懂),使得高度变化传递被中止,后续回溯的节点都是平衡的。(插入的过程当旋转操作后,可以直接中止回溯)
- 删除,BST删除节点(不明白的可以自行了解一下),会引起操作链路上的子树高度-1,且同样会向上传递,不同的是,删除操作在回溯检查平衡性并做旋转操作后,平衡性不会停止向上传递(删除节点做旋转,树的整体高度还是回不到原来),需要回溯整个链路。
节点定义
```java
static class Node {
public Node left;
public Node right;
public int value;
public int height;
public Node(int value, Node left, Node right, int height) {
this.value = value;
this.left = left;
this.right = right;
this.height = height;
}
}
计算树高
private static int height(Node node) {
if (node == null) {
return -1;
}
return Math.max(height(node.left), height(node.right)) + 1;
}
计算平衡因子
private static int getBalance(Node node) {
if (node == null) {
return 0;
}
return height(node.left) - height(node.right);
}
平衡(基于左右子树高度最多相差2,我们的avl树不支持并发插入,所以高度最多相差2,也就是我们旋转模型的三层树结构)
private static Node balance(Node node) {
if (node == null) {
return null;
}
int balance = getBalance(node);
// 不偏
if (balance < 2 && balance > -2) {
return node;
} else if (balance > 1) {
// 左偏
int leftBalance = getBalance(node.left);
if (leftBalance >= 0) {
// 左左
return turnRight(node);
} else {
// 左右
node.left = turnLeft(node.left);
return balance(node);
}
} else {
// 右偏
int rightBalance = getBalance(node.right);
if (rightBalance <= 0) {
// 右右
return turnLeft(node);
} else {
// 右左
node.right = turnRight(node.right);
return turnLeft(node);
}
}
}
插入
private static Node insert(Node node, int data) {
if (node == null) {
// 插入节点
return new Node(data, null, null, 0);
}
if (node.value > data) {
// 向左扫描
node.left = insert(node.left, data);
} else if (node.value < data) {
// 向右扫描
node.right = insert(node.right, data);
} else {
throw new RuntimeException("节点已存在");
}
// 回溯- 判断是否平衡,
if (getBalance(node) > 1 || getBalance(node) < -1) {
// 不平衡则平衡操作,并结束递归
return balance(node);
}
// 更新高度
node.height = height(node);
return node;
}
删除
private static Node delete(Node node, int data) {
// 删除节点,判断平衡
if (node.value == data) {
Node newNode = deleteNode(node, data);
if (getBalance(newNode) > 1 || getBalance(newNode) < -1) {
return balance(newNode);
} else {
return newNode;
}
}
// 判断递归
if (node.value < data) {
node.right = delete(node.right, data);
} else {
node.left = delete(node.left, data);
}
// 回溯-判断平衡
if (getBalance(node) > 1 || getBalance(node) < -1) {
node = balance(node);
}
// 更新高度
node.height = height(node);
return node;
}
private static Node deleteNode(Node node, int data) {
if (node == null) {
throw new RuntimeException("节点不存在");
}
if (node.value == data) {
if (node.left == null && node.right == null) {
return null;
}
if (node.left == null) {
return node.right;
}
if (node.right == null) {
return node.left;
}
Node min = findMin(node.right);
node.value = min.value;
node.right = deleteNode(node.right, node.value);
} else {
if (node.value > data) {
node.left = deleteNode(node.left, data);
} else {
node.right = deleteNode(node.right, data);
}
}
if (getBalance(node) > 1 || getBalance(node) < -1) {
return balance(node);
}
node.height = height(node);
return node;
}
private static Node findMin(Node node) {
if (node.left == null) {
return node;
}
return findMin(node.left);
}
启动类
public static void main(String[] args) {
int[] arr = {7, 4, 10, 3, 1, 9, 2, 6, 5, 8};
Node node = null;
for (int i : arr) {
if (node == null) {
node = new Node(i, null, null, 0);
} else {
insert(node, i);
}
}
Node findNode = query(node, 8);
if (findNode == null) {
System.out.println("node not exist");
} else {
System.out.println(findNode.value);
}
delete(node, 3);
findNode = query(node, 3);
if (findNode == null) {
System.out.println("node not exist");
} else {
System.out.println(findNode.value);
}
}
三 扩展部分
树的分类维度多样,核心是 “节点的子节点数量” 和 “特殊约束条件”,以下是最常用的几类:
- 二叉树(Binary Tree) 定义:每个节点最多有 2 个子节点(左子节点、右子节点),是应用最广的树结构。核心性质(基于 “根节点深度为 0”): 第 k 层最多有 2^k 个节点; 高度为 h 的二叉树最多有 2^(h+1) - 1 个节点(满二叉树)。 常见子类: 满二叉树:所有叶子节点在同一层,且非叶子节点都有 2 个子节点(如高度为 2 的满二叉树有 7 个节点)。 完全二叉树:除最后一层外,其他层全满;最后一层的节点从左到右连续排列(适合用数组存储,如堆)。 平衡二叉树:任意节点的左右子树高度差绝对值≤1(如 AVL 树、红黑树,解决普通二叉树失衡问题)。
- 二叉搜索树(BST,Binary Search Tree) 定义:满足 “左子树所有节点值 < 根节点值 < 右子树所有节点值” 的二叉树,支持高效的查找、插入、删除(平均时间复杂度 O(log n))。优势:中序遍历可得到 “有序序列”;缺陷:可能退化为链表(如插入有序数据时,高度变为 n,复杂度降为 O(n)),因此衍生出平衡二叉树。
- 平衡二叉树(Self-Balancing BST) 核心目标:通过旋转操作(左旋转、右旋转)维持树的平衡,避免 BST 的退化问题。常见类型: AVL 树:严格平衡(左右子树高度差≤1),插入 / 删除后需回溯调整平衡因子,旋转次数少但实现复杂。 红黑树:近似平衡(通过 “颜色规则” 维持平衡,红节点不连续、根叶为黑),旋转次数少,工业界应用广(如 Java 的TreeMap、Linux 内核的调度器)。
- 堆(Heap) 定义:基于完全二叉树的 “优先队列” 实现,分为两种: 大顶堆:任意节点值 ≥ 其子节点值(根节点是最大值); 小顶堆:任意节点值 ≤ 其子节点值(根节点是最小值)。 优势:获取最值(O(1))、插入 / 删除(O(log n)),用于排序(堆排序)、任务调度、Top-K 问题。
- 字典树(Trie,前缀树) 定义:专门处理字符串的树结构,每个节点代表一个字符,从根到叶子的路径构成一个字符串。优势:高效的字符串前缀匹配(如自动补全、拼写检查)、去重;应用:搜索引擎关键词提示、IP 路由查找。
- B 树与 B + 树(多路平衡查找树) 定义:节点可拥有多个子节点(多路),适合 “磁盘存储”(减少 IO 次数),是数据库索引的核心结构。 B 树:所有节点(包括中间节点和叶子节点)都存储数据,适合随机访问; B + 树:仅叶子节点存储数据,且叶子节点按顺序链接(适合范围查询),是 MySQL、Oracle 等数据库的默认索引结构。
- 其他特殊树 哈夫曼树(Huffman Tree):带权路径长度最短的二叉树,用于哈夫曼编码(数据压缩,如 JPEG); 线段树(Segment Tree):用于区间查询与更新(如区间求和、最大值),时间复杂度 O(log n); 红黑树:如前所述,工业界应用最广的平衡树。