在上一篇非“递归学习树结构(四)--BST(二叉排序)树”中我们介绍了怎么样创建一颗二叉排序树,以及如何删除二叉排序树中的一个结点。但是考虑到BST的效率,着实让人纠结,回想一下上一篇中的最后一张图,是不是有一种DT的感觉,BST直接退化成了一个链表(但是与链表还是有本质上的不同),查找的时间复杂度回到了解放前(O(n))。这种结果我们是不愿意看到的,我们就要分析,是什么原因导致了这种结果?是因为树严重失衡,所以,我们在建树和删除树中节点的过程中要时刻保证树是平衡的,所谓的平衡,就是:“对于树中的任一节点N,其左子节点L与其右子节点R的高度差的绝对值小于等于1”。可见,对于AVL树的结点,需要添加一个height值。
头文件的定义如下:
/*AVL.h*/
#ifndef __AVL__H_
#define __AVL__H_
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int RESULT;
#ifndef SUCCESS
#define SUCCESS 0
#endif
#ifndef FAILURE
#define FAILURE -1
#endif
typedef int ValueType;
typedef struct _Node
{
struct _Node* plc; /*左字结点指针*/
struct _Node* prc; /*右子结点指针*/
int height; /*当前结点的高度*/
int key; /*key字段*/
int cnt; /*计数*/
ValueType val; /*val字段*/
}Node;
/*建树 插入节点*/
RESULT insert(Node** root, int key, int val);
/*删除指定key的元素*/
RESULT delete(Node** root, const int key);
/* 堆栈所存储的值的数据类型 */
#define ELEM_TYPE Node
#endif//__AVL__H_
AVL树是通过什么来调整数的平衡(调平)的呢?通过节点的旋转,何谓旋转?如下图所示
上(A)图是一个最简单的旋转,10本来是根节点,其左子节点的高度值是-1,右子节点的高度值是1,两者差的绝对值是2,因此需要调平,变成了(B)。
下面贴上两张动态图片:
相信的旋转情况,请参见http://blog.youkuaiyun.com/collonn/article/details/20128205文章中的图示,情况考虑的很周全,有八种情况,但是我们实际编码要考虑的就四种情况,那就是:左旋转(L),右旋转(R),先左后右旋转(LR),先右后左旋转(RL)。
旋转看上去很复杂,好像整棵树都变化很大,实际上变化的只是失衡结点相关两的几个结点之间的重新拼接的过程,其实,旋转操作并不困难,困难的是
1.如何判断树是否失衡,(当前结点的左右子树高度差是否达到2)
2.如何找到失衡点,(将插入判断过程中入栈的结点一一出栈,如果当前出栈的结点的左右子树高度差等于2,则失衡)
3.如何判断失衡点应该进行什么样的旋转操作(根据失衡点左右子节点高度值的关系来判断。)
具体的还是来看代码吧,插入和删除都有很详细的注释说明
/*BST.c*/
#include"AVL.h"
#include "stack.h"
/*创建节点*/
Node* createnode(int key,int val)
{
Node* node = (Node*)malloc(sizeof(Node));
node->height = 0;
node->key = key;
node->val = val;
node->cnt = 1;
node->plc = NULL;
node->prc = NULL;
return node;
}
/*返回当前结点的height*/
static int Getheight(Node* p)
{
return (p == NULL) ? -1 : p->height;
}
/*返回高度值较大的值*/
static int Max(int h1, int h2)
{
return h1 > h2 ? h1 : h2;
}
/*左旋转*/
Node* Left_Rotate(Node* p1)
{
Node* p2 = p1->plc;
p1->plc = p2->prc;
p2->prc = p1;
p1->height = Max(Getheight(p1->plc),Getheight(p1->prc)) + 1;
p2->height = Max(Getheight(p2->plc),Getheight(p2->prc)) + 1;
return p2;
}
/*右旋转*/
Node* Right_Rotate(Node* p1)
{
Node* p2 = p1->prc;
p1->prc = p2->plc;
p2->plc = p1;
p1->height = Max(Getheight(p1->plc),Getheight(p1->prc)) + 1;
p2->height = Max(Getheight(p2->plc),Getheight(p2->prc)) + 1;
return p2;
}
/*先左,再右旋转*/
Node* LR_Rotate(Node* p)
{
p->plc = Right_Rotate(p->plc);
return Left_Rotate(p);
}
/*先右,再左旋转*/
Node* RL_Rotate(Node* p)
{
p->prc = Left_Rotate(p->prc);
return Right_Rotate(p);
}
/*结点调整函数,从树中删除节点时使用此函数主要是判断当前结点是否失衡,
属于哪种失衡,应该用哪种旋转解决*/
Node* Rotate( Node* node )
{
if (Getheight(node->plc) - Getheight(node->prc) == 2) {
if (Getheight(node->plc->plc) >= Getheight(node->plc->prc)) {
node = Left_Rotate(node);
}
else {
node = LR_Rotate(node);
}
}
if (Getheight(node->prc) - Getheight(node->plc) == 2) {
if (Getheight(node->prc->prc) >= Getheight(node->prc->plc)) {
node = Right_Rotate(node);
}
else {
node = RL_Rotate(node);
}
}
return node;
}
RESULT _insert_(Node* tmp, Node* node, Stack* stack)
{
/*插入过程的所“走过”的结点入栈:*/
while (tmp != NULL) {
(*stack).push(stack, tmp);
if (node->val < tmp->val) {
if (tmp->plc == NULL) {
tmp->plc = node;
break;
}
else {
tmp = tmp->plc;
}
}
else if (node->val > tmp->val) {
if (tmp->prc == NULL) {
tmp->prc = node;
break;
}
else {
tmp = tmp->prc;
}
}
else {
tmp->cnt++;
return SUCCESS;
}
}
return SUCCESS;
}
/*插入节点*/
RESULT insert(Node** root, int key, ValueType val)
{
Node* node = createnode(key,val);
if (NULL == node)
{
perror("malloc failure!!!\n");
return FAILURE;
}
if (*root == NULL) {
*root = node;
return SUCCESS;
}
Stack stack;
registstack(&stack);
stack.init_size(&stack,1000);
Node* tmp = *root;
/*插入节点过程*/
if (SUCCESS != _insert_(tmp,node,&stack))
{
return FAILURE;
}
/*这里为什么要提前弹出一次操作呢?因为最后入栈的是插入节点的直接父节点,
如果插入节点后失衡,说明在插入之前就已经失衡了,这种情况是不存在的,这里要清楚再某个结点失衡这一概念
因为树一直在自动调整平衡,在插入操作开始之时,树肯定是个平衡树,
如果插入节点后引起失衡,那么肯定是在插入节点的父节点的父节点开始失衡的,
所以,插入节点的父节点就不需要进行判断是否失衡了,从插入节点的爷爷节点开始判断就行了*/
tmp = stack.top(&stack);/*保存插入节点的直接父节点*/
stack.pop(&stack);
int ori_height = tmp->height;/*保存插入节点的父节点的原始高度值*/
/*父节点的高度值是否有必要改变,如果一开始父节点有个子节点,
则高度值不变,如果父节点原来也是个叶子节点,此时高度值变为1*/
tmp->height = Max(Getheight(tmp->plc), Getheight(tmp->prc)) + 1;
/*如果插入节点的原始高度不是0,说明插入节点的父节点的高度值不会改变,
也不会一起其他节点高度值改变,所以不可能造成失衡,直接返回原来的根节点即可
但是如果插入节点的高度值是0,说明插入节点的父节点原来是个叶子节点,
插入新节点之后成为父节点,其高度值改变,其他节点的高度也也会跟着改变,有可能造成失衡*/
if (ori_height == 0)/*如果有可能造成失衡*/
{
/*出栈过程,判断插入了该节点以后,是否引起失衡*/
while (!stack.is_empty(&stack)) {
Node* n = stack.top(&stack);
if (Getheight(n->plc) - Getheight(n->prc) == 2) /*判断当前结点的左子树高度是否比右子树高度高出2*/
{
/*在判断插入的元素是插入到了左子树还是右子树,如果是插入左子树就是LL(左左)旋转*/
if (val < n->plc->val) {
/*左旋*/
n = Left_Rotate(n);
}
else { /*否则就是LR(先左后右旋转)*/
/*先左后右旋*/
n = LR_Rotate(n);
}
}
else if (Getheight(n->prc) - Getheight(n->plc) == 2)/*判断当前结点的右子树高度比左子树高度高2*/
{
/*判断是不是RR(右右)旋转*/
if (val > n->prc->val) {
/*右旋*/
n = Right_Rotate(n);
}
else {/*判断是不是RL(先右后左旋转)*/
/*先右后左旋*/
n = RL_Rotate(n);
}
}
n->height = Max(Getheight(n->plc), Getheight(n->prc)) + 1;
tmp = n;
stack.pop(&stack);
}
/*tmp最后保存的是根节点,根节点可能不变,如果在根节点出现旋转,就有可能会改变根节点,
因此将tmp最后保存(必然是根节点,不管是旧的还是新生成的)的结点当做根节点赋值给root*/
*root = tmp;
}
return SUCCESS;
}
/*处理要删除节点的右子树*/
void handlerightbranch( Node*tmp )
{
Stack stack; /*用来保存寻找要删除节点过车中的结点*/
registstack(&stack);
stack.init_size(&stack, 1000);
Node* r = tmp->prc;
while (r->plc != NULL) {
stack.push(&stack, r);
r = r->plc;
}
tmp->val = r->val;/*右子树的最左子节点的val替换掉要删除节点的val*/
tmp->key = r->key;
if (stack.is_empty(&stack))/*如果栈为空,说明待删除节点右子树的最左节点就是待删除结点的右子节点,
此时只需要把待删除节点右子节点指向原右子节点的右子节点*/ {
tmp->prc = r->prc;
free(r);
r = NULL;
}
else {
Node* sn = stack.top(&stack);
sn->plc = r->prc;
free(r);
r = NULL;
stack.pop(&stack);
Node* n = sn;/*用作判断栈顶结点是其下一个结点(栈顶结点的父节点)的左子节点还是右子节点*/
Node* newnode = Rotate(sn);/*获取原来以sn为根节点的子树调平后的新的根节点(也有可能不需要进行调平)*/
/*对入栈的每一个一点依次进行调平处理,最后newnode保存的是一个新的平衡的右子树*/
while (!stack.is_empty(&stack)) {
Node* t = stack.top(&stack);
if (t->plc == n) {
t->plc = newnode;
}
else {
t->prc = newnode;
}
n = t;
newnode = Rotate(t);
stack.pop(&stack);
}
tmp->prc = newnode;
}
}
/*在AVL树中,如果一个结点的左子节点是NULL,
那么它的右子节点必然是个叶子节点,
否则就违背了AVL树的平衡*/
/*删除指定key的元素*/
RESULT delete(Node** root,const int key)
{
if (*root == NULL)
return SUCCESS;
Stack stack; /*用来保存寻找要删除节点过车中的结点*/
registstack(&stack);
stack.init_size(&stack, 1000);
stack.push(&stack,*root);
Node* tmp = *root;
while (tmp != NULL) {
if (key < tmp->key) {
tmp = tmp->plc;
stack.push(&stack, tmp);
}
else if (key > tmp->key) {
tmp = tmp->prc;
stack.push(&stack, tmp);
}
else {
break;
}
}
stack.pop(&stack);
/*如果要删除的元素不在树中*/
if (NULL == tmp) {
printf("don't find the node whit this key!!!\n");
return SUCCESS;
}
/*判断一下要删除的结点是不是叶子节点*/
if (tmp->plc == NULL && tmp->prc == NULL) {
Node* sn = stack.top(&stack);
if (sn == NULL)/*如果栈为空,说明要删除的是root节点*/ {
*root = NULL;
return SUCCESS;
}
else {
if (sn->plc == tmp) {
sn->plc = NULL;
}
else {
sn->prc = NULL;
}
free(tmp);
tmp = NULL;
sn->height = Max(Getheight(sn->plc), Getheight(sn->prc)) + 1;
stack.pop(&stack);
stack.push(&stack, sn);/*删除的叶子节点的父节点更新了高度之后重新入栈*/
}
}
else if (tmp->prc == NULL && tmp->plc != NULL)/*如果右子节点是NULL,直接用左子节点替换掉要删除的结点,然后判断树是否平衡*/ {
Node* sn = stack.top(&stack); /*要删除节点的父节点*/
if (sn->plc == tmp) {
sn->plc = tmp->plc;
}
else {
sn->prc = tmp->plc;
}
free(tmp);
tmp = NULL;
sn->height = Max(Getheight(sn->plc), Getheight(sn->prc)) + 1;
stack.pop(&stack);
stack.push(&stack, sn);/*替换节点的父节点更新了高度之后重新入栈*/
}
else/*如果右子节点不是NULL,这种情况比较复杂,因为要用右子节点的最左子节点来替换掉要删除的结点,
删除了右子树的最左子节点之后,还要对右子树进行调平*/ {
/**/
handlerightbranch(tmp);
stack.push(&stack,tmp);
}
Node* n = stack.top(&stack);
Node* newnode = Rotate(stack.top(&stack));
stack.pop(&stack);
while (!stack.is_empty(&stack)) {
Node* t = stack.top(&stack);
if (t->plc == n) {
t->plc = newnode;
}
else {
t->prc = newnode;
}
n = t;
newnode = Rotate(t);
stack.pop(&stack);
}
*root = newnode;
/*经过以上操作,以要删除节点为根节点的子树是平衡的,但是可能会引起树整体上的不平衡,
因此需要判断从替换节点开始,向上逐一是否有失衡,有失衡就需要调平,一直到根节点为止*/
return SUCCESS;
}
测试程式的代码如下:
/*test.c*/
#include "AVL.h"
int main()
{
Node* root = NULL;
int elemarry[] = { 16, 12, 18, 10, 14, 17, 20, 9,11,13, 15, 8 };
for (int i = 0; i < sizeof(elemarry)/sizeof(int); i++)
{
if (SUCCESS != insert(&root, elemarry[i], elemarry[i]))
{
perror("insert failure!!!\n");
return FAILURE;
}
}
/*delete(&root, 11);*/
delete(&root, 22);
return 0;
}
以上代码中用到的栈结构的实现代码在此代码
可能看到那么长的代码,很多人就觉得郁闷,这里面也许是LZ本人能力有限,代码写的有些冗余,没有细致的去修改,只是尽量用非递归的方式去说明插入和删除的过程。我们知道,其实所有的递归都可以用非递归的迭代方式实现,之所以如此,是因为递归的本质也是一个迭代的过程,只不过这个过程是系统来操纵的,我们不用关心数据栈的问题,所以代码看起来很简洁很优美,而非递归实现其实就是我们自己创建一个栈,对栈进行入栈出栈的操作来实现了递归的过程,这样让我们更容易跟踪程序的执行过程,理解起来也比较方便。
插入过程其实可以分为两部分,第一部分是跟BST树的插入一样一样的,插入过程涉及到的点都一一入栈,第二部分就是插入新节点后,判断哪些入栈的结点是否有失衡,如果失衡,就要进行调平,依次出栈进行判断调平。
删除过程基本上也是分两步来走的,就是入栈和出栈。
入栈是在寻找要删除的结点时发生,
出栈是删除了节点以后,依次出栈判断是否有失衡。
在入栈和出栈中间,我们有一个删除要删除节点的过程,这个过程我们分了三种情况,第一是要删除的结点是叶子节点(直接删),
第二是要删除的结点右子节点是NULL,左子节点不是NULL(左子节点顶上去),第三种情况是右子节点不是NULL,前两种情况好处理,
第三种情况其实相当于执行一遍“对要删除节点的右子树执行删除其最左子节点“的删除操作,删除的最左子节点其实是用来去替换真正要删除的结点,这个过程在BST的删除中也有。这个过程也有入栈和出栈的过程,因为也要判断删除了右子树的最左子节点是否引发右子树由失衡,如果失衡,要进行调整。
好了,AVL的非递归实现基本上完成了,如果实在觉得上面的代码又长又乱,没有胃口读下去,那就找一找AVL树的递归实现过程吧,起码代码直观上感觉很清晰。
以上代码仅供学习原理使用,代码并不严谨,并有存在Bug的可能。如果您读完了以上代码,发现了BUG,请不吝指出,谢谢。