目录
二叉查找树BST与平衡树Treap
一、二叉查找树BST
二叉查找树(简称BST
),是满足以下条件的二叉树:
- 树上每一个节点都有一个权值;
- 对于树上任意一个节点
u
,若左子树不为空,则左子树上所有节点权值均小于u
的权值; - 对于树上任意一个节点
u
,若右子树不为空,则右子树上所有节点权值均大于u
的权值;
如下图:
1. 二叉查找树的建立
struct BST
{
int l,r; //左右子节点的编号
int val; //节点权值
}s[SIZE]; //数组模拟链表
int tot,root=1; //tot记录当前节点数目,root为根节点
int INF=0x7fffffff; //int的最大值
//主函数main()中
a[++tot].val=INF; //添加一个无穷大的节点,就不需要判断BST是否为空,方便操作
二叉查找树的中序遍历,就是权值从小到大的排列顺序。
2. 二叉查找树的查找
在BST
中查找是否存在权值为x
的节点,执行流程如下:
设变量p
等于根节点root
(1) 若a[p].val==x
,则查找成功。
(2) 若a[p].val>x
① 若p
的左子节点为空,查找失败,说明不存在。
② 若p
的左子节点不为空,则继续在p
的左子树中递归查找。
(3) 若a[p].val<x
① 若p
的右子节点为空,查找失败,说明不存在。
② 若p
的右子节点不为空,则继续在p
的右子树中递归查找。
int Find(int p,int x) //当前节点为p,查找值为x的节点
{
if(p==0) return 0; //节点为空,查找失败
if(a[p].val==x) return p; //查找成功,返回x的位置
return a[p].val<x ? Find(a[p].r,x) : Find(a[p].l,x);
}
3. 二叉查找树找最值
以找最小值为例,从根节点root
开始,若左子节点不为空,访问左子节点,直到左子节点为空。
int Find_min(int p)
{
if(p) while(a[p].l) p=a[p].l;
return a[p].val; //返回最小值
}
找最大值方法类似。
4. 二叉查找树插入
BST
中插入一个权值x
,其实就是在BST
中查找权值为x
的节点:当权值x
的节点存在时,添加一个域,记录重复个数;当权值x
的节点不存在时,即找到了空节点,插入到空节点。
void Insert(int &p,int x) //插入权值x,当前结点为p
{
if(p==0)
{
a[++tot].val=x; //添加一个新的节点
p=tot; //p是引用,其父节点的l或r值会被同时更新,指向节点tot
return;
}
if(x==a[p].val) return; //已经存在,不插入,根据题意,也可以记录重复的个数
return x<a[p].val ? Insert(a[p].l,x) : Insert(a[p].r,x);
}
引用
这里的&
表示引用,引用在程序中相当于一个变量的别名,代表的都是同一个变量,只是名称不相同而已(类似每个人都有一个外号、小名等),对别名进行操作等同于对原变量进行操作,例如:
int a=10;
int &b=a; //b是a的别名
b+=2;
cout<<a; //输出答案为12
例如在下图BST
中插入权值6
:
设插入的权值顺序依次为8、12、15、5、7、10、9、3
,对应的节点编号依次为1~8
.
程序执行流程:
(1) 首先调用函数Insert(root,6)
,root
的别名为p
(root
为根节点,root=1
),接着递归调用函数Insert(a[root].l,6)
,
(2)a[root].l
的别名为p
,a[root].l
等于4
,即执行函数Insert(4,6)
,接着递归调用函数Insert(a[4].r,6)
;
(3)a[4].r
的别名为p
,a[4].r
等于5
,即执行函数Insert(5,6)
,接着递归调用函数Insert(a[5].l,6)
;
(4)a[5].l
的别名为p
,p
为空,添加一个新的节点,执行p=tot
,相当于a[5].l=tot
。
这样,添加一个节点的同时,可以同时更新其父节点的l
或r
。
5. 二叉查找树的删除
在BST
中删除权值为x
的节点,需要先查找是否存在。
若查找到权值为x
的节点编号为p
,需要分三种情况:
(1)p
为叶子节点,则直接删除。
(2)p
只有一个子节点,则让子节点代替p
的位置,(1)的情况也可以按照此方式进行处理,叶节点的子节点就是空节点,空节点代替等同于删除。
(3)p
既有左子树也有右子树,则在BST
中寻找节点p
的后继节点nxt
来代替p
的位置,后继节点就是大于x
且权值最小的节点,后继节点nxt
一定不存在左子树,所以可以直接删除后继节点nxt
,并令后继节点nxt
的右子树代替nxt
的位置。
后继节点nxt
一定不存在左子树,如果后继节点存在左子树,那么左子树的节点权值大于x
,且小于nxt
的权值,与nxt
是后继节点矛盾。
后继节点的寻找方法:
找到权值为x
的节点p时,令nxt=a[p].r
,然后一直往节点nxt
的左子节点递归往下找,最后一个非空的节点即为后继节点。
void Remove(int &p,int x) //从子树p中删除权值为x的节点,p是引用,是父结点的l或r的别名
{
if(p==0) return;
if(x==a[p].val) //查找到x
{
if(a[p].l==0) p=a[p].r; //无左子树,右子节点代替
else if(a[p].r==0) p=a[p].l; //无右子树,左子节点代替
else //既有左子树,又有右子树
{
//后继节点往p的右子节点的左子节点一直往下找
int nxt=a[p].r;
while(a[nxt].l) nxt=a[nxt].l;
Remove(a[p].r,a[nxt].val); //删除后继节点nxt
a[nxt].l=a[p].l; //nxt代替p的位置
a[nxt].r=a[p].r;
p=nxt; //p是引用,是父结点的l或r的别名,指向添加的节点nxt
}
return;
}
if(x<a[p].val) Remove(a[p].l,x);
else Remove(a[p].r,x);
}
在随机数据中,BST
的每次操作时间复杂度为O(logN)
。然而,如果按照权值顺序插入,则BST
的形态是一条链。
依次插入权值:3、5、6、8、12、15
,BST
形态如下:
时间复杂度会降低到O(N)
,为了使得BST
的形态“平衡”,每次操作时间复杂度尽可能接近O(logN)
,从而产生了平衡树。
样例
二叉查找树
【题目描述】
N
个操作,分别对应插入一个数和删除一个数,如果插入的数已存在,输出has been
,否则将这个数插入,并输出insert success
,对于删除一个数,如果该数不存在,输出not exist
,如果存在,删除它,并输出delete success
。
【输入】
第一行n
(n<=100000
),表示有n
个操作数; 之后n
行,每行一个字母(I
表示插入,D
表示删除),一个数字(数字为正整数,整型范围内)。
【输出】
n
行,每行按描述的方式输出。
【样例输入】
10
D 1
I 3
I 5
I 3
I 4
D 4
D 5
D 3
I 2
D 1
【样例输出】
not exist
insert success
insert success
has been
insert success
delete success
delete success
delete success
insert success
not exist
【参考程序】
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
struct BST
{
int l,r;
int val;
}a[N];
int n,tot,root=1,INF=0x7fffffff; //INF为int最大值
int flag=0; //标记是否删除
void Insert(int &p,int x) //插入权值x,当前结点为p
{
if(p==0)
{
a[++tot].val=x; //添加一个新的节点
p=tot; //p是引用,其父节点的l或r值会被同时更新,指向节点tot
printf("insert success\n");
return;
}
if(x==a[p].val) {printf("has been\n");return;} //已经存在,不插入
return x<a[p].val ? Insert(a[p].l,x) : Insert(a[p].r,x);
}
void Remove(int &p,int x) //从子树p中删除权值为x的节点,p是引用,是父结点的l或r的别名
{
if(p==0) {printf("not exist\n");return;} //不存在
if(x==a[p].val) //查找到x
{
if(a[p].l==0) p=a[p].r; //无左子树,右子节点代替
else if(a[p].r==0) p=a[p].l; //无右子树,左子节点代替
else //既有左子树,又有右子树
{
//后继节点往p的右子节点的左子节点一直往下找
int nxt=a[p].r;
while(a[nxt].l) nxt=a[nxt].l;
Remove(a[p].r,a[nxt].val); //删除后继节点nxt
a[nxt].l=a[p].l; //nxt代替p的位置
a[nxt].r=a[p].r;
p=nxt; //p是引用,是父结点的l或r的别名,指向添加的节点nxt
}
flag=1; //如果删除后继节点,会有两次删除,记录一次
return;
}
if(x<a[p].val) Remove(a[p].l,x);
else Remove(a[p].r,x);
}
int main()
{
a[++tot].val=INF; //添加一个无穷大的节点,就不需要判断BST是否为空,方便操作
scanf("%d",&n);
char ch[5];
int x;
for(int i=1;i<=n;i++)
{
flag=0;
scanf("%s%d",ch,&x);
if(ch[0]=='I') Insert(root,x);
else Remove(root,x);
if(flag) printf("delete success\n");
}
return 0;
}
二、平衡树Treap
Treap
是一种简单的平衡树,在普通二叉查找树的基础上,赋予每个节点一个属性:优先级。
对于Treap
中的节点,除了权值满足二叉查找树的性质外,节点的优先级还要满足大根堆(小根堆)的性质。
简单来说,从权值上看,Treap
是一棵二叉查找树,从优先级上看,Treap
是一个堆(Heap
)。所有Treap
就是Tree+Heap
,单词Treap
就是Tree
与Heap
的合成词。
BST
不平衡是因为有序的数据会使查找的路径退化成链,而随机数据使其退化的概率非常小。因此,Treap
中每个节点的优先级的值可以通过随机函数生成,这样Treap
的结构就会趋于平衡了。
sconst int N=100010;
struct Treap
{
int l,r; //左右子节点下标
int val; //权值
int pr; //优先级
int cnt; //重复个数
int size; //子树大小
}a[N];
int n,tot,root=1,INF=0x3fffffff;
void Update(int p) //更新子树大小
{
a[p].size=a[a[p].l].size+a[a[p].r].size+a[p].cnt;
}
1. Treap旋转
为了使Treap
在满足BST
的性质的同时,也同时满足大根堆的性质,需要对Treap
的结构进行调整,而调整的方法为旋转,旋转分为左旋和右旋,如下图所示:
注意:
- 旋转操作是保证在
BST
性质的基础上,根据节点优先级调整。 - 旋转不会改变
BST
的性质,只会改变节点优先级的位置。
(1)若x
的优先级大于y
的优先级,如上左图,需要进行右旋:将x
变为y
的父节点,x
原来的右子树变为y
的左子树。
void Zig(int &p) // 右旋,p的左子节点变为p的父节点
{
int q=a[p].l; //p的左子节点为q
a[p].l=a[q].r; //q的右子树变为p的左子树
a[q].r=p; //q变为p的父节点
p=q; //引用,p是其父节点的l或r的别名,指向q
Update(a[p].r);Update(p); //改变结构后,更新子树大小
}
(2)若y
的优先级大于x
的优先级,如上右图,需要进行左旋:将y
变为x
的父节点,y
原来的左子树变为x
的右子树。
void Zag(int &p) //左旋,p的右子节点变为p的父节点
{
int q=a[p].r; //p的右子节点为q
a[p].r=a[q].l; //q的左子树变为p的右子树
a[q].l=p; //q变为p的父节点
p=q; //引用,p是其父节点的l或r的别名,指向q
Update(a[p].l);Update(p); //改变结构后,更新子树大小
}
2. Treap插入
Treap
在插入每个新节点时,在有权值属性的基础上,会给该节点生成一个随机值,作为优先级的属性。然后像堆的插入过程一样,自底向上依次检查,当某个节点不满足大根堆性质时,就执行旋转,使该节点与父节点交换
。
插入节点的过程,先按照BST
性质,即权值大小插入到合适位置;再按照堆的性质,调整优先级,使其满足大根堆性质,步骤如下:
设变量p
初始时等于root
,插入权值为x
的节点:
(1)若x==a[p].val
,节点p
的数量+1
。
(2)若x<a[p].val
,递归往p
的左子树a[p].l
插入。
(3)若x>a[p].val
,递归往p
的右子树a[p].r
插入。
(4)若p
为空节点,插入到当前节点,生成一个随机值作为当前节点的优先级。
此时,是满足BST
性质的,但是不一定满足大根堆的性质。因此在回溯的时候,若不满足大根堆性质,进行旋转。
(5)若p
的左子节点优先级大于p
,则进行右旋,交换左子节点与p
的位置。
(6)若p
的右子节点优先级大于p
,则进行左旋,交换右子节点与p
的位置。
void Insert(int &p,int x) //插入
{
if(x==a[p].val) //存在权值为x的节点
{
a[p].cnt++; //数量+1
Update(p);
return;
}
if(p==0) //添加节点
{
a[++tot].val=x;
a[tot].pr=rand(); //rand()随机函数,优先级为随机值
a[tot].cnt=1;
a[tot].size=1;
p=tot; //引用,p是其父节点l或r的别名,指向tot
return;
}
if(x<a[p].val)
{
Insert(a[p].l,x); //递归插入
if(a[p].pr<a[a[p].l].pr) Zig(p); //回溯,不满足堆的性质,右旋
}
if(x>a[p].val)
{
Insert(a[p].r,x); //递归插入
if(a[p].pr<a[a[p].r].pr) Zag(p);//回溯,不满足堆的性质,左旋
}
Update(p); //回溯,更新子树大小
}
3. Treap删除
如果删除的节点为叶节点,可以直接删除。对于非叶子节点,因为Treap
支持旋转,可以通过旋转将其变为叶子节点。
(1)若删除的节点p
是非叶子节点,选择左右子节点优先级大的和节点p
交换:
(2)若左子节点优先级大,则右旋,保证优先级大的左子节点在右子节点上方。
(3)若右子节点优先级大,则左旋,保证优先级大的右子节点在左子节点上方。
如下图,删除节点15
:
首先查找到权值为15
的位置,比较左右子节点优先级,左子节点优先级高,右旋:
继续比较左右子节点优先级,右子节点优先级高,左旋:
继续比较左右子节点优先级,左子节点优先级高,右旋:
此时,删除的节点为叶子节点,可以直接删除。
void Remove(int &p,int x) //删除权值x
{
if(p==0) return; //不存在
if(x==a[p].val)
{
if(a[p].cnt>1) //有重复元素,减少个数
{
a[p].cnt--;
Update(p); //更新子树大小
return;
}
if(a[p].l||a[p].r) //非叶子节点
{
if(a[a[p].l].pr>a[a[p].r].pr) Zig(p),Remove(a[p].r,x); //右旋
else Zag(p),Remove(a[p].l,x); //左旋
a[p].size=a[a[p].l].size+a[a[p].r].size+a[p].cnt; //回溯更新子树大小
}
else //叶子节点
p=0; //引用,p是父节点l或r的别名,指向0,即删除
return;
}
if(x<a[p].val) Remove(a[p].l,x);
if(x>a[p].val) Remove(a[p].r,x);
Update(p); //回溯更新子树大小
}
4. 求数值x的前驱与后继
前驱定义为小于x
的最大数,后继定义为大于x
的最小数。
求解前驱:
设ans
为当前最优解,令变量p
等于根节点root
,从p
开始访问:
(1)若当前节点a[p].val<x
,且a[p].val>ans
,则更新最优解,继续寻找:若a[p].val<x
,往右子节点a[p].r
找,否则往左子节点a[p].l
找。
(2)若a[p].val==x
,则先找到p
的左子节点,再从左子节点的右子节点一直往下找。
(3)若p
为空,则停止寻找。
int Getpre(int x) //前驱
{
int ans=-INF;
int p=root;
while(p)
{
if(x==a[p].val) //找到相等
{
if(a[p].l)
{
p=a[p].l;
while(a[p].r) p=a[p].r;
ans=a[p].val;
}
break;
}
if(a[p].val<x&&a[p].val>ans) ans=a[p].val; //更新最优解
if(a[p].val<x) p=a[p].r; //选择合适的方向继续找
else p=a[p].l;
}
return ans;
}
求解后驱方法类似:
int Getnxt(int x) //后驱
{
int ans=INF;
int p=root;
while(p)
{
if(x==a[p].val) //找到相等
{
if(a[p].r)
{
p=a[p].r;
while(a[p].l) p=a[p].l;
ans=a[p].val;
}
break;
}
if(a[p].val>x&&a[p].val<ans) ans=a[p].val; //更新最优解
if(a[p].val<x) p=a[p].r; //选择合适的方向继续找
else p=a[p].l;
}
return ans;
}
5. 求排名为k的元素
求排名,需要维护每个节点为根的子树大小,即程序中的size
属性。
一个节点的排名,取决于其左子树的大小。设当前访问的子树根节点为p
,重复元素个数为a[p].cnt
,p
在作为子树根节点,在子树中的排名介于:左子树节点总数+1
~ 左子树节点总数+p
的重复个数,即区间[ a[ a[p].l ].size + 1 , a[ a[p].l] ].size + a[p].cnt ]
之间。因为左子树节点权值都小于p
。
(1)若 a[a[p].l].size +1<=k && k<=a[a[p].l]].size + a[p].cnt
,则排名k
的节点就是当前节点p
。
(2)若k <= a[a[p].l].size
,则排名k
的节点在左子树中。
(3)若k > a[a[p].l]].size + a[p].cnt
,则排名k
的节点子在右子树中,相当于在右子树中找排名为k-(a[a[p].l]].size + a[p].cnt)
的节点。
在插入删除的时候,都需要从下往上更新size
,一般采用递归的形式,以便于在回溯的时候更新size
。
int GetRank(int p,int k) //在根节点p的子树中,查找排名为k的元素
{
if(a[a[p].l].size+1<=k && k<=a[a[p].l].size+a[p].cnt) return a[p].val;
if(k<=a[a[p].l].size) return GetRank(a[p].l,k);
return GetRank(a[p].r,k-(a[a[p].l].size + a[p].cnt));
}
6. 求元素x的排名
求元素x
的排名,类似于查找元素x
的位置,在查找的过程中,统计比x
小的元素个数。
设当前访问节点为p
:
(1)若x==a[p].val
,则比x
小的元素有a[a[p].l].size
个。
(2)若x<a[p].val
,则在左子树继续找。
(3)若x>a[p].val
,则在右子树继续找,且比x
小的元素有a[a[p].l].size+a[p].cnt
个。
int Getx_Rank(int p,int x) //当前访问节点p,查找x的排名
{
if(p==0) return 0;
if(x==a[p].val) return a[a[p].l].size+1; //排名+1
if(x<a[p].val) return Getx_Rank(a[p].l,x);
return Getx_Rank(a[p].r,x)+a[a[p].l].size+a[p].cnt;
}
Treap
通过旋转,在维持权值满足BST
性质之外,还能使节点的优先级满足大根堆的性质。
Treap
的树的深度期望是O(logN)
,所以各个操作的期望时间复杂度为O(logN)
。
样例
普通平衡树
【题目描述】
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入
x
数 - 删除
x
数(若有多个相同的数,因只删除一个) - 查询
x
数的排名(若有多个相同的数,因输出最小的排名) - 查询排名为
x
的数 - 求
x
的前驱(前驱定义为小于x
,且最大的数) - 求
x
的后继(后继定义为大于x
,且最小的数)
(ps
:排名指的是从小到大排,且若有两个数相同则两个数均占名额。执行3
操作时,输出排名最靠前的x
数的排名)
【输入】
第一行为n
,表示操作的个数,下面n
行每行有两个数opt
和x
,opt
表示操作的序号(1<=opt<=6
)
【输出】
对于操作3,4,5,6
每行输出一个数,表示对应答案
【样例输入】
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
【样例输出】
106465 84185 492737
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
struct Treap
{
int l,r; //左右子节点下标
int val; //权值
int pr; //优先级
int cnt; //重复个数
int size; //子树大小
}a[N];
int n,tot,root=1,INF=0x3fffffff;
void Update(int p) //更新子树大小
{
a[p].size=a[a[p].l].size+a[a[p].r].size+a[p].cnt;
}
void Zig(int &p) // 右旋,p的左子节点变为p的父节点
{
int q=a[p].l; //p的左子节点为q
a[p].l=a[q].r; //q的右子树变为p的左子树
a[q].r=p; //q变为p的父节点
p=q; //引用,p是其父节点的l或r的别名,指向q
Update(a[p].r);Update(p); //改变结构后,更新子树大小
}
void Zag(int &p) //左旋,p的右子节点变为p的父节点
{
int q=a[p].r; //p的右子节点为q
a[p].r=a[q].l; //q的左子树变为p的右子树
a[q].l=p; //q变为p的父节点
p=q; //引用,p是其父节点的l或r的别名,指向q
Update(a[p].l);Update(p); //改变结构后,更新子树大小
}
void Insert(int &p,int x) //插入
{
if(x==a[p].val) //存在权值为x的节点
{
a[p].cnt++; //数量+1
Update(p);
return;
}
if(p==0) //添加节点
{
a[++tot].val=x;
a[tot].pr=rand(); //rand()随机函数,优先级为随机值
a[tot].cnt=1;
a[tot].size=1;
p=tot; //引用,p是其父节点l或r的别名,指向tot
return;
}
if(x<a[p].val)
{
Insert(a[p].l,x); //递归插入
if(a[p].pr<a[a[p].l].pr) Zig(p); //回溯,不满足堆的性质,右旋
}
if(x>a[p].val)
{
Insert(a[p].r,x); //递归插入
if(a[p].pr<a[a[p].r].pr) Zag(p);//回溯,不满足堆的性质,左旋
}
Update(p); //回溯,更新子树大小
}
void Remove(int &p,int x) //删除权值x
{
if(p==0) return; //不存在
if(x==a[p].val)
{
if(a[p].cnt>1) //有重复元素,减少个数
{
a[p].cnt--;
Update(p); //更新子树大小
return;
}
if(a[p].l||a[p].r) //非叶子节点
{
if(a[a[p].l].pr>a[a[p].r].pr) Zig(p),Remove(a[p].r,x); //右旋
else Zag(p),Remove(a[p].l,x); //左旋
a[p].size=a[a[p].l].size+a[a[p].r].size+a[p].cnt; //回溯更新子树大小
}
else //叶子节点
p=0; //引用,p是父节点l或r的别名,指向0,即删除
return;
}
if(x<a[p].val) Remove(a[p].l,x);
if(x>a[p].val) Remove(a[p].r,x);
Update(p); //回溯更新子树大小
}
int Getpre(int x) //前驱
{
int ans=-INF;
int p=root;
while(p)
{
if(x==a[p].val) //找到相等
{
if(a[p].l)
{
p=a[p].l;
while(a[p].r) p=a[p].r;
ans=a[p].val;
}
break;
}
if(a[p].val<x&&a[p].val>ans) ans=a[p].val; //更新最优解
if(a[p].val<x) p=a[p].r; //选择合适的方向继续找
else p=a[p].l;
}
return ans;
}
int Getnxt(int x) //后驱
{
int ans=INF;
int p=root;
while(p)
{
if(x==a[p].val) //找到相等
{
if(a[p].r)
{
p=a[p].r;
while(a[p].l) p=a[p].l;
ans=a[p].val;
}
break;
}
if(a[p].val>x&&a[p].val<ans) ans=a[p].val; //更新最优解
if(a[p].val<x) p=a[p].r; //选择合适的方向继续找
else p=a[p].l;
}
return ans;
}
int Getx_Rank(int p,int x) //在根节点p的子树中,查找x的排名
{
if(p==0) return 0;
if(x==a[p].val) return a[a[p].l].size+1; //排名+1
if(x<a[p].val) return Getx_Rank(a[p].l,x);
return Getx_Rank(a[p].r,x)+a[a[p].l].size+a[p].cnt;
}
int GetRank(int p,int k) //在根节点p的子树中,查找排名为k的元素
{
if(a[a[p].l].size+1<=k && k<=a[a[p].l].size+a[p].cnt) return a[p].val;
if(k<=a[a[p].l].size) return GetRank(a[p].l,k);
return GetRank(a[p].r,k-(a[a[p].l].size + a[p].cnt));
}
int main()
{
a[++tot].val=INF; //添加一个超级根节点,方便操作
a[tot].pr=INF;
scanf("%d",&n);
int x,opt;
while(n--)
{
scanf("%d%d",&opt,&x);
if(opt==1) Insert(root,x);
else if(opt==2) Remove(root,x);
else if(opt==3) printf("%d\n",Getx_Rank(root,x));
else if(opt==4) printf("%d\n",GetRank(root,x));
else if(opt==5) printf("%d\n",Getpre(x));
else if(opt==6) printf("%d\n",Getnxt(x));
}
return 0;
}