自平衡树-AVL树
前言
二叉搜索树(BST)存在一个问题,如果添加的节点数多,可能会有树的一条边特别的深,这样的状态虽然也符合二叉搜索树(BST)的特性,但是查找的性能大打折扣,几乎变成了线性查找。解决二叉搜索树多次插入新节点导致的不平衡,AVL树便是其中一种方案。
什么是Adelson-Velskii-Landi 树(AVL 树)
AVL树的命名取自两位发明者的首字母(G.M.Adelson-Velsky和E.M.Landis),AVL树也被称为平衡二叉树,AVL 树是一种自平衡二叉搜索树,遵循高度平衡,添加或移除节点时,AVL树会尝试保持自平衡,它的任何一个节点左右两侧子树的高度之差最多为 1。
AVL树相关的概念
平衡因子
概念
小灰的文章有非常好的解释,我就把小灰的这段摘取过来了。
在AVL树当中,我们通过“平衡因子”来判断一颗二叉树是否符合高度平衡。
到底什么是AVL树的平衡因子呢?
对于AVL树的每一个结点,平衡因子是它的左子树高度和右子树高度的差值。只有当二叉树所有结点的平衡因子都是-1, 0, 1这三个值的时候,这颗二叉树才是一颗合格的AVL树。
其中结点4的左子树高度是1,右子树不存在,所以该结点的平衡因子是1-0=1。
结点7的左子树不存在,右子树高度是1,所以平衡因子是0-1=-1。
所有的叶子结点,不存在左右子树,所以平衡因子都是0。
—笔记摘自公众号”程序员小灰“ 《漫画:什么是AVL树?(修订版)》
如果结果不符合上述的三个值(-1, 0, 1)之一,则需要平衡该AVL树,这就是平衡因子的概念。
相关代码
定义平衡因子的常量
1 |
|
获取节点的高度,以便计算平衡因子
1 |
|
获取节点的平衡因子
1 |
|
AVL旋转-用于树的平衡操作
在对AVL树完成插入或者移除节点的操作后,需要计算节点的平衡因子,验证树是否需要进行平衡操作,要重新恢复AVL树的平衡操作,需要靠旋转。
AVL树的旋转有这四种场景:
左-左(LL):向右的单旋转(右旋转)
这种情况出现于节点的左侧子节点的高度大于右侧子节点的高度时,并且左侧子节点也是平衡或左侧较重
顺时针旋转AVL树的两个结点X和Y,使得父结点被自己的左孩子取代,而自己成为自己的右孩子。
图中,身为左孩子的Y取代了X的位置,而X变成了自己的右孩子。此为右旋转。
—笔记摘自公众号”程序员小灰“ 《漫画:什么是AVL树?(修订版)》
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* 左-左(LL):向右的单旋转
* 这种情况出现于节点的左侧子节点的高度大于右侧子节点的高度时,并且左侧子节点也是平衡或左侧较重
* Left left case: rotate right
*
* b a
* / \ / \
* a e -> rotationLL(b) -> c b
* / \ / \
* c d d e
*
* @param node Node<T> 传入需要向右的单旋转的节点
*/
rotationLL(node) {
const tmp = node.left; // 将a节点置于b所在的位置
node.left = tmp.right; // 将b节点的左子节点置为a节点的右子节点
tmp.right = node; // 将a节点的右子节点设置为b节点
return tmp;
}右-右(RR):向左的单旋转(左旋转)
右-右的情况和左-左的情况相反。它出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点也是平衡或右侧较重的
逆时针旋转AVL树的两个结点X和Y,使得父结点被自己的右孩子取代,而自己成为自己的左孩子。说起来有些绕,见下图(标号1,2,3的三角形,是结点X和Y的子树):
图中,身为右孩子的Y取代了X的位置,而X变成了自己的左孩子。此为左旋转。
—笔记摘自公众号”程序员小灰“ 《漫画:什么是AVL树?(修订版)》
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* 右-右(RR):向左的单旋转
* 右-右的情况和左-左的情况相反。它出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点也是平衡或右侧较重的
* Right right case: rotate left
*
* a b
* / \ / \
* c b -> rotationRR(a) -> a e
* / \ / \
* d e c d
*
* @param node Node<T> 传入需要向左的单旋转的节点
*/
rotationRR(node) {
const tmp = node.right; // 将b节点置于a所在的位置
node.right = tmp.left; // 将a节点的右子节点置为b节点的左子节点
tmp.left = node; // 将b节点的左子节点设置为a节点
return tmp;
}左-右(LR):向右的双旋转(先 LL 旋转,再 RR 旋转)
这种情况出现于左侧子节点的高度大于右侧子节点的高度,并且左侧子节点右侧较重。在这种情况下,我们可以对左侧子节点进行左旋转来修复,这样会形成左-左的情况,然后再对不平衡的节点进行一个右旋转来修复
祖父结点A有一个左孩子结点B,而结点B又有一个右孩子结点C。
在这种局面下,我们先以结点B为轴,进行左旋操作:
这样就转化成了左左局面。我们继续以结点A为轴,进行右旋操作:
—笔记摘自公众号”程序员小灰“ 《漫画:什么是AVL树?(修订版)》
1
2
3
4
5
6
7
8
9
10/**
* 左-右(LR):向右的双旋转
* 这种情况出现于左侧子节点的高度大于右侧子节点的高度,并且左侧子节点右侧较重。在这种情况下,我们可以对左侧子节点进行左旋转来修复,这样会形成左-左的情况,然后再对不平衡的节点进行一个右旋转来修复
* Left right case: rotate left then right
* @param node Node<T> 传入需要向右的双旋转的节点
*/
rotationLR(node) {
node.left = this.rotationRR(node.left); // 对左侧子节点进行左旋转(旋转后仍然会存在左-左的情况)
return this.rotationLL(node); // 再对节点进行一个右旋转来修复平衡
}右-左(RL):向左的双旋转(先 RR 旋转,再 LL 旋转)
右-左的情况和左-右的情况相反。这种情况出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点左侧较重。在这种情况下我们可以对右侧子节点进行右旋转来修复,这样会形成右-右的情况,然后我们再对不平衡的节点进行一个左旋转来修复
祖父结点A有一个右孩子结点B,而结点B又有一个左孩子结点C。
在这种局面下,我们先以结点B为轴,进行右旋操作:
这样就转化成了右右局面。我们继续以结点A为轴,进行左旋操作:
—笔记摘自公众号”程序员小灰“ 《漫画:什么是AVL树?(修订版)》
1
2
3
4
5
6
7
8
9
10/**
* 右-左(RL):向左的双旋转
* 右-左的情况和左-右的情况相反。这种情况出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点左侧较重。在这种情况下我们可以对右侧子节点进行右旋转来修复,这样会形成右-右的情况,然后我们再对不平衡的节点进行一个左旋转来修复
* Right left case: rotate right then left
* @param node Node<T> 传入需要向左的双旋转的节点
*/
rotationRL(node) {
node.right = this.rotationLL(node.right); // 对右侧子节点进行右旋转(旋转后仍然会存在右-右的情况)
return this.rotationRR(node); // 再对节点进行一个左旋转来修复平衡
}代码实现
AVL树继承自BST(二叉搜索树),实现一棵AVL树只需要重构BST(二叉搜索树)类的insert和remove方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214import {
Compare,
defaultCompare,
Node,
BinarySearchTree
} from '/BinarySearchTree/app.js';
/**
* AVL树是一种自平衡树。添加或移除节点时,AVL树会尝试保持自平衡。任意一个节点(不论深度)的左子树和右子树高度最多相差 1。添加或移除节点时,AVL树会尽可能尝试转换为完全树。
*/
// 平衡因子的常量对象(保证代码优雅)
export const BalanceFactor = {
UNBALANCED_RIGHT: 1, // 右子树不平衡
SLIGHTLY_UNBALANCED_RIGHT: 2, // 右子树稍不平衡
BALANCED: 3, // 平衡
SLIGHTLY_UNBALANCED_LEFT: 4, // 左子树稍不平衡
UNBALANCED_LEFT: 5 // 左子树不平衡
};
export default class AVLTree extends BinarySearchTree {
constructor(compareFn = defaultCompare) {
super(compareFn);
this.compareFn = compareFn;
this.root = null;
}
/**
* 获取节点的高度
* @param {*} node 传入树的节点
* @return {number} 节点的高度 -1节点高度不存在,>=0则为节点的高度
*/
getNodeHeight(node) {
if (node == null) { // 如果节点为空
return -1; // 则返回-1,表示节点不存在
}
// 对比左侧树和右侧树的长度,获取长度最大的数,再把自身节点的长度加上,便是节点的高度
return Math.max(this.getNodeHeight(node.left), this.getNodeHeight(node.right)) + 1;
}
/**
* 左-左(LL):向右的单旋转
* 这种情况出现于节点的左侧子节点的高度大于右侧子节点的高度时,并且左侧子节点也是平衡或左侧较重
* Left left case: rotate right
*
* b a
* / \ / \
* a e -> rotationLL(b) -> c b
* / \ / \
* c d d e
*
* @param node Node<T> 传入需要向右的单旋转的节点
*/
rotationLL(node) {
const tmp = node.left; // 将a节点置于b所在的位置
node.left = tmp.right; // 将b节点的左子节点置为a节点的右子节点
tmp.right = node; // 将a节点的右子节点设置为b节点
return tmp;
}
/**
* 右-右(RR):向左的单旋转
* 右-右的情况和左-左的情况相反。它出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点也是平衡或右侧较重的
* Right right case: rotate left
*
* a b
* / \ / \
* c b -> rotationRR(a) -> a e
* / \ / \
* d e c d
*
* @param node Node<T> 传入需要向左的单旋转的节点
*/
rotationRR(node) {
const tmp = node.right; // 将b节点置于a所在的位置
node.right = tmp.left; // 将a节点的右子节点置为b节点的左子节点
tmp.left = node; // 将b节点的左子节点设置为a节点
return tmp;
}
/**
* 左-右(LR):向右的双旋转
* 这种情况出现于左侧子节点的高度大于右侧子节点的高度,并且左侧子节点右侧较重。在这种情况下,我们可以对左侧子节点进行左旋转来修复,这样会形成左-左的情况,然后再对不平衡的节点进行一个右旋转来修复
* Left right case: rotate left then right
* @param node Node<T> 传入需要向右的双旋转的节点
*/
rotationLR(node) {
node.left = this.rotationRR(node.left); // 对左侧子节点进行左旋转(旋转后仍然会存在左-左的情况)
return this.rotationLL(node); // 再对节点进行一个右旋转来修复平衡
}
/**
* 右-左(RL):向左的双旋转
* 右-左的情况和左-右的情况相反。这种情况出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点左侧较重。在这种情况下我们可以对右侧子节点进行右旋转来修复,这样会形成右-右的情况,然后我们再对不平衡的节点进行一个左旋转来修复
* Right left case: rotate right then left
* @param node Node<T> 传入需要向左的双旋转的节点
*/
rotationRL(node) {
node.right = this.rotationLL(node.right); // 对右侧子节点进行右旋转(旋转后仍然会存在右-右的情况)
return this.rotationRR(node); // 再对节点进行一个左旋转来修复平衡
}
/**
* 获取节点的平衡因子
* 计算一个节点的平衡因子并返回其值
*/
getBalanceFactor(node) {
// 左子树相对右子树的高度差
const heightDifference = this.getNodeHeight(node.left) - this.getNodeHeight(node.right);
// 根据高度差,计算一个节点的平衡因子并返回其值
switch (heightDifference) {
case -2: // 当高度差为-2时
return BalanceFactor.UNBALANCED_RIGHT; //返回"右子树不平衡"的常量
case -1:
return BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT; //返回"右子树稍不平衡"的常量
case 1:
return BalanceFactor.SLIGHTLY_UNBALANCED_LEFT; //返回"左子树稍不平衡"的常量
case 2:
return BalanceFactor.UNBALANCED_LEFT; //返回"左子树不平衡"的常量
default:
return BalanceFactor.BALANCED; //返回"平衡"的常量
}
}
/**
* 向树中插入一个新的键
* @param {number} key 需要插入的新键
*/
insert(key) {
this.root = this.insertNode(this.root, key);
}
/**
* insert的辅助方法
* @param {*} node 传入树的根节点
* @param {*} key 传入需要插入的新键
* insertNode 方法会帮助我们找到新节点应该插入的正确位置
*/
insertNode(node, key) {
// 像在 BST 树中一样插入节点
if (node == null) { // 检查Node类型的根节点是否为空,为空表示插入新键在第一个节点
return new Node(key);
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN) { // 如果新节点的键小于当前节点的键,新键根据二叉搜索树的规则,需要插入到左侧
node.left = this.insertNode(node.left, key); // 递归调用insertNode方法,找到节点为空的位置插入新键
} else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) { // 如果新节点的键大于当前节点的键,新键根据二叉搜索树的规则,需要插入到右侧
node.right = this.insertNode(node.right, key); // 递归调用insertNode方法,找到节点为空的位置插入新键
} else { // 如果两个键的键值相等(重复)
return node; // 直接返回,不插入新键
}
// 验证树是否平衡
const balanceFactor = this.getBalanceFactor(node); // 获取节点的平衡因子计算后的常量结果
if (balanceFactor === BalanceFactor.UNBALANCED_LEFT) { // 如果在向左侧子树插入节点后树不平衡了
if (this.compareFn(key, node.left.key) === Compare.LESS_THAN) { //比较是否插入的键小于左侧子节点的键
// Left left case
// 进行一次向右的单旋转(LL)
node = this.rotationLL(node);
} else {
// 否则进行一次向右的双旋转(LR)
// Left right case
return this.rotationLR(node);
}
}
if (balanceFactor === BalanceFactor.UNBALANCED_RIGHT) { // 如果在向右侧子树插入节点后树不平衡了
// 在从 AVL 树中移除节点后,我们需要检查树是否需要进行平衡
if (this.compareFn(key, node.right.key) === Compare.BIGGER_THAN) { // 比较是否插入的键小于右侧子节点的键
// Right right case
// 进行一次向左的单旋转(RR)
node = this.rotationRR(node);
} else {
// 否则进行一次向左的双旋转(RL)
// Right left case
return this.rotationRL(node);
}
}
return node;
}
/**
* remove的辅助方法
* @param {*} node 传入树的根节点
* @param {*} key 传入需要删除的key值
* removeNode 方法可以用来删除一棵树或其任意子树中的一个特定的值。
*/
removeNode(node, key) {
node = super.removeNode(node, key); // 以使用 BST 的 removeNode方法来从 AVL 树中移除节点
if (node == null) {
return node; // null,不需要进行平衡
}
// 检测树是否平衡
const balanceFactor = this.getBalanceFactor(node); // 获取节点的平衡因子计算后的常量结果
if (balanceFactor === BalanceFactor.UNBALANCED_LEFT) { // 如果在从左侧子树移除节点后树不平衡了
// Left left case
if (
this.getBalanceFactor(node.left) === BalanceFactor.BALANCED ||
this.getBalanceFactor(node.left) === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT
) { // 如果左侧节点平衡或者稍不平衡
return this.rotationLL(node); // 进行一次向右的单旋转(LL)
}
// Left right case
if (this.getBalanceFactor(node.left) === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT) { // 如果左侧子树向右稍不平衡
return this.rotationLR(node.left); //进行一次向右的双旋转(LR)
}
}
if (balanceFactor === BalanceFactor.UNBALANCED_RIGHT) { //如果在从右侧子树移除节点后树不平衡了
// Right right case
if (
this.getBalanceFactor(node.right) === BalanceFactor.BALANCED ||
this.getBalanceFactor(node.right) === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT
) { // 如果右侧节点平衡或者稍不平衡
return this.rotationRR(node); // 进行一次向左的单旋转(RR)
}
// Right left case
if (this.getBalanceFactor(node.right) === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT) { // 如果右侧子树向左不平衡
return this.rotationRL(node.right); // 进行一次向左的双旋转(RL)
}
}
return node; // 返回被删除的节点
}
}