平衡二叉树
1.性质
- 首先必须是一个二叉搜索树
- 左右子树深度相差绝对值不超过1
- 平衡因子BF:该结点右子树的深度减去它的左子树的深度,因此平衡二叉树上所有结点的平衡因子只可能是1,-1或0
2.AVL树代码实现
创建AVL树结构
typedef struct AVLnode
{
int data;
AVLtree*lchild;
AVLtree*rchild;
int bf;
}AVLnode;
typedef struct AVLtree
{
AVLnode*root;
}AVLtree;
插入元素
bool insertAVL(AVLnode*&t,int key)
{
AVLnode*p=t;
AVLnode*parent=NULL;
Stack st;
initStack(&st);
while(p!=NULL)//循环找到插入值的具体位置
{
if(key==p->data)//搜索二叉树中不能有相同的元素
return false;
parent=p;
push(&st,parent);
if(key<p->data)
p=p->lchild;
else
p=p->rchild;
}
//进行插入
p=buynode(key);//此时p为NULL,再为p赋值
if(parent==NULL)//如果父亲为空,就只可能是根节点
{
t=p;
return true;
}
if(key<parent->data)//连接父节点和子节点
parent->lchild=p;
else
parent->rchild=p;
//调正bf
while(!isEmpty(&st))//每插入一个节点后,该循环会不断将栈中的父节点每一个父节点都取出并判断他们的bf值是否合理,
//但实际上将进行调整的只是bf不正确的节点
{
parent=gettop(&st);
pop(&st);
if(parent->lchild==p)//如果栈中的父节点有左子树,父节点的bf--;如果插入到是右子树,bf++
parent->bf--;
else
parent->bf++;
//父节点和子节点会有三种情况:
if(parent->bf==0)//等于0时对之后的父节点没影响
break;
if(parent->bf==1||parent->bf==-1)//对之后父节点的bf值会有影响,因此要将p(记录子节点)重新赋值,
//不断从栈顶取出父节点进行比较,改变之前所有与其相关的父节点的bf
{
p=parent;
}
else
{
//该情况中二叉搜索树不再是平衡树,必须用旋转的方法将其进行平衡调整
//该处的parent实际上是bf值不合理的节点,不一定是刚插入的点的父节点,因为栈中不只存了刚插入点的父节点,还有它的祖祖辈辈
//该处只会对插入后bf不合理的节点但进行修改
int flag=(parent->bf<0)?-1:1;
if(p->bf==flag)//单旋转
{
if(flag==-1)
RotateR(parent);//右旋转
else
RotateL(parent);//左单旋转
}
else//双旋转
{
if(flag==1)
RotateRL(parent);//从右向左的旋转
else
RotateLR(parent);//从左向右的选择
}
break;//由于是从栈中一个一个取出,在树中就是从与插入点相距最接近的父节点开始查找,因为每插入一个最多只可能对一个父节点的
//bf值有影响,因此,在找到给父节点并进行调整后,就不需要查找栈中其他元素了,直接跳出循环,
//紧接着修改t的真实值或者连接修改点与其父节点
}
}
//由于旋转算法是按引用传递,因此parent修改后,指向的是修改后正确的根节点,
//但是由于insert函数传递的根节点也是按引用传递,因此根节点尚未被修正,因此根节点t需要用parent重新修正
if(IsEmpty(&st)//如果是栈顶元素,就直接将parent赋值给根节点就行
t=parent;
else//如果不是,就要找寻下一个栈顶元素,将他们连接起来,然后就继续循环,直到栈为空,再次找到根节点
{
AVLnode*q=gettop(&st);
if(q->data>parent->data)
q->lchild=parent;
else
q->rchild=parent;
}
return true;
}
插入步骤
- 遍历循环,从根节点出发,判断与key与树节点的大小,找到值为key的节点应该在树中的位置,注意:此步骤需要记录寻找时所走过的路径,并将他们放入栈中
- 进行插入,连接父节点和该新节点,如果父节点为空说明插入的节点是根节点
- 插入后得到所有节点新的bf值,即遍历栈中元素,记录他们左右子树的个数,对bf进行加减
- 调整bf值:在插入后父节点的bf值会出现三种情况:
- (1)如果父节点的bf为0,插入前后并未使该父节点出现不平衡,并且也为改变整棵树的深度,因此不需要进行调整直接而退出
- (2)如果父节点的bf为1或-1,说明插入前后改变了这棵子树的深度,也就有可能造成其他父节点的bf值的改变,因此需要不断遍历栈中的父节点
- (3)如果父节点不平衡,就需要使用旋转算法进行调整:
(1)如果父节点(不平衡点)和其子节点的bf正负一致,需要使用单旋转算法;
(2)如果不一致需要使用双旋转算法。
注意
- 因为每次插入时都需要对bf进行改进和调整,因此插入后之可能出现一个不平衡的节点,当改进后就可以跳出循环。
- 因为函数传参都是按引用传递,因此parent值确实被修改了,而在调整bf后,不论是整个树的根节点还是子树的根节点都已经不是原来的根节点了,因此在跳出循环之后需要对根节点进行调整:如果栈为空说明最后一个父节点就是根节点,直接让根结点指向它;如果不为空就需要再从栈中取出一个,连接新的节点与他上一个父节点。
单旋转算法
右旋转
//右旋转
void RotateR(AVLnode*&ptr)
{
AVLnode*subR=ptr;
ptr=subR->lchhild;
subR->lchild=ptr->rchild;
ptr->rchild=subR;
ptr->bf=subR->bf=0;
}
左旋转
//左旋转
void RotateL(AVLnode*&ptr)
{
AVLnode*subR=ptr;
ptr=subR->rchhild;
subR->rchild=ptr->lchild;
ptr->lchild=subR;
ptr->bf=subR->bf=0;
}
双旋转算法
先左后右的旋转
//先左后右的旋转
void RotateLR(*&ptr)
{
AVLnode*subR=ptr;
AVLnode*subL=subR->lchild;
ptr=subL->rchild;
subL->rchild=ptr->lchild;//如果ptr没有左子树,那subL的右子树就是NULL,这个语句依然正确
ptr->lchild=subL;
if(ptr->bf<=0)//对于subR和subL的bf值,对subL,如果ptr有右子树,那么他将成为subL上的一个左子树
//对subR,如果ptr有左子树,那么他将成为subR上的一个右子树
//ptr永远指向根节点,bf调整为0
{
subL->bf=0;
}
else
{
subL->bf=-1;
}
subR->lchild=ptr->rchild;//如果ptr没有右子树,那subR的左子树就是NULL,这个语句依然正确
ptr->rchild=subR;
if(ptr->bf==-1)
subR->bf=1;
else
subR->bf=0;
ptr->bf=0;
}
先右后左的旋转
void RotateRL(*&ptr)
{
AVLnode*subL=ptr;
AVLnode*subR=subL->rchild;
ptr=subR->lchild;
subR->lchild=ptr->rchild;//如果ptr没有左子树,那subR的左子树就是NULL,这个语句依然正确
ptr->rchild=subR;
if(ptr->bf>=0)//对于subR和subL的bf值,对subL,如果ptr有右子树,那么他将成为subL上的一个左子树
//对subR,如果ptr有左子树,那么他将成为subR上的一个右子树
//ptr永远指向根节点,bf调整为0
{
subR->bf=0;
}
else
{
subR->bf=1;
}
subL->rchild=ptr->lchild;//如果ptr没有右子树,那subL的右子树就是NULL,这个语句依然正确
ptr->lchild=subL;
if(ptr->bf==1)
subL->bf=-1;
else
subR->bf=0;
ptr->bf=0;
}
删除
bool remove(AVLnode*&t,int key)
{
AVLnode*ppr;
AVLnode*parent=NULL;
AVLnode*p=t;
Stack st;
initStack(&st);
while(p!=NULL)//查找key的节点在树中的位置
{
if(p->data==key)
break;
parent=p;
push(&st,parent);
if(key<p->data)
{
p=p->rchild;
}
else
{
p=p->lchild;
}
}
if(p==NULL)//说明没有在树中找到
return false;
//判断要删除的点是否有子树,分为两种情况:
//第一种:如果有左右子树,就找到其左子树最右边的点,用这个点将要删除的点代替,并删除这个点,这个点没有左右子树,因此转换为了第二种情况;
//第二种:如果要删除的点至多有一棵子树,用q记录这个节点,将要删除的点删除,将父节点直接连接到q上
AVLnode*q;
int f=0;//但是如果要删除的点没有左右子树,q就为NULL,就无法清楚删除点是父节点的右树还是左树,因此在删除时要用f记录,f=0代表左数,1为右树
if(p->lchild!=NULL&&p->rchild!=NULL)
{
parent=p;
push(&st,parent);
q=p->lchild;
while(q->rchild!=NULL)
{
parent=q;
push(&st,parent);
q=q->rchild;
}
p->data=q->data;
p=q;
}
//根据左右子树是否为空,找到要删除的点的子树,用q记录下来
if(p->lchild!=NULL)
q=p->lchild;
else
q=p->rchild;
//删除p节点
if(parent==NULL)//父亲为空说明一定是根节点
t=parent;
else
{
if(parent->lchild==p)//将父节点与子节点的子节点删除
parent->lchild=q;
f=0;//记录删除点时parent的左树
else
parent->rchild=q;
f=1;
//调整bf
int link_flag=0;
while(!isEmpty(&st))
{
//根据插入的点重写栈中所有父节点的bf
parent=gettop(&st);
pop(&st);
if(parent->rchild==q&&f=1)
parent->bf--;
else
parent->bf++;
if(!isEmpty(&st)
{
ppr=gettop(&st);
link_flag=(ppr->lchild==parent)?-1:1;//
}
else
{
link_flag=0;//不连接
}
//判断父节点的bf是否处于平衡状态
//第一种情况:如果原来父节点的bf为0,则删除一个节点后并不影响其他树的深度,即这棵树的深度依旧没有改变,此时父节点的bf应该是1或者-1
if(parent->bf==1||parent->bf==-1)
break;
//第二种情况,父节点原来是有一个子树的,当将这棵子树删除后,bf变成了0,而树的深度也就改变了,因此可能对之前父节点的bf造成影响,因此需要将栈中所有父节点取出,一一查看是否有不平衡的bf值。
if(parent==0)
q=parent;
//第三种情况,即查询到了不平衡的父节点,因此需要使用旋转算法
else
{
int flag=0;
if(parent->bf<0)
{
flag=-1;
q=parent->lchild;
}
else
{
flag=1;
q=parent->rchild;
}
if(q->bf==0)//单旋转
{
if(flag==-1)//右旋转
RotateR(parent);
parent->bf=1;
parent->rchild->bf=-1;
else
{
RotateL(parent);//左单旋转
parent->bf=-1;
parent->lchild->bf=1;
}
}
if(flag==q->bf)
{
if(flag==-1)//左树高,右单旋转
RotateR(parent);
else
RotateL(parent);
}
else//双旋转
{
if(flag==-1)
RotateLR(parent);
else
RotateRL(parent);
}
if(link_flag==1)
ppr->rchild=parent;
else if(link_flag==-1)
ppr->lchild=parent;
}
}
if(isEmpty(&st))
{
t=parent;
}
//删除和插入不同,插入时每次插入只可能产生一个不平衡的父节点,因此一旦找到这个父节点之后,就可以跳出循环;
//而删除时,可能产生不止一个不平衡节点,因此要将栈中元素都循环遍历,查找所有不平衡的节点。所以在最后改变根节点时有不同:
//在跳出循环时,栈顶元素必为根节点,因此直接将t指向根节点,但是在双旋转时没有了插入时连接的方法,因此需要使用link_flag和ppr(父节点的父节点)判断旋转的是左子树还是右子树,然后将ppr与新的根节点连接
}
free(p);//删除节点
return true;
}
删除步骤:
- 找寻key在树中的节点的位置,并记录所有路径,即将所有查询过的父节点放入栈中
- 判断要删除的节点是否有子树:如果至多有一个子树,用q记录子树;如果有两个子树,需要将其左子树中最右下的子树将其替代,将问题转换成删除最右下那个子树的问题
- 删除节点:直接将父节点指向q,并释放p
- 修改bf:遍历栈中元素,如果有右子树bf就++,如果有左子树bf就–
- 调整平衡:第一种情况:如果原来父节点的bf为0,则删除一个节点后并不影响其他树的深度,即这棵树的深度依旧没有改变,此时父节点的bf应该是1或者-1,可以直接跳出循环;第二种情况,父节点原来是有一个子树的,当将这棵子树删除后,bf变成了0,而树的深度也就改变了,因此可能对之前父节点的bf造成影响,因此需要将栈中所有父节点取出,一一查看是否有不平衡的bf值;第三种情况:bf不平衡,需要使用旋转算法。
- 使用旋转算法:用q记录要修改点的子树:1.如果子树的bf为0,那么使用单旋转算法;2.如果子树的bf和要修改点(父节点)的bf相同,使用单旋转算法;3.如果不相同,使用双旋转算法。
注意:
- 在删除节点时,如果该节点的左右子树都为空,那么在修改bf时,不能通过判断parent的左子树或右子树是否为q来判断,因为此时q为NULL,并且parent左右子树都为NULL,有可能无法正确使bf进行加减,需要在删除时记录f的值,在调整bf时再次判断f代表左还是右。
- 删除时,可能产生不止一个不平衡节点,因此要将栈中元素都循环遍历,查找所有不平衡的节点。所以在最后改变根节点时有不同:在跳出循环时,栈顶元素必为根节点,因此直接将t指向根节点,但是在双旋转时没有了插入时连接的方法,因此需要使用link_flag和ppr(父节点的父节点)判断旋转的是左子树还是右子树,然后将ppr与新的根节点连接。