Treap小解析(Part 1 of 2)

来源:互联网 发布:夜猫子软件 编辑:程序博客网 时间:2024/04/29 12:04

我们先来看一道题:POJ1442

这道题题面很长,大意是给n各数字和m个询问,对于每一个m[i],要从原来n个数字的前m[i]个数字中挑出第i小的数字。

由于题目是先告诉所有数据,再不断询问第x小的数字,那么就可以先给原数组排个序。输入的询问是非递减的,所以从实现的角度来说就是不断往排序好了的数组中继续添加数字,然后查找第i小的元素。我们可以选用二叉排序树来存储数据,因为往里添数据的话时间复杂度只有O(logn)左右,但是标准的二叉排序树有可能退化成一条链,这样插入的时间复杂度就会是O(n)。所以这里我们使用一种更好的建立在排序树之上的解决方案,就是treap。

treap等于tree+heap。也就是说结构都是二叉树,在满足了左孩子小于根节点而右孩子大于根节点的基础上,在每个节点上都增加了一个变量叫做优先级,优先级是用来调整树结构用的。举个例子,比如输入的数是15 5 4,那么排序二叉树是这样建立的:

假设我们已经在每个节点上放了个优先级,再继续假设节点5的优先级高于15,那么我们的treap就做出如下调整:

看过这个例子后我们可以知道两件事:

①。加上了优先级这个东西,并不会破坏原有的排序树的性质。就上例来说,4、5、15本来就在排序树上有好几种写法,调整可以看成是换了种写法。

②。剧透一下,每个节点上的优先级都是随机生成的,所以treap的复杂度叫做"期望复杂度",顾名思义就是treap仍有可能退化成一条链。比如上例的5的优先级没有大于15的话,那就是条链了。不过基本上treap的性能还是挺好的(一条链的意思是每个节点的优先级不断减小,这概率实在是不怎么大吧?)。

先放上结构体,就是在排序树的struct中放个优先级和其他一点小操作就行了

struct node{int val; // 值int pri; // 优先级int size;// 树的大小(左子树数量+右子树数量+1(自身))node *ch[2];node(int v, node *n):val(v) // 初始化结点{ch[0] = ch[1] = n;pri = rand();size = 1;}void calsize()  // 计算树的大小{size = ch[0]->size + ch[1]->size + 1;}};

接下来看看treap要如何操作,它的精华就是旋转,还是以上面的4、5、15为例,我们就先来看看如何旋转。

代码肯定是这样的:

// 这个节点的孩子节点的优先级大于根节点,那么就要旋转,上例来说是左孩子优先级大于根节点

if(node->leftChild->priority > node->priority)

    rot(node, left);

那么再来看看rot()的代码如何来写,上例的旋转叫做“右旋”,还有个叫"左旋",我们画个抽象的图来表达一下:

上图中红色的结点代表了即将旋转到上面去的结点。圆和三角形没有任何区别,纯粹是为了连线上更加便于理解而已。

具体来看一下右旋吧:

其实也就改变了两个指向而已。

void rot() // 假设根节点是t,待旋转结点是y,右旋{node *y = t->ch[0];t->ch[0] = y->ch[1];y->ch[1] = t;t = y;}

最后为什么会有个t = y呢,原因很简单,比如之前的4、5、15,旋转以后的根节点就不是15了而是5了,如果旋转以后不把根节点也更新了当然就出错了。

看到这里希望大家能自行写出左旋的代码,几乎是一样的。

既然左旋右旋代码几乎一样,我们就合并在一个函数里面好了

void rot(node *&t, bool d) // *&是引用,这样在调用rot()的时候就能把形参那边的根节点标成正确的根节点了{node *r = t->ch[d];t->ch[d] = r->ch[!d];r->ch[!d] = t;t->calsize(); // 计算t的子节点数, 先略过r->calsize(); // 计算r的子节点数t = r;}
treap的旋转操作基本上讲完了,treap的代码非常简单,重点就在于理解。

接下来是往treap插入结点、删除结点、查找等等,我将放到下一篇博客中。


0 0