Treap图文详解、效率分析与拓展应用——清华大学计算机系 郭家宝
先给出我自己的一份Treap的代码——传送门
一、什么是 Treap
Treap=Tree+HeapTreap=Tree+HeapTreap=Tree+Heap
TreapTreapTreap是一种平衡树
TreapTreapTreap发音为[tri:p]
这个单词的构造选取了TreeTreeTree(树)的前两个字符和HeapHeapHeap(堆)的后三个字符,Treap=Tree+HeapTreap=Tree+HeapTreap=Tree+Heap
顾名思义
TreapTreapTreap把BSTBSTBST和HeapHeapHeap结合了起来
它和BSTBSTBST一样满足许多优美的性质
而引入堆目的就是为了维护平衡。
TreapTreapTreap在BSTBSTBST的基础上添加了一个修正值
在满足BSTBSTBST性质的基础上
TreapTreapTreap节点的修正值还满足最小堆性质
最小堆性质可以被描述为每个子树根节点都小于等于其子节点
于是,TreapTreapTreap可以定义为有以下性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值,而且它的根节点的修正值小于等于左子树根节点的修正值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值,而且它的根节点的修正值小于等于右子树根节点的修正值;
- 它的左、右子树也分别为TreapTreapTreap。
图555为一个TreapTreapTreap
修正值是节点在插入到TreapTreapTreap中时随机生成的一个值
它与节点的值无关
下述代码给出了TreapTreapTreap的一般定义。
struct Treap_Node {Treap_Node *left, *right; //节点的左右子树的指针int value, fix; //节点的值和修正值
};
修正值全部满足最大堆性质也是可以的,在本文的介绍中,修正值全部是满足最小堆性质的。
为什么平衡
我们发现,BSTBSTBST会遇到不平衡的原因是因为有序的数据会使查找的路径退化成链
而随机的数据使BSTBSTBST退化的概率是非常小的
在TreapTreapTreap中,修正值的引入恰恰是使树的结构不仅仅取决于节点的值,还取决于修正值的值
然而修正值的值是随机生成的
出现有序的随机序列是小概率事件
所以TreapTreapTreap的结构是趋向于随机平衡的。
二、如何构建 Treap
旋转
为了使TreapTreapTreap中的节点同时满足BSTBSTBST性质和最小堆性质
不可避免地要对其结构进行调整
调整方式被称为旋转
在维护TreapTreapTreap的过程中,只有两种旋转
分别是左旋转(简称左旋)和右旋转(简称右旋)
旋转是相对于子树而言的
左旋和右旋的命名体现了旋转的一条性质:
旋转的性质1
左旋一个子树,会把它的根节点旋转到根的左子树位置,同时根节点的右子节点成为子树的根
右旋一个子树,会把它的根节点旋转到根的右子树位置,同时根节点的左子节点成为子树的根
如图666所示,我们可以从图中清晰地看出左旋后的根节点降到了左子树,右旋后根节点降到了右子树
而且仍然满足BSTBSTBST性质,于是有:
旋转的性质2
对子树旋转后,子树仍然满足BSTBSTBST性质。
利用旋转的两条重要性质
我们可以来改变树的结构
实际上我们恰恰是通过旋转使TreapTreapTreap节点之间满足堆序。
如图777所示的左边的一个TreapTreapTreap,它仍然满足BSTBSTBST性质,但是由于某些原因,节点444和节点222之间不满足最小堆序,444作为222的父节点,它的修正值大于左子节点的修正值
我们只有将222变成444的父节点,才能维护堆序
根据旋转的性质我们可以知道,由于222是444的左子节点,为了使222成为444的父节点,我们需要把以444为根的子树右旋
右旋后,222成为了444的父节点,满足堆序。
由此我们可以总结出,旋转的意义在于:
旋转可以使不满足堆序的两个节点通过调整位置,重新满足堆序,而不改变BSTBSTBST性质。
下述代码给出了两种旋转的实现。
void Treap_Left_Rotate(Treap_Node *&a) {//左旋 节点指针一定要传递引用Treap_Node *b = a->right;a->right = b->left;b->left = a;a = b;
}
void Treap_Right_Rotate(Treap_Node *&a) {//右旋 节点指针一定要传递引用Treap_Node *b = a->left;a->left = b->right;b->right = a;a = b;
}
遍历和查找
像大多数平衡树一样
在TreapTreapTreap中查找和遍历不会改变TreapTreapTreap的结构
所以在TreapTreapTreap中查找和遍历的方法与基本的二叉查找树完全相同
具体方法参见二叉查找树
插入
在TreapTreapTreap中插入元素与在BSTBSTBST中插入方法相似
首先找到合适的插入位置
然后建立新的节点,存储元素
但是要注意建立新的节点的过程中
会随机地生成一个修正值
这个值可能会破坏堆序
因此我们要根据需要进行恰当的旋转
具体方法如下:
- 从根节点开始插入;
- 如果要插入的值小于等于当前节点的值,在当前节点的左子树中插入,插入后如果左子节点的修正值小于当前节点的修正值,对当前节点进行右旋;
- 如果要插入的值大于当前节点的值,在当前节点的右子树中插入,插入后如果右子节点的修正值小于当前节点的修正值,对当前节点进行左旋;
- 如果当前节点为空节点,在此建立新的节点,该节点的值为要插入的值,左右子树为空,插入成功。
举例说明
如图888,在已知的TreapTreapTreap中插入值为444的元素
找到插入的位置后,随机生成的修正值为151515。
新建的节点444与他的父节点333之间不满足堆序
对以节点333为根的子树左旋,如图999。
节点444与其父节点555仍不满足最小堆序
对以节点555为根的子树右旋,如图101010
至此,节点444与其父亲222满足堆序,调整结束。
在TreapTreapTreap中插入元素的期望时间是O(logN)O(logN)O(logN)
下述代码为在TreapTreapTreap中插入一个值为777的元素。
Treap_Node *root;
void Treap_Insert(Treap_Node *&P, int value) {//节点指针一定要传递引用if (!P) {//找到位置,建立节点P = new Treap_Node;P->value = value;P->fix=rand();//生成随机的修正值}else if (value <= P->value) {Treap_Insert(P->left, value);if (P->left->fix < P->fix)Treap_Right_Rotate(P);//左子节点修正值小于当前节点修正值,右旋当前节点}else {Treap_Insert(P->right, value);if (P->right->fix < P->fix)Treap_Left_Rotate(P);//右子节点修正值小于当前节点修正值,左旋当前节点}
}
int main() {Treap_Insert(root, 7);//在 Treap 中插入值为 7 的元素return 0;
}
删除
与BSTBSTBST一样,在TreapTreapTreap中删除元素要考虑多种情况
我们可以按照在BSTBSTBST中删除元素同样的方法来删除TreapTreapTreap中的元素
即用它的后继(或前驱)节点的值代替它,然后删除它的后继(或前驱)节点
为了不使TreapTreapTreap向一边偏沉
我们需要随机地选取是用后继还是前驱代替它
并保证两种选择的概率均等
上述方法期望时间复杂度为O(logN)O(logN)O(logN)
但是这种方法并没有充分利用TreapTreapTreap已有的随机性质
而是重新得随机选取代替节点
我们给出一种更为通用的删除方法,这种方法是基于旋转调整的
首先要在TreapTreapTreap树中找到待删除节点的位置,然后分情况讨论:
情况一,该节点为叶节点或链节点,则该节点是可以直接删除的节点
若该节点有非空子节点,用非空子节点代替该节点的,否则用空节点代替该节点,然后删除该节点。
情况二,该节点有两个非空子节点
我们的策略是通过旋转,使该节点变为可以直接删除的节点。如果该节点的左子节点的修正值小于右子节点的修正值,右旋该节点,使该节点降为右子树的根节点,然后访问右子树的根节点,继续讨论;反之,左旋该节点,使该节点降为左子树根节点,然后访问左子树的根节点,继续讨论,知道变成可以直接删除的节点。
下面给一个删除例子:在TreapTreapTreap中删除值为666的元素
如图111111,首先在TreapTreapTreap中找到666的位置
发现节点666有两个子节点,且左子节点的修正值小于右子节点的修正值,需要右旋节点666,如图121212。
旋转后,节点666仍有两个节点,右子节点修正值较小,于是左旋节点666,如图 131313。
此时,节点666只有一个子节点,可以直接删除,用它的左子节点代替它,删除本身,如图141414。
删除的复杂度稍高,但是期望时间仍为O(logN)O(logN)O(logN)
但是在程序中更容易实现
下述代码给出了后一种(即上述图例中)的删除方法
在给定的TreapTreapTreap中删除值为666的节点。
BST_Node *root;
void Treap_Delete(Treap_Node *&P, int value) {//节点指针要传递引用if (value == P->value) {//找到要删除的节点 对其删除if (!P->right or !P->left) {//情况一,该节点可以直接被删除Treap_Node *t = P;if (!P->right) P = P->left; //用左子节点代替它else P = P->right; //用右子节点代替它delete t; //删除该节点}else {//情况二if (P->left->fix < P->right->fix) {//左子节点修正值较小,右旋Treap_Right_Rotate(P);Treap_Delete(P->right, value);}else {//左子节点修正值较小,左旋Treap_Left_Rotate(P);Treap_Delete(P->left, value);}}}else if (value < P->value) Treap_Delete(P->left, value); //在左子树查找要删除的节点else Treap_Delete(P->right, value); //在右子树查找要删除的节点
}
int main() {Treap_Delete(root, 6);//在Treap中删除值为6的元素return 0;
}
三、为什么要用 Treap
TreapTreapTreap的特点
- TreapTreapTreap简明易懂
TreapTreapTreap只有两种调整方式,左旋和右旋
而且即使没有严密的数学证明和分析
TreapTreapTreap的构造方法,平衡原理也是不难理解的
只要能够理解BSTBSTBST和堆的思想,理解Treap当然不在话下 - TreapTreapTreap易于编写
TreapTreapTreap只需维护一个满足堆序的修正值,修正值一经生成无需修改
相比较其他各种平衡树,TreapTreapTreap拥有最少的调整方式,仅仅两种相互对称的旋转
所以TreapTreapTreap当之无愧是最易于编码调试的一种平衡树 - TreapTreapTreap稳定性佳
TreapTreapTreap的平衡性虽不如AVLAVLAVL、红黑树、SBTSBTSBT等平衡树
但是TreapTreapTreap也不会退化,可以保证期望O(logN)O(logN)O(logN)的深度
TreapTreapTreap的稳定性取决于随机数发生器 - TreapTreapTreap具有严密的数学证明
TreapTreapTreap期望O(logN)O(logN)O(logN)的深度,是有严密的数学证明的
但这不是本文介绍的重点,大多略去 - TreapTreapTreap具有良好的实践效果
各种实际应用中,TreapTreapTreap的稳定性表现得相当出色,没有因为任何的构造出的数据而退化
于是在信息学竞赛中,不少选手习惯于使用TreapTreapTreap,均取得了不俗的表现。
TreapTreapTreap与其他平衡树的比较
与BSTBSTBST相比:
显而易见的,BSTBSTBST更加容易编程实现
对于完全随机的数据,BSTBSTBST会比TreapTreapTreap更快,因为BSTBSTBST没有旋转等操作
但是在实际的应用中,往往会存在大量有序的数据,这时BSTBSTBST会退化,而TreapTreapTreap仍旧能够保持随机的平衡。
与SplaySplaySplay相比:
SplaySplaySplay和BSTBSTBST一样,不需要维护任何附加域,比TreapTreapTreap在空间上有节约
SplaySplaySplay伸展操作中要用到的旋转相对于TreapTreapTreap要稍复杂,编程实现不如TreapTreapTreap容易
而且SplaySplaySplay在查找时也会调整结构,这使得SplaySplaySplay灵活性稍有欠缺
SplaySplaySplay的查找插入删除等基本操作的时间复杂度为均摊O(logN)O(logN)O(logN)而非期望
可以故意构造出使SplaySplaySplay变得很慢的数据,这在信息学竞赛中是很不利的
SplaySplaySplay找到了一个时间、空间和编程效率上的平衡点。
与AVLAVLAVL、红黑树相比:
AVLAVLAVL、红黑树的平衡性是严格的,稳定性表现得十分出色
与TreapTreapTreap一样,它们都要维护附加的域(高度、颜色)来实现平衡
AVLAVLAVL和红黑树在调整的过程中,旋转都是均摊O(1)O(1)O(1)的,而TreapTreapTreap要O(logN)O(logN)O(logN)
与TreapTreapTreap的随机修正值不同,它们维护的附加域要动态的调整,而TreapTreapTreap的随机修正值一经生成不再改变,这一点使得灵活性不如TreapTreapTreap
最重要的是,AVLAVLAVL和红黑树都是时间效率很高的经典算法,在许多专业的应用领域(如STLSTLSTL)有着十分重要的地位。
然而AVLAVLAVL和红黑树的编程实现的难度要比TreapTreapTreap大得多,正是由于过于复杂的编程,使得它们在信息学竞赛中备受冷落。
与SBTSBTSBT相比:
作为平衡树中的新秀,SBTSBTSBT有着能与AVLAVLAVL和红黑树相媲美的严格平衡性,而实现的难度却远小于AVLAVLAVL和红黑树
SBTSBTSBT的平衡附加域是子树的大小,而非其他的“无用”的值
SBTSBTSBT十分简洁高效,灵活性也很优秀
编程实现的难度要稍大于TreapTreapTreap
然而SBTSBTSBT没有受到学术界重视,原因是因为它只是在AVLAVLAVL的基础上进行常数级的优化,而并没有突破性的进展。
表格111为几种平衡树的各种特性的对比。
四、Treap 的更多操作与技巧
查找、插入、删除是平衡树最基本的三种操作,但是在实际的应用中许多其他的操作都是必要的,而且TreapTreapTreap这种强大的数据结构的功能远远不止此,下面我们要讨论的是TreapTreapTreap更多的操作,以及一些技巧。
懒惰删除
基本的删除操作,比起插入和查找要稍有复杂
有时候,我们不愿意再写一段删除的程序代码,于是采用了懒惰删除(lazylazylazy deletiondeletiondeletion)的方法
懒惰删除就是在删除时,仅仅将元素找到后给元素打上“已被删除”的标记,而实际上不把它从平衡树中删除。
这种做法的优点是节约代码量,减少编程时间
但它的缺点也是很严重的:如果插入量和删除量都很大,这种删除方式会在平衡树中留下大量的“废节点”,浪费空间,还影响效率。
而且为了标记节点删除,我们还需要在节点定义中添加一个记录节点是否被删除的域
所以,只有在能够确定平衡树的吞吐量不是很大,或者不同数据的个数有限时,可以使用懒惰删除。
查找最值
在BSTBSTBST的删除中,我们需要通过找待删除节点的后继(或前驱),也就是其右子树的最大值(左子树的最小值)
在平衡树中查找最值也是经常会用到的一种操作,方法很简单。
查找一个子树的最小值,从子树的根开始访问,如果当前节点左子节点非空,访问当前节点的左子节点
如果当前节点左子节点已经为空,那么当前节点的值就是这个子树的最小值。
同理,查找一个子树的最大值,从子树的根开始访问,如果当前节点右子节点非空,访问当前节点的右子节点
如果当前节点右子节点已经为空,那么当前节点的值就是这个子树的最大值。
下述代码为在一个给定的TreapTreapTreap中查找最大值和最小值的非递归实现
Treap_Node *root;
int Treap_FindMin(Treap_Node *P) {while (P->left) P = P->left;//如果有左子节点,访问左子节点return P->value;
}
int Treap_FindMax(Treap_Node *P) {while (P->right) P = P->right;//如果有右子节点,访问右子节点return P->value;
}
int main() {int Min, Max;Min = Treap_FindMin(root);//在 Treap 中查找最小值Max = Treap_FindMax(root);//在 Treap 中查找最大值return 0;
}
前驱与后继
求一个元素在平衡树(或子树)中的前驱,定义为查找该元素在平衡树中不大于该元素的最大元素
相似的,求一个元素在平衡树(或子树)中的后继,定义为查找该元素在平衡树中不小于该元素的最小元素。
从定义中看出,求一个元素在平衡树中的前驱和后继,这个元素不一定是平衡树中的值,而且如果这个元素就是平衡树中的值,那么它的前驱与后继一定是它本身
我们给出求前驱的基本思想:贪心逼近法
在树中查找,一旦遇到一个不大于这个元素的值的节点,更新当前的最优的节点,然后在当前节点的右子树中继续查找,目的是希望能找到一个更接近于这个元素的节点
如果遇到大于这个元素的值的节点,不更新最优值,在当前节点的左子树中继续查找
直到遇到空节点,查找结束,当前最优的节点的值就是要求的前驱
求后继的方法与上述相似,只是要找不小于这个元素的值的节点
下面是具体的算法描述。
求前驱:
- 从根节点开始访问,初始化最优节点为空节点;
- 如果当前节点的值不大于要求前驱的元素的值,更新最有节点为当前节点,访问当前节点的右子节点;
- 如果当前节点的值大于要求前驱的元素的值,访问当前节点的左子节点;
- 如果当前节点是空节点,查找结束,最优节点就是要求的前驱。
求后继:
- 从根节点开始访问,初始化最优节点为空节点;
- 如果当前节点的值不小于要求前驱的元素的值,更新最有节点为当前节点,访问当前节点的左子节点;
- 如果当前节点的值小于要求前驱的元素的值,访问当前节点的右子节点;
- 如果当前节点是空节点,查找结束,最优节点就是要求的后继。
在求前驱和后继的过程中,我们恰恰访问了从根到叶节点的一条完整的路径
由于TreapTreapTreap的深度是O(logN)O(logN)O(logN)的,所以求前驱和后继算法的时间复杂度为O(logN)O(logN)O(logN)
下述代码为在一个已知的TreapTreapTreap中求值为555的元素前驱和后继
Treap_Node *root;
//访问节点 P,查找 value 的前驱,当前最优节点为 optimal
Treap_Node * Treap_pred(Treap_Node *P, int value, Treap_Node *optimal) {if (!P) return optimal; //访问到空节点,返回最优节点,查找结束if (P->value <= value) return Treap_pred(P->right, value, P); //更新最优值,在右子树中继续查找else return Treap_pred(P->left, value, optimal); //左子树中继续查找
}
//访问节点 P,查找 value 的后继,当前最优节点为 optimal
Treap_Node * Treap_succ(Treap_Node *P, int value, Treap_Node *optimal) {if (!P) return optimal; //访问到空节点,返回最优节点,查找结束if (P->value >= value) return Treap_succ(P->left, value, P); //更新最优值,在左子树中继续查找else return Treap_succ(P->right, value, optimal); //在右子树中继续查找
}
int main() {Treap_Node *pred, *succ;pred = Treap_pred(root, 5, 0); //查找 5 在 Treap 中的前驱succ = Treap_succ(root, 5, 0); //查找 5 在 Treap 中的后继return 0;
}
根据前驱和后继的定义,我们还可以以此来查找某个元素与TreapTreapTreap中所有元素绝对值之差最小元素
如果按照数轴上的点来解释的话,就是求一个点的最近距离点
方法就是分别求出该元素的前驱和后继,比较前驱和后继哪个距离基准点最近。
求前驱、后继和距离最近点是许多算法中经常要用到的操作,TreapTreapTreap都能够高效地实现。
合并重复节点
TreapTreapTreap中很可能存在值相同的节点,在某些特殊情况下,重复的节点可能会有很多,但是我们却把它们分别存成一个个节点
我们有一种常数级的优化,把重复的节点合并为一个节点
方法就是在TreapTreapTreap节点中增加一个域,记录相同的这个值的个数,称为节点的权值,记为weightweightweight
在插入时,如果找到了已存在的相同的值,不必再开辟新的节点,只需把已有的节点的权值增加111
删除时,只需把权值减少111,如果权值为000时,才对节点正式删除
这种优化的效果很好,首先是在插入时节省了开辟空间的时间
更好的是在删除时,避免了大量的旋转
当重复的值非常多的时候,这种优化是十分惊人的
下述代码为带重复计数的TreapTreapTreap节点的定义
struct Treap_Node {Treap_Node *left, *right; //节点的左右子树的指针int value, fix, weight; //节点的值和修正值,weight 为权值
};
TreapTreapTreap中元素的类型与排序的规则
到这里为止,上文中提到的TreapTreapTreap中元素的类型,我们都默认为了整数型
但实际上类型并没有限制,只要是能够比较大小的类型,例如浮点数型、字符串型,也可以是复合类型(结构类型、对象类型)
假若我们要实现一种多关键字类型排序的TreapTreapTreap,我们只需自定义大于、小于、等于运算符的意义(运算符重载),使它们有确定的大小关系,这样就可以在不修改 TreapTreapTreap各种操作代码的基础上实现多关键字类型排序的TreapTreapTreap
TreapTreapTreap定义中“左小于中小于右”,仅仅是逻辑上的定义,实际上我们可以以任何有序的规则排序,即使是左大于中大于右,只需要在比较元素大小的函数中修改定义即可
在以上时间复杂度的分析中,我们默认两个元素大小比较时间为O(1)O(1)O(1),但实际上某些复杂的类型间比较大小不是O(1)O(1)O(1)的,如字符串,是O(L)O(L)O(L),LLL为字符串长度
平衡树并不适合作为所有数据类型的数据的有序存储容器,因为可能有些类型的两个元素直接相互比较大小是十分耗时的,这个常数时间的消耗是无法忍受的
例如字符串,作为检索字符串的容器,我们更推荐TrieTrieTrie,而不是平衡树
平衡树仅适合做元素间相互比较时间很少的类型的有序存储容器。
维护子树大小的必要性
TreapTreapTreap是一种排序的数据结构,如果我们想查找第kkk小的元素或者询问某个元素在TreapTreapTreap中从小到大的排名时,我们就必须知道每个子树中节点的个数
我们称以一个子树的所有节点的权值之和,为子树的大小
由于插入、删除、旋转等操作,会使每个子树的大小改变,所以我们必须对子树的大小进行动态的维护。
对于旋转,我们要在旋转后对子节点和根节点分别重新计算其子树的大小。
对于插入,新建立的节点的子树大小为111。在寻找插入的位置时,每经过一个节点,都要先使以它为根的子树的大小增加111,再递归进入子树查找。
对于删除,在寻找待删除节点,递归返回时要把所有的经过的节点的子树的大小减少111。要注意的是,删除之前一定要保证待删除节点存在于 Treap 中。
下述代码为维护子树大小的TreapTreapTreap节点的定义,以及旋转
struct Treap_Node {Treap_Node *left, *right; //节点的左右子树的指针int value, fix, weight, size; //节点的值,修正值,重复计数,子树大小inline int lsize() {return left ? left->size ? 0; } //返回左子树的节点个数inline int rsize() {return right ? right->size ? 0; } //返回右子树的节点个数
};
void Treap_Left_Rotate(Treap_Node *&a) {//左旋 节点指针一定要传递引用Treap_Node *b = a->right;a->right = b->left;b->left = a;a = b;b = a->left;b->size = b->lsize() + b->rsize() + b->weight;a->size = a->lsize() + a->rsize() + a->weight;
}
void Treap_Right_Rotate(Treap_Node *&a) {//右旋 节点指针一定要传递引用Treap_Node *b = a->left;a->size = b->rsize() + a->rsize() + a->weight;b->size = b->lsize() + a->size + b->weight;a->left = b->right;b->right = a;a = b;b = a->left;b->size = b->lsize() + b->rsize() + b->weight;a->size = a->lsize() + a->rsize() + a->weight;
}
查找排名第k的元素
只有当我们维护以每个节点为根的子树的大小,才能查找排名第kkk的元素
根据TreapTreapTreap的一个重要性质,TreapTreapTreap的子树也是TreapTreapTreap,我们可以用分而治之的思想来查找排名第kkk的元素
首先,在一个子树中,根节点的排名取决于其左子树的大小,如果根节点有权值 weightweightweight,则根节点PPP的排名是一个闭区间AAA,且A=[P.left.size+1,P.left.size+P.weight]A=[P.left.size+1,P.left.size+P.weight]A=[P.left.size+1,P.left.size+P.weight]
根据此,我们可以知道,如果查找排名第kkk的元素,k∈Ak∈Ak∈A,则要查找的元素就是PPP所包含元素
如果k<Ak<Ak<A,那么排名第kkk的元素一定在左子树中,且它还一定是左子树的排名第kkk的元素
如果k>Ak>Ak>A,则排名第kkk的元素一定在右子树中,是右子树排名第k−(P.left.size+P.weight)k-(P.left.size+P.weight)k−(P.left.size+P.weight)的元素
根据这种策略,我们可以总结出算法121212:
- 定义PPP为当前访问的节点,从根节点开始访问,查找排名第kkk的元素;
- 若满足P.left.size+1<=k<=P.left.size+P.weightP.left.size+1<=k<=P.left.size+P.weightP.left.size+1<=k<=P.left.size+P.weight,则当前节点包含的元素就是排名第kkk的元素;
- 若满足k<P.left.size+1k<P.left.size+1k<P.left.size+1,则在左子树中查找排名第kkk的元素;
- 若满足k>P.left.size+P.weightk>P.left.size+P.weightk>P.left.size+P.weight,则在右子树中查找排名第k−(P.left.size+P.weight)k-(P.left.size+P.weight)k−(P.left.size+P.weight)的元素。
下述代码为在一个给定的TreapTreapTreap中查找排名第888的元素。
Treap_Node *root;
Treap_Node * Treap_Findkth(Treap_Node *P, int k) {if (k < P.lsize() + 1) //左子树中查找排名第 k 的元素return Treap_Findkth(P->left, k);else if (k > P.lsize() + P.weight) //在右子树中查找排名第 k-(P.lsize() + P.weight)的元素return Treap_Findkth(P->right, k - (P.lsize() + P.weight));else return P; //返回当前节点
}
int main() {Treap_Node *result;result = Treap_Findkth(root, 8); //在 Treap 中查找排名第 8 的元素return 0;
}
根据上述算法,我们还可以实现查找逻辑第kkk大元素,即查找第(root.size−k+1)(root.size-k+1)(root.size−k+1)小元素,root.sizeroot.sizeroot.size为整个TreapTreapTreap的大小
甚至我们可以取代专门写的查找最值的算法
由于查找路径必定是一条子树上的路径,长度不会超过TreapTreapTreap的深度,所以时间复杂度为O(logN)O(logN)O(logN)。
求元素的排名
我们通过排名可以找到对应元素,也希望求出元素在TreapTreapTreap中排名,或者成为求元素的秩
我们规定,如果在TreapTreapTreap中有多个重复的元素,则这个元素的排名为最小的排名
例如1,2,4,4,4,61,2,4,4,4,61,2,4,4,4,6中,444的排名为333
在TreapTreapTreap中求元素的排名的方法与查找第kkk小的数是很相似的,可以近似认为是互为逆运算。
我们的基本思想是查找要求的元素在TreapTreapTreap中的位置,且在查找路径中统计出小于要求的元素的节点的总个数,要求的元素的排名就是总个数+111
算法为:
- 定义PPP为当前访问的节点,curcurcur为当前已知的比要求的元素小的元素个数。从根节点开始查找要求的元素,初始化curcurcur为000;
- 若要求的元素等于当前节点元素,要求的元素的排名为区间[P.left.size+cur+1,P.left.size+cur+weight][P.left.size+cur+1, P.left.size+cur+weight][P.left.size+cur+1,P.left.size+cur+weight]内任意整数;
- 若要求的元素小于当前节点元素,在左子树中查找要求的元素的排名;
- 若要求的元素大于当前节点元素,更新curcurcur为cur+P.left.size+weightcur+P.left.size+weightcur+P.left.size+weight,在右子树中查找要求的元素的排名。
下述代码为在一个已知的TreapTreapTreap中求元素555的排名。
Treap_Node *root;
int Treap_Rank(Treap_Node *P, int value, int cur) {//求元素 value 的排名if (value == P->value) return P->lsize() + cur + 1; //返回元素的排名else if (value < P->value) //在左子树中查找return Treap_Rank(P->left, value, cur);else //在右子树中查找return Treap_Rank(P->right, value, cur + P->lsize() + weight);
}
int main() {int rank;rank = Treap_Rank(root, 8, 0); //在 Treap 中求元素 8 的排名return 0;
}
维护附加关键字
有时候,我们建立TreapTreapTreap维护的顺序关键字并不是我们主要关心的内容,而要关心的是附加关键字。根据不同目的维护的附加关键字的处理方法也不尽相同,下文仅仅以一个例子介绍附加关键字的处理方法。
顺序前缀和
[问题描述]
要求维护一个由二元组构成的序列,使序列中每个元素按第一关键字升序排列
操作包括:添加一个新元素,删除一个已有元素,查询这个序列的第二关键字最大前缀和
例如已知的序列(1,0),(3,−2),(4,−3),(6,3),(7,−1){(1,0), (3,-2), (4,-3), (6,3), (7,-1)}(1,0),(3,−2),(4,−3),(6,3),(7,−1)
有如下几项操作:
添加(3,1)(3,1)(3,1)入序列,添加(1,1)(1,1)(1,1)入序列,从序列中删除(4,−3)(4,-3)(4,−3),查询最大前缀和
第111次操作后,序列变成了(1,0),(3,1),(3,−2),(4,−3),(6,3),(7,−1){(1,0), (3,1), (3,-2),(4,-3), (6,3), (7,-1)}(1,0),(3,1),(3,−2),(4,−3),(6,3),(7,−1)
第222次操作后,序列变成了(1,1),(1,0),(3,1),(3,−2),(4,−3),(6,3),(7,−1){(1,1), (1,0), (3,1), (3,-2), (4,-3), (6,3), (7,-1)}(1,1),(1,0),(3,1),(3,−2),(4,−3),(6,3),(7,−1)
第333次操作后,序列变成了(1,1),(1,0),(3,1),(3,−2),(6,3),(7,−1){(1,1), (1,0), (3,1), (3,-2), (6,3), (7,-1)}(1,1),(1,0),(3,1),(3,−2),(6,3),(7,−1)
此时序列的 i 项前缀的和S[i]=1,1,2,0,3,2S[i]={1,1, 2, 0, 3, 2}S[i]=1,1,2,0,3,2
所以序列的最大前缀和是前555项和,值为333。
解析
由于序列总是要求升序排列,我们可以想到以元素第一关键字升序排列,使用TreapTreapTreap维护。
由于每次要查询的是第二关键字构成的序列的最大前缀和,我们可以容易想到,对于第一关键字相同的元素,第二关键字大的元素应放在前面。
规定排序的顺序之后,我们要考虑如何维护前iii项元素的第二关键字的和(以下简称前 i 项的和)。设每个节点的第一关键字为aaa,第二关键字为bbb,我们要在TreapTreapTreap中的节点添加附加域sumsumsum,表示以该节点为根的子树中所有元素的第二关键字和,以及附加域maxmaxmax,表示以该节点为根的子树中所有元素构成的顺序序列最大的前缀和。
sumsumsum值可以像维护子树的大小sizesizesize值一样的递归地维护,而且旋转时也要重新计算
而对于节点ppp的maxmaxmax值则要分情况讨论:
情况一,当前子树最大前缀的结尾在该节点的左子树,此时p.max=p.left.maxp.max=p.left.maxp.max=p.left.max;
情况二,当前子树最大前缀的结尾恰好是该节点,此时p.max=p.left.sum+p.bp.max=p.left.sum+p.bp.max=p.left.sum+p.b;
情况三,当前子树最大前缀的结尾在该节点的右子树,p.max=p.left.s+p.b+p.right.maxp.max=p.left.s+p.b+p.right.maxp.max=p.left.s+p.b+p.right.max。
在实际的插入和删除过程中,每次旋转后都要重新计算sumsumsum值,然后依次计算旋转后的子节点和根节点的maxmaxmax值
维护好后,每次查询最大前缀和,只需要O(1)O(1)O(1)的时间。
时刻要记住TreapTreapTreap是一种二叉树结构,它具有良好的分治结构,所以在维护各种具体的附加关键字时,二分或三分递推的思想一般都是解决问题的关键。
Treap图文详解、效率分析与拓展应用——清华大学计算机系 郭家宝相关推荐
- 单细胞分析的 Python 包 Scanpy(图文详解)
文章目录 一.安装 二.使用 1.准备工作 2.预处理 过滤低质量细胞样本 3.检测特异性基因 4.主成分分析(Principal component analysis) 5.领域图,聚类图(Neig ...
- 基于深度神经网络的图像分类与训练系统(MATLAB GUI版,代码+图文详解)
摘要:本博客详细介绍了基于深度神经网络的图像分类与训练系统的MATLAB实现代码,包括GUI界面和数据集,可选择模型进行图片分类,支持一键训练神经网络.首先介绍了基于GoogleNet.ResNet进 ...
- python爬虫图片实例-【图文详解】python爬虫实战——5分钟做个图片自动下载器...
我想要(下)的,我现在就要 python爬虫实战--图片自动下载器 之前介绍了那么多基本知识[Python爬虫]入门知识(没看的赶紧去看)大家也估计手痒了.想要实际做个小东西来看看,毕竟: talk ...
- 【图文详解】一文全面彻底搞懂HBase、LevelDB、RocksDB等NoSQL背后的存储原理:LSM-tree日志结构合并树...
LSM 树广泛用于数据存储,例如 RocksDB.Apache AsterixDB.Bigtable.HBase.LevelDB.Apache Accumulo.SQLite4.Tarantool.W ...
- 面渣逆袭:Redis连环五十二问,图文详解,这下面试稳了
大家好,我是老三,面渣逆袭系列继续,这节我们来搞定Redis--不会有人假期玩去了吧?不会吧? 基础 1.说说什么是Redis? Redis是一种基于键值对(key-value)的NoSQL数据库. ...
- 图文详解crond定时任务
第1章crontd的介绍 1.1crond的含义 crond是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,与windows下的计划任务类似,当安装完成操作系统后,默认会安 ...
- Linux下sysstat安装使用图文详解
文章目录 Linux下sysstat安装使用图文详解 1.iostat 2.mpstat 3.sadc 4.sadf 5.sar 6.pidstat Linux下sysstat安装使用图文详解 Sys ...
- 关于分布式存储,这是你应该知道的(图文详解)(关于存储的一些好文转载--1)
转自:http://stor.51cto.com/art/201711/556946.htm 关于分布式存储,这是你应该知道的(图文详解) 前言 分布式存储存在的风险,其实就是因为"共享&q ...
- 兄弟机cnc系统面板图解_数控机床操作面板图文详解
<数控机床操作面板图文详解>由会员分享,可在线阅读,更多相关<数控机床操作面板图文详解(53页珍藏版)>请在人人文库网上搜索. 1.数 控 车 床 编 程 和 操 作(一) 熟 ...
最新文章
- 藤本植物和攀爬植物模型包 Globe Plants – Bundle 23 – Vines and Creepers 03 (3D Models)
- 还原dede数据后系统基本参数空白无显示的解决方法
- SAP传统电商解决方案的技术挑战以及SAP的应对措施
- Putty 重新启动 linux sqlserver服务
- UWP锁、解屏后无法响应操作
- Ubuntu16.04显卡驱动安装和Cuda安装
- 【数据分析】Superset 之三 Docker操作管理
- 数据库工作笔记013---如果存在表则删除表然后创建Mysql_drop table
- cocos2d-x中CCEditbox导出到lua
- Atitit 减少财政支出----普通人如何蹭政府补贴措施 attilax大总结.docx
- 谷歌app使用的是什么字体_如何使用Google字体
- kuangbin专题-简单搜索
- 高校校园网络设计与实现
- 蒙特卡洛模拟 matlab实例,蒙特卡洛模拟的简单例子
- 拉格朗日乘子法、惩罚函数法
- openGL渲染管线流程-顶点着色器,曲面细分着色器,几何着色器,片元着色器顺序
- 微信公众号开发部署服务器
- 4.1EF Core
- 07-HTML5举例:简单的视频播放器
- 一张图读懂一个产业短视频第4期