文章目录
- 1. AVL树
- 1.1 AVL树的概念
- 1.2 AVL树节点的定义
- 1.3AVL树的插入
- 1.4 AVL树的删除
- 查找要删除的节点
- 判断要删除节点的类型
- 从下往上调节平衡因子
- 真正删除节点
- 整体代码
- 1.5 AVL树的性能分析
1. AVL树
1.1 AVL树的概念
二叉搜索树虽然能够缩短查找的效率,但是如果数据有序或者接近于有序的时候,二叉搜索树将会退化成单分支的树,查询元素无异于在顺序表中查找,效率低下
两位俄罗斯的数学家G.M.Adelson-Velski和和E.M.Landis发明了一种解决上述问题的方法:
当我们往二叉搜索树中插入元素的时候,如果能够保证每个节点的左右子树的高度的绝对值不超过1(这里的值可以根据实际调整),即可降低树的高度,从而减少平均搜索的长度,即AVL树
此时,如果一颗二叉搜索树是高度平衡的,那么它就是AVL树. 如果它有n个节点,其高度就可保持在 l o g 2 N log_2N log2N,
搜索的时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N)
1.2 AVL树节点的定义
给每个节点添加一个平衡因子,表示当前节点的 右子树高度 - 左子树高度
class AVLTreeNode {private int val;private AVLTreeNode left;private AVLTreeNode right;private AVLTreeNode parrent;private int bf; //平衡因子public AVLTreeNode(int val) {this.val = val;}
}
1.3AVL树的插入
- 按照二叉搜索树的插入方法,将节点插入到AVL树中
- 此时由于插入了新的节点,AVL树的平衡可能会被破坏,就需要调节平衡因子,保证树的高度平衡
public boolean insert (int val) {AVLTreeNode newNode = new AVLTreeNode(val);if(root == null) {root = newNode;return true;}AVLTreeNode cur = root;AVLTreeNode parent = null;while (cur != null) {parent = cur;if(cur.val == val) {// 不能插入return false;}else if(cur.val > val) {cur = cur.left;}else {cur = cur.right;}}// 循环出来之后,parent就是要插入的新节点的双亲节点,需要判定新节点是在parent的左边还是右边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 {// 高度不平衡,需要进行旋转,下面进行讨论}}return true;}
对于需要旋转的情况,分为以下几种情况:
- 某个节点
bf == -1
,该节点的双亲节点bf == -2
,即左树偏高
如图所示,调整到24节点的时候就要进行旋转,像这种直接单对224节点进行右单旋即可
具体方法就是,将需要右旋的节点(parent)的 左孩子节点(subL) 向上提, 将parent节点作为subL的右孩子,如果subL原本存在右孩子(subLR),那么将subLR作为旋转后parent的左孩子
具体代码:
private void rotateRight(AVLTreeNode parent) {AVLTreeNode subL = parent.left;AVLTreeNode subLR = subL.right; // 可能为空AVLTreeNode pParent = parent.parent; // 需要记录parent的双亲节点parent.parent = subL;subL.right = parent;parent.left = subLR;if(subLR != null) {subLR.parent = parent;}if(root == parent) {root = subL;root.parent = null;}else {if(pParent.left == parent) {pParent.left = subL;}else {pParent.right = subL;}subL.parent = pParent;}parent.bf = 0;subL.bf = 0;}
- 某个节点
bf == 1
,该节点的双亲节点bf == 2
,即左树偏高,即右树偏高
此时的方法与右单旋类似,需要进行左单旋
private void rotateLeft(AVLTreeNode parent) {AVLTreeNode subR = parent.right;AVLTreeNode subRL = subR.left;AVLTreeNode pParent = parent.parent;parent.parent = subR;subR.left = parent;parent.right = subRL;if(subRL != null) {subRL.parent = parent;}if(root == parent) {root = subR;subR.parent = null;}else {if(pParent.left == parent) {pParent.left = subR;}else {pParent.right = subR;}subR.parent = pParent;}parent.bf = 0;subR.bf = 0;}
- 上述两种情况是**需要旋转的节点与偏高子树的孩子节点在同一条直线上,也就是平衡因子要么是2 1搭配2要么是-2 -1搭配,都是同号的,**但是存在异号的情况
对于这种情况,简单的单旋已经没有作用了,此时需要先对subL进行右单旋,目的就是将"曲线" 变成我们上面讨论的"直线",再对parent进行左单旋即可
值得注意的是,旋转完修改平衡因子的时候,需要修改parent,subL,subLR
三处的平衡因子
但是原本subLR的平衡因子可能是1,即我们插入的是右节点,就会有一点区别
此时我们在代码中进行特判即可
private void rotateLR(AVLTreeNode parent) {AVLTreeNode subL = parent.left;AVLTreeNode subLR = subL.right; // 此时由于subL.br == 1,因此subLR不为空int bf = subLR.bf;rotateLeft(subL);rotateRight(parent);if(bf == -1) {subLR.bf = 0;parent.bf = 1;subL.bf = 0;}else if(bf == 1) { // 这里不能直接写else,下面进行解释subLR.bf = 0;parent.bf = 0;subL.bf = -1;}}
代码中这里不能直接写else,因为bf可能为0
而这种情况的平衡因子我们已经在单旋中调整过了,就不必再进行调整了
- 第4种实际上就是第三种反过来,原理是一样的
代码实现为:
private void rotateRL(AVLTreeNode parent) {AVLTreeNode subR = parent.right;AVLTreeNode subRL = subR.left;int bf = subRL.bf;rotateRight(subR);rotateLeft(parent);if(bf == 1) {parent.bf = -1;subR.bf = 0;subRL.bf = 0;}else if(bf == -1){parent.bf = 0;subR.bf = 1;subRL.bf = 0;}}
因此插入的全部代码为:
public boolean insert (int val) {AVLTreeNode newNode = new AVLTreeNode(val);if(root == null) {root = newNode;return true;}AVLTreeNode cur = root;AVLTreeNode parent = null;while (cur != null) {parent = cur;if(cur.val == val) {// 不能插入return false;}else if(cur.val > val) {cur = cur.left;}else {cur = cur.right;}}// 循环出来之后,parent就是要插入的新节点的双亲节点,需要判定新节点是在parent的左边还是右边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 {rotateRL(parent);}}else {if(cur.bf == -1) {rotateRight(parent);}else {rotateLR(parent);}}break; //此时新的parent.bf会变成0,不必再进行调整了}}return true;}
1.4 AVL树的删除
AVL树的删除步骤为:
- 找到要删除的节点
- 判断删除节点的类型.有不同的处理方式(下面进行讨论)
- 待删除节点的左孩子为空
- 待删除节点的右孩子为空
- 左右节点都不为空
- 从下往上调节平衡因子
查找要删除的节点
这一步比较简单,我们直接代码演示:
public AVLTreeNode remove(int key) {if(root == null) {return null;}AVLTreeNode cur = root;AVLTreeNode parent = cur;while(cur != null) {if(cur.val > key) {parent = cur;cur = cur.left;}else if(cur.val < key) {parent = cur;cur = cur.right;}else {return removeNode(parent,cur);}}return null;}
判断要删除节点的类型
- 待删除节点的左孩子为空
此时直接让改变指向即可,但是需要考虑平衡因子的问题(下面再进行讨论)
但是需要注意,如果删除的节点刚好是根节点,那就直接让根节点指向原本根节点的右边即可,并且此时不必关心平衡因子的问题,因为根节点的右孩子所在的树,本身就是平衡的,那就直接让根节点指向原本根节点的右边即可,并且此时不必关心平衡因子的问题,因为根节点的右孩子所在的树,本身就是平衡的
- 待删除节点的右孩子为空
同样直接改变引用即可,也需要注意平衡因子的变化
如果删除的节点刚好是根节点,那就直接让根节点指向原本根节点的左边即可,并且此时不必关心平衡因子的问题,因为根节点的左孩子孩子所在的树,本身就是平衡的
- 待删除节点的右孩子均不为空
此时关于节点的删除类似于二叉搜索树节点的删除,需要寻找替换节点,
即可以找到待删除节点的左树的最大值(或者右树的最小值)进行值的替换,接着将替罪羊删除即可,本文采取方式二
同样需要考虑平衡因子的替换
因为需要调节平衡因子,因此我们可以先找到最终要删除的节点以及要删除节点的父亲节点的引用,进行平衡因子的调节之后,再进行真正的删除操作,以下是这一部分的代码:
private AVLTreeNode removeNode(AVLTreeNode parent, AVLTreeNode cur) {AVLTreeNode delNode = null; // 最终要删除的节点AVLTreeNode pDelNode = null;// 最终要删除的节点的父亲节点if(cur.left == null) {if(cur == root) {root = cur.right;if(root != null) {root.parent = null;}return cur; // 不需要考虑平衡因子的影响,因为右树本身就平衡}else {pDelNode = parent;delNode = cur;}}else if(cur.right == null) {if(cur == root) {root = cur.left;if(root != null) {root.parent = null;}return cur;// 不需要考虑平衡因子的影响,因为左树本身就平衡}else {pDelNode = parent;delNode = cur;}}else {AVLTreeNode minRightNode = cur.right;AVLTreeNode pMinRightNode = cur;// 找到右树的最小值while (minRightNode.left != null) {pMinRightNode = minRightNode;minRightNode = minRightNode.left;}cur.val = minRightNode.val;delNode = minRightNode;pDelNode = pMinRightNode;}}
从下往上调节平衡因子
注意,如果调节后的bf是-1 或者 1,说明原本的高度是没有整体的高度是没有变化的,不会影响上层,因此不必再进行调整(这点与插入节点有区别)
代码为:
AVLTreeNode curDelNode = delNode;AVLTreeNode pCurDelNode = pDelNode;// 记录下两个节点,以防旋转的过程丢失原本的引用// 更新平衡因子while (delNode != root) {if(delNode == pDelNode.left) {pDelNode.bf++;}else {pDelNode.bf--;}if(pDelNode.bf == 0) {// 继续向上调整delNode = pDelNode;pDelNode = pDelNode.parent;}else if(pDelNode.bf == -1 || pDelNode.bf == 1) {break;}else {// 进行旋转.下面进行讨论}delNode = pDelNode;pDelNode = pDelNode.parent;}}
接下来我们进行旋转的讨论:
前两种情况与插入操作一致,但是需要继续向上调整平衡因子
第三种是删除操作出现的特殊情况,旋转完后,整棵树一定是平衡的,因此需要手动需将平衡因子更改,不必再向上调整了
上面三种情况存在反方向的另外三种情况,原理一致
代码为:
AVLTreeNode curDelNode = delNode;AVLTreeNode pCurDelNode = pDelNode;// 记录// 更新平衡因子while (delNode != root) {if(delNode == pDelNode.left) {pDelNode.bf++;}else {pDelNode.bf--;}if(pDelNode.bf == 0) {// 继续向上调整delNode = pDelNode;pDelNode = pDelNode.parent;}else if(pDelNode.bf == -1 || pDelNode.bf == 1) {break;}else {// 进行旋转if(pDelNode.bf == 2) {if(pDelNode.right.bf == 1) {AVLTreeNode tmp = pDelNode.right;//先记录,以供继续向上调整rotateLeft(pDelNode);pDelNode = tmp;}else if(pDelNode.right.bf == -1){AVLTreeNode tmp = pDelNode.right.left;rotateRL((pDelNode));pDelNode = tmp;}else {// 特殊讨论AVLTreeNode tmp = pDelNode.right;rotateLeft(pDelNode);pDelNode = tmp;pDelNode.bf = -1;pDelNode.left.bf = 1;break;}}else {if(pDelNode.left.bf == -1) {AVLTreeNode tmp = pDelNode.left;rotateRight(pDelNode);pDelNode = tmp;}else if(pDelNode.left.bf == 1){AVLTreeNode tmp = pDelNode.left.right;rotateLR(pDelNode);pDelNode = tmp;}else {// 特殊讨论AVLTreeNode tmp = pDelNode.left;rotateRight(pDelNode);pDelNode = tmp;pDelNode.bf = 1;pDelNode.right.bf = -1;break;}}delNode = pDelNode;pDelNode = pDelNode.parent;}}
真正删除节点
最后利用我们先前记录下的引用,进行删除节点即可
//删除if(curDelNode.left == null) {if(curDelNode == pCurDelNode.left) {pCurDelNode.left = curDelNode.right;if(curDelNode.right != null) {curDelNode.right.parent = pCurDelNode;}}else {pCurDelNode.right = curDelNode.right;if(curDelNode.right != null) {curDelNode.right.parent = pCurDelNode;}}}else {if(curDelNode == pCurDelNode.left) {pCurDelNode.left = curDelNode.left;if(curDelNode.left != null) {curDelNode.left.parent = pCurDelNode;}}else {pCurDelNode.right = curDelNode.left;if(curDelNode.left != null) {curDelNode.left.parent = pCurDelNode;}}}
整体代码
public AVLTreeNode remove(int key) {if(root == null) {return null;}AVLTreeNode cur = root;AVLTreeNode parent = cur;while(cur != null) {if(cur.val > key) {parent = cur;cur = cur.left;}else if(cur.val < key) {parent = cur;cur = cur.right;}else {return removeNode(parent,cur);}}return null;}private AVLTreeNode removeNode(AVLTreeNode parent, AVLTreeNode cur) {AVLTreeNode delNode = null;AVLTreeNode pDelNode = null;if(cur.left == null) {if(cur == root) {root = cur.right;if(root != null) {root.parent = null;}return cur;}else {pDelNode = parent;delNode = cur;}}else if(cur.right == null) {if(cur == root) {root = cur.left;if(root != null) {root.parent = null;}return cur;}else {pDelNode = parent;delNode = cur;}}else {AVLTreeNode minRightNode = cur.right;AVLTreeNode pMinRightNode = cur;while (minRightNode.left != null) {pMinRightNode = minRightNode;minRightNode = minRightNode.left;}cur.val = minRightNode.val;delNode = minRightNode;pDelNode = pMinRightNode;}AVLTreeNode curDelNode = delNode;AVLTreeNode pCurDelNode = pDelNode;// 记录// 更新平衡因子while (delNode != root) {if(delNode == pDelNode.left) {pDelNode.bf++;}else {pDelNode.bf--;}if(pDelNode.bf == 0) {// 继续向上调整delNode = pDelNode;pDelNode = pDelNode.parent;}else if(pDelNode.bf == -1 || pDelNode.bf == 1) {break;}else {// 进行旋转if(pDelNode.bf == 2) {if(pDelNode.right.bf == 1) {AVLTreeNode tmp = pDelNode.right;rotateLeft(pDelNode);pDelNode = tmp;}else if(pDelNode.right.bf == -1){AVLTreeNode tmp = pDelNode.right.left;rotateRL((pDelNode));pDelNode = tmp;}else {// 特殊讨论AVLTreeNode tmp = pDelNode.right;rotateLeft(pDelNode);pDelNode = tmp;pDelNode.bf = -1;pDelNode.left.bf = 1;break;}}else {if(pDelNode.left.bf == -1) {AVLTreeNode tmp = pDelNode.left;rotateRight(pDelNode);pDelNode = tmp;}else if(pDelNode.left.bf == 1){AVLTreeNode tmp = pDelNode.left.right;rotateLR(pDelNode);pDelNode = tmp;}else {// 特殊讨论AVLTreeNode tmp = pDelNode.left;rotateRight(pDelNode);pDelNode = tmp;pDelNode.bf = 1;pDelNode.right.bf = -1;break;}}delNode = pDelNode;pDelNode = pDelNode.parent;}}//删除if(curDelNode.left == null) {if(curDelNode == pCurDelNode.left) {pCurDelNode.left = curDelNode.right;if(curDelNode.right != null) {curDelNode.right.parent = pCurDelNode;}}else {pCurDelNode.right = curDelNode.right;if(curDelNode.right != null) {curDelNode.right.parent = pCurDelNode;}}}else {if(curDelNode == pCurDelNode.left) {pCurDelNode.left = curDelNode.left;if(curDelNode.left != null) {curDelNode.left.parent = pCurDelNode;}}else {pCurDelNode.right = curDelNode.left;if(curDelNode.left != null) {curDelNode.left.parent = pCurDelNode;}}}return curDelNode;}
1.5 AVL树的性能分析
AVL树是一颗绝对平衡的二叉搜索树,要求每个节点的左右孩子的高度差绝对值不超过1,可以保证查询高效的时间复杂度,避免出现单分支的情况,即 l o g 2 N log_2N log2N
弊端在于,如果要对AVL树做一些结构的修改,必须插入节点、删除节点,就很可能会涉及到旋转操作,性能低下,最差情况下的删除甚至要一直延续到根节点
因此:AVL树适合数据的个数是静态的(不会改变)的情况