上篇文章介绍了排序二叉树的内容,这篇文章介绍平衡二叉树的创建。
排序二叉树虽说查找的复杂度是O(logn),但这也是平均情况,极端情况可能就是O(n)了.比如一个没有分叉的排序二叉树,这时候树的高度就和节点的数量一致了。
平衡二叉树就是解决这个问题的,平衡二叉树(Balanced Binary Tree)是指一个二叉树中,任一节点的左右子树的高度差不超过1。平衡二叉树的性质保证了二叉搜索树(BST)的查找、插入和删除操作的时间复杂度为O(log n)。
平衡二叉树的维护需要通过旋转操作来保证树的平衡。旋转操作包括左旋(Left Rotation)和右旋(Right Rotation),通过旋转操作,可以改变节点在树中的位置,从而使树重新平衡。
像上图,左边的二叉树是非平衡二叉树,对于节点4,左子树和右子树的高度差值为2;经过处理,右边就是平衡二叉树了,即任一节点的左右子树的高度差不超过1
下面就用JS代码构建一个平衡二叉树,超级简单
准备数据
js
class Node {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
this.height = 1;
}
}
let arr = [4, 2, 7, 1, 3, 6, 9];
准备了一个节点对象,对象含有四个属性。还准备了一个数组,下面就会将这个数组转成一个平衡二叉树
平衡二叉树的构建
js
class AVLTree {
constructor() {
this.root = null;
}
getHeight(node) {
if (!node) return 0;
return node.height;
}
updateHeight(node) {
node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
}
getBalance(node) {
return this.getHeight(node.left) - this.getHeight(node.right);
}
leftRotate(node) {
let newRoot = node.right;
node.right = newRoot.left;
newRoot.left = node;
this.updateHeight(node);
this.updateHeight(newRoot);
return newRoot;
}
rightRotate(node) {
let newRoot = node.left;
node.left = newRoot.right;
newRoot.right = node;
this.updateHeight(node);
this.updateHeight(newRoot);
return newRoot;
}
balance(node) {
if (this.getBalance(node) > 1) {
if (this.getBalance(node.left) < 0) {
node.left = this.leftRotate(node.left);
}
return this.rightRotate(node);
} else if (this.getBalance(node) < -1) {
if (this.getBalance(node.right) > 0) {
node.right = this.rightRotate(node.right);
}
return this.leftRotate(node);
}
return node;
}
insert(value) {
this.root = this._insert(this.root, value);
}
_insert(node, value) {
if (!node) return new Node(value);
if (value < node.value) {
node.left = this._insert(node.left, value);
} else if (value > node.value) {
node.right = this._insert(node.right, value);
} else {
return node;
}
this.updateHeight(node);
return this.balance(node);
}
}
捋一下构建的过程:
- 如果插入的节点的值大于当前节点,就往当前节点的右子树上插;否则就往左子树上插
- 插入之后,需要更新当前节点的高度
- 然后判断当前节点是否需要平衡一下,即左右子树的高度之差大于2
- 平衡之后插入结束
首先创建一个AVLTree对象
js
class AVLTree {
updateHeight(){
}
balance(){
}
insert(value) {
this.root = this._insert(this.root, value);
}
_insert(node, value) {
if (!node) return new Node(value);
if (value < node.value) {
node.left = this._insert(node.left, value);
} else if (value > node.value) {
node.right = this._insert(node.right, value);
} else {
return node;
}
this.updateHeight(node);
return this.balance(node);
}
}
逻辑很简单,大致过程就按照之前描述的那样。至于为什么要设计成两个insert函数,后面会讲。
然后把updateHeight函数完善一下
js
getHeight(node) {
if (!node) return 0;
return node.height;
}
updateHeight(node) {
node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
}
完善balance需要补充一点知识
旋转操作
如果插入的节点使得某个子树不平衡,需要进行旋转操作。不同的平衡情况,需要不同的旋转操作。
1. LL(Left Left Rotation)
当前节点的balance的值为-2,即右子树的高度比左子树高2。然后对于右子树的根节点来说,右子树的balance为-1,即右子树的高度比左子树高1
像下面这样:
css
A
/ \
B C
/ \
D E
\
F
A的balance为-2,C的balance为-1,此时需要LL,将该树重新变成平衡二叉树
css
c
/ \
A E
/ \ \
B D F
就好像整棵树围绕着C向左旋转。A变成C的左子树,C的左子树变成A的右子树。
整棵树依然是二叉排序树
2. RR(Right Right Rotation)
当前节点的balance的值为2,即左子树的高度比右子树高2。然后对于左子树的根节点来说,左子树的balance为1,即左子树的高度比右子树高1
像下面这样:
css
A
/ \
B C
/ \
D E
/
F
A的balance为2,B的balance为-1,此时需要RR,将该树重新变成平衡二叉树
css
B
/ \
D A
/ / \
F E C
就好像整棵树围绕着B向右旋转。A变成B的右子树,B的右子树变成A的左子树。
整棵树依然是二叉排序树
3. LR(Left Right Rotation)
当前节点的balance的值为2,即左子树的高度比右子树高2。然后对于左子树的根节点来说,左子树的balance为-1
,即左子树的高度比右子树小1。这种不平衡的情况,需要旋转两次。
像下面这样:
css
A
/ \
B C
/ \
D E
/ \
F G
A的balance为2,B的balance为-1,此时需要RR,将该树重新变成平衡二叉树
经过了第一次L:
css
A
/ \
E C
/ \
B G
/ \
D F
经过了第二次R:
css
E
/ \
B A
/ \ / \
D FG C
旋转逻辑,以及子树的分配和上面的LL,RR都是一致的
整棵树依然是二叉排序树
4. RL(Right Left Rotation)
当前节点的balance的值为-2
,即右子树的高度比左子树高2。然后对右子树的根节点来说,右子树的balance为1
,即左子树的高度比右子树高1。这种不平衡的情况,需要旋转两次,先向右旋转,然后向左旋转
像下面这样:
css
A
/ \
B C
/ \
D E
/ \
G F
经过了第一次R:
css
A
/ \
B D
/ \
G C
/ \
F E
经过了第二次L:
css
D
/ \
A C
/ \ / \
B GF E
旋转逻辑,以及子树的分配和上面的LL,RR都是一致的
整棵树依然是二叉排序树
虽然是四种情况,但归根结底,还是两种变换,一个是左旋,还有一个是右旋
左旋代码
js
leftRotate(node) {
let newRoot = node.right;
node.right = newRoot.left;
newRoot.left = node;
this.updateHeight(node);
this.updateHeight(newRoot);
return newRoot;
}
左旋操作是将一个节点的右子节点的左子节点旋转上来,以维持节点的平衡。具体来说,左旋操作会交换节点 node 和其右子节点 newRoot 的左右子节点,把node当作newRoot的左节点,并且把newRoot
的左子树变成node的右节点。
最后更新节点 node 和右子节点 newRoot 的高度。最后返回新的根节点 newRoot。
右旋代码
js
rightRotate(node) {
let newRoot = node.left;
node.left = newRoot.right;
newRoot.right = node;
this.updateHeight(node);
this.updateHeight(newRoot);
return newRoot;
}
右旋操作是将一个节点的左子节点的右子节点旋转上来,以维持节点的平衡。具体来说,右旋操作会交换节点 node 和其左子节点 newRoot 的左右子节点,把node当作newRoot的右节点,并且把newRoot
的右子树变成node的左节点。
最后更新节点 node 和左子节点 newRoot 的高度。最后返回新的根节点 newRoot。
好了现在知道了必要的知识,可以完善balance方法了
js
/**
* 保持节点的平衡
* @param {AVLNode} node - 需要保持平衡的节点
* @returns {AVLNode} - 返回保持平衡后的节点
*/
balance(node) {
// 如果当前节点的平衡因子 > 1
if (this.getBalance(node) > 1) {
// 如果当前节点的左子节点的平衡因子 < 0
if (this.getBalance(node.left) < 0) {
// 则对当前节点的左子节点进行左旋操作
node.left = this.leftRotate(node.left);
}
// 对当前节点进行右旋操作
return this.rightRotate(node);
} else if (this.getBalance(node) < -1) {
// 如果当前节点的右子节点的平衡因子 > 0
if (this.getBalance(node.right) > 0) {
// 则对当前节点的右子节点进行右旋操作
node.right = this.rightRotate(node.right);
}
// 对当前节点进行左旋操作
return this.leftRotate(node);
}
// 如果当前节点的平衡因子在 -1 和 1 之间,则不需要进行旋转操作,直接返回当前节点
return node;
}
首先判断当前节点的平衡因子是否大于 1,如果是,则需要进行平衡操作。平衡操作分为两种情况:
- 如果当前节点的左子节点的平衡因子小于 0,则对当前节点的左子节点进行左旋操作。(LR)
- 对当前节点进行右旋操作,使得当前节点的左子节点成为新的根节点,从而保持树的平衡。(RR/LR)
这里可以看到,LR也就是比RR多了一个左旋,没有什么特别的
如果当前节点的平衡因子小于 -1,则需要进行平衡操作。平衡操作也分为两种情况:
- 如果当前节点的右子节点的平衡因子大于 0,则对当前节点的右子节点进行右旋操作(RL)
- 对当前节点进行左旋操作,使得当前节点的右子节点成为新的根节点,从而保持树的平衡。(LL/RL)
RL也就是比LL多了一个右旋,没有什么特别的
如果当前节点的平衡因子在 -1 和 1 之间,则不需要进行平衡操作,直接返回当前节点。
完整代码
js
class AVLTree {
constructor() {
this.root = null;
}
getHeight(node) {
if (!node) return 0;
return node.height;
}
updateHeight(node) {
node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
}
getBalance(node) {
return this.getHeight(node.left) - this.getHeight(node.right);
}
leftRotate(node) {
let newRoot = node.right;
node.right = newRoot.left;
newRoot.left = node;
this.updateHeight(node);
this.updateHeight(newRoot);
return newRoot;
}
rightRotate(node) {
let newRoot = node.left;
node.left = newRoot.right;
newRoot.right = node;
this.updateHeight(node);
this.updateHeight(newRoot);
return newRoot;
}
balance(node) {
if (this.getBalance(node) > 1) {
if (this.getBalance(node.left) < 0) {
node.left = this.leftRotate(node.left);
}
return this.rightRotate(node);
} else if (this.getBalance(node) < -1) {
if (this.getBalance(node.right) > 0) {
node.right = this.rightRotate(node.right);
}
return this.leftRotate(node);
}
return node;
}
insert(value) {
this.root = this._insert(this.root, value);
}
_insert(node, value) {
if (!node) return new Node(value);
if (value < node.value) {
node.left = this._insert(node.left, value);
} else if (value > node.value) {
node.right = this._insert(node.right, value);
} else {
return node;
}
this.updateHeight(node);
return this.balance(node);
}
}
测试下代码:
js
const printNode = (tree) => {
if (!tree) return null;
printNode(tree.left);
console.log(tree.value);
printNode(tree.right);
};
let tree = new AVLTree();
let arr = [4, 2, 7, 1, 3, 6, 9];
arr.forEach((value) => tree.insert(value));
printNode(tree.root);
//1
//2
//3
//4
//6
//7
//9
打印结果没有问题,确实是一个升序序列。再看看实际的json结构,确认是否为平衡二叉树:
yaml
{
root: {
value: 4,
left: {
value: 2,
left: {
value: 1,
left: null,
right: null,
height: 1,
},
right: {
value: 3,
left: null,
right: null,
height: 1,
},
height: 2,
},
right: {
value: 7,
left: {
value: 6,
left: null,
right: null,
height: 1,
},
right: {
value: 9,
left: null,
right: null,
height: 1,
},
height: 2,
},
height: 3,
},
}
没有问题
总结
这篇文章分享了平衡二叉树的创建,体会了一把面向对象的鬼斧神工般的代码。要我自己想肯定是想不出来
你觉得这篇文章怎么样?喜欢就点赞+关注吧