一、二叉查找树
我们先来讲讲二叉查找树。
大家应该听说过二分查找吧,二分查找是对一个有序序列的快速查找,时间复杂度为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));
}
三、例题
营业额统计
题目描述
输入
第一行为正整数 ,表示该公司从成立一直到现在的天数
接下来的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);
}