具体内容:
该文章的源代码仓库为:https://github.com/MeteorCh/DataStructure/blob/master/Java/DataStructure/src/Searching/BinarySortingTree/AVLTree.java
在上一篇博客中实现了二叉排序树。我们发现,查找的效率取决于树的高度。比如对于{62,88,58,47,35,73,51,99,37,93}这样的数组,利用上一篇博客实现的插入,得到的二叉排序树为:
但当我们改变一下数组中元素的顺序,比如改成{35,37,47,51,58,62,73,88},那得到的二叉排序树就为:
不难看出,这时的二叉排序树退化成了一个线性表。这时已经完全没有了二叉排序树的优势。次吃的解决方案是就是尽量让树的高度降低,那就要求树的左右孩子的高度尽可能相近,即树要尽可能平衡。
一、相关定义
- 1.平衡二叉树: 是一种二叉排序树, 其中每个节点的左右子树的高度差至多等于1。这里有两个地方要注意:第一,平衡二叉树首先是一颗二叉排序树;第二,每个节点的左右子树高度差要么是0,要么是1。
- 2.平衡因子: 为了描述节点的左右子树差,我们将二叉树上节点的左子树高度减去右子树高度得到的值称为该节点的平衡因子。根据二叉树的定义,每个节点的平衡因子为-1,0,1三个中的一个。
- 3.最小不平衡子树: 距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树, 称为最小不平衡子树。如下所示的二叉排序树,当插入35时,58的左子树高度为2,右子树高度为0,该树变不平衡,因此以节点58为根的子树为最小不平衡子树。
二、平衡调整
从上面的图可以看出,要让不平衡的二叉排序树重新平衡,就需要对最小不平衡子树进行调整,使其平衡。有四种调整方式:LL型、RR型、LR型、RL型。在月雲之霄 大佬的博客中,对这四种调整讲的相当清楚了,我这里将月雲之霄 大佬的博客中的图整理成一张图(本文重点讲解删除的各种情况,插入的情况可以先去看看月雲之霄大佬的博客,讲的可以说非常好了),用来解释调整情况:
三、节点插入
通过上面的图示,我们知道LR和RL是LL和RR的组合情况。知道了这个,我们在插入节点的时候,从上往下,利用递归,插入后,判断插入过程路径上的节点的平衡因子,当平衡因子大于1或小于-1时,就去调整该接节点(根据递归的特性,在寻找插入点位置时,是从上到下的,那在判断平衡因子时,就是从下往上的,这样其实调整的肯定就是最小不平衡子树了)。在月雲之霄 大佬的博客中对插入讲的相当详细了,我这里就不赘述了,可以详细看一遍大佬的博客,肯定能理解。我这里用Java实现平衡二叉树,Java中没有C++中delete的操作,所以每个节点都要有一个成员变量存储其父节点信息。在左旋和右旋的时候,要调整对应的父节点信息,我们这里先来看看左旋和右旋的两种情况。
1.LL型旋转(即右旋)
这里仍然使用月雲之霄 大佬的博客中的图来说明:
实现代码为:
protected TreeNode llRotate(TreeNode node){//LL型旋转
TreeNode xNode=node.lChild;
node.lChild=xNode.rChild;
if (xNode.rChild!=null)
xNode.rChild.parent=node;//父节点调整
xNode.rChild=node;
if (xNode.rChild!=null)
xNode.rChild.parent=xNode;//父节点调整
node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
return xNode;
}
可以看到上面对x和y的父节点都进行了一些调整操作。
2.RR型旋转(即左旋)
使用月雲之霄 大佬的博客中的图来说明:
实现代码为:
protected TreeNode rrRotate(TreeNode node){//RR型旋转
TreeNode xNode=node.rChild;
node.rChild=xNode.lChild;
if (xNode.lChild!=null)
xNode.lChild.parent=node;//父节点调整
xNode.lChild=node;
if (xNode.lChild!=null)
xNode.lChild.parent=xNode;//父节点调整
node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
return xNode;
}
上面也对x和y的父节点都进行了一些调整操作。
LR型插入和RL型插入是LL和RR型的组合,都会调用上面两个函数,我这里就不写了。有这两个函数,我们再来看平衡二叉树中的节点删除问题。
四、节点删除
我感觉节点删除是平衡二叉树中最难的部分了,删除的以后要判断调整的类型。当然,平衡调整类型仍然是LL、RR、LR、RL这四种,但是,与插入不同的是,在结点删除的时候,要考虑调整结点是父节点的左孩子还是右孩子,所以每种类型又有两种情况,总共是8种情况。 关于为什么在插入的时候不需要考虑父节点,而在删除的时候要考虑父节点,我们在讨论部分讨论。
1.LL型删除
1.1 LLL型删除
这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LLL型调整,具体情况如下所示,要删除20节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的左孩子(LLL中第一个L的含义)。
假设y的父节点为parent,LLL型删除对应的代码为
parent.lChild=llRotate(y);
parent.lChild.parent=parent;
1.2 RLL型删除
这种情况是最小不平衡子树的根节点为其父节点的右孩子的情况,我这里称之为RLL型调整,具体情况如下所示,要删除80节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的右孩子(RLL中R的含义)。
假设y的父节点为parent,RLL型删除对应的代码为
parent.rChild=llRotate(y);
parent.rChild.parent=parent;
2.RR型删除
2.1 LRR型删除
这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LRR型调整,具体情况如下所示,要删除0节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的左孩子(LRR中L的含义)。
假设y的父节点为parent,LRR型删除对应的代码为
parent.lChild=rrRotate(y);
parent.lChild.parent=parent;
2.2 RRR型删除
这种情况是最小不平衡子树的根节点为其父节点的右孩子的情况,我这里称之为RRR型调整,具体情况如下所示,要删除80节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的右孩子(RRR中第一个R的含义)。
假设y的父节点为parent,RRR型删除对应的代码为
parent.rChild=rrRotate(y);
parent.rChild.parent=parent;
3.LR型删除
3.1 LLR型删除
这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LLR型调整,具体情况如下所示,要删除20节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行RR型调整,再对y节点进行LL型调整。调整部分中的y为其父节点的左孩子(LLR中第一个L的含义)。
假设y的父节点为parent,LLR型删除对应的代码为:
y.lChild = rrRotate(y.lChild);
y.lChild.parent=y;
parent.lChild=llRotate(y);
parent.lChild.parent=parent;
3.2 RLR型删除
这种情况是最小不平衡子树的根节点为父节点的右孩子的情况,我这里称之为RLR型调整,具体情况如下所示,要删除90节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行RR型调整,再对y节点进行LL型调整。调整部分中的y为其父节点的右孩子(RLR中第一个R的含义)。
假设y的父节点为parent,RLR型删除对应的代码为:
y.lChild = rrRotate(y.lChild);
y.lChild.parent=y;
parent.rChild=llRotate(y);
parent.rChild.parent=parent;
4. RL型删除
4.1 LRL型删除
这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LRL型调整,具体情况如下所示,要删除0节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行LL型调整,再对y节点进行RR型调整。调整部分中的y为其父节点的左孩子(LRL中第一个L的含义)。
假设y的父节点为parent,LRL型删除对应的代码为:
y.rChild =llRotate(y.rChild);
y.rChild.parent=y;
parent.lChild=rrRotate(y);
parent.lChild.parent=parent;
4.2 RRL型删除
这种情况是最小不平衡子树的根节点为父节点的右孩子的情况,我这里称之为RRL型调整,具体情况如下所示,要删除50节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行LL型调整,再对y节点进行RR型调整。调整部分中的y为其父节点的右孩子(RRL中第一个L的含义)。
假设y的父节点为parent,RRL型删除对应的代码为:
y.rChild =llRotate(y.rChild);
y.rChild.parent=y;
parent.rChild=rrRotate(y);
parent.rChild.parent=parent;
五、代码实现
用Java实现平衡二叉树,代码如下:
package Searching.BinarySortingTree;
/**
* 平衡二叉树的实现
* 使用示例:
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* int findKey=30;
* System.out.println("键"+findKey+"在平衡二叉树中"+(tree.find(findKey)==null?"不存在":"存在"));
* 删除的各类情况示例:
* 1.右子树中LL型删除
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* tree.deleteNode(70);
* tree.deleteNode(90);
* tree.deleteNode(80);
* 2.左子树中LL型删除
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* tree.insertData(-1);
* tree.deleteNode(20);
* 3.右子树中RR型删除
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* tree.deleteNode(70);
* tree.deleteNode(90);
* tree.deleteNode(80);
* 4.左子树中RR型删除
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* tree.insertData(25);
* tree.deleteNode(0);
* 5.右子树中LR型删除
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* tree.deleteNode(40);
* tree.deleteNode(50);
* tree.deleteNode(90);
* 6.左子树中LR型删除
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* tree.insertData(5);
* tree.deleteNode(20);
* 7.右子树中RL型删除
* int[] data={20,10,0,30,40,50,60,80};
* AVLTree tree=new AVLTree(data);
* tree.deleteNode(40);
* tree.insertData(75);
* tree.deleteNode(50);
* 8.左子树中RL型删除
* int[] data={20,10,0,30,40,50,60,90,80,70};
* AVLTree tree=new AVLTree(data);
* tree.insertData(15);
* tree.deleteNode(0);
*/
public class AVLTree {
public static class TreeNode{
protected int data;//数据
protected TreeNode lChild,rChild,parent;//左孩子、右孩子、父节点
protected int height;//树的高度
public TreeNode(int data){
this.data=data;
lChild=rChild=parent=null;
height=1;
}
public int getData() {
return data;
}
}
TreeNode root;
public AVLTree(int[] data){
for (int item:data)
insertData(item);
}
public TreeNode find(int key){//利用非递归的方式实现查找
TreeNode curNode=root;
while (curNode!=null){
if (key==curNode.data){
return curNode;
}else if (key<curNode.data){
curNode=curNode.lChild;
}else
curNode=curNode.rChild;
}
return null;
}
public int getHeight(TreeNode node){
return node==null?0:node.height;
}
public int getBalanceFactor(TreeNode node){
if (node==null)
return 0;
return getHeight(node.lChild)-getHeight(node.rChild);
}
protected TreeNode llRotate(TreeNode node){//LL型旋转
TreeNode xNode=node.lChild;
node.lChild=xNode.rChild;
if (xNode.rChild!=null)
xNode.rChild.parent=node;//父节点调整
xNode.rChild=node;
if (xNode.rChild!=null)
xNode.rChild.parent=xNode;//父节点调整
node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
return xNode;
}
protected TreeNode rrRotate(TreeNode node){//RR型旋转
TreeNode xNode=node.rChild;
node.rChild=xNode.lChild;
if (xNode.lChild!=null)
xNode.lChild.parent=node;//父节点调整
xNode.lChild=node;
if (xNode.lChild!=null)
xNode.lChild.parent=xNode;//父节点调整
node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
return xNode;
}
public void insertData(int data){
root=insertData(root,data);
root.parent=null;
}
/**
* 插入数据
* @param data
*/
protected TreeNode insertData(TreeNode node,int data) {
if (node==null) {
return new TreeNode(data);
}
else {
if (data<node.data) {
node.lChild=insertData(node.lChild,data);
node.lChild.parent=node;
}
else if (data>node.data) {
node.rChild=insertData(node.rChild,data);
node.rChild.parent=node;
}
//更新树中节点的高度
node.height=1+Math.max(getHeight(node.lChild),getHeight(node.rChild));
int balance=getBalanceFactor(node);
if (balance>1&&data<node.lChild.data)//LL型
return llRotate(node);
if (balance<-1&&data>node.rChild.data)//RR型
return rrRotate(node);
if (balance>1&&data>node.lChild.data){//LR型
node.lChild=rrRotate(node.lChild);
node.lChild.parent=node;
return llRotate(node);
}
if (balance<-1&&data<node.rChild.data){//RL型
node.rChild=llRotate(node.rChild);
node.rChild.parent=node;
return rrRotate(node);
}
}
return node;
}
public void deleteNode(int data){//提供给外部操作的删除节点接口
deleteNode(root,data);
}
public void deleteNode(TreeNode node,int key){
if (node==null)
System.out.println("数据表中不存在"+key);
else {
if (key==node.data)
deleteNode(node);
else if (key<node.data)
deleteNode(node.lChild,key);
else
deleteNode(node.rChild,key);
node.height=1+Math.max(getHeight(node.lChild),getHeight(node.rChild));
int balance=getBalanceFactor(node);
TreeNode tmp=node.parent;//保存变量,因为node.parent在llRotate时会变化
boolean flag=checkChild(tmp,node);//flag为false代表在左子树上,true代表在右子树上
if (balance>1&&getBalanceFactor(node.lChild)>=0) {//LL型
if (flag){//右子树
System.out.println("RLL型删除");
tmp.rChild=llRotate(node);
tmp.rChild.parent=tmp;
}else {//左子树
System.out.println("LLL型删除");
tmp.lChild=llRotate(node);
tmp.lChild.parent=tmp;
}
}
if (balance > 1 && getBalanceFactor(node.lChild) < 0){ //LR型
node.lChild = rrRotate(node.lChild);
node.lChild.parent=node;
if (flag){
System.out.println("RLR型删除");
tmp.rChild=llRotate(node);
tmp.rChild.parent=tmp;
}else {
System.out.println("LLR型删除");
tmp.lChild=llRotate(node);
tmp.lChild.parent=tmp;
}
}
if (balance < -1 && getBalanceFactor(node.rChild) <= 0) {//RR型
if (flag){//右子树
System.out.println("RRR型删除");
tmp.rChild=rrRotate(node);
tmp.rChild.parent=tmp;
}else {//左子树
System.out.println("LRR型删除");
tmp.lChild=rrRotate(node);
tmp.lChild.parent=tmp;
}
}
if (balance < -1 && getBalanceFactor(node.rChild) > 0){ //Rl型
node.rChild = llRotate(node.rChild);
node.rChild.parent=node;
if (flag){
System.out.println("RRL型删除");
tmp.rChild=rrRotate(node);
tmp.rChild.parent=tmp;
}else {
System.out.println("LRL型删除");
tmp.lChild=rrRotate(node);
tmp.lChild.parent=tmp;
}
}
}
}
protected boolean checkChild(TreeNode parent,TreeNode child){
//这里需要判断是接到左子树上还是右子树上
if (parent!=null){
if (parent.lChild==child)
return false;
else
return true;
}
return false;
}
protected void deleteNode(TreeNode node){//节点的移除操作,分为节点有左子树或右子树为空,左右子树均不为空两种情况
if (node.rChild==null||node.lChild==null){//右子树或右子树为空
TreeNode replace=node.rChild==null?node.lChild:node.rChild;
if (node.parent!=null){
if (node==node.parent.lChild)
node.parent.lChild=replace;
else if (node==node.parent.rChild)
node.parent.rChild=replace;
if (replace!=null)
replace.parent=node.parent;
}else { //如果node为根节点
root=replace;
replace.parent=null;
}
//清空node释放内存
node.parent=node.lChild=node.rChild=null;
}else {
//找到node的左子树中最右边的叶节点
TreeNode curNode=node.lChild;
while (curNode.rChild!=null){
curNode=curNode.rChild;
}
//将curNode的值直接赋值给删除节点
node.data=curNode.data;
deleteNode(curNode);
}
}
}
在最上面的注释里,我将各种删除情况的示例都备注了,想试的小伙伴可以试试。其实我们发现,上面的删除部分,deleteNode(TreeNode node) 这个函数的实现和上一篇博客中的代码是一模一样的,不理解的可以先看看上一篇博客。
六、讨论
1.为什么插入的时候不需要考虑左右子树问题?
这个思考一下,就可以知道,插入时,是确定了在左子树还是右子树上插入后,再进行插入调整操作。而删除时,是先删除,然后再判断二叉树是否平衡,然后调整。