赞
踩
要想了解AVL树,就得先知道二插搜树的性质:
如上图就是一棵二插搜索树
二插搜索树的查找效率正常情况下是 l o g 2 n log_{2}n log2n,但是在极端情况下如果这颗树转变成了单分支,也就是变成了链表形式,查找效率就是 O ( n ) O(n) O(n)了,这个时候AVL树的优势就来了。
AVL树又叫平衡二插搜索树,二插搜索树的查找效率在极端情况下是比较低的,而AVL树会保证左右子树的高度差的绝对值不会超过1,每次在插入新的节点后都会进行对应的调整,保证树的平衡。
一棵AVL数或者是空树,或者是会具有以下性质:
如果一棵 n n n个节点的二插搜索树的高度是平衡的,那么这个搜索树就是AVL树,那么可以它的高度就可以保持为 l o g 2 n log_{2}n log2n,查找的时间复杂度为 l o g 2 n log_{2}n log2n
AVL树的每一个节点的定义方式如下:
static class TreeNode {
// 值
int val;
// 左子树高度-右子树高度
int bf;// 平衡因子
TreeNode left; //左孩子
TreeNode right; // 右孩子
TreeNode parent;// 父节点引用
public TreeNode(int val) {
this.val = val;
}
}
AVL树遵循了搜索树的性质,按照搜索树的插入方式进行 插入就行
插入有几个逻辑:
接着就是进行平衡因子的调整
修改平衡因子后就需要进行判断,有三种情况:
上图是正常情况下的插入,插入元素后整棵树还是平衡的。但如果是其它情况就要进行旋转了:
/** * 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; }
当新节点插入到较高左子树的左侧,此时就会出现平衡因子为-2,其子节点为-1,就需要进行右单旋。右单旋其实就是降低左树的高度来提升右树的高度
右单旋步骤:
/** * 右单旋 * @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,此时就需要进行左单旋了。左单旋其实就是降低右树的高度来提升左树的高度。
左单选步骤:
/** * 左单旋 * @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的平衡因子来判断新元素插入左节点还是右节点
插入到较高左子树的右侧的左节点
插入到较高左子树的右侧的右节点
假设我们以元素插入到较高左子树的右侧的右节点为例子:
最后调整平衡因子有两种情况:
/** * 进行左右双旋 * @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; } }
右左双旋是当元素插入在较高右子树的左侧发生的。插入后要考虑两种情况,一个是元素插入在较高右子树的左侧的左节点,另外一种是元素插入在较高右子树的左侧的右节点。
插入到较高右子树左侧的左节点
插入到较高右子树左侧的右节点
以插入到较高右子树左侧的右节点为例子
最后调整平衡因子有两种情况:
/** * 进行右左双旋 * @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树删除元素,先要找到该元素再进行删除,但这里需要考虑到多种情况。
针对左右不为空的情况采用替换删除:
验证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; } }
AVL是一棵高度绝对平衡的二插搜索树,该树要求每个节点的左右子树高度差的绝对值不能超过1,这样可以保证其查询的时间复杂度为 O ( l o g 2 n ) O(log_{2}n) O(log2n),但如果频繁对AVL进行删除和插入操作,性能是非常低的。插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置 。所以如果需要一种查询速度快且数据有序的数据结构,并且只对这些数据进行查询就可以使用AVL树,一旦设计到插入和删除就不适合使用AVL树了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。