伸展树(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):

  1. 先建立两个空子树,一个是LeftTreeMax(简称LTMax),另一个是RightTreeMin(简称RTMin)。其中LTMax子树保存遍历过程中所有小于X的节点,并且LTMax在遍历过程中始终指向该子树中最大的那个节点。不难看出LTMax->Right始终为空。RTMin子树保存遍历过程中所有大于X的节点,RTMin在遍历过程中始终指向该子树中最小的那个节点。同样RTMin->Left也始终为空。因此在合并RTMin和LTMax前,RTMin是所在子树上所有大于X节点中那个最小的并接近X的节点,同理LTMax中所在子树中所有小于X节点中那个最大的并接近X的节点。

  2. 从根节点T开始遍历,查找X节点,直到找到或者未找到

    1. 如果X节点小于T并且小于T->Left(LL型)则实行一次围绕T的右单旋转,之后T->Left成为新的根节点T',X又小于T',所以从T'劈开的右子树(包括T',也就是从T'的左子树连接处劈开)上的所有节点都大于X,我们将T'挂在RTMin->Left上,并更新RTMin指向T'节点,T'节点是所有大于X节点中最小的那个。

    2. 如果X节点小于T,但T->Left为NULL,则未找到X节点,退出循环并将三个子树合并

    3. 如果X节点大于T并且大于T->Right(RR型),则实行一次围绕T的左单旋转,这样T->Right成为新的根节点T',X又大于T',所以从T'劈开的左子树(包括T',也就是从T'的右子树连接处劈开)上所有的节点都小于X,我们将T'子树挂在LTMax->Right上,并将LTMax指向新的T'节点,T'节点就是所有小于X节点中最大的那个。

    4. 如果X节点大于T,但T-Right为NULL,则未找到X节点,退出循环将三个子树合并

  3. 当找到或者未找到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量级。

应用

  1. Splay Tree可以方便的解决一些区间问题,根据不同形状二叉树中序遍历结果不变的特性,可以将区间按顺序建二叉查找树。每次自下而上的一套splay都可以将x移动到根节点的位置,利用这个特性,可以方便的利用Lazy的思想进行区间操作。对于每个节点记录size,代表子树中节点的数目,这样就可以很方便地查找区间中的第k小或第k大元素。对于一段要处理的区间[x, y],首先splay x-1到root,再splay y+1到root的右孩子,这时root的右孩子的左孩子对应子树就是整个区间。这样,大部分区间问题都可以很方便的解决,操作同样也适用于一个或多个条目的添加或删除,和区间的移动。

  2. 用于热点数据的存放,比如网络应用中,某些固定内容会被大量重复访问。伸展树可以让这种重复搜索以很高的效率完成。

参考自:Splay Tree纸上谈兵: 伸展树 (splay tree)以及数据结构与算法分析之伸展树(splaytree)

K:伸展树(splay tree)的更多相关文章

  1. 树-伸展树(Splay Tree)

    伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二 ...

  2. 纸上谈兵: 伸展树 (splay tree)[转]

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢!  我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每 ...

  3. 高级搜索树-伸展树(Splay Tree)

    目录 局部性 双层伸展 查找操作 插入操作 删除操作 性能分析 完整源码 与AVL树一样,伸展树(Splay Tree)也是平衡二叉搜索树的一致,伸展树无需时刻都严格保持整棵树的平衡,也不需要对基本的 ...

  4. 【BBST 之伸展树 (Splay Tree)】

    最近“hiho一下”出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codefor ...

  5. 伸展树 Splay Tree

    Splay Tree 是二叉查找树的一种,它与平衡二叉树.红黑树不同的是,Splay Tree从不强制地保持自身的平衡,每当查找到某个节点n的时候,在返回节点n的同时,Splay Tree会将节点n旋 ...

  6. 伸展树(Splay tree)的基本操作与应用

    伸展树的基本操作与应用 [伸展树的基本操作] 伸展树是二叉查找树的一种改进,与二叉查找树一样,伸展树也具有有序性.即伸展树中的每一个节点 x 都满足:该节点左子树中的每一个元素都小于 x,而其右子树中 ...

  7. HDU 4453 Looploop (伸展树splay tree)

    Looploop Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  8. hdu 2871 Memory Control(伸展树splay tree)

    hdu 2871 Memory Control 题意:就是对一个区间的四种操作,NEW x,占据最左边的连续的x个单元,Free x 把x单元所占的连续区间清空 , Get x 把第x次占据的区间输出 ...

  9. 伸展树 Splay 模板

    学习Splay的时候参考了很多不同的资料,然而参考资料太杂的后果就是模板调出来一直都有问题,尤其是最后发现网上找的各种资料均有不同程度的错误. 好在啃了几天之后终于算是啃下来了. Splay也算是平衡 ...

随机推荐

  1. jvm内存结构(一)(结构总览)

    jvm内存结构:<Java虚拟机原理图解>3.JVM运行时数据区 程序计数器: ,是执行的字节码的行号指示器,记录的是正在执行的虚拟机字节码指令的地址. ,每个线程都有独立计数器,互不干扰 ...

  2. WWDC: Thread Sanitizer and Static Analysis

    Thread Sanitizer 过程 编译过程中链接了一个新的库.  也可以通过命令行来操作: $ clang -fsanitize=thread source.c -o executable $ ...

  3. Python小白学习之路(八)—【变量】【基本数据类型分类】【集合】【集合的功能】

    一.变量 变量的作用:记录状态的变化变量的值:描述不同的状态 二.五大基本数据类型的分类 五大基本数据类型(数字 字符串 列表 元祖 字典) 按照可变不可变来进行分类 可变:列表.字典 不可变:字符串 ...

  4. git连接通过ssh连接github

    解决 git连接通过ssh连接github 1. 首先产生一个rsa的私钥和公钥 ssh-keygen -t rsa -C "15950093214@163.com"  //你的g ...

  5. logcat -- 基本用法

    1.Log类是一个日志类,我们可以在代码中使用logcat打印出消息 常见的日志记录方法有: v(String,String)  --verbose 显示全部信息 d(String,String) - ...

  6. js实现隔行变色-------Day40

    版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/marSmile_tbo/article/details/31837367 就这样開始了自己的第二个项 ...

  7. 课程一(Neural Networks and Deep Learning),第一周(Introduction to Deep Learning)—— 1、经常提及的问题

    Frequently Asked Questions Congratulations to be part of the first class of the Deep Learning Specia ...

  8. 使用Jacob操作Wrod文档的工具类代码

    一.需要有jacob的jar包支持 import java.util.Iterator; import java.util.List; import java.util.HashMap; import ...

  9. 如何安装windows7

    前因:之前安装的win7的系统,用了激活工具,刚开始的几个星期还没察觉有何问题.直到有天系统给出异常提示:系统资源不足,无法完成请求的服务.仔细排查之后发现是系统内核句柄数一直增加不释放,句柄数大概有 ...

  10. ActiveMQ--HelloWorld

    下载windows版本ActiveMQ,apache-activemq-5.15.3\bin\win64\activemq.bat 启动mq,ActiveMQ内置jetty,默认端口8161,默认用户 ...