伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(lgN)内完成插入、查找和删除操作。在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。其插入、删除、查找操作基本与二叉搜索树的相同。其唯一的不同之处在于每次的插入、删除、查找操作都需要将其对应的节点通过旋转的方式转变为该树的根节点 (ps: 对于插入操作,是插入相应的节点之后,将其转变为根节点。对于删除操作,其实在找到对应的节点,通过旋转将其转变为根节点之后,才进行删除操作)。
类别:二叉排序树
空间效率:O(n)
时间效率:O(log n)内完成插入、查找、删除操作
创造者:Daniel Sleator和Robert Tarjan
优点:每次查询会调整树的结构,使被查询频率高的条目更靠近树根(一般是将每次查询的节点调整为树根节点)。
树的旋转是splay的基础,对于二叉查找树来说,树的旋转不破坏查找树的结构。
Splaying
Splaying是Splay Tree中的基本操作,为了让被查询的条目更接近树根,Splay Tree使用了树的旋转操作,同时保证二叉排序树的性质不变。
伸展树的实现方式:
伸展树有两种实现的方式,一种是自顶向下的方式,一种是自底向上的方式。
自底向上的实现方式:
Splaying的操作受以下三种因素影响:
节点x是父节点p的左孩子还是右孩子
节点p是不是根节点
节点p是父节点g的左孩子还是右孩子
自底向上的实现方式定义了三种基本操作:
Zig Step
当p为根节点时,进行zip step操作。
当x是p的左孩子时,对x右旋(以P为顶点);
当x是p的右孩子时,对x左旋(以P为顶点)。
ps:zig操作,可以记忆为一次左旋或者一次右旋操作
Zig-Zig Step
当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。
当x和p同为左孩子时,依次将p(以g为顶点)和x右旋(以P为顶点);
当x和p同为右孩子时,依次将p(以g为顶点)和x左旋(以P为顶点)。
ps:zig-zig操作,可以记忆为两次方向相同的旋转操作
zig-zag step
当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。
当p为左孩子,x为右孩子时,将x左旋(以P为顶点)后再右旋(以g为顶点)。
当p为右孩子,x为左孩子时,将x右旋(以P为顶点)后再左旋(以g为顶点)。
ps:zig-zag操作,可以记忆为两次方向不同的旋转操作
下面我们用一个具体的例子示范。我们将从树中搜索节点2:
上面的第一次查询需要n次操作。然而经过一次查询后,2节点成为了根节点,树的深度大减小。整体上看,树的大部分节点深度都减小。此后对各个节点的查询将更有效率。
自顶向下的实现方式:
普通伸展树的伸展操作需要从根沿树往下的一次遍历,以及而后的从底向上的一次遍历,并在此过程中不断的对树进行相应的旋转操作,使得相应的节点称为根节点。这可以通过保存一些父指针来完成,或者通过将访问路径存储到一个栈中来完成。这两种方法均需要大量的开销,这种方式也被称为自底向上的伸展树实现方式。幸运的是,我们可以采用自顶向下的伸展树的实现方式,这个过程产生的开销可以基本忽略不计。
自顶向下方式的伸展树实现,其原理是将一棵树分割成了三棵,之后再进行相应的合并。
具体操作过程是(假设查找节点X):
先建立两个空子树,一个是LeftTreeMax(简称LTMax),另一个是RightTreeMin(简称RTMin)。其中LTMax子树保存遍历过程中所有小于X的节点,并且LTMax在遍历过程中始终指向该子树中最大的那个节点。不难看出LTMax->Right始终为空。RTMin子树保存遍历过程中所有大于X的节点,RTMin在遍历过程中始终指向该子树中最小的那个节点。同样RTMin->Left也始终为空。因此在合并RTMin和LTMax前,RTMin是所在子树上所有大于X节点中那个最小的并接近X的节点,同理LTMax中所在子树中所有小于X节点中那个最大的并接近X的节点。
-
从根节点T开始遍历,查找X节点,直到找到或者未找到
如果X节点小于T并且小于T->Left(LL型)则实行一次围绕T的右单旋转,之后T->Left成为新的根节点T',X又小于T',所以从T'劈开的右子树(包括T',也就是从T'的左子树连接处劈开)上的所有节点都大于X,我们将T'挂在RTMin->Left上,并更新RTMin指向T'节点,T'节点是所有大于X节点中最小的那个。
如果X节点小于T,但T->Left为NULL,则未找到X节点,退出循环并将三个子树合并
如果X节点大于T并且大于T->Right(RR型),则实行一次围绕T的左单旋转,这样T->Right成为新的根节点T',X又大于T',所以从T'劈开的左子树(包括T',也就是从T'的右子树连接处劈开)上所有的节点都小于X,我们将T'子树挂在LTMax->Right上,并将LTMax指向新的T'节点,T'节点就是所有小于X节点中最大的那个。
如果X节点大于T,但T-Right为NULL,则未找到X节点,退出循环将三个子树合并
当找到或者未找到X节点退出循环后,合并三个子树。
ps:需要注意的是,对于rl型和lr型其并不进行旋转操作,而直接对其进行劈开,并将其连接到对应的子树节点上。
此时LTMax是所有小于X节点的最大的那个,所以要将中子树(简称为M)的M->Left挂在LTMax->Right上,将M->Right挂在RTMin->Left上。最后更新M为LTMax和RTMin的根节点。
伸展树的节点类代码:
class Node<T extends Comparable<T>>{
/**
* 对应的键值
*/
T key;
/**
*左孩子节点
*/
Node<T> left;
/**
* 右孩子节点
*/
Node<T> right;
public Node(){
this(null,null,null);
}
public Node(T key){
this(key,null,null);
}
public Node(T key,Node left,Node right){
this.key=key;
this.left=left;
this.right=right;
}
}
核心代码如下:
/**
* @param tree 需要进行伸展操作的树的根节点
* @param key 其需要进行旋转到根节点位子的元素节点的值
* @return 其伸展后树的根节点元素的指针
*/
private Node splay(Node tree,T key){
/*
用于判空排除空指针异常的干扰
*/
if(tree==null||key==null){
return tree;
}
/*
此处采用一个节点node来维护其相应的LeftTreeMax以及RightTreeMin子树部分
其中node.left用于标记RightTreeMin子树
node.right用于标记LeftTreeMax子树
以便于和最后的子树合并操作
*/
Node node =new Node();
//用于标记其左右子树的所有小于key的元素中的最大值以及所有大于key的元素中的最小值
Node leftTreeMax=node;
Node rightTreeMin=node;
while(true){
int cmp=key.compareTo((T) tree.key);
if(cmp<0){
//当不存在需要查找的键的情况下,跳出循环,合并三棵子树
if(tree.left==null){
break;
}
//当其为LL型时,将其进行右旋操作
if(key.compareTo((T)tree.left.key)<0){
tree=rotateRight(tree);
//当树中没有对应的节点时,进行三树合并操作
if(tree.left==null){
break;
}
}
//用于劈开其对应的根节点的左子树,将其拼接到对应的rightTreeMin的左分支上,同时修改对应的中树的节点
rightTreeMin.left=tree;
rightTreeMin=tree;
tree=tree.left;
}
else if(cmp>0){
//当不存在需要查找的键的情况下,跳出循环,合并三棵子树
if(tree.right==null){
break;
}
//当为rr型时,进行左旋操作
if(key.compareTo((T)tree.right.key)>0){
tree=rotateLeft(tree);
if(tree.right==null){
break;
}
}
//用于劈开对应的根节点的右子树,并进行拼接和调整操作
leftTreeMax.right=tree;
leftTreeMax=tree;
tree=tree.right;
}
else{
break;
}
}
//将中树的左子树合并到L树中,将中树的右子树合并到R树中,将中树的左子树合并到L树中
leftTreeMax.right=tree.left;
rightTreeMin.left=tree.right;
//用于合并三棵子树,node.right对应的为L树,node.left对应的为R树
tree.left=node.right;
tree.right=node.left;
return tree;
}
/**
* 用于伸展操作的相应的接口
* @param key
*/
public void splay(T key){
this.root=splay(this.root,key);
}
伸展树的具体代码如下:
/**
* @author 学徒
* 该类用于实现伸展树
*/
public class SplayTree <T extends Comparable<T>>{
//伸展树的根节点
private Node<T> root;
//伸展树的节点的定义
private class Node<T extends Comparable<T>>{
/**
* 对应的键值
*/
T key;
/**
*左孩子节点
*/
Node<T> left;
/**
* 右孩子节点
*/
Node<T> right;
public Node(){
this(null,null,null);
}
public Node(T key){
this(key,null,null);
}
public Node(T key,Node left,Node right){
this.key=key;
this.left=left;
this.right=right;
}
}
/**
* 对某个节点进行相应的右旋操作
* @param node 需要进行右旋操作的节点本身
* @return 旋转后的子树的根节点
*/
private Node rotateRight(Node node){
Node temp=node.left;
node.left=temp.right;
temp.right=node;
return temp;
}
/**
* 对某个节点进行相应的左旋操作
* @param node 需要进行左旋操作的节点本身
* @return 旋转后的子树的根节点
*/
private Node rotateLeft(Node node){
Node temp=node.right;
node.right=temp.left;
temp.left=node;
return temp;
}
/**
* @param tree 需要进行伸展操作的树的根节点
* @param key 其需要进行旋转到根节点位子的元素节点的值
* @return 其伸展后树的根节点元素的指针
*/
private Node splay(Node tree,T key){
/*
用于判空排除空指针异常的干扰
*/
if(tree==null||key==null){
return tree;
}
/*
此处采用一个节点node来维护其相应的LeftTreeMax以及RightTreeMin子树部分
其中node.left用于标记RightTreeMin子树
node.right用于标记LeftTreeMax子树
以便于和最后的子树合并操作
*/
Node node =new Node();
//用于标记其左右子树的所有小于key的元素中的最大值以及所有大于key的元素中的最小值
Node leftTreeMax=node;
Node rightTreeMin=node;
while(true){
int cmp=key.compareTo((T) tree.key);
if(cmp<0){
//当不存在需要查找的键的情况下,跳出循环,合并三棵子树
if(tree.left==null){
break;
}
//当其为LL型时,将其进行右旋操作
if(key.compareTo((T)tree.left.key)<0){
tree=rotateRight(tree);
//当树中没有对应的节点时,进行三树合并操作
if(tree.left==null){
break;
}
}
//用于劈开其对应的根节点的左子树,将其拼接到对应的rightTreeMin的左分支上,同时修改对应的中树的节点
rightTreeMin.left=tree;
rightTreeMin=tree;
tree=tree.left;
}
else if(cmp>0){
//当不存在需要查找的键的情况下,跳出循环,合并三棵子树
if(tree.right==null){
break;
}
//当为rr型时,进行左旋操作
if(key.compareTo((T)tree.right.key)>0){
tree=rotateLeft(tree);
if(tree.right==null){
break;
}
}
//用于劈开对应的根节点的右子树,并进行拼接和调整操作
leftTreeMax.right=tree;
leftTreeMax=tree;
tree=tree.right;
}
else{
break;
}
}
//将中树的左子树合并到L树中,将中树的右子树合并到R树中,将中树的左子树合并到L树中
leftTreeMax.right=tree.left;
rightTreeMin.left=tree.right;
//用于合并三棵子树,node.right对应的为L树,node.left对应的为R树
tree.left=node.right;
tree.right=node.left;
return tree;
}
/**
* 用于伸展操作的相应的接口
* @param key
*/
public void splay(T key){
this.root=splay(this.root,key);
}
/**
* 用于往树中插入相应的值
* @param value 插入的值
*/
public void insert(T value){
Node<T> node=new Node<T>(value);
if(this.root==null){
this.root=node;
}
else{
//用于进行比较的当前节点
Node<T> temp=this.root;
//当前节点的父节点
Node<T> tem=null;
//当其值不相等的时候
while(temp!=null){
int cmp=temp.key.compareTo(value);
tem=temp;
if(cmp<0){
temp=temp.right;
}
else if(cmp>0){
temp=temp.left;
}
//想插入的节点的值已经存在
else{
break;
}
}
//节点不存在,需要插入相应的节点
if (temp==null){
int cmp=tem.key.compareTo(value);
//往右节点中插入
if(cmp<0){
tem.right=node;
}
else{
tem.left=node;
}
}
//对其进行伸展操作
splay(value);
}
}
/**
* 用于删除相应的节点的操作
* @param key 删除的节点的关键字
*/
public void delete(T key) {
//当存在其对应的键
if (search(key) == null) {
return;
}
//由于search(key)!=null,为此,其根节点为其对应的删除节点
Node node = this.root;
//将其前驱节点转化为根节点
if (this.root.left != null) {
this.root = splay(this.root.left, key);
//移除node节点
this.root.right = node.right;
} else {
this.root = node.right;
}
}
/**
* 用于查找相应的节点的操作
* @param key 查找的节点的关键字
* @return 相应的节点,没找到时,返回null
*/
public Node search(T key){
Node node=this.root;
while(node!=null){
int cmp=node.key.compareTo(key);
if(cmp<0){
node=node.right;
}
else if(cmp>0){
node=node.left;
}
else{
break;
}
}
//当存在相应的节点时,对其进行伸展操作
if(node!=null){
splay(key);
}
return node;
}
/**
* 对其进行前序遍历操作
* @param node 进行遍历的树的根节点
*/
private void preOrder(Node<T> node){
if(node!=null){
System.out.print(node.key+" ");
preOrder(node.left);
preOrder(node.right);
}
}
public void preOrder(){
preOrder(this.root);
}
/**
* 对其进行中序遍历操作
* @param node 进行遍历的树的根节点
*/
private void inOrder(Node<T> node){
if(node!=null){
inOrder(node.left);
System.out.print(node.key+" ");
inOrder(node.right);
}
}
public void inOrder(){
inOrder(this.root);
}
/**
* 获取整个伸展树的最小节点值
* @return 树的最小节点的值
*/
public T getMin(){
Node node=this.root;
while(node.left!=null){
node=node.left;
}
if(node!=null){
return (T)node.key;
}
else{
return null;
}
}
/**
* 获取整个伸展树的最大节点值
* @return 树的最大节点的值
*/
public T getMax(){
Node node=this.root;
while(node.right!=null){
node=node.right;
}
if(node!=null){
return (T)node.key;
}
else{
return null;
}
}
/**
* 打印"伸展树"
*
* key -- 节点的键值
* direction -- 0,表示该节点是根节点;
* -1,表示该节点是它的父结点的左孩子;
* 1,表示该节点是它的父结点的右孩子。
*/
private void print(Node<T> tree, T key, int direction) {
if(tree != null) {
// tree是根节点
if(direction==0) {
System.out.printf("%2d is root\n", tree.key);
}
// tree是分支节点
else {
System.out.printf("%2d is %2d's %6s child\n", tree.key, key, direction == 1 ? "right" : "left");
}
print(tree.left, tree.key, -1);
print(tree.right,tree.key, 1);
}
}
public void print() {
if (root != null) {
print(root, root.key, 0);
}
}
}
测试伸展树的代码如下:
/**
* @author 学徒
* 伸展树的测试程序
*/
public class SplayTreeTest {
private static final int arr[] = {10,50,40,30,20,60};
public static void main(String[] args) {
int i, ilen;
SplayTree<Integer> tree=new SplayTree<Integer>();
System.out.print("== 依次添加: ");
ilen = arr.length;
for(i=0; i<ilen; i++) {
System.out.print(arr[i]+" ");
tree.insert(arr[i]);
}
System.out.println("\n== 最小值: "+ tree.getMin());
System.out.println("== 最大值: "+ tree.getMax());
System.out.println("== 树的详细信息: ");
tree.print();
System.out.print("\n== 前序遍历: ");
tree.preOrder();
System.out.print("\n== 中序遍历: ");
tree.inOrder();
i = 30;
System.out.printf("\n== 旋转节点(%d)为根节点\n", i);
tree.splay(i);
System.out.printf("== 树的详细信息: \n");
tree.print();
i=60;
System.out.printf("\n== 查找节点(%d)\n", i);
tree.search(i);
System.out.printf("== 树的详细信息: \n");
tree.print();
i=50;
System.out.printf("\n== 删除节点(%d)\n", i);
tree.delete(i);
System.out.printf("== 树的详细信息: \n");
tree.print();
}
}
运行结果:
== 依次添加: 10 50 40 30 20 60
== 前序遍历: 60 30 20 10 50 40
== 中序遍历: 10 20 30 40 50 60
== 最小值: 10
== 最大值: 60
== 树的详细信息:
60 is root
30 is 60's left child
20 is 30's left child
10 is 20's left child
50 is 30's right child
40 is 50's left child
== 旋转节点(30)为根节点
== 树的详细信息:
30 is root
20 is 30's left child
10 is 20's left child
60 is 30's right child
50 is 60's left child
40 is 50's left child
== 查找节点(60)
== 树的详细信息:
60 is root
30 is 60's left child
20 is 30's left child
10 is 20's left child
50 is 30's right child
40 is 50's left child
== 删除节点(50)
== 树的详细信息:
40 is root
30 is 40's left child
20 is 30's left child
10 is 20's left child
60 is 40's right child
Process finished with exit code 0
注意点:
伸展树并没有AVL的平衡要求,任意节点的左右子树可以相差任意深度。与二叉搜索树类似,伸展树的单次搜索也可能需要n次操作。但伸展树可以保证,m次的连续搜索操作的复杂度为mlog(n)的量级,而不是mn量级。
应用
Splay Tree可以方便的解决一些区间问题,根据不同形状二叉树中序遍历结果不变的特性,可以将区间按顺序建二叉查找树。每次自下而上的一套splay都可以将x移动到根节点的位置,利用这个特性,可以方便的利用Lazy的思想进行区间操作。对于每个节点记录size,代表子树中节点的数目,这样就可以很方便地查找区间中的第k小或第k大元素。对于一段要处理的区间[x, y],首先splay x-1到root,再splay y+1到root的右孩子,这时root的右孩子的左孩子对应子树就是整个区间。这样,大部分区间问题都可以很方便的解决,操作同样也适用于一个或多个条目的添加或删除,和区间的移动。
用于热点数据的存放,比如网络应用中,某些固定内容会被大量重复访问。伸展树可以让这种重复搜索以很高的效率完成。
参考自:Splay Tree和纸上谈兵: 伸展树 (splay tree)以及数据结构与算法分析之伸展树(splaytree)
K:伸展树(splay tree)的更多相关文章
-
树-伸展树(Splay Tree)
伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二 ...
-
纸上谈兵: 伸展树 (splay tree)[转]
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每 ...
-
高级搜索树-伸展树(Splay Tree)
目录 局部性 双层伸展 查找操作 插入操作 删除操作 性能分析 完整源码 与AVL树一样,伸展树(Splay Tree)也是平衡二叉搜索树的一致,伸展树无需时刻都严格保持整棵树的平衡,也不需要对基本的 ...
-
【BBST 之伸展树 (Splay Tree)】
最近“hiho一下”出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codefor ...
-
伸展树 Splay Tree
Splay Tree 是二叉查找树的一种,它与平衡二叉树.红黑树不同的是,Splay Tree从不强制地保持自身的平衡,每当查找到某个节点n的时候,在返回节点n的同时,Splay Tree会将节点n旋 ...
-
伸展树(Splay tree)的基本操作与应用
伸展树的基本操作与应用 [伸展树的基本操作] 伸展树是二叉查找树的一种改进,与二叉查找树一样,伸展树也具有有序性.即伸展树中的每一个节点 x 都满足:该节点左子树中的每一个元素都小于 x,而其右子树中 ...
-
HDU 4453 Looploop (伸展树splay tree)
Looploop Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Su ...
-
hdu 2871 Memory Control(伸展树splay tree)
hdu 2871 Memory Control 题意:就是对一个区间的四种操作,NEW x,占据最左边的连续的x个单元,Free x 把x单元所占的连续区间清空 , Get x 把第x次占据的区间输出 ...
-
伸展树 Splay 模板
学习Splay的时候参考了很多不同的资料,然而参考资料太杂的后果就是模板调出来一直都有问题,尤其是最后发现网上找的各种资料均有不同程度的错误. 好在啃了几天之后终于算是啃下来了. Splay也算是平衡 ...
随机推荐
-
IOS8解决获取位置坐标信息出错(Error Domain=kCLErrorDomain Code=0)(转)
标题:IOS8解决获取位置坐标信息出错(Error Domain=kCLErrorDomain Code=0) 前几天解决了在ios8上无法使用地址位置服务的问题,最近在模拟器上调试发现获取位置坐标信 ...
-
工厂模式/factory模式/创建型模式
工厂模式 普通工厂模式 原本需要new出来的对象,通过一个类的方法去搞定,Factory.build(parameter),类似这种. public interface Sender { public ...
-
华为OJ平台——求最大连续bit数
题目描述: 求一个byte数字对应的二进制数字中1的最大连续数,例如3的二进制为00000011,最大连续2个1 输入: 一个byte型的数字 输出: 对应的二进制数字中1的最大连续数 思路: ...
-
Scala学习笔记(三)类层级和特质
无参方法 功能:将方法的定义转换为属性字段的定义: 作用范围:方法中没有参数,并且方法仅能通过读取所包含的对象属性去访问可变状态,而不改变可变状态,就可使用无参方法: 例子: abstract cla ...
-
java实现的可以无限级别添加子节点的菜单树
网上大部分菜单树,都是单独用js代码来实现的,这样做的缺点是:用户无法动态的设置菜单项,比如,超级管理员可能需要根据每个用户的权限,赋予他们不同的系统功能,不同的功能对应着不同数量的菜单项. 对于此问 ...
-
nodejs学习笔记-1
nodejs入门-安装 nodejs是什么,刚接触了一段时间,我自己也说不清楚它.按我个人的简单理解,nodejs就是一个javascript的解析器,它让javascript不在局限于浏览器客户端. ...
-
CloseHandle(),TerminateThread(),ExitThread()的差别
线程的handle用处: 线程的handle是指向"线程的内核对象"的,而不是指向线程本身.每一个内核对象仅仅是内核分配的一个内存块,而且仅仅能由内核訪问.该内存块是一种数据结构, ...
-
uva1267 Network
https://vjudge.net/problem/UVA-1267 题意: 有一棵树,上面有一个放着水源的点s,给出一个数k,这个水源可以覆盖路径长度到s不超过k的叶子节点.现在需要把所有的叶子节 ...
-
Linux显示inode的信息
Linux显示inode的信息 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ df -i 文件系统 Inode 已用(I) 可用(I) 已用(I)% 挂载点 ...
-
cxgrid动态多表头
function TForm15.CreateBand(View: TcxGridDBBandedTableView; BandCaption, ParentBandCaption: String) ...