数据结构----AVL平衡树----AVL平衡树的基本操作

一、二叉查找树

我们先来讲讲二叉查找树。

大家应该听说过二分查找吧,二分查找是对一个有序序列的快速查找,时间复杂度为O(log2(n)),

但是二分查找也有它的缺点,当序列加入一个元素时,我们就需要对这个有序序列进行维护,要么就用sort(),要么就用插入排序(附带大量的数据移动),时间复杂度就会陡然提升。

于是就有了一种新的数据结构:二叉查找树!

二叉查找树的运用比较灵活,支持插入O(log2(n))、查找O(log2(n))、删除和前驱后继的查询。

插入操作:

(1)将要插入的节点与根节点比较,如果没有根节点,则把该节点作为根节点。

(2)如果插入的节点于根节点,就把该节点插入到子树,并把子树的根节点作为比较对象。

(3)如果插入的节点于根节点,就把该节点插入到子树,并把子树的根节点作为比较对象。

(4)递归进行以上操作,直到达到叶子节点。

(5)将它插入到叶子节点的下方。

查找操作:

(1)将它与根节点比较,如果比根节点小,就向左子树查找;如果比根节点大,就向右子树查找。

(2)如果到了叶子节点下方还没有找到返回-1。

(3)如果找到了,就返回该节点的下表或值。

删除操作:

(1)找到该节点a。(最重要的一步)

(2)在这个节点的左子树里找到最大的节点b(即前驱)。

(3)删掉节点b,因为节点b没有右儿子,所以就把节点b的左子树的根节点来代替节点b。

(4)用节点b来代替节点a。

查询前驱与后继操作:

(1)找到这个节点a。

(2)a的前驱为它的子树里最的儿子。

(3)a的后继为它的子树里最的儿子。


但是,二叉查找树也有它的缺点,因为当输入数据是有序的时候(如:1 2 3 4 5 6),经过插入操作之后,建成的树为这个样子:


之后,所有的操作的时间复杂度都变成了O(n)了,怎么处理这样的情况呢?

于是就有了二叉查找树的升级版:AVL平衡树!


二、AVL平衡树

AVL平衡树其实就是在每一个节点上面加了一个平衡因子h,h表示的是以这个节点为根节点的树总深度,当一个节点的左右儿子的平衡因子的差大于1时,就会对此进行平衡化旋转操作。

平衡树的定义方法:(注意maxn大小最好为输入数据数量n的4倍,即4n)

struct node{
    int lc,rc,h,v;
}tree[maxn];

平衡化旋转大致分为4类

1、zig旋转:

当平衡树中插入的节点在第一个不平衡的节点的左子树左子树中,我们就要对平衡树进行zig旋转

代码:

int zig(int r)
{
    int t=tree[r].lc;
    tree[r].lc=tree[t].rc;
    tree[t].rc=r;
    tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;
    tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
    return t;
}

2、zag旋转:

当平衡树中插入的节点在第一个不平衡的节点的右子树右子树中,我们就要对平衡树进行zag旋转

代码:

int zag(int r)
{
    int t=tree[r].rc;
    tree[r].rc=tree[t].lc;
    tree[t].lc=r;
    tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;
    tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
    return t;
}

3、zigzag旋转:

当平衡树中插入的节点在第一个不平衡的节点的左子树右子树中,我们就要对平衡树进行zigzag旋转

代码:

int zigzag(int r)
{
    tree[r].rc=zig(tree[r].rc);
    return zag(r);
}

4、zagzig旋转:

当平衡树中插入的节点在第一个不平衡的节点的右子树左子树中,我们就要对平衡树进行zagzig旋转

代码:

int zagzig(int r)
{
    tree[r].lc=zag(tree[r].lc);
    return zig(r);
}

插入操作跟二叉查找树差不多,只不过要注意插入后的调整:

int insert(int x,int r)
{
    if(r==0){
        tree[++cnt].v=x;
        tree[cnt].h=1;
        return cnt;
    }
    if(x<tree[r].v){
        tree[r].lc=insert(x,tree[r].lc);
        if(tree[tree[r].lc].h==tree[tree[r].rc].h+2){
            if(x<tree[tree[r].lc].v) r=zig(r);
            else if(x>tree[tree[r].lc].v) r=zagzig(r);
        }
    }
    else if(x>tree[r].v){
        tree[r].rc=insert(x,tree[r].rc);
        if(tree[tree[r].rc].h==tree[tree[r].lc].h+2){
            if(x>tree[tree[r].rc].v) r=zag(r);
            else if(x<tree[tree[r].rc].v) r=zigzag(r);
        }
    }
    tree[r].h=max(tree[tree[r].lc].h,tree[tree[r].rc].h)+1;
    return r;
}

删除操作的思路跟二叉查找树的思路是一样的,但是调整整棵树就比较麻烦了(因为一次旋转不能达到平衡),所以说就要写一个maintain()函数,在每次调用dele()的最后都要调用一下maintain()。

void maintain(int &r)
{
    if(tree[tree[r].lc].h==tree[tree[r].rc].h+2){
        int t=tree[r].lc;
        if(tree[tree[t].lc].h==tree[tree[r].rc].h+1)
           r=zig(r);
        else if(tree[tree[t].rc].h==tree[tree[r].rc].h+1){
            tree[r].lc=zag(tree[r].lc);
            r=zig(r);
        }
    }
    else if(tree[tree[r].lc].h+2==tree[tree[r].rc].h){
        int t=tree[r].rc;
        if(tree[tree[t].rc].h==tree[tree[r].lc].h+1)
            r=zag(r);
        else if(tree[tree[t].lc].h==tree[tree[r].lc].h+1){
            tree[r].rc=zig(tree[r].rc);
            r=zag(r);
        }
    }
    tree[r].h=max(tree[tree[r].lc].h,tree[tree[r].rc].h)+1;
}
int dele(int &r,int x)
{
    int tx;
    if(x==tree[r].v||(x<tree[r].v&&tree[r].lc==0)||(x>tree[r].v&&tree[r].rc==0)){
        if(tree[r].lc==0||tree[r].rc==0){
            tx=tree[r].v;
            r=tree[r].lc+tree[r].rc;
            return tx;
        }
        else
            tree[r].v=dele(tree[r].lc,x);
    }
    else{
        if(x<tree[r].v)
            tx=dele(tree[r].lc,x);
        else tx=dele(tree[r].rc,x);
    }
    maintain(r);
    return tx;
}

AVL仍然可以采用惰性删除,对于要删除的节点,只需要给它进行一个标记。平衡树的高度仍然是log(N),这样并不会降低效率,而删除操作却要快速的多。

如果遇到那些删除过后还要恢复的节点,则惰性删除更优,不需要额外占用空间,将原来的节点恢复即可。

如果遇到那种允许节点重复的AVL,惰性删除更可取,将删除标记设为整型变量,表示该节点出现的数量即可。


查询操作和二叉查找树也大致相同。以下代码是查询一个数在序列中与其他数的最小的差值(即最接近的数与它的差)。

int find(int root,int x)
{
    if(root==0) return 1<<30;
    if(x==tree[root].v) return 0;
    if(x>tree[root].v)
        return min(x-tree[root].v,find(tree[root].rc,x));
    else
        return min(tree[root].v-x,find(tree[root].lc,x));
}


三、例题

营业额统计

题目描述

Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况。
Tiger拿出了公司的账本,账本上记录了公司成立以来每天的营业额。分析营业情况是一项相当复杂的工作。由于节假日,大减价或者是其他情况的时候,营业额会出现一定的波动,当然一定的波动是能够接受的,但是在某些时候营业额突变得很高或是很低,这就证明公司此时的经营状况出现了问题。经济管理学上定义了一种最小波动值来衡量这种情况:
该天的最小波动值 = min {该天以前某一天的营业额 - 该天营业额}
当最小波动值越大时,就说明营业情况越不稳定。
而分析整个公司的从成立到现在营业情况是否稳定,只需要把每一天的最小波动值加起来就可以了。你的任务就是编写一个程序帮助Tiger来计算这一个值。
第一天的最小波动值为第一天的营业额。
天数小于100000.

输入

第一行为正整数 ,表示该公司从成立一直到现在的天数

接下来的n行每行有一个整数(一定有数据小于〇) ,表示第i天公司的营业额。

输出

输出文件仅有一个正整数,即每一天的最小波动值之和。答案保证在int范围内。

样例输入

6
5
1
2
5
4
6

样例输出

12

提示

结果说明:5+|1-5|+|2-1|+|5-5|+|4-5|+|6-5|=5+4+1+0+1+1=12


四、分析

一道典型的平衡树题目。

思路:

(1)输入一个数a。如果a为第一个,则sum+=a,进行步骤(3)。

(2)查找最接近a的数b与a之差,即abs(a-b)。sum+=abs(a-b)。

(3)插入a。


代码:

#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
struct node{
    int lc,rc,h,v;
}tree[100005];
int cnt;
int zig(int r)
{
    int t=tree[r].lc;
    tree[r].lc=tree[t].rc;
    tree[t].rc=r;
    tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;
    tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
    return t;
}
int zag(int r)
{
    int t=tree[r].rc;
    tree[r].rc=tree[t].lc;
    tree[t].lc=r;
    tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;
    tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
    return t;
}
int zagzig(int r)
{
    tree[r].lc=zag(tree[r].lc);
    return zig(r);
}
int zigzag(int r)
{
    tree[r].rc=zig(tree[r].rc);
    return zag(r);
}
int find(int root,int x)
{
    if(root==0) return 1<<30;
    if(x==tree[root].v) return 0;
    if(x>tree[root].v)
        return min(x-tree[root].v,find(tree[root].rc,x));
    else
        return min(tree[root].v-x,find(tree[root].lc,x));
}
int insert(int x,int r)
{
    if(r==0){
        tree[++cnt].v=x;
        tree[cnt].h=1;
        return cnt;
    }
    if(x<tree[r].v){
        tree[r].lc=insert(x,tree[r].lc);
        if(tree[tree[r].lc].h==tree[tree[r].rc].h+2){
            if(x<tree[tree[r].lc].v) r=zig(r);
            else if(x>tree[tree[r].lc].v) r=zagzig(r);
        }
    }
    else if(x>tree[r].v){
        tree[r].rc=insert(x,tree[r].rc);
        if(tree[tree[r].rc].h==tree[tree[r].lc].h+2){
            if(x>tree[tree[r].rc].v) r=zag(r);
            else if(x<tree[tree[r].rc].v) r=zigzag(r);
        }
    }
    tree[r].h=max(tree[tree[r].lc].h,tree[tree[r].rc].h)+1;
    return r;
}
int main()
{
    int n,i,x,sum=0,root=0,s;
    scanf("%d",&n);
    for(i=1;i<=n;i++){
        scanf("%d",&x);
        if(i==1){sum+=x;root=insert(x,root);continue;}
        s=find(root,x);
        if(s!=0){
            root=insert(x,root);
            sum+=abs(s);
        }
    }
    printf("%d",sum);
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值