数据结构(二) --- 伸展树(Splay Tree)
文章图片和代码来自邓俊辉老师课件
概述
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的。(出处百度百科)
它的操作就是将访问到的元素放在根节点处。主要的操作就是 zip 和 zag

下面是空间/时间复杂度(出处)

算法分析
双层伸展

双层伸展的作用是提升了树平均的访问性能。构思的精髓 : 向上追溯两层,而非一层。 右下角是伸展树需要处理的四种情况。具体的处理是怎么样的呢?
双层伸展主要在 zig-zip 和 zag-zag 的情况下发挥作用,例如要使v 升到根节点,双层伸展要求我们使用先对祖父节点zig,然后再做一次zip ,即是下部分三幅图演示的那样。下面我们看一下使用这种方法真的可以提升性能吗。

分摊性能
我们可以看到左边是逐层调整的方法,而右边是双层调整的方法,右边的子树的高度很明显比左边的矮了一半,当下一次又遇到最坏节点时,由于高度矮了一半了,那么性能自然就提升了。所以平均分摊时间可以达到 logn .

算法实现
代码是根绝邓老师的提供的代码用java改写的,添加了部分注释
主要处理的四种情况可以详见下面的代码
1 package Splay;
2
3
4 public class Node {
5 Node left;
6 Node right;
7 Node parent;
8 int value;
9
10 public Node(int value) {
11 this.value = value;
12 }
13
14 public boolean isLeftChild() {
15 return parent != null && (parent.left == this);
16 //根节点,我们直接返回false
17 }
18
19 public boolean isRightChild() {
20 return parent != null && (parent.right == this);
21 //根节点,我们直接返回false
22 }
23
24 @Override
25 public String toString() {
26 return "该节点的值为:" + value + " 左节点:" + ((left!=null)? left.value:"无") + " 右节点:" + ((right!=null)? right.value:"无") + " 父节点:" + ((parent!=null)? parent.value:"无");
27 }
28 }
29
1 package Splay;
2
3
4 import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
5
6 import javax.management.modelmbean.ModelMBean;
7
8 /**
9 * Splay Tree (伸展树)
10 *
11 *
12 */
13 public class SplayTree {
14
15 public Node root;
16
17 /**
18 * @param val 插入节点的值
19 */
20 public Node insert(int val) throws Exception {
21 if (root == null) {
22 root = new Node(val);
23 return null;
24 }
25 Node node = search(val);
26 //search操作查找是否存在该节点
27 if (node.value == val) {
28 return node;
29 } else { //search操作找不到该节点,查找返回的是hot节点,插入的节点再和hot重新装配
30 Node newRoot = new Node(val);
31 if (val > node.value) {
32 node.parent = newRoot;
33 newRoot.left = node;
34 newRoot.right = node.right;
35 if (node.right != null) {
36 node.right.parent = newRoot;
37 node.right = null;
38 }
39 } else {
40 node.parent = newRoot;
41 newRoot.right = node;
42 newRoot.left = node.left;
43 if (node.left != null) {
44 node.left.parent = newRoot;
45 node.left = null;
46 }
47 }
48 root = newRoot;
49 return newRoot;
50 }
51 }
52
53 /**
54 * 删除某个节点
55 * @param val 删除的节点
56 * @return true 成功删除,反之
57 */
58 public boolean delete(int val)throws Exception{
59 Node node = search(val);
60 if (node.value == val) { //找到该节点
61 if (node.left == null) { //没有左子树
62 root = node.right;
63 node.right.parent = null;
64 node.right = null;
65 } else if (node.right == null) { //没有右子树
66 root = node.left;
67 node.left.parent = null;
68 node.left = null;
69 }else { //节点存在左右子树
70 //暂时切除左子树,然后右子树中最小的节点,在连接起来
71 Node lTree = node.left;
72 lTree.parent = null;
73 root.left = null;
74 search(node.value);
75 //最小的节点必然上升到了根节点,此时的root应该是右子树中最小的节点
76 root.left = lTree;
77 lTree.parent = root;
78 node = null; //原来的节点置null
79 }
80 return true;
81 }else {
82 return false;
83 }
84 }
85
86 /**
87 * 计算整个树的高度
88 *
89 * @return
90 */
91 public int height() {
92 Node node;
93 int lh = 1, rh = 1;
94 node = root.left;
95 while (node != null) {
96 lh++;
97 node = node.left;
98 }
99
100 node = root.right;
101 while (node != null) {
102 rh++;
103 node = node.right;
104 }
105
106 return Math.max(lh, rh);
107 }
108
109 /**
110 * 中序递归打印
111 * @param node
112 */
113 public void printMidNum(Node node) {
114 if (node != null) {
115 printMidNum(node.left);
116 System.out.print(node.value + " ");
117 // System.out.println(node.toString());
118 printMidNum(node.right);
119 }
120 }
121
122
123 /**
124 * @param value 使用伸展策略搜寻的某个值,
125 * @return 返回查找到的node, 树中没有该元素返回null
126 */
127 public Node search(int value) throws Exception {
128 /*
129 搜索某个节点是不是存在,调用splay方法
130 */
131 Node compare;
132 Node target; //目标节点
133 Node hot = null; //目标附近节点
134 compare = root;
135
136 if (compare == null) {
137 throw new Exception("该树为空树");
138 }
139 //查找某个节点
140 while (true) {
141 if (compare == null) {
142 target = null;
143 break;
144 } else if (compare.value == value) {
145 target = compare;
146 break;
147 }
148 if (compare.value > value) {
149 hot = compare;
150 compare = compare.left;
151 } else {
152 hot = compare;
153 compare = compare.right;
154 }
155 }
156
157 if (target == null) {
158 root = splay(hot);
159 return null;
160 }
161
162 return (root = splay(target));
163 }
164
165
166 public void attachAsLChild(Node parent, Node lChild) {
167 parent.left = lChild;
168 if (lChild != null)
169 lChild.parent = parent;
170 }
171
172 void attachAsRChild(Node parent, Node rChild) {
173 parent.right = rChild;
174 if (rChild != null)
175 rChild.parent = parent;
176 }
177
178 /**
179 * 传入的 v 必须存在于树中,由调用的方法保证
180 * splay 方法针对情况进行转换,转换的思路是重新对各个节点的位置装配,装配的意思指的是
181 * 根据初始的位置和最终的位置拆解-重连的操作
182 *
183 * @param v v为因最近访问而需伸展的节点位置
184 * @return 调整之后新树根应为被伸展的节点,故返回该节点的位置以便上层函数更新树根
185 */
186 public Node splay(Node v) {
187 if (v == null) return null;
188 //*v的父亲与祖父
189 Node p;
190 Node g;
191 while ((p = v.parent) != null && (g = p.parent) != null) { //自下而上,反复对*v做双层伸展
192 Node gg = g.parent; //每轮之后*v都以原曾祖父(great-grand parent)为父
193 if (v.isLeftChild()) {
194 if (p.isLeftChild()) { //zig-zig
195 attachAsLChild(g, p.right);
196 attachAsLChild(p, v.right);
197 attachAsRChild(p, g);
198 attachAsRChild(v, p);
199 } else { //zig-zag
200 attachAsLChild(p, v.right);
201 attachAsRChild(g, v.left);
202 attachAsLChild(v, g);
203 attachAsRChild(v, p);
204 }
205 } else if (p.isRightChild()) { //zag-zag
206 attachAsRChild(g, p.left);
207 attachAsRChild(p, v.left);
208 attachAsLChild(p, g);
209 attachAsLChild(v, p);
210 } else { //zag-zig
211 attachAsRChild(p, v.left);
212 attachAsLChild(g, v.right);
213 attachAsRChild(v, g);
214 attachAsLChild(v, p);
215 }
216
217 //重连子树,并判断是否loop是否结束
218 if (gg == null)
219 v.parent = null; //若*v原先的曾祖父*gg不存在,则*v现在应为树根
220 else //否则,*gg此后应该以*v作为左或右孩子
221 if (g == gg.left) {
222 attachAsLChild(gg, v);
223 } else {
224 attachAsRChild(gg, v);
225 }
226
227 }
228 //双层伸展结束时,必有g == NULL,但p可能非空,即是目标到根节点之间还有一个节点,需要一次单旋解决
229 if ((p = v.parent) != null) {
230 if (v.isLeftChild()) {
231 attachAsLChild(p, v.right);
232 attachAsRChild(v, p);
233 } else {
234 attachAsRChild(p, v.left);
235 attachAsLChild(v, p);
236 }
237 }
238 v.parent = null;
239 return v;
240 }
241
242
243 }
244
245
246
代码中的注释已经说明了各个操作的流程,需要注意的是insert 和 delete方法,由于这两个方法在实现的时候都会先调用search方法,我们使用了一个hot 节点,表示离目标节点最近的节点,让hot 节点上升到根节点,方便我们在insert和delete后续的使用。
综合评价

不能保证单次最坏情况的出现的原因是,假如我们一开始要找的那个点就在最底下,那么就可能达到了最坏的情况。
参考资料
- 邓俊辉老师的数据结构课程
数据结构(二) --- 伸展树(Splay Tree)的更多相关文章
- 纸上谈兵: 伸展树 (splay tree)[转]
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每 ...
- K:伸展树(splay tree)
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(lgN)内完成插入.查找和删除操作.在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使 ...
- 高级搜索树-伸展树(Splay Tree)
目录 局部性 双层伸展 查找操作 插入操作 删除操作 性能分析 完整源码 与AVL树一样,伸展树(Splay Tree)也是平衡二叉搜索树的一致,伸展树无需时刻都严格保持整棵树的平衡,也不需要对基本的 ...
- 树-伸展树(Splay Tree)
伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二 ...
- 伸展树(Splay tree)的基本操作与应用
伸展树的基本操作与应用 [伸展树的基本操作] 伸展树是二叉查找树的一种改进,与二叉查找树一样,伸展树也具有有序性.即伸展树中的每一个节点 x 都满足:该节点左子树中的每一个元素都小于 x,而其右子树中 ...
- 【BBST 之伸展树 (Splay Tree)】
最近“hiho一下”出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codefor ...
- 伸展树 Splay Tree
Splay Tree 是二叉查找树的一种,它与平衡二叉树.红黑树不同的是,Splay Tree从不强制地保持自身的平衡,每当查找到某个节点n的时候,在返回节点n的同时,Splay Tree会将节点n旋 ...
- 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)
#0.0 写在前面 Splay(伸展树)是较为重要的一种平衡树,理解起来也依旧很容易,但是细节是真的多QnQ,学一次忘一次,还是得用博客加深一下理解( #1.0 Splay! #1.1 基本构架 Sp ...
随机推荐
- while 小项目练习
# (1) 用双层while 写十行十列小星星 j = 0 while j < 10: #打印一行十个小星星 i = 0 while i <10: print("*", ...
- 【maven】---初识
前言 最近在研究自动化测试,其中的一个研究点儿就是maven,去download了一本书,看了看.下面是自己的从书中摘录的一些关于maven的概念性的东西. 内容 maven是什么? Maven是一个 ...
- 洛谷P5211 [ZJOI2017]字符串(线段树+乱搞)
题面 传送门 题解 为什么大佬们全都是乱搞的--莫非这就是传说中的暴力能进队,乱搞能AC-- 似乎有位大佬能有纯暴力+玄学优化\(AC\)(不算上\(uoj\)的\(Hack\)数据的话--这要是放到 ...
- PHP如何根据数组中的键值进行排序
主要是使用PHP的排序函数,asort()和arsort(). 为了减少代码的耦合性,我们将根据数组中的键值进行排序封装成一个函数 <?php $array = array( array(), ...
- Laravel for Windows 开发环境配置
本文为CSDN Choris 原创,转载请事先征得作者同意,以示尊重! 原文:http://blog.csdn.net/choris/article/details/50215835 Laravel配 ...
- cap-insets
原文链接 在很多编程语言中都有resizable image这样的概念,比如android中的NinePatch graphic ,css3中的border image,微软的Nine-Grid Re ...
- Django 学习资源
相关的分享: 开发者头条:http://toutiao.io/search?utf8=%E2%9C%93&q=django 极客头条及Django资讯:http://www.csdn.net/ ...
- __getitem__
如果在类中定义了__getitem__()方法,那么他的实例对象(假设为P)就可以这样P[key]取值.当实例对象做P[key]运算时,就会调用类中的__getitem__()方法.
- 编程开发之--Oracle数据库--存储过程和存储函数(1)
1.存储过程和存储函数 描述:指存储在数据库中供所有用户程序调用的子程序叫做存储过程.存储函数 区别:存储函数可以通过return子句返回一个函数的值 (1)存储过程 语法:create [or re ...
- editplus 编辑 php双击选中变量问题
windows下,在很多地方双击鼠标左键可以选中一个连续的英文字符串. 在editplus 编辑器里可以双击选中一个变量,方便了编程,但是使用phptools(php.stx)增强语法插件后,在一个变 ...