替罪羊树
Posted alessandro
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了替罪羊树相关的知识,希望对你有一定的参考价值。
前篇
替罪羊树真的好简单的说,还不需要记怎么旋转之类的。代码也短,才100多行就可以轻松实现
替罪羊树是一种平衡树,支持插入,删除,查找第k小元素,查找元素的排名等操作
替罪羊树就是一种暴力平衡树,旋转?不存在的!
替罪羊树
替罪羊树保持平衡的方法就是暴力重构,即当树不平衡时拍扁重新建树,那么如何才能知道一棵树是否平衡呢?
在替罪羊树中选用了一种叫做平衡因子的东西,听起来很高端,其实就是一个0.5~1之间的任意浮点数,保持平衡的方法是这样的:
如果一棵树的左子树/右子树的存在的节点数量大于这棵树的存在的节点数量( imes)旋转因子,那么就要重构这棵树
为什么我特意标出了是存在的节点数呢?是因为替罪羊树的删除不是真正的删除,而是惰性删除。
所以我们就可以写出代表替罪羊树的每个节点的结构体
const double alpha = 0.75; //旋转因子
struct Node {
Node* ch[2]; //左右子节点
int key,siz,cover; //key是值,siz是以该节点为根的树的存在的节点数,cover是所有节点数量
bool exist; //exist标志该节点是否被删除
void pushup() { //更新函数
this->siz=ch[0]->siz+ch[1]->siz+(int)exist;
this->cover=ch[0]->cover+ch[1]->cover+1;
}
int isbad() { //判断是否要重构
return (ch[0]->cover>this->cover*alpha+5)||(ch[1]->cover>this->cover*alpha+5);
}
};
内部操作
内部操作指实现操作的函数需要调用的子函数,这些函数不会在主程序中调用
总的来说就只有四种内部操作:新节点,插入,删除,重构
新节点有什么可讲的?不就是一个new Node就行的事吗?
不不不,动态分配内存的速度可以说是非常慢了,所以我们要手写内存池。
Node mempol[maxn]; //内存池
Node *tail; //tail为指向内存池元素的指针
Node *bc[maxn]; //内存回收池(栈)
int bc_top; //内存回收池(栈)顶指针
分配机制很简单,如果bc为空,那么从mempol里获得一个节点的内存,当删除节点时,把节点存入bc,需要时再从*bc中取出即可。
另外还有两个不可缺少的东西,根节点和null节点,为Node*类型。
(null是为了代替NULL的,看到错误就像砸电脑,所以就自己模拟一个空节点)
有了这些东西,新节点操作也就不难写出:
Node* newnode(int key) { //返回一个新节点
Node* p=bc_top?bc[--bc_top]:tail++; //分配内存
p->ch[0]=p->ch[1]=null;
p->cover=p->siz=p->exist=1;
p->key=key;
return p;
}
接下来就是插入操作了,插入操作和二叉搜索树是一样的,如果当前节点小于等于要插入的节点权值,那么递归操作左子树,反之递归操作右子树
同时维护siz和cover
Node** insert(Node*& p,int val) { //返回指向距离根节点最近的一棵不平衡的子树的指针
if(p==null) {
p=newnode(val);
return &null;
} else {
p->siz++,p->cover++; //维护节点数
Node** res=insert(p->ch[val>=p->key],val);
if(p->isbad()) res=&p;
return res;
}
}
这里插入也是有返回值的,返回的是指向距离根节点最近的一棵不平衡的子树的指针,因为我们定义的树是指针实现的,所以这个地方要用到指向指针的指针,即双重指针。那么返回这么个拗口的东西又有什么用呢?前面有说过重构平衡树的方法,找到距离根节点最近的不平衡的子树就是为了能够一遍将树重构成平衡树,而不用多遍。
删除比较简单,因为不用像其他树一样真正的删除然后旋转,替罪羊树的删除操作只用将exist置为false然后更新siz值即可
还有一点就是这里的删除操作并不是删除值为k的节点,而是删除第k小的节点。
求第k小的节点要用到siz,根据二叉搜索树的性质,我们可以知道一个节点的左子树的值都要小于这个节点,所以当k小于左子树的节点时递归操作左子树,反之递归操作右子树,同时减去左子树的节点数即可
void erase(Node*& p,int k) {
p->siz--; //维护siz
int offset=p->ch[0]->siz+p->exist; //计算左子树的存在的节点总数
if(p->exist&&k==offset) { //判断当前节点权值是否第k小
p->exist=false; //删除节点
} else {
if(k<=offset) erase(p->ch[0],k); //如果k小于等于offset,递归操作左子树
else erase(p->ch[1],k-offset); //反之递归操作右子树,不要忘记将k减去offset
}
}
插入和删除都说完了,接下来是有些难度的重构操作。
重构需要用到三个函数,第一个是将树转化成有序序列的函数,第二个是建树的函数,而第三个就是将前两个函数连接在一起的函数
转化成序列的函数是怎么做到有序的呢?别忘了替罪羊树也是一颗二叉搜索树,所以我们可以跑一边中序遍历,把结果存入vector中,序列就一定有序
前面说过的删除标记exist在这里起到了作用,把树拍扁的同时判断节点是否存在,如果不存在直接扔到内存回收池中就好
所以说替罪羊树的删除操作只在重构操作中执行,erase函数只是惰性删除而已
void travel(Node* p,vector<Node*>& x) { //将一棵树转化成序列,保存在vector中
if(p==null) return; //如果是空树则退出
travel(p->ch[0],x); //递归操作左子树
if(p->exist) x.push_back(p); //如果该节点存在则放入序列中
else bc[bc_top++]=p; //回收内存,将不用的节点扔到内存回收池(栈)中
travel(p->ch[1],x); //递归操作右子树
}
建树函数就不用说啦,这个是基本操作吧,取序列的中间值作为树的根,然后递归操作左子树和右子树即可,显然能保证平衡
Node* divide(vector<Node*>& x,int l,int r) { //返回建好的树
if(l>=r) return null; //序列为空不用建树
int mid=(l+r)>>1;
Node* p=x[mid]; //mid保证平衡
p->ch[0]=divide(x,l,mid); //递归操作
p->ch[1]=divide(x,mid+1,r); //递归操作
p->pushup(); //维护节点信息
return p;
}
接下来用一个函数把前两个函数连接,就是一个完整的重构函数了
void rebuild(Node*& p) {
static vector<Node*> v;
v.clear();
travel(p,v); //拍扁
p=divide(v,0,v.size()); //建树
}
至此所有内部函数已经全部实现,替罪羊树的全部操作函数可以由以上函数完成。
操作函数
哈哈,想不到吧,接下来才是实现替罪羊树操作的真正函数
有了上面的内部函数,操作函数的实现就显得非常简单
初始化
首先是初始化函数,其实没有什么可以初始化的,只是定义了一下空节点和根节点而已
把初始化函数放到构造函数中可以自动调用
void init() {
tail=mempol; //tail指向内存池的第一个元素
null=tail++; //为null指针分配内存
null->ch[0]=null->ch[1]=null; //null的两个儿子也是null
null->cover=null->siz=null->key=0; //null的所有标记都是0
root=null; //初始化根节点
bc_top=0; //清空栈
}
STree() {
init();
}
插入
插入函数的实现只有两行,调用内部插入函数,然后判断是否需要重建,如果需要调用重建函数即可
void insert(int val) {
Node** res=insert(root,val);
if(*res!=null) rebuild(*res);
}
求值为val的节点的名次
和内部删除的算法差不多,但是因为只是求rank而不是删除,所以没必要用递归函数,用循环实现即可
int rank(int val) {
Node* now=root;
int ans=1;
while(now!=null) {
if(now->key>=val) now=now->ch[0];
else {
ans+=now->ch[0]->siz+now->exist;
now=now->ch[1];
}
}
return ans;
}
求第k小元素
由判断值变成判断名次,根据siz判断即可,思想和求rank差不多,用循环可以解决
int kth(int val) {
Node* now=root;
while(now!=null) {
if(now->ch[0]->siz+1==val&&now->exist) return now->key;
else if(now->ch[0]->siz>=val) now=now->ch[0];
else val-=now->ch[0]->siz+now->exist,now=now->ch[1];
}
}
删除
删除操作分两种,删除第k小和删除值为val的元素,但不是只删除就行的。
为了不让无用的节点过多,我们可以在有用节点与全部节点的比值小于平衡因子的的情况时重构整棵树,以提高效率。
void erase(int k) { //删除值为k的元素
erase(root,rank(k));
if(root->siz<root->cover*alpha) rebuild(root);
}
void erase_kth(int k) { //删除第k小
erase(root,k);
if(root->siz<root->cover*alpha) rebuild(root);
}
模板
#include <cstdio>
#include <vector>
using std::vector;
namespace Scapegoat_Tree {
const int maxn = 100000 + 10;
const double alpha = 0.75; //旋转因子
struct Node {
Node* ch[2]; //左右子节点
int key,siz,cover; //key是值,siz是以该节点为根的树的存在的节点数,cover是所有节点数量
bool exist; //exist标志该节点是否被删除
void pushup() { //更新函数
this->siz=ch[0]->siz+ch[1]->siz+(int)exist;
this->cover=ch[0]->cover+ch[1]->cover+1;
}
int isbad() { //判断是否要重构
return (ch[0]->cover>this->cover*alpha+5)||(ch[1]->cover>this->cover*alpha+5);
}
};
struct STree {
protected:
Node mempol[maxn]; //内存池
Node *tail,*null,*root; //tail为指向内存池元素的指针
Node *bc[maxn]; //内存回收池(栈)
int bc_top; //内存回收池(栈)顶指针
Node* newnode(int key) {
Node* p=bc_top?bc[--bc_top]:tail++;
p->ch[0]=p->ch[1]=null;
p->cover=p->siz=p->exist=1;
p->key=key;
return p;
}
void travel(Node* p,vector<Node*>& x) { //将一棵树转化成序列,保存在vector中
if(p==null) return; //如果是空树则退出
travel(p->ch[0],x); //递归操作左子树
if(p->exist) x.push_back(p); //如果该节点存在则放入序列中
else bc[bc_top++]=p; //回收内存,将不用的节点扔到内存回收池(栈)中
travel(p->ch[1],x); //递归操作右子树
}
Node* divide(vector<Node*>& x,int l,int r) { //返回建好的树
if(l>=r) return null; //序列为空不用建树
int mid=(l+r)>>1;
Node* p=x[mid]; //mid保证平衡
p->ch[0]=divide(x,l,mid); //递归操作
p->ch[1]=divide(x,mid+1,r); //递归操作
p->pushup(); //维护节点信息
return p;
}
void rebuild(Node*& p) {
static vector<Node*> v;
v.clear();
travel(p,v); //拍扁
p=divide(v,0,v.size()); //建树
}
Node** insert(Node*& p,int val) { //返回指向距离根节点最近的一棵不平衡的子树的指针
if(p==null) {
p=newnode(val);
return &null;
} else {
p->siz++,p->cover++; //维护节点数
Node** res=insert(p->ch[val>=p->key],val);
if(p->isbad()) res=&p;
return res;
}
}
void erase(Node*& p,int k) {
p->siz--; //维护siz
int offset=p->ch[0]->siz+p->exist; //计算左子树的存在的节点总数
if(p->exist&&k==offset) { //判断当前节点权值是否第k小
p->exist=false; //删除节点
} else {
if(k<=offset) erase(p->ch[0],k); //如果k小于等于offset,递归操作左子树
else erase(p->ch[1],k-offset); //反之递归操作右子树
}
}
void iod(Node* p) {
if(p!=null) {
iod(p->ch[0]);
printf("%d ",p->key);
iod(p->ch[1]);
}
}
public:
void init() {
tail=mempol; //tail指向内存池的第一个元素
null=tail++; //为null指针分配内存
null->ch[0]=null->ch[1]=null; //null的两个儿子也是null
null->cover=null->siz=null->key=0; //null的所有标记都是0
root=null; //初始化根节点
bc_top=0; //清空栈
}
STree() {
init();
}
void insert(int val) {
Node** res=insert(root,val);
if(*res!=null) rebuild(*res);
}
int rank(int val) {
Node* now=root;
int ans=1;
while(now!=null) {
if(now->key>=val) now=now->ch[0];
else {
ans+=now->ch[0]->siz+now->exist;
now=now->ch[1];
}
}
return ans;
}
int kth(int val) {
Node* now=root;
while(now!=null) {
if(now->ch[0]->siz+1==val&&now->exist) return now->key;
else if(now->ch[0]->siz>=val) now=now->ch[0];
else val-=now->ch[0]->siz+now->exist,now=now->ch[1];
}
}
void erase(int k) { //删除值为k的元素
erase(root,rank(k));
if(root->siz<root->cover*alpha) rebuild(root);
}
void erase_kth(int k) { //删除第k小
erase(root,k);
if(root->siz<root->cover*alpha) rebuild(root);
}
void iod() { //调试用的中序遍历
Node* p=root;
iod(p);
}
};
}
using namespace Scapegoat_Tree;
STree st;
int main(int argc,char** argv) {
int n,opt,que;
scanf("%d",&n);
while(n--) {
scanf("%d%d",&opt,&que);
if(opt==1) st.insert(que);
if(opt==2) st.erase(que);
if(opt==3) printf("%d
",st.rank(que));
if(opt==4) printf("%d
",st.kth(que));
if(opt==5) printf("%d
",st.kth(st.rank(que)-1));
if(opt==6) printf("%d
",st.kth(st.rank(que+1)));
if(opt==7) st.iod();
}
return 0;
}
P3369 普通平衡树
转载:https://www.cnblogs.com/beilili/p/8722721.html
以上是关于替罪羊树的主要内容,如果未能解决你的问题,请参考以下文章
平衡树合集(Treap,Splay,替罪羊,FHQ Treap)