treap是排序二叉树的一种改进,因为排序二叉树有可能会造成链状结构的时候复杂度变成O(n^2)所以通过随机一个优先级的方法来维持每次让优先级最大的作为树根,然后形成一个满足:
A. 节点中的key满足排序二叉树(二叉查找树)
B. 节点中的“优先级”满足大顶堆。
可以证明通过这种方法维持的插入、删除、查找的期望时间复杂度为O(logn)
一、节点的定义:左右孩子用指针数组的形式储存
1 struct Node{ 2 Node*ch[2];//左右子树 3 int r;//优先值 4 int v;//值 5 int cmp(int x) const{ 6 if(x==v)return -1; 7 return x<v?0:1;//0在ch[0]中正好是左孩子,对应向左搜索和操作,1在ch[1]中是右孩子,对应向右搜索和操作 8 } 9 };
二、旋转:和大顶堆的调整方法类似,左旋和右旋,用于对优先级的堆排序,这里不会影响二叉排序树的性质,介绍一种位运算可以巧妙的进行左旋和右旋的选择——异或,相同为0,不同为1,那么x^1就是对x取反的操作。
现在以下图为例
o 表示的指向根节点的指针
有旋转的代码如下:
1 //左旋代码 2 k = o->ch[0]; 3 o->ch[0] = k->ch[1]; 4 k->ch[1] = o; 5 o = k; 6 //右旋代码 7 k = o->ch[1]; 8 o->ch[1] = k->ch[0]; 9 k->ch[0] = o; 10 o = k; 11 //两种代码可以写成一种,利用异或 12 void rotate(Node* &o,int d){//d = 0 左旋,d = 1 右旋 13 Node *k = o->ch[d^1]; 14 o->ch[d^1] = k->ch[d]; 15 k->ch[d] = o; 16 o = k; 17 }
三、插入: 在插入的时候除了要在满足排序二叉树的插入要求,即递归的操作之外,还要满足堆的相应操作,所以,要通过旋转来实现,下面是代码:
1 srand(time(NULL)); 2 void insert(Node *&o, int x){ 3 if(o==NULL){ 4 o = new Node(); 5 o->ch[0] = o->ch[1] = NULL; 6 o->v = x; 7 o->r = rand(); 8 } 9 else { 10 int d = o->cmp(x);//如果要插入的值x比当前根节点的值小则d==0,向左子树寻找,这里可以看出定义指针数组的好处 11 insert(o->ch[d],x); 12 if(o->ch[d]->r > o->r) rotate(o,d^1);//左孩子权值大右旋,右孩子权值大左旋 13 } 14 }
四、删除:如果待删除结点为叶子结点或只有一棵子树,则用其子树(可能为空)替代它即可。如果有两棵子树,则将孩子中优先级高的旋转到根,然后在另一棵子树中递归删除目标点。
代码如下:
1 void remove(Node *&o,int x){ 2 int d = o->cmp(x); 3 if(d==-1){//找到了 4 if(o->ch[0] == NULL) o = o->ch[1]; 5 else if(o->ch[1] == NULL) o = o->ch[0]; 6 else { 7 int d2 = ((o->ch[0]->r)>(o->ch[1]->r)?1:0);//删除节点的左孩子权值大右旋,右孩子权值大左旋 8 rotate(o,d2); 9 remove(o->ch[d2],x); 10 } 11 } 12 else remove(o->ch[d],x); 13 }
五、查找:在插入和查找前进行查找,防止特殊情况,代码如下
1 int find(Node*o, int x){ 2 while(o!=NULL){ 3 int d = o->cmp(x); 4 if(d==-1) return 1;//找到了 5 else o = o->ch[d]; 6 } 7 return 0;//不存在 8 }
六、应用,这个数据结构可以用来解决二叉排序树的超时问题