平衡二叉树
在之前的blog中讲到,平衡二叉树是一棵树,任意一个节点的左树的所有节点都小于这个节点,右树的所有节点都大于这个节点
因此,可以利用这个性质来中序遍历,就可以得到一个有序的序列,而如果我们要查找某一个值所在的节点,如果这个平衡二叉树的每个子树的左右子树高度接近,那么就会和二分查找一样,每经过一个节点就会直接淘汰一半的值,因此时间复杂度就是log以2为底的n
但是,由于并不是所有平衡二叉树都是左右子树高度接近,因此,假如出现了一棵平衡二叉树所有节点都是只有右子树,那么查找速度就会退化为链表
为了避免这种情况发生,AVL树应运而生
AVL树
是一颗平衡二叉树,并且满足左右子树的高度差不超过1
这样的话,我们的查找效率就得到了保证
平衡因子
就是用来计算二叉树左右子树的高度差的
平衡因子 = 右子树高度 - 左子树高度
节点定义
在AVL树中,不仅要定义左右孩子,还要定义平衡因子,和父亲树
static class TreeNode {
public int val;
public int bf;// 平衡因子
public TreeNode left;
public TreeNode right;
public TreeNode parent;
public TreeNode(int val){
this.val = val;
}
}
插入操作
由于AVL树需要时刻满足左右子树的高度差不大于1,那么在插入一个元素时就需要不断的调整
首先按照平衡二叉树的方法进行插入
先判断根节点是否为空,为空则直接插入
然后定义一个cur和parent,cur先为root,cur一直向下遍历,如果val小于cur.val,cur就变成cur.left,大于的话就变成cur.right,如果等于,就说明已经有这个节点了,return false,在cur遍历的过程中,使parent始终为cur的上一个节点,使得当cur变成空时,能够记录上一个节点的位置
然后判断val和parent.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){
parent = cur;
cur = cur.left;
} else {
return false;
}
}
if(parent.val < val){
parent.right = node;
} else {
parent.left = node;
}
node.parent = parent;
cur = node;
接下来,让cur重新变成插入的节点,parent变成插入节点的父节点,然后就应该调整树的结构,修改平衡因子,分为以下几种情况
如果插入节点是右子树,那么说明父亲节点的平衡因子应该++,否则就应该–
如果parent的bf已经是0了,那么上面也就不需要继续调整了,可以直接break
如果parent的bf是1或者-1,说明其子树已经平衡了,但是上面的结构不一定平衡,因此需要向上迭代,让cur为parent,parent为parent的parent
如果parent的平衡因子是2,cur的平衡因子是1,也就是下图的情况,那么需要左单旋(后面讲)
如果parent的平衡因子是2,cur的平衡因子是-1,也就是下图的情况,那么需要右左双旋(后面讲)
如果parent的平衡因子是-2,cur的平衡因子是-1,也就是下图的情况,那么需要右单旋(后面讲)
如果parent的平衡因子是-2,cur的平衡因子是1,也就是下图的情况,那么需要左右双旋,也就是先左旋,再右旋(后面讲)
//调节平衡因子
while(parent != null){
//判断cur是parent的左还是右,决定parent的平衡因子变换
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){
//右树高,降低右树的高度
if(cur.bf == 1){
//左单旋
rotateLeft(parent);
} else {
//cur.bf == -1
rotateRL(parent);
}
} else {
//左树高,降低左树的高度
//parent.bf = -2
if(cur.bf == -1){
//右单旋
rotateRight(parent);
} else {
//cur.bf == 1
rotateLR(parent);
}
}
//调整后就平衡了
break;
}
}
return true;
而对于循环条件,此循环是为了从下到上调节所有与插入节点有关的节点的平衡因子,因此需要向上迭代,最终的终止条件就是cur迭代到根,parent迭代到空
左单旋
定义parent的右孩子为subR,subR的左孩子为subRL,并依据图上关系重新组织其之间的结构。判断parent是不是根节点,如果为根就让subR为根,如果不为根,就让parent的父节点的孩子指针指向subR
然后更改平衡因子,按照图上关系,subR的平衡因子应该改为0,parent的平衡因子应该改成0(subRR的平衡因子在第一次迭代的时候就改过了)
private void rotateLeft(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRl = subR.left;
parent.right = subRl;
subR.left = parent;
if(subRl != null){
subRl.parent = parent;
}
TreeNode pParent = parent.parent;
parent.parent = subR;
if(root == parent){
root = subR;
root.parent = null;
} else {
if (pParent.left == parent){
pParent.left = subR;
} else {
pParent.right = subR;
}
subR.parent = pParent;
}
parent.bf = 0;
subR.bf = 0;
}
右单旋
定义parent的左孩子为subL,subL的右孩子为subLR,并依据图上关系重新组织其之间的结构。判断parent是不是根节点,如果为根就让subL为根,如果不为根,就让parent的父节点的孩子指针指向subL
然后更改平衡因子,按照图上关系,subL的平衡因子应该改为0,parent的平衡因子应该改成0(subLL的平衡因子在第一次迭代的时候就改过了)
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
parent.left = subLR;
subL.right = parent;
if(subLR != null) {
subLR.parent = parent;
}
TreeNode pParent = parent.parent;
parent.parent = subL;
//检查是否parent为根节点
if(parent == root){
//使subL为根节点
root = subL;
root.parent = null;
} else {
//使得parent的原parent节点的孩子为subL
if (pParent.left == parent) {
pParent.left = subL;
} else {
pParent.right = subL;
}
subL.parent = pParent;
}
//修改平衡因子
parent.bf = 0;
subL.bf = 0;
}
右左双旋
定义parent的右孩子为subR,subR的左孩子为subRL,并记录subRL的平衡因子bf
右左双旋有两种情况:
情况一:
新节点插到subRL的右孩子处,也就是bf为1
情况二:
新节点插到subRL的左孩子处,也就是bf为-1
这两种情况的修改树结构的流程是一样的,但是在修改平衡因子时有所不同
先修改树的结构:
先让parent的右孩子进行右旋转,然后让parent进行左旋转
修改平衡因子:
判断bf为-1还是1还是0
如果是1,那么就是第一张图的情况,所以parent的平衡因子会修改成-1,subRL和subR的平衡因子会修改为0
如果是-1,那么就是第二张图的情况,所以subR的平衡因子会修改为1,subRL和parent的平衡因子是0
如果是0,就相当于没插新孩子,那么就不用修正平衡因子了
private void rotateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf = subRL.bf;
rotateRight(parent.right);
rotateLeft(parent);
//修改负载因子,依据subRL的负载因子的大小
if(bf == -1){
subR.bf = 0;
subRL.bf = 1;
parent.bf = 0;
} else if (bf == 1){
//bf == 1
subR.bf = 0;
subRL.bf = 0;
parent.bf = -1;
}
//bf为0不需要修改平衡因子
}
左右双旋
和右左双旋的思路大致相同
定义parent的左孩子为subL,subL的右孩子为subLR,并记录subLR的平衡因子bf
右左双旋有两种情况:
情况一:
新节点插到subRL的右孩子处,也就是bf为1
情况二:
新节点插到subRL的左孩子处,也就是bf为-1
这两种情况的修改树结构的流程是一样的,但是在修改平衡因子时有所不同
先修改树的结构:
先让parent的右孩子进行右旋转,然后让parent进行左旋转
修改平衡因子:
判断bf为-1还是1还是0
如果是1,那么就是第一张图的情况,所以subL的平衡因子会修改成-1,subLR和parent的平衡因子会修改为0
如果是-1,那么就是第二张图的情况,所以parent的平衡因子会修改为1,subLR和subL的平衡因子是0
如果是0,就相当于没插新孩子,那么就不用修正平衡因子了
private void rotateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
int bf = subLR.bf;
rotateLeft(parent.left);
rotateRight(parent);
//修改负载因子,依据subLR的负载因子的大小
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;
}
//bf为0不需要修改平衡因子
}
到这里,插入代码就写完了
删除节点
删除节点和平衡二叉树是一样的,先找到一个替罪的,然后交换位置后再删除,唯一设计到的就是调节平衡因子,具体方法和添加节点是一样的,这里就不详细讲解了
验证AVL树
avl树不仅要通过平衡因子来判定是否正确,还需要判断其中序遍历是否为有序的,判断其是否为平衡二叉树(因为我们的平衡因子有可能算错了),因此下面的代码就比较简单了
中序遍历
public void inorder(TreeNode root){
if(root == null){
return;
}
inorder(root.left);
System.out.println(root.val);
inorder(root.right);
}
判断一个节点的高度
private int height(TreeNode root){
if(root == null){
return 0;
}
int left = height(root.left);
int right = height(root.right);
return Math.max(left,right) + 1;
}
判断是否为平衡二叉树
public boolean isBalance(TreeNode root){
if (root == null){
return true;
}
int leftHeight = height(root.left);
int rightHeight = height(root.right);
if(rightHeight - leftHeight != root.bf){
System.out.println("次节点:" + root.val + "平衡因子异常!");
return false;
}
return Math.abs(leftHeight - rightHeight) <= 1
&& isBalance(root.left)
&& isBalance(root.right);
}
如果平衡因子和高度不一致,那么可以直接终止判定,打印一个错误
验证代码
package AVLTree;
public class Test {
public static void main(String[] args) {
//int[] array = {16,3,7,11,9,26,18,14,15};
int[] array = {4,2,6,1,3,5,15,7,16,14};
AVLTree avlTree = new AVLTree();
for (int i = 0; i < array.length; i++) {
avlTree.insert(array[i]);
}
System.out.println(avlTree.isBalance(avlTree.root));
avlTree.inorder(avlTree.root);
}
}
通过上述代码,可以看到,我们的avl树的插入是正确的
完整插入和验证代码
package AVLTree;
import apple.laf.JRSUIUtils;
public class AVLTree {
static class TreeNode {
public int val;
public int bf;// 平衡因子
public TreeNode left;
public TreeNode right;
public TreeNode parent;
public TreeNode(int val){
this.val = val;
}
}
public TreeNode root;
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){
parent = cur;
cur = cur.left;
} else {
return false;
}
}
if(parent.val < val){
parent.right = node;
} else {
parent.left = node;
}
node.parent = parent;
cur = node;
//调节平衡因子
while(parent != null){
//判断cur是parent的左还是右,决定parent的平衡因子变换
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){
//右树高,降低右树的高度
if(cur.bf == 1){
//左单旋
rotateLeft(parent);
} else {
//cur.bf == -1
rotateRL(parent);
}
} else {
//左树高,降低左树的高度
//parent.bf = -2
if(cur.bf == -1){
//右单旋
rotateRight(parent);
} else {
//cur.bf == 1
rotateLR(parent);
}
}
//调整后就平衡了
break;
}
}
return true;
}
/**
* 先右旋,再左旋
* @param parent
*/
private void rotateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf = subRL.bf;
rotateRight(parent.right);
rotateLeft(parent);
//修改负载因子,依据subRL的负载因子的大小
if(bf == -1){
subR.bf = 1;
subRL.bf = 0;
parent.bf = 0;
} else if (bf == 1){
//bf == 1
subR.bf = 0;
subRL.bf = 0;
parent.bf = -1;
}
//bf为0不需要修改平衡因子
}
/**
* 先左旋,后右旋
* @param parent
*/
private void rotateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
int bf = subLR.bf;
rotateLeft(parent.left);
rotateRight(parent);
//修改负载因子,依据subLR的负载因子的大小
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;
}
//bf为0不需要修改平衡因子
}
/**
* 左单旋
* @param parent
*/
private void rotateLeft(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRl = subR.left;
parent.right = subRl;
subR.left = parent;
if(subRl != null){
subRl.parent = parent;
}
TreeNode pParent = parent.parent;
parent.parent = subR;
if(root == parent){
root = subR;
root.parent = null;
} else {
if (pParent.left == parent){
pParent.left = subR;
} else {
pParent.right = subR;
}
subR.parent = pParent;
}
parent.bf = 0;
subR.bf = 0;
}
/**
* 右单旋
* @param parent
*/
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
parent.left = subLR;
subL.right = parent;
if(subLR != null) {
subLR.parent = parent;
}
TreeNode pParent = parent.parent;
parent.parent = subL;
//检查是否parent为根节点
if(parent == root){
//使subL为根节点
root = subL;
root.parent = null;
} else {
//使得parent的原parent节点的孩子为subL
if (pParent.left == parent) {
pParent.left = subL;
} else {
pParent.right = subL;
}
subL.parent = pParent;
}
//修改平衡因子
parent.bf = 0;
subL.bf = 0;
}
/**
* 中序遍历
* @param root
*/
public void inorder(TreeNode root){
if(root == null){
return;
}
inorder(root.left);
System.out.println(root.val);
inorder(root.right);
}
private int height(TreeNode root){
if(root == null){
return 0;
}
int left = height(root.left);
int right = height(root.right);
return Math.max(left,right) + 1;
}
/**
* 判断是不是平衡二叉树
* @param root
* @return
*/
public boolean isBalance(TreeNode root){
if (root == null){
return true;
}
int leftHeight = height(root.left);
int rightHeight = height(root.right);
if(rightHeight - leftHeight != root.bf){
System.out.println("次节点:" + root.val + "平衡因子异常!");
return false;
}
return Math.abs(leftHeight - rightHeight) <= 1
&& isBalance(root.left)
&& isBalance(root.right);
}
}
分析
可以看到,我们的avl树的插入过程十分复杂,如果改动一个数据就需要从下到上重新组织数据,因此avl树只适合查找数据,并不适合频繁的插入删除数据