前言
上一篇文章讲到,二叉搜索树有一个很大的弊端就是很不稳定。输入数字的顺序不同可能导致二叉树退化成链表;比如:顺序输入: 1 -> 5;与乱序输入:3,2,4,1,5;会有不同结果;
苏联的科学家G. M. Adelson-Velsky和E. M. Landis发明了自平衡的二叉搜索树:AVL(名字缩写)树;AVL树本质上还是一棵二叉搜索树,它的特点是:
- 1.本身首先是一棵二叉搜索树。
- 2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
也就是说,AVL树,本质上是带了平衡功能的二叉查找树;
AVL原理
树高
htarget = max(hleft , hright) + 1;
例子:节点有2个子节点;比较左右子节点高度,取较大的值作为高度;
这个例子中,节点1的高度:h = max(hleft ,hright) + 1 = 2 + 1 = 3;
失衡类型
在AVL树的定义中左右子树高度差大于或等于2时,就破坏了AVL树的平衡;在AVL中总共有4种类型的失衡:
- LL型
-
LR型
-
RR型
- RL 型
以LL 型来讲如何确定节点是否失衡?如何判断失衡的节点属于哪种类型的失衡?
100节点的左分支高度:hleft = 2,右分支高度:hright = 0 ;左右分支高度差2,因此100节点左右分支失衡;确定100节点失衡之后,接下来就是确定失衡类型。
以100节点的视角来看,左分支的高度大于右分支,是:L 型; 继续观察100节点的左节点:50,再来看50的左右分支高度:50节点的左分支高度大于右分支,因此是 : L 型;这2个组合在一起就是 :LL型了;其他三种类型都是一样判断方法;
如何调节失衡节点
那么如何让左右平衡呢?通过让高度大的分支向高度小的分支旋转,使高度大的分支节点减 1,高度小分支的节点加 1;这样就可以让左右分支重新平衡。
上面四种类型以旋转次数来划分又可以分为:单旋转,双旋转;
- 单旋转:旋转1次可以让失衡节点恢复平衡;LL,RR;
- 双旋转:旋转2次可以让失衡节点恢复平衡;LR,RL;
单旋转以LL为例:
在旋转时要注意2个节点:50,100之间的关系变化;
- 旋转之前: 100是50的父节点;50是100的左节点;
- 旋转之后:
- 对于50节点来讲:50的父节点是100的父节点,50的右节点是100;
- 对于100节点来讲:100的父节点是50,100的左节点是50的右节点75;
- 对于75节点来讲:需要更新父节点,将父节点更改为 100;
- 对于100的父节点来讲:100的父节点parent要更新子节点,将指向100的指针指向 50;
这就是单旋其实不难,只需要看图就能想清楚节点之间的关系变化;提到的75节点,100的父节点;这些都是围绕50,100节点来看的,因为 50,100节点的位置关系发生了变化75以及100的parent节点才随之改变;如果还是不太清楚建议手动画一下,在画图的过程中就明白了;
双旋转以 LR 为例:
LR型的失衡,如果还是按照单旋那样,直接旋转50节点;这样做仍然不能平衡,旋转之后50节点失衡;没有达到平衡的目的;因此不能直接将50节点旋转到顶点;对于LR型失衡如何调整:
- 首先将LR型通过左旋 转换成LL型;
- LL型通过右旋使节点平衡;
对于RL型,就先将RL -> RR ;再对RR进行旋转;通过上面对LL,LR旋转的分析可以看出只要弄清楚了LL 型其余三个就都明白了,逻辑都一样;
AVL树代码
add
add的流程:
add过程和二叉搜索树相比多了一条:新加入Node节点之后,判断该节点的父节点的左右分支是否是平衡的(左右分支高度差不超过1)?
如何判断呢?在Node节点中加入高度属性:h;要判断一个节点的左右分支是否失衡,就可以直接获取左右子节点的高度计算出高度差来判断;在确认了节点左右子节点高度失衡之后,接着利用高度差来判断出节点的失衡类型;
无论是判断节点是否失衡或者是失衡的类型都是依据节点左右子节点高度差来判断的,因此节点高度是关键。那如何更新每个节点的高度?
每个节点在刚插入AVL时都是在叶子节点,因此每个新插入的节点高度都是0;而新插入的节点直接影响了它parent节点的高度;我们看一个例子:
在上面的两个例子中第一个例子高度更新完25节点高度时,继续向上更新50节点的高度,50节点根据左右子节点高度计算出新高度与之前没有变化,因此50节点高度不变,这个时候没有必要继续向上更新节点高度;
第二个例子:更新完25时,继续更新50,发现50节点高度也更新了就继续向上更新50的parent节点高度。
通过这2个例子可以看出当更新到某个节点时,节点高度不变这个时候就不用继续向上更新高度了;如果节点高度更新就继续向上更新直到更新到root节点为止;
为什么失衡节点只需要一次调节就结束,而不用继续向上检查?
在100节点失衡之前:节点p指向100的left分支高度是 2;在100失衡调节完之后 P的left指向50,这个分支的高度仍然是 2;就是说100失衡并调节之后,对于p节点的left分支来说高度是没有改变的。因此在100失衡并调节之后不用继续向上调节了;
源码:(PS:会使用到二叉搜索BST的add和delete相关知识,因为AVL树的add和remove操作都是在BST相关操作之后再调节平衡BST)
public class AVL {
private Node root;
class Node{
int data;
int h;
Node left;
Node right;
Node parent;
Node(int data,int h,Node left,Node right,Node parent){
this.data = data;
this.h = h;
this.left = left;
this.right = right;
this.parent = parent;
}
Node (int data){
this(data,0,null,null,null);
}
}
/**
* LL型失衡:向右旋转
* @param node :失衡节点
* @return
*/
Node LL_rotate(Node node){
Node parent = node.parent;
Node left = node.left;
Node lRight = left.right;
//父节点
if(parent == null){
root = left;
}else{
if(parent.left == node)parent.left = left;
else parent.right = left;
}
//left
left.parent = parent;
left.right = node;
//原顶点
node.parent = left;
node.left = lRight;
//left.right
if(lRight != null) lRight.parent = node;
//更新高度
updateH(node);
updateH(left);
return left;
}
/**
*
* RR型:向左旋转
* @param node
* @return
*/
Node RR_rotate(Node node){
Node parent = node.parent;
Node right = node.right;
Node rLeft = right.left;
//parent
if(parent == null){
root = right;
}else{
if(parent.left == node) parent.left = right;
else parent.right = right;
}
//right
right.parent = parent;
right.left = node;
//node
node.parent = right;
node.right = rLeft;
if(rLeft != null)rLeft.parent = node;
updateH(node);
updateH(right);
return right;
}
Node LR_rotate(Node node){
RR_rotate(node.left);
return LL_rotate(node);
}
Node RL_rotate(Node node){
LL_rotate(node.right);
return RR_rotate(node);
}
public boolean addVal(int val){
Node node = new Node(val);
if(root == null){
root = node;
return true;
}
boolean addSuccess = addNode(node,root);//添加节点
if(!addSuccess)return false;
insertBalance(node.parent);//调节平衡
return true;
}
boolean addNode(Node node, Node cur){
if(cur.data == node.data)return false;
else if(cur.data > node.data){
if(cur.left == null){
cur.left = node;
node.parent = cur;
return true;
}
return addNode(node,cur.left);
}else{
if(cur.right == null){
cur.right = node;
node.parent = cur;
return true;
}
return addNode(node,cur.right);
}
}
void insertBalance(Node node){
if(node == null)return ;
boolean update = updateH(node);
if(!update)return;//没有更新高度
if(rotate(node))return;//旋转一次失衡节点高度不变,不用向上更新高度;
insertBalance(node.parent);//更新高度之后没有旋转,说明节点高度增加,要继续向上更新高度;
}
boolean rotate(Node node){
int hDif = getNodeH(node.left) - getNodeH(node.right);
if(hDif == 0 || hDif == -1 || hDif == 1)return false;
if(hDif == 2){// h_left > h_right
Node left = node.left;
int dif = getNodeH(left.left) - getNodeH(left.right);
if(dif >= 0){//ll
LL_rotate(node);
}else{//lr
LR_rotate(node);
}
}else{// h_left < h_right
Node right = node.right;
int dif = getNodeH(right.right) - getNodeH(right.left);
if(dif >= 0){//RR
RR_rotate(node);
}else{//RL
RL_rotate(node);
}
}
return true;
}
/**
* 更新高度
* @return
*/
boolean updateH(Node node){
int leftH = getNodeH(node.left);
int rightH = getNodeH(node.right);
int oldH = node.h;
node.h = leftH > rightH ? leftH + 1 : rightH + 1;
return oldH != node.h;
}
int getNodeH(Node node){
if(node == null)return -1;
return node.h;
}
}
add测试
void printNodeVal(List<Node> nodes){
List<Node> children = new ArrayList<>();
for(Node node : nodes ){
System.out.print(node.data+",");
if(node.left!=null)children.add(node.left);
if(node.right!=null)children.add(node.right);
}
System.out.println("\n*******************************");
if(children.isEmpty())return;
printNodeVal(children);
}
public void print(){
List<Node> nodes = new ArrayList<>();
nodes.add(root);
printNodeVal(nodes);
}
@Test
public void test(){
for (int i = 0; i <10; i++) {
addVal(i);
}
System.out.println("+++++++++++++++++++++++++++TEST ADD+++++++++++++++++++++++++++");
print();
}
remove
remove的平衡调节与add差不多,也是在remove节点之后更新判断节点的左右高度差是否 超过 1 ;但是remove 与 add不同的是,add一次调节就可以结束;不用继续调节;而remove操作,在调节失衡节点之后还要继续向上检查节点;举个例子:
node的左右节点高度:
- h_left = n;
- h_right = n-1;
node的右节点node_right的左右子节点高度:
- h_right_node_left = n-3;
- h_right_node_right=n-2;
现在删除了node_right的左分支一个节点导致h_right_node_left = n - 4;进而导致node_right的左右节点失衡;经过调整平衡之后 node_right的左右子节点高度都是: n - 3; 这又导致了node的右分支高度变为:n-2;与node的左分支高度差失衡;因此还要继续调整node;
public boolean removeVal(int val){
if(root == null)return false;
Node node = searchNode(val,root);
if(node == null)return false;
removeNode(node);
return true;
}
Node searchNode(int val ,Node cur){
if(cur == null)return null;
if(cur.data == val)return cur;
else if(cur.data > val)return searchNode(val,cur.left);
else return searchNode(val,cur.right);
}
void removeNode(Node node){
Node parent = node.parent;
if(node.left == null && node.right == null){//叶子节点
// root节点
if(parent == null){
root = null;
return;
}
//删除叶子节点
removeAndBalance(node,parent,null);
}else{//非叶子节点
if(node.right == null){//只有left子节点
if(parent == null){
root = node.left;
root.parent = null;
return ;
}
removeAndBalance(node,parent,node.left);
}else if(node.left == null){//只有right子节点
if(parent == null){
root = node.right ;
root.parent = null;
return;
}
removeAndBalance(node,parent,node.right);
}else{//有2个子节点
Node replace = leftMaxNode(node.left);
node.data = replace.data;
removeNode(replace);
}
}
}
/**
* 1. 在删除调整时,调节一次之后;整棵树中仍然存在不平衡节点的可能;
* 因为调节一次会导致 高度 -1;这样有可能会导致其他节点失衡;因此在调整平衡之后,需要继续向上检测是否需要调节节点的平衡;
*
* 2. 当node节点没有更新高度,并不能说明左右子节点没有失衡;==》有可能是删除了高度较低的分支,导致node节点的高度没有变
*
*
* 3 .要同时满足:节点高度没更新 && 没有调整平衡;说明删除该节点没影响;
*
* @param node
*/
void removeBalance(Node node){
if(node == null)return ;
boolean update = updateH(node);
boolean rotate = rotate(node);
if(!update && !rotate)return;
else if(!rotate)
removeBalance(node.parent);
else
removeBalance(node.parent.parent);//不管是哪种类型的失衡,调整之后都会导致node下沉;因此向上更新需要:node.parent.parent;
}
void removeAndBalance(Node node,Node parent,Node val){
removeNode(node,parent,val);
removeBalance(parent);
}
void removeNode(Node node, Node parent, Node val){
if(parent.left == node) {
parent.left = val;
}else{
parent.right = val;
}
if(val != null)val.parent = parent;
}
Node leftMaxNode(Node node){
if(node.right == null)return node;
return leftMaxNode(node.right);
}
remove测试
@Test
public void test(){
for (int i = 0; i <10; i++) {
addVal(i);
}
System.out.println("+++++++++++++++++++++++++++TEST ADD+++++++++++++++++++++++++++");
print();
System.out.println("\n+++++++++++++++++++++++++++TEST ADD+++++++++++++++++++++++++++");
removeVal(3);
print();
}