文章目录
- 回顾二叉搜索树
- AVL树
- 在AVL树中插入新节点
- AVL树中的各种旋转
- 右单旋
- 左单旋
- 左右双旋
- 右左双旋
- 验证是否是AVL树
- 验证是否是二叉搜索树
- 验证是否是平衡树
- 总结AVL树
回顾二叉搜索树
二叉搜索树的一些特点回顾:
(1)每一个节点左树上所有节点的值都是小于当前节点的值;每一个节点右树上的所有节点的值都是大于当前节点的值。
(2)对二叉搜索树进行层序遍历的时候,输出的是有序的序列,。
(3)二叉搜索树,顾名思义就是用来进行搜索的,当这棵二叉搜索树是平衡的时候,查询效率是最高的,但是当节点是有序插入二叉搜索树中的时候,这棵二叉搜索树就会退化成一个链表,此时的查询效率是很低的。所以总的来说,二叉搜索树的搜索效率取决于树的高度。
本文是通过上面二叉搜索树中特性三进行展开的,由于二叉搜索树可能会存在树的高度不平衡,从而导致查询效率低下的情况,对此就引出了一种比较好的解决方案 — 本文主要的内容就是约束这棵树的左右树高度差不能超过1(当然也有其他的约束规则, 后面文章会再说到),通过这个约束来尽可能地将树的高度控制平衡,进而提高查询效率,而这就是我们这篇文章中所要总结的AVL树。
AVL树
AVL树的特点是:树上的每一个节点在定义的时候都会维护一个平衡因子,而这个平衡因子其实就是当前节点右子树的高度-当前节点左子树的高度。并保持这个平衡因子的绝对值不大于1,当大于1的时候则会发生旋转来保持树高度的平衡(如何旋转会在下面总结)。
定义AVL树的代码:
static class TreeNode{
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode parent;
public int bf; //平衡因子
public TreeNode(int val){
this.val = val;
}
}
在AVL树中插入新节点
AVL树的插入主要分为两个步骤:
1.按照二叉搜索树的规则来将节点插入到AVL树中。具体就是判断要插入的数是否大于当前节点的值,如果大于的话就继续往右子树上遍历,如果小于的话就继续往左子树上遍历,如果等于的话就说明是非法插入……直到遍历到叶子节点处插入即可。
2.当插入新节点后,并且AVL树的平衡遭到破坏的时候,就需要更新平衡因子,以保证AVL树的平衡性。具体的逻辑是:
(1)修改平衡因子(从插入节点一直往上进行修改,如果当前节点在parent节点的左侧,则对parent节点的平衡因子-1;如果当前节点在parent节点的右侧,则对parent节点的平衡因子+1)。
(2)当每次修改完parent的平衡因子后,其平衡因子会出现三种情况:
一、如果平衡因子是0,那么说明在插入之前parent节点的平衡因子是±1,插入之后才可能变成0,那么进一步说明此时的AVL树已经平衡,停止继续往上调整;
二、如果平衡因子是±1,那么说明在插入之前parent节点的平衡因子是0,进一步说明AVL树的高度增加,需要继续向上调整;
三、如果平衡因子是±2,此情况只会在向上调整的过程中出现,一旦遇到这种情况,那么就说明parent节点上的平衡因子违反了AVL树平衡的性质,那么就需要对树进行旋转操作,具体如何旋转请继续往下看。
public boolean insert(int val){
TreeNode node = new TreeNode(val);
if(root == null){
root = node;
return true;
}
TreeNode parent = null;
TreeNode cur = root;
while(cur != null){
if(cur.val < val){
parent = cur;
cur = cur.right;
}else if(cur.val == val){
return false;
}else{
parent = cur;
cur = cur.left;
}
}
//插入到叶子节点上
if(parent.val < val){
parent.right = node;
}else{
parent.left = node;
}
node.parent = parent;
cur = node;
//修改平衡因子
while(parent != null){
if(cur == parent.right){
parent.bf++;
}else{
parent.bf--;
}
//确定是否需要继续向上调整
if(parent.bf == 0){
//当出现这种情况说明已经平衡
break;
}else if(parent.bf == 1 || parent.bf == -1){
//当出现这种情况需要继续向上判断
cur = parent;
parent = cur.parent;
}else{
if(parent.bf == 2){
}else{
//相当于parent.bf == -2
}
}
}
}
AVL树中的各种旋转
如果一颗树原来是平衡的AVL树,当插入一个新的节点并导致树不平衡的时候,就需要对树的结构进行调整,可以通过旋转的方式来让树变得平衡,具体有以下四种旋转方式(右单旋、左单旋、左右双旋、右左双旋):
右单旋
右旋的本质其实就是降低左树的高度,下面用一个图来进行解释:
我们假设一开始的AVL树是这样子的(所有节点的平衡因子的绝对值都不大于1,符合):
当往这个AVL树中插入值为10的节点后(向上调整平衡因子后会发现在值为60的节点上的平衡因子为-2,违反了AVL树平衡的性质):
通过右单旋操作之后就可以让AVL树恢复平衡(由于是左树高,需要降低左树的高度就必须使用右旋解决):
由上述的这个旋转规则我们发现我们只需要改变两处指向以及两处平衡因子就可以让AVL树恢复平衡,那么我们就可以使用代码来实现:
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
TreeNode pParent = parent.parent;
//修改指向
parent.left = subLR;
subL.right = parent;
if(subLR != null) {
subLR.parent = parent;
}
parent.parent = subL;
if(parent == root){
root = subL;
subL.parent = null;
}else{
if(pParent.left == parent){
pParent.left = subL;
subL.parent = pParent;
}else{
pParent.right = subL;
subL.parent = pParent;
}
}
//调整平衡因子
subL.bf = 0;
parent.bf = 0;
}
左单旋
左旋的本质其实就是降低右树的高度,其实左单旋与上面的右单旋是非常类似的,基本上就只是上面右单旋图的镜像图,此处不再详细画出旋转图。
代码实现:
private void rotateLeft(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
TreeNode pParent = parent.parent;
//修改指向
parent.right = subRL;
subR.left = parent;
if(subRL != null){
subRL.parent = parent;
}
parent.parent = subR;
if(parent == root){
root = subR;
subR.parent = null;
}else{
if(pParent.left == parent){
pParent.left = subR;
subR.parent = pParent;
}else{
pParent.right = subR;
subR.parent = pParent;
}
}
//调整平衡因子
subR.bf = 0;
parent.bf = 0;
}
左右双旋
我们假设一开始的AVL树是这样子的(所有节点的平衡因子的绝对值都不大于1,符合):
当往这个AVL树中插入值为35的节点后(向上调整平衡因子后会发现在值为60的节点上的平衡因子为-2,违反了AVL树平衡的性质):
通过左右双旋操作之后就可以让AVL树恢复平衡(因为我们无论是只进行左单旋或者是只进行右单旋,都无法使得这棵树平衡):
此外,还有另外一种是插入节点值为45的情况,我们也依旧可以使用左右双旋完成平衡:
综上两种情况可以确定规律:
1.当出现 parent 的平衡因子是 -2 且 parent.left 是 1 的时候,都是可以进行左右双旋来达到AVL树的平衡。
2.当插入节点后 parent.left.right 的平衡因子是 -1 的时候,就会构成 0-0-1 的形式;当插入节点后 parent.left.right 的平衡因子是 1 的时候,就会构成 -1-0-0 的形式。
由此两个规律以及旋转示意图,我们就可以使用代码来实现:
private void rotateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
int bf = subLR.bf;
//先左旋
rotateLeft(parent.left);
//再右旋
rotateRight(parent);
//调整平衡因子
if(bf == -1){
subL.bf = 0;
subLR.bf = 0;
parent.bf = 1;
}else if(bf == 1){
subL.bf = -1;
subLR.bf = 0;
parent.bf = 0;
}
}
右左双旋
右左双旋的本质其实左右双旋是非常类似的,基本上就只是上面左右双旋图的镜像图,具体的操作步骤也是镜像的,此处不再详细画出旋转图。
代码实现:
private void rotateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf = subRL.bf;
//先右旋
rotateRight(parent.right);
//再左旋
rotateLeft(parent);
//调整平衡因子
if(bf == 1){
parent.bf = -1;
subR.bf = 0;
subRL.bf = 0;
}else if(bf == -1){
parent.bf = 0;
subR.bf = 0;
subRL.bf = 1;
}
}
验证是否是AVL树
我们前面说过,AVL树其实就是一颗高度平衡的二叉搜索树,所以我们在对AVL树进行验证的时候,可以分为两步来进行验证:验证这棵树是否是二叉搜索树 & 验证这棵树是否是平衡的。
验证是否是二叉搜索树
我们在验证一棵树是否是二叉搜索树的时候,一般都是使用中序遍历来查看遍历的结果是否是有序的。
//中序遍历判断结果是否有序
public void inorder(TreeNode root){
if(root == null) return;
inorder(root.left);
System.out.println(root.val + " ");
inorder(root.right);
}
验证是否是平衡树
我们在判断AVL树是否平衡不能够直接判断所有节点上的平衡因子的绝对值是否小于等于1,原因是有可能我们前面计算出来的平衡因子就是错误的,正确的验证步骤应该是遍历判断节点的左右子树的高度差小于等于1。
//获取树的高度
public int height(TreeNode root){
if(root == null) return 0;
int leftH = height(root.left);
int rightH = height(root.right);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
//判断树是否平衡
public boolean isBalanced(TreeNode root){
if(root == null) return true;
int leftH = height(root.left);
int rightH = height(root.right);
if(rightH - leftH != root.bf){
System.out.println("节点:" + root.val + "平衡因子异常");
return false;
}
return Math.abs(leftH - rightH) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
总结AVL树
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效(logN)的时间复杂度。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合,这时候就可以考虑一下使用红黑树(下一章的内容)。
当然,说道AVL树节点的删除操作,这种情况几乎不会使用到(原因如上所述),但是这里总结一下删除操作的基本思路:
1.找到需要删除的节点;
2.按照二叉搜索树的删除规则删除这个节点;
3.每次更新一下平衡因子,如果出现不平衡的情况,则需要采用旋转(左单旋、右单旋、左右双旋、右左双旋)来进行调节(注意也是需要画图分析)。
最后附上AVL树的完整代码,有需要可以访问此gitee账户(链接)进行参考:AVL树代码。