文章目录
- AVL树(平衡二插搜索树)
- 1.概念
- 二插搜索树
- AVL树的基本概念
- 2.AVL数的实现
- 定义AVL树
- AVL树的插入
- AVL树的旋转
- 右单旋
- 左单旋
- 左右双旋
- 右左双旋
- 删除元素
- 3. 验证AVL树
- 4.AVL树性能分析
AVL树(平衡二插搜索树)
1.概念
二插搜索树
要想了解AVL树,就得先知道二插搜树的性质:
- 二插搜索树的左子树的值要小于父亲节点的值
- 二插搜索树的右子树的值要大于父亲节点的值
如上图就是一棵二插搜索树
- 二插搜搜树的最小值在左子树,最大值在右子树
- 二插搜索树的中序遍历时一个有序序列
二插搜索树的查找效率正常情况下是 l o g 2 n log_{2}n log2n,但是在极端情况下如果这颗树转变成了单分支,也就是变成了链表形式,查找效率就是 O ( n ) O(n) O(n)了,这个时候AVL树的优势就来了。
AVL树的基本概念
AVL树又叫平衡二插搜索树,二插搜索树的查找效率在极端情况下是比较低的,而AVL树会保证左右子树的高度差的绝对值不会超过1,每次在插入新的节点后都会进行对应的调整,保证树的平衡。
一棵AVL数或者是空树,或者是会具有以下性质:
- 它的左右子树都是AVL树
- 左右子树高度只差(简称平衡因子)的绝对值不超过1(1/-1/0)
如果一棵 n n n个节点的二插搜索树的高度是平衡的,那么这个搜索树就是AVL树,那么可以它的高度就可以保持为 l o g 2 n log_{2}n log2n,查找的时间复杂度为 l o g 2 n log_{2}n log2n
2.AVL数的实现
定义AVL树
AVL树的每一个节点的定义方式如下:
- bf为平衡因子:这里采用右子树高度-左子树高度来计算平衡因子(并不是唯一方式)
static class TreeNode {
// 值
int val;
// 左子树高度-右子树高度
int bf;// 平衡因子
TreeNode left; //左孩子
TreeNode right; // 右孩子
TreeNode parent;// 父节点引用
public TreeNode(int val) {
this.val = val;
}
}
AVL树的插入
AVL树遵循了搜索树的性质,按照搜索树的插入方式进行 插入就行
- 第一步按照二插搜索树的方式插入节点
- 第二步就是调整平衡因子,如果发现某一棵子树树已经不平衡就需要进行旋转
插入有几个逻辑:
- 如果插入的元素比当前节点元素大,就插入到当前节点的右子树
- 如果插入的元素比当前节点元素小,就插入到当前节点的左子树
- 如果插入的元素和当前节点元素相等就插入失败
- 插入新节点后,要将插入节点的parent指向其父节点
接着就是进行平衡因子的调整
- 如果插入节点是在parent节点的左边,parent节点的平衡因子就减一
- 同理如果插入节点在parent节点的右边,parent节点的平衡因子就加一
修改平衡因子后就需要进行判断,有三种情况:
- 当前节点的平衡因子为0,说明插入之前树的平衡因子为 1 1 1或者- 1 1 1,插入节点后平衡因子变成0,此时满足AVL树的性质,插入成功。
- 当前节点的平衡因子为1或者-1,说明当前子树是平衡的,但并不代表整个AVL树是平衡的,所以要继续从下往上修改对应路径上的平衡因子
- 如果当前节点的平衡因子为2或者-2,说明当前树已经不平衡需要进行旋转。
上图是正常情况下的插入,插入元素后整棵树还是平衡的。但如果是其它情况就要进行旋转了:
/**
* AVL树插入元素
* @param val
*/
public boolean insert(int val) {
TreeNode newNode = new TreeNode(val);
if (root == null) {
// 第一次插入
root = newNode;
return true;
}
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
parent = cur;
if (cur.val > val) {
cur = parent.left;
} else if (cur.val == val) {
System.out.println("插入失败元素已经存在");
return false;
} else {
cur = parent.right;
}
}
// 在对应位置插入新元素
if (parent.val > val) {
parent.left = newNode;
} else {
parent.right = newNode;
}
newNode.parent = parent;
cur = newNode;
// 调整平衡因子
while (parent != null) {
if (parent.left == cur) {
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;
}
AVL树的旋转
右单旋
当新节点插入到较高左子树的左侧,此时就会出现平衡因子为-2,其子节点为-1,就需要进行右单旋。右单旋其实就是降低左树的高度来提升右树的高度
右单旋步骤:
- 先记录相关节点,parentL、parentLR、pParent
- parentLR可能出现不存在的情况,如果存在则将该节点的的parent指向当前调整的parent
- 将parent的left指向parentLR
- 将parent的parent指向parentL
- 再将parentL的right指向parent
- 接着需要判断调整的节点是否是根节点
- 如果是根节点只需要将,root指向parentL,再将parentL的parent置为null
- 入过不是根节点则需要判断parent是pParent的左节点还是右节点,对应修改引用
- 最后再调整对应的平衡因子
/**
* 右单旋
* @param parent
*/
private void rotateRight(TreeNode parent) {
// 记录对应节点
TreeNode parentL = parent.left;
TreeNode parentLR = parentL.right;
TreeNode pParent = parent.parent;
// 如果parentLR存在
if (parentLR != null) {
parentLR.parent = parent;
}
parent.left = parentLR;
parent.parent = parentL;
parentL.right = parent;
// 要调整的是根节点
if (parent == root){
root = parentL;
parentL.parent = null;
} else {
// 如果不是根节点就需要判断,当前子树是parent的左子树还是右子树
if (pParent.left == parent) {
pParent.left = parentL;
} else {
pParent.right = parentL;
}
parentL.parent = pParent;
}
// 调整平衡因子
parentL.bf = 0;
parent.bf = 0;
}
左单旋
当把新节点插入到AVL树中较高右子树的右侧后,调整平衡因子发现节点的平衡因子为2且它的子树为1,此时就需要进行左单旋了。左单旋其实就是降低右树的高度来提升左树的高度。
左单选步骤:
- 记录相关节点parentR、parentRL、pParent
- 让parent的right指向parentRL
- parentRL可能有不存在的情况,如果存在则让其的parent指向parent
- 再让parent的parent指向parentR
- 接着让parentR的left指向parent
- 判断旋转的是否是根节点,如果是在pParent是为空的,所以要进行特殊判断
- 最后更新平衡因子
/**
* 左单旋
* @param parent
*/
private void rotateLeft(TreeNode parent) {
// 记录对应节点
TreeNode parentR = parent.right;
TreeNode parentRL = parentR.left;
TreeNode pParent = parent.parent;
// 修改节点
parent.right = parentRL;
// 如果parentRL存在
if (parentRL != null) {
parentRL.parent = parent;
}
parent.parent = parentR;
parentR.left = parent;
// 如果旋转的是根节点
if (parent == root) {
root = parentR;
parentR.parent = null;
} else {
// 如果旋转的不是根节点就判断旋转的是pParent的左子树还是右子树
if (pParent.left == parent) {
pParent.left = parentR;
} else {
pParent.right = parentR;
}
parentR.parent = pParent;
}
// 更新平衡因子
parent.bf = 0;
parentR.bf = 0;
}
左右双旋
有些情况下,单纯对树进行左旋或者右旋还是无法保证树是平衡状态,所以此时就需要双旋。比如在较高左子树的右侧插入一个新元素,就需要进行左右双旋。
插入时需要考虑两种情况,一个是插入到左节点和插入到右节点:根据不同情况下的修改负载因子是不一样的,要进行特判。通过parentLR的平衡因子来判断新元素插入左节点还是右节点
插入到较高左子树的右侧的左节点
插入到较高左子树的右侧的右节点
假设我们以元素插入到较高左子树的右侧的右节点为例子:
- 先对parentL进行左单旋
- 再对parent进行右单旋
最后调整平衡因子有两种情况:
- 通过记录的parentLR的平衡因子来判断修改,如果 p a r e n t L R . b f = = 1 parentLR.bf==1 parentLR.bf==1,说明新元素插入到了右节点,如果 p a r e n t L R . b f = = − 1 parentLR.bf==-1 parentLR.bf==−1说明新元素插入到了左节点
- 如果是记录的 b f = = − 1 bf==-1 bf==−1,说明插入的元素在左子树,则需要修改对应3个节点的平衡因子, p a r e n t . b f = 1 parent.bf=1 parent.bf=1、 p a r e n t L . b f = 0 parentL.bf=0 parentL.bf=0、 p a r e n t L R . b f = 0 parentLR.bf=0 parentLR.bf=0的平衡因子
- 如果记录的 b f = = 1 bf == 1 bf==1,说明插入的元素在右子树,则需要修对应3个节点的平衡因子, p a r e n t L . b f = − 1 parentL.bf=-1 parentL.bf=−1、 p a r e n t L R . b f = 0 parentLR.bf=0 parentLR.bf=0、 p a r e n t . b f = 0 parent.bf=0 parent.bf=0
/**
* 进行左右双旋
* @param parent
*/
private void rotateLR(TreeNode parent) {
// 记录相关节点
TreeNode parentL = parent.left;
TreeNode parentLR = parentL.right;
int bf = parentLR.bf;
// 先左旋parent.left
rotateLeft(parentL);
// 再右旋parent
rotateRight(parent);
// 修改平衡因子
// 分两种情况
if (bf == -1) {
// 插入到较高左子树右侧的左子树
parent.bf = 1;
parentL.bf = 0;
parentLR.bf = 0;
} else if (bf == 1) {
//bf == 1
// 插入到较高左子树右侧的右子树
parentL.bf = -1;
parentLR.bf = 0;
parent.bf = 0;
}
}
右左双旋
右左双旋是当元素插入在较高右子树的左侧发生的。插入后要考虑两种情况,一个是元素插入在较高右子树的左侧的左节点,另外一种是元素插入在较高右子树的左侧的右节点。
插入到较高右子树左侧的左节点
插入到较高右子树左侧的右节点
以插入到较高右子树左侧的右节点为例子
- 先对parentR进行右旋
- 再对parent进行左旋
最后调整平衡因子有两种情况:
- 通过记录parentRL的平衡因子来进行判断修改,如果 p a r e n t R L . b f = = 1 parentRL.bf==1 parentRL.bf==1说明新元素插入到了右节点,如果 p a r e n t R L . b f = = − 1 parentRL.bf==-1 parentRL.bf==−1说明新元素插入到了左节点
- 如果parentRL的平衡因子 b f = = 1 bf==1 bf==1,说明新元素插入到了右子树,则需要修改 p a r e n t . b f = = − 1 parent.bf==-1 parent.bf==−1、 p a r e n t R . b f = 0 parentR.bf=0 parentR.bf=0、 p a r e n t R L . b f = 0 parentRL.bf=0 parentRL.bf=0
- 如果parentRL的平衡因子 b f = = − 1 bf == -1 bf==−1,说明新元素插入到了左子树,则需要修改 p a r e n t R . b f = 1 parentR.bf=1 parentR.bf=1、 p a r e n t . b f = 0 parent.bf=0 parent.bf=0、 p a r e n t R L = 0 parentRL=0 parentRL=0
/**
* 进行右左双旋
* @param parent
*/
private void rotateRL(TreeNode parent) {
// 记录相关节点
TreeNode parentR = parent.right;
TreeNode parentRL = parentR.left;
int bf = parentRL.bf;
rotateRight(parentR);
rotateLeft(parent);
if (bf == -1) {
parentR.bf = 1;
parent.bf = 0;
parentRL.bf = 0;
} else if (bf == 1) {
parent.bf = -1;
parentR.bf = 0;
parentRL.bf = 0;
}
}
删除元素
AVL树删除元素,先要找到该元素再进行删除,但这里需要考虑到多种情况。
- 要删除的是根节点
- 删除的节点的左子树为空
- 删除的节点的右子树为空
- 删除的节点的左右子树都不为空
- 要删除的不是根节点
- 删除的节点的左子树为空
- 删除的节点的右子树为空
- 删除的节点的左右子树都不为空
针对左右不为空的情况采用替换删除:
- 去删除节点的左子树找最大值,或者去删除节点的右子树找最小值
- 更新平衡因子的时候这里和插入相反的
- 如果删除后平衡因子是 1 1 1或者 − 1 -1 −1,说明调整前的平衡因子是0,修改后变成-1和1,并不影响上一层,依旧是平衡的
- 如果删除后平衡因子是 0 0 0,说明修改前平衡因子是 b f = = − 1 bf==-1 bf==−1或者 b f = = 1 bf==1 bf==1,说明了把高的那一棵子树的节点删掉了,此时当前子树是平衡的,但并不代表上一层就是平衡的,所以要继续向上调整
- 如果删除后更新平衡因子 b f = = 2 bf == 2 bf==2或者 b f = = − 2 bf == -2 bf==−2,说明不平衡需要进行旋转
3. 验证AVL树
验证AVL树采用判断每一个子树的左右子树高度差作为判断(右子树高度 − - −左子树高度),同时验证父节点的平衡因子的是否对应该差值。
才用后序遍历进行减枝,从AVL树的叶子节点从底之顶进行判断,可以避免重复判断,只要有一棵子树不平衡就无需判断其它节点了。
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( n ) O(n) O(n)
/**
* 判断是否AVL树
* @return
*/
public boolean isBalanced() {
return balanced(root) >= 0;
}
public int balanced(TreeNode root) {
if (root == null) {
return 0;
}
int left = balanced(root.left);
int right = balanced(root.right);
if (right-left != root.bf) {
System.out.println("节点:"+root+" 平衡因子出现问题");
return -1;
}
// 当有一颗子树不平衡时就无需判断其它节点了
if (left >= 0 && right >= 0 && Math.abs(right-left) < 2) {
return Math.max(right,left)+1;
} else {
return -1;
}
}
4.AVL树性能分析
AVL是一棵高度绝对平衡的二插搜索树,该树要求每个节点的左右子树高度差的绝对值不能超过1,这样可以保证其查询的时间复杂度为 O ( l o g 2 n ) O(log_{2}n) O(log2n),但如果频繁对AVL进行删除和插入操作,性能是非常低的。插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置 。所以如果需要一种查询速度快且数据有序的数据结构,并且只对这些数据进行查询就可以使用AVL树,一旦设计到插入和删除就不适合使用AVL树了。