排序二叉树:它是这样的一棵树,根的左儿子的键值小于根节点的键值,右儿子的键值大于根节点的键值,并且他的子树也满足这样的性质。为什么会叫它排序二叉树呢,因为如果按照中序排序,那么结果是一个递增的序列。可以证明的是,在一颗排序二叉树中查找、插入和删除的算法复杂度都是树的高度,但是由于排序二叉树的建树和建树过程中选择的序列顺序有关,那么建树的好坏会直接影响其结构的效率,理论上数学期望为O(logn),最坏为O(n),即为一条链的情况,所以如何建树,和在操作过程中调整树的结构式尤其重要的。
下图是二叉树的一个图例:
C++中的STL就可以set,map,multiset等就是BST(排序二叉树)的实现,但是由于过度封装,只能支持一些简单的操作
平衡二叉树:即二叉树的一个特殊形态,根节点的左右子树深度不会超过1,平衡二叉树有很多实现结构,如红黑树,伸展树,treap。在这里我们介绍的是treap。
简单的说treap是这样的:一个节点有两个值,一个是键值,一个是优先级。对于键值而言这个二叉树是排序二叉树,对于优先级而言,这个是树是堆(最大堆,记根节点优先级最大),不难证明,如果每个节点的优先级提前给定,那么这个堆也就确定了,即二叉树也就确定了,那么我们随机给每个接地啊一个优先级,因此每个节点的插入操作也是随机的,那么由于二叉树的插入操作的数学期望为O(logn),那么我们所得到的的BST对于插入,删除和查找的复杂度期望为O(logn)。实际表现也是不错的。节点定义如下:
struct node{
node *ch[2];//左右儿子
int r;//优先级
int v;//键值
int cmp(int x) const{
if(x==v)return -1;
return x<v?0:1;
}
}
在treap的操作中常常要用到树的旋转,如下图:
向右我们称之为右旋,反之为左旋。
代码如下:
//d代表旋转,0是左旋,1是右旋
void rotate(node* &o,int d){
node *k=o->ch[d^1];
o->ch[d^1]=k->ch[d];
k->ch[d]=o;
o=k;
}
插入节点时,首先随机给节点一个优先级,然后执行普通的排序二叉树插入算法(根据键值大小判断节点应该插到那个子树中去)。执行完插入操作后,利用左右旋转让这个节点往上走,从而保持堆的性质。
代码如下:
void insert(node* &o,int x){
if(o==NULL){
o=new node();
o->ch[0]=o->ch[1]=NULL;
o-v=x;
o-r=rand();
}else{
int d=o->cmp(x);
insert(o->ch[d],x);
if(o->ch[d]->r>o->r)rotate(o,d^1);
}
}
删除节点时,首先找到该节点,如果它只有一颗子树,那么只要把这个子树的根节点代替这个待删除的节点即可。但是如果o有两颗子树,那么我们需要把优先级高的一颗子树旋转到根,然后递归地在另一颗子树中删除节点o。
代码如下:
void remove(node* &o,int x){
int d=o->cmp(x);
if(d==-1){
if(o->ch[0]==NULL)o=o->ch[1];
else if(o->ch[1]==NULL)o=o->ch[0];
else{
int d2=(o->ch[0]->r>o-ch[1]->r?1:0);
rotate(o,d2);remove(o->ch[d2],x);
}
}else remove(o->ch[d],x);
}
查找就很简单啦,即是普通的排序二叉树查找
代码如下:
int find(node* o,int x){
while(o!=NULL){
int d=o->cmp(x);
if(d==-1)return 1;
else o=o->ch[d];
}
return 0;
}
利用treap可以做很多事情,如实现名次树(名次树中每个节点均有一个size域,表示以它为根的节点总数)。
名次树支持两种操作:
1)Kth(k)找出第k小的元素
2)Rank(k)值x的名次,即比x小的节点个数+1(表现为以节点x为根的节点总数)。
实现方法如下:treap新增一个成员变量size;
需要额外编写一个maintain函数
void maintain(node* o){
o->s=1;
if(o->ch[0]!=NULL)o->s+=o->ch[0]->s;
if(o->ch[1]!=NULL)o->s+=o->ch[1]->s;
}