首先思考一下,二叉搜索树的可能的缺陷是什么?
二叉搜索树在最差情况下,比如依次插入节点1、2、3、4、5、6、7....,查找的时间复杂度达到了O(N)(不限于查找,增删查改的时间复杂度在最差情况下都是O(N)),也就是说最差情况下,二叉搜索树的搜索时间复杂度和顺序查找是一样的。怎么解决呢?那就是让树尽可能平衡,试想上面的1、2、3、4、5、6、7,如果换成下图的方式,增删查改的时间复杂度是不是大幅下降:
显然,上图右边的显得更加"平衡",而左边则显得非常"线性"和"偏坠",。
对于二叉树,尽可能的树的平衡,可以达到更高的增删查改效率。
AVL树就是一种极端追求平衡的二叉树,它要求任何一个父节点,它的左子树和右子树的高度之差不可以超过1,如下都是不允许的:
如果在AVL树的插入、删除过程中,发生了上面的4种情况任意之一,那就AVL树就要调整节点来解决掉这种情况,这个调整的过程就叫翻转,对于上面的4种情况也有4种翻转方式:
1、所谓的右右翻转,中间节点2作为新父节点,原父节点1作为新父节点2的左子节点,2原先的左节点转而挂在节点1的右节点
3、所谓的左左翻转,中间节点2作为新父节点,原父节点3作为新父节点2的右子节点,2原先的右节点转而挂在节点1的左节点
首先一定理解好左左/右右两个翻转。
2、所谓的左右翻转,子节点3需要首先做左左翻转,翻转后和图1的情况一样,再对父节点1进行右右翻转
4、所谓的右左翻转,子节点1需要首先做右右翻转,翻转后和图3的情况一样,再对父节点3进行左左翻转
AVL树在实际的插入/删除过程中,只会有上面的四种翻转的情况,简言之就是,AVL树在插入/删除一条数据后,都会判断是否插入/删除这条数据后,出现了上面四种情况任意之一,如出现,则做出翻转;同时,翻转后可能导致上级节点也出现不平衡的情况出现,那就继续翻转...保证整个AVL树都是肯定平衡的。
由上可得到如下重要结论:
1、AVL树任何时候都保证高度的平衡,即任意子树的父节点,左子树和右子树的高度差不超过1
1.1、这就保证了AVL树,增删查改的时间复杂度都是logN,最坏也都是logN。(AVL树保证查找最坏logN这个特性,对于不频繁插入/删除的场景来说确实优于红黑树)
1.2、但是要注意,插入/删除后可能要做最多logN次的翻转,简言之每一级的不平衡,都可能导致需要做翻转,这是AVL树之所以最终无过多实际用途的原因。
2、每次插入/删除时都要判断是否出现左子树和右子树的高度差,如何做到的?
每一个节点都记录自己的高度,在插入/删除后都(递归的)更新自己的高度,这样即可准确发现是否不平衡需要做翻转,翻转后也要(递归的)更新自己的高度
更直观的代码如下:
规定了节点的定义(node)和AVL树的方法,包括二叉树都有的前中后遍历(pre/mid/post)、4类翻转(ll/lr/rl/rr)、获取节点高度(height)、翻转(adjust)、查找(find)、删除(remove)、插入(insert)
template<class T> struct node {
T data;
int height;
struct node *lchild;
struct node *rchild;
node(T _data, int _height, struct node * _lchild, struct node *_rchild): data(_data), height(_height), lchild(_lchild), rchild(_rchild) {}
};
template<class T> class avltree {
node<T> *root;
void show(T data);
void free(node<T> *root);
void pre(node<T> *node);
void mid(node<T> *node);
void post(node<T> *node);
node<T> *ll(node<T> *node);
node<T> *lr(node<T> *node);
node<T> *rl(node<T> *node);
node<T> *rr(node<T> *node);
int height(node<T> *nd);
node<T> *adjust(node<T> *nd);
void update(node<T> *nd);
node<T> *findmin(node<T> *rt);
node<T> *del(node<T> *node, T data);
node<T> *insert(node<T> *node, T data);
public:
avltree(){}
avltree(T *data, int size);
~avltree();
void add(T data);
void remove(T data);
node<T> *find(T data);
void preshow();
void midshow();
void postshow();
int get_height() {return root->height;}
};
下面是AVL树逻辑实现部分了:
#include "avl.h"
#include <iostream>
//AVL树的构造, 其实也是节点依次插入的过程即insert的过程. 注意要保证根节点首先存在
template<class T> avltree<T>::avltree (T *data, int size) {
root = 0;
root = new node<T>(data[0], 1, 0, 0);
for (int i = 1; i < size; i++) {
root = insert(root, data[i]);
}
}
//AVL树释放, 和所有二叉树是一样的
template<class T> void avltree<T>::free (node<T> *rt) {
if (rt && !rt->lchild && !rt->rchild) {
delete rt;
rt = 0;
return;
}
if (rt->lchild) {
free(rt->lchild);
}
if (rt->rchild) {
free(rt->rchild);
}
}
template<class T> avltree<T>::~avltree () {
free(root);
root = 0;
}
template<class T> int avltree<T>::height (node<T> *nd) {
return (nd)?nd->height:0;
}
//根据子节点的高度更新父节点高度
template<class T> void avltree<T>::update (node<T> *nd) {
node<T> *lchild = nd->lchild, *rchild = nd->rchild;
int height_l = height(lchild), height_r = height(rchild);
nd->height = (height_l > height_r)?(height_l + 1):(height_r + 1);
}
//左旋. 父节点的左节点要提到父节点, 父节点转为其右子节点
template<class T> node<T> *avltree<T>::ll (node<T> *rt) {
node<T> *tmp = rt->lchild;
rt->lchild = tmp->rchild;
tmp->rchild = rt;
update(rt);
update(tmp);
return tmp;
}
//右左, 首先对左子节点做右旋, 然后再对父节点做左旋
template<class T> node<T> *avltree<T>::lr (node<T> *rt) {
rt->rchild = ll(rt->rchild);
node<T> *tmp = rt->rchild;
rt->rchild = tmp->lchild;
tmp->lchild = rt;
update(rt);
update(tmp);
return tmp;
}
//左右, 首先对右子节点做左旋, 然后再对父节点做右旋
template<class T> node<T> *avltree<T>::rl (node<T> *rt) {
rt->lchild = rr(rt->lchild);
node<T> *tmp = rt->lchild;
rt->lchild = tmp->rchild;
tmp->rchild = rt;
update(rt);
update(tmp);
return tmp;
}
//右旋. 父节点的右节点要提到父节点, 父节点转为其左子节点
template<class T> node<T> *avltree<T>::rr (node<T> *rt) {
node<T> *tmp = rt->rchild;
rt->rchild = tmp->lchild;
tmp->lchild = rt;
update(rt);
update(tmp);
return tmp;
}
//首先获取左子树高度和右子树的高度, 然后判断是否高度差已超过1, 如已超过再根据是四种情况中的哪种决定如何旋转
template<class T> node<T> *avltree<T>::adjust (node<T> *rt) {
//先获取到左子树高度height_l和右子树高度height_r
node<T> *lchild = rt->lchild, *rchild = rt->rchild;
int height_l = height(lchild), height_r = height(rchild);
//判断是否命中四种情况任意之一,还是没有命中,如命中应该是怎样翻转
if (height_l - height_r >= 2) {
if (lchild->lchild) {
return ll(rt);//图3的情况
} else {
return rl(rt);//图4的情况
}
} else if (height_r - height_l >= 2) {
if (rchild->rchild) {
return rr(rt);//图1的情况
} else {
return lr(rt);//图2的情况
}
} else {
return rt;
}
}
//插入节点, 和二叉搜索树方式近似, 但在插入节点后需要首先调用update更新本节点的高度, 然后调用adjust判断是否需要翻转, 如需要则做出翻转
//返回给递归上一级的是可能翻转后的本层节点, 作为上一层节点的新的左/右子节点
template<class T> node<T> *avltree<T>::insert (node<T> *rt, T data) {
if (rt) {
T curdata = rt->data;
if (curdata > data) {
rt->lchild = insert(rt->lchild, data);
update(rt);
return adjust(rt);
} else if (curdata < data) {
rt->rchild = insert(rt->rchild, data);
update(rt);
return adjust(rt);
} else {
return rt;
}
} else {
node<T> *newnode = new node<T>(data, 1, 0, 0);
return newnode;
}
}
//查找, 和二叉搜索树方式完全一样
template<class T> node<T> *avltree<T>::find (T data) {
node<T> *rt = root;
while (rt) {
T curdata = rt->data;
if (curdata == data) {
return rt;
} else if (curdata > data) {
if (rt->lchild) {
rt = rt->lchild;
} else {
return 0;
}
} else if (curdata < data) {
if (rt->rchild) {
rt = rt->rchild;
} else {
return 0;
}
}
}
return 0;
}
//必须注意的细节:
//1、只有在待删除节点左右子节点都实际存在的情况下, 才会调用到这里, 所以不可能返回空
//2、实际返回情况分为3类:
// 2.1、返回待删除节点的右子节点: 右子节点无左子节点的情况下
// 2.2、返回一个没有左子节点的节点(右子节点的左子节点): 右子节点的左子节点, 没有左子节点的情况
// 2.3、返回一个有左子节点的节点: 右子节点的左子树中的某节点, 这时实际要删除的是返回节点的左子节点
template<class T> node<T> *avltree<T>::findmin (node<T> *rt) {
node<T> *rchild = rt->rchild;
if (!rchild->lchild) {
return rchild;
}
node<T> *rlchild = rchild->lchild;
while (rlchild) {
if (rlchild->lchild && rlchild->lchild->lchild) {
rlchild = rlchild->lchild;
} else {
return rlchild;
}
}
}
//1、没找到...
//2、找到, 且没有左右子节点, 直接删
//3、找到, 只有左子节点或只有右子节点, 直接删, 但还要把左/右子树接上
//4、找到, 同时有左和右子节点, 根据上面的注释里的3种情况分别处理
//5、注意, 任何一种删除处理之后, 上级节点都必须更新高度, 以及可能需要做出的翻转
template<class T> node<T> *avltree<T>::del (node<T> *rt, T data) {
if (rt) {
//任何删除后,上级父节点必须根据子节点的最新高度,更新自己的高度,判断是否需要做出翻转
T curdata = rt->data;
if (curdata > data) {
rt->lchild = del(rt->lchild, data);
update(rt);
return adjust(rt);
} else if (curdata < data) {
rt->rchild = del(rt->rchild, data);
update(rt);
return adjust(rt);
} else {
//左子树和右子树统统缺失,直接删
//左子树和右子树都有,首先判断"替死鬼"节点是哪个(比待删除大的节点中最小的一个,findmin),//左子树和右子树只有一个,直接删然后接上即可
//同时判断属于3种情况中的哪种情况(右子节点/右子节点的唯一左子节点/右子节点的左子树中某节点,"替死鬼"节点的父节点),然后分别处理
if (!rt->lchild && !rt->rchild) { delete rt; return 0; } else if (rt->lchild && !rt->rchild) { rt->data = rt->lchild->data; delete rt->lchild; rt->lchild = 0; update(rt); return rt; } else if (!rt->lchild && rt->rchild) { rt->data = rt->rchild->data; delete rt->rchild; rt->rchild = 0; update(rt); return rt; } else { node<T> *min = findmin(rt); if (min == rt->rchild) { rt->data = rt->rchild->data; rt->rchild = rt->rchild->rchild; delete rt->rchild; update(rt); } else if (min->lchild) { rt->data = min->lchild->data; delete min->lchild; min->lchild = 0; } else { rt->data = min->data; rt->rchild->lchild = min->rchild; delete min; } update(rt); return rt; } } } return 0; }
//删除节点的外部接口 template<class T> void avltree<T>::remove (T data) { node<T> *nd = find(data); if (0 == nd) { return; } root = del(root, data); } template<class T> void avltree<T>::show (T data) { std::cout << data << " "; } template<class T> void avltree<T>::mid (node<T> *rt) { if (!rt) { return; } mid(rt->lchild); show(rt->data); mid(rt->rchild); } template<class T> void avltree<T>::midshow () { mid(root); std::cout << std::endl; } template<class T> void avltree<T>::pre (node<T> *rt) { if (!rt) { return; } show(rt->data); pre(rt->lchild); pre(rt->rchild); } template<class T> void avltree<T>::preshow () { pre(root); std::cout << std::endl; } template<class T> void avltree<T>::post (node<T> *rt) { if (!rt) { return; } post(rt->lchild); post(rt->rchild); show(rt->data); } template<class T> void avltree<T>::postshow () { post(root); std::cout << std::endl; }
最后是测试程序:#include "avl_funcss.h" #include <stdlib.h> using namespace std; void init (int *testdata, int size) { srand((int)time(0)); for (int i = 0; i < size; i++) { testdata[i] = rand() % 1000; std::cout << testdata[i]; if (i != size - 1) { std::cout << ","; } } std::cout << std::endl << "inited" << std::endl; } int main () { int testdata[100] = {0}; init(testdata, sizeof(testdata)/sizeof(testdata[0])); //int testdata[10] = {542,783,319,641,779,267,799,868,477,206}; //int testdata[11] = {59,162,352,347,380,236,9,5,176,385,3}; avltree<int> at(testdata, sizeof(testdata)/sizeof(testdata[0])); at.midshow(); /* at.midshow(); at.preshow(); at.remove(162); at.midshow(); at.preshow(); at.add(3); at.midshow(); at.preshow(); */ return 0; }
通过gdb或者调用遍历方法,可以清晰的感受到AVL树的插入、删除、查找的过程。