概要
前面分别通过C和C++实现了伸展树,本章给出伸展树的Java版本。基本算法和原理都与前两章一样。
1. 伸展树的介绍
2. 伸展树的Java实现(完整源码)
3. 伸展树的Java测试程序
转载请注明出处:http://www.cnblogs.com/skywang12345/p/3604286.html
更多内容: 数据结构与算法系列 目录
(01) 伸展树(一)之 图文解析 和 C语言的实现
(02) 伸展树(二)之 C++的实现
(03) 伸展树(三)之 Java的实现
伸展树的介绍
伸展树(Splay Tree)是特殊的二叉查找树。
它的特殊是指,它除了本身是棵二叉查找树之外,它还具备一个特点: 当某个节点被访问时,伸展树会通过旋转使该节点成为树根。这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点。
伸展树的Java实现
1. 基本定义
public class SplayTree<T extends Comparable<T>> {
private SplayTreeNode<T> mRoot; // 根结点
public class SplayTreeNode<T extends Comparable<T>> {
T key; // 关键字(键值)
SplayTreeNode<T> left; // 左孩子
SplayTreeNode<T> right; // 右孩子
public SplayTreeNode() {
this.left = null;
this.right = null;
}
public SplayTreeNode(T key, SplayTreeNode<T> left, SplayTreeNode<T> right) {
this.key = key;
this.left = left;
this.right = right;
}
}
...
}
SplayTree是伸展树,而SplayTreeNode是伸展树节点。在此,我将SplayTreeNode定义为SplayTree的内部类。在伸展树SplayTree中包含了伸展树的根节点mRoot。SplayTreeNode包括的几个组成元素:
(01) key -- 是关键字,是用来对伸展树的节点进行排序的。
(02) left -- 是左孩子。
(03) right -- 是右孩子。
2. 旋转
旋转是伸展树中需要重点关注的,它的代码如下:
/*
* 旋转key对应的节点为根节点,并返回根节点。
*
* 注意:
* (a):伸展树中存在"键值为key的节点"。
* 将"键值为key的节点"旋转为根节点。
* (b):伸展树中不存在"键值为key的节点",并且key < tree.key。
* b-1 "键值为key的节点"的前驱节点存在的话,将"键值为key的节点"的前驱节点旋转为根节点。
* b-2 "键值为key的节点"的前驱节点存在的话,则意味着,key比树中任何键值都小,那么此时,将最小节点旋转为根节点。
* (c):伸展树中不存在"键值为key的节点",并且key > tree.key。
* c-1 "键值为key的节点"的后继节点存在的话,将"键值为key的节点"的后继节点旋转为根节点。
* c-2 "键值为key的节点"的后继节点不存在的话,则意味着,key比树中任何键值都大,那么此时,将最大节点旋转为根节点。
*/
private SplayTreeNode<T> splay(SplayTreeNode<T> tree, T key) {
if (tree == null)
return tree;
SplayTreeNode<T> N = new SplayTreeNode<T>();
SplayTreeNode<T> l = N;
SplayTreeNode<T> r = N;
SplayTreeNode<T> c;
for (;;) {
int cmp = key.compareTo(tree.key);
if (cmp < 0) {
if (tree.left == null)
break;
if (key.compareTo(tree.left.key) < 0) {
c = tree.left; /* rotate right */
tree.left = c.right;
c.right = tree;
tree = c;
if (tree.left == null)
break;
}
r.left = tree; /* link right */
r = tree;
tree = tree.left;
} else if (cmp > 0) {
if (tree.right == null)
break;
if (key.compareTo(tree.right.key) > 0) {
c = tree.right; /* rotate left */
tree.right = c.left;
c.left = tree;
tree = c;
if (tree.right == null)
break;
}
l.right = tree; /* link left */
l = tree;
tree = tree.right;
} else {
break;
}
}
l.right = tree.left; /* assemble */
r.left = tree.right;
tree.left = N.right;
tree.right = N.left;
return tree;
}
public void splay(T key) {
mRoot = splay(mRoot, key);
}
上面的代码的作用:将"键值为key的节点"旋转为根节点,并返回根节点。它的处理情况共包括:
(a):伸展树中存在"键值为key的节点"。
将"键值为key的节点"旋转为根节点。
(b):伸展树中不存在"键值为key的节点",并且key < tree->key。
b-1) "键值为key的节点"的前驱节点存在的话,将"键值为key的节点"的前驱节点旋转为根节点。
b-2) "键值为key的节点"的前驱节点存在的话,则意味着,key比树中任何键值都小,那么此时,将最小节点旋转为根节点。
(c):伸展树中不存在"键值为key的节点",并且key > tree->key。
c-1) "键值为key的节点"的后继节点存在的话,将"键值为key的节点"的后继节点旋转为根节点。
c-2) "键值为key的节点"的后继节点不存在的话,则意味着,key比树中任何键值都大,那么此时,将最大节点旋转为根节点。
下面列举个例子分别对a进行说明。
在下面的伸展树中查找10,,共包括"右旋" --> "右链接" --> "组合"这3步。

01, 右旋
对应代码中的"rotate right"部分

02, 右链接
对应代码中的"link right"部分

03. 组合
对应代码中的"assemble"部分

提示:如果在上面的伸展树中查找"70",则正好与"示例1"对称,而对应的操作则分别是"rotate left", "link left"和"assemble"。
其它的情况,例如"查找15是b-1的情况,查找5是b-2的情况"等等,这些都比较简单,大家可以自己分析。
3. 插入
插入代码
/*
* 将结点插入到伸展树中,并返回根节点
*
* 参数说明:
* tree 伸展树的
* z 插入的结点
*/
private SplayTreeNode<T> insert(SplayTreeNode<T> tree, SplayTreeNode<T> z) {
int cmp;
SplayTreeNode<T> y = null;
SplayTreeNode<T> x = tree;
// 查找z的插入位置
while (x != null) {
y = x;
cmp = z.key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else if (cmp > 0)
x = x.right;
else {
System.out.printf("不允许插入相同节点(%d)!\n", z.key);
z=null;
return tree;
}
}
if (y==null)
tree = z;
else {
cmp = z.key.compareTo(y.key);
if (cmp < 0)
y.left = z;
else
y.right = z;
}
return tree;
}
public void insert(T key) {
SplayTreeNode<T> z=new SplayTreeNode<T>(key,null,null);
// 如果新建结点失败,则返回。
if ((z=new SplayTreeNode<T>(key,null,null)) == null)
return ;
// 插入节点
mRoot = insert(mRoot, z);
// 将节点(key)旋转为根节点
mRoot = splay(mRoot, key);
}
insert(key)是提供给外部的接口,它的作用是新建节点(节点的键值为key),并将节点插入到伸展树中;然后,将该节点旋转为根节点。
insert(tree, z)是内部接口,它的作用是将节点z插入到tree中。insert(tree, z)在将z插入到tree中时,仅仅只将tree当作是一棵二叉查找树,而且不允许插入相同节点。
4. 删除
删除代码
/*
* 删除结点(z),并返回被删除的结点
*
* 参数说明:
* bst 伸展树
* z 删除的结点
*/
private SplayTreeNode<T> remove(SplayTreeNode<T> tree, T key) {
SplayTreeNode<T> x;
if (tree == null)
return null;
// 查找键值为key的节点,找不到的话直接返回。
if (search(tree, key) == null)
return tree;
// 将key对应的节点旋转为根节点。
tree = splay(tree, key);
if (tree.left != null) {
// 将"tree的前驱节点"旋转为根节点
x = splay(tree.left, key);
// 移除tree节点
x.right = tree.right;
}
else
x = tree.right;
tree = null;
return x;
}
public void remove(T key) {
mRoot = remove(mRoot, key);
}
remove(key)是外部接口,remove(tree, key)是内部接口。
remove(tree, key)的作用是:删除伸展树中键值为key的节点。
它会先在伸展树中查找键值为key的节点。若没有找到的话,则直接返回。若找到的话,则将该节点旋转为根节点,然后再删除该节点。
关于"前序遍历"、"中序遍历"、"后序遍历"、"最大值"、"最小值"、"查找"、"打印伸展树"、"销毁伸展树"等接口就不再单独介绍了,Please RTFSC(Read The Fucking Source Code)!这些接口,与前面介绍的"二叉查找树"、"AVL树"的相关接口都是类似的。
伸展树的Java实现(完整源码)
伸展树的实现文件(SplayTree.java)

1 /**
2 * Java 语言: 伸展树
3 *
4 * @author skywang
5 * @date 2014/02/03
6 */
7
8 public class SplayTree<T extends Comparable<T>> {
9
10 private SplayTreeNode<T> mRoot; // 根结点
11
12 public class SplayTreeNode<T extends Comparable<T>> {
13 T key; // 关键字(键值)
14 SplayTreeNode<T> left; // 左孩子
15 SplayTreeNode<T> right; // 右孩子
16
17 public SplayTreeNode() {
18 this.left = null;
19 this.right = null;
20 }
21
22 public SplayTreeNode(T key, SplayTreeNode<T> left, SplayTreeNode<T> right) {
23 this.key = key;
24 this.left = left;
25 this.right = right;
26 }
27 }
28
29 public SplayTree() {
30 mRoot=null;
31 }
32
33 /*
34 * 前序遍历"伸展树"
35 */
36 private void preOrder(SplayTreeNode<T> tree) {
37 if(tree != null) {
38 System.out.print(tree.key+" ");
39 preOrder(tree.left);
40 preOrder(tree.right);
41 }
42 }
43
44 public void preOrder() {
45 preOrder(mRoot);
46 }
47
48 /*
49 * 中序遍历"伸展树"
50 */
51 private void inOrder(SplayTreeNode<T> tree) {
52 if(tree != null) {
53 inOrder(tree.left);
54 System.out.print(tree.key+" ");
55 inOrder(tree.right);
56 }
57 }
58
59 public void inOrder() {
60 inOrder(mRoot);
61 }
62
63
64 /*
65 * 后序遍历"伸展树"
66 */
67 private void postOrder(SplayTreeNode<T> tree) {
68 if(tree != null)
69 {
70 postOrder(tree.left);
71 postOrder(tree.right);
72 System.out.print(tree.key+" ");
73 }
74 }
75
76 public void postOrder() {
77 postOrder(mRoot);
78 }
79
80
81 /*
82 * (递归实现)查找"伸展树x"中键值为key的节点
83 */
84 private SplayTreeNode<T> search(SplayTreeNode<T> x, T key) {
85 if (x==null)
86 return x;
87
88 int cmp = key.compareTo(x.key);
89 if (cmp < 0)
90 return search(x.left, key);
91 else if (cmp > 0)
92 return search(x.right, key);
93 else
94 return x;
95 }
96
97 public SplayTreeNode<T> search(T key) {
98 return search(mRoot, key);
99 }
100
101 /*
102 * (非递归实现)查找"伸展树x"中键值为key的节点
103 */
104 private SplayTreeNode<T> iterativeSearch(SplayTreeNode<T> x, T key) {
105 while (x!=null) {
106 int cmp = key.compareTo(x.key);
107
108 if (cmp < 0)
109 x = x.left;
110 else if (cmp > 0)
111 x = x.right;
112 else
113 return x;
114 }
115
116 return x;
117 }
118
119 public SplayTreeNode<T> iterativeSearch(T key) {
120 return iterativeSearch(mRoot, key);
121 }
122
123 /*
124 * 查找最小结点:返回tree为根结点的伸展树的最小结点。
125 */
126 private SplayTreeNode<T> minimum(SplayTreeNode<T> tree) {
127 if (tree == null)
128 return null;
129
130 while(tree.left != null)
131 tree = tree.left;
132 return tree;
133 }
134
135 public T minimum() {
136 SplayTreeNode<T> p = minimum(mRoot);
137 if (p != null)
138 return p.key;
139
140 return null;
141 }
142
143 /*
144 * 查找最大结点:返回tree为根结点的伸展树的最大结点。
145 */
146 private SplayTreeNode<T> maximum(SplayTreeNode<T> tree) {
147 if (tree == null)
148 return null;
149
150 while(tree.right != null)
151 tree = tree.right;
152 return tree;
153 }
154
155 public T maximum() {
156 SplayTreeNode<T> p = maximum(mRoot);
157 if (p != null)
158 return p.key;
159
160 return null;
161 }
162
163 /*
164 * 旋转key对应的节点为根节点,并返回根节点。
165 *
166 * 注意:
167 * (a):伸展树中存在"键值为key的节点"。
168 * 将"键值为key的节点"旋转为根节点。
169 * (b):伸展树中不存在"键值为key的节点",并且key < tree.key。
170 * b-1 "键值为key的节点"的前驱节点存在的话,将"键值为key的节点"的前驱节点旋转为根节点。
171 * b-2 "键值为key的节点"的前驱节点存在的话,则意味着,key比树中任何键值都小,那么此时,将最小节点旋转为根节点。
172 * (c):伸展树中不存在"键值为key的节点",并且key > tree.key。
173 * c-1 "键值为key的节点"的后继节点存在的话,将"键值为key的节点"的后继节点旋转为根节点。
174 * c-2 "键值为key的节点"的后继节点不存在的话,则意味着,key比树中任何键值都大,那么此时,将最大节点旋转为根节点。
175 */
176 private SplayTreeNode<T> splay(SplayTreeNode<T> tree, T key) {
177 if (tree == null)
178 return tree;
179
180 SplayTreeNode<T> N = new SplayTreeNode<T>();
181 SplayTreeNode<T> l = N;
182 SplayTreeNode<T> r = N;
183 SplayTreeNode<T> c;
184
185 for (;;) {
186
187 int cmp = key.compareTo(tree.key);
188 if (cmp < 0) {
189
190 if (tree.left == null)
191 break;
192
193 if (key.compareTo(tree.left.key) < 0) {
194 c = tree.left; /* rotate right */
195 tree.left = c.right;
196 c.right = tree;
197 tree = c;
198 if (tree.left == null)
199 break;
200 }
201 r.left = tree; /* link right */
202 r = tree;
203 tree = tree.left;
204 } else if (cmp > 0) {
205
206 if (tree.right == null)
207 break;
208
209 if (key.compareTo(tree.right.key) > 0) {
210 c = tree.right; /* rotate left */
211 tree.right = c.left;
212 c.left = tree;
213 tree = c;
214 if (tree.right == null)
215 break;
216 }
217
218 l.right = tree; /* link left */
219 l = tree;
220 tree = tree.right;
221 } else {
222 break;
223 }
224 }
225
226 l.right = tree.left; /* assemble */
227 r.left = tree.right;
228 tree.left = N.right;
229 tree.right = N.left;
230
231 return tree;
232 }
233
234 public void splay(T key) {
235 mRoot = splay(mRoot, key);
236 }
237
238 /*
239 * 将结点插入到伸展树中,并返回根节点
240 *
241 * 参数说明:
242 * tree 伸展树的
243 * z 插入的结点
244 */
245 private SplayTreeNode<T> insert(SplayTreeNode<T> tree, SplayTreeNode<T> z) {
246 int cmp;
247 SplayTreeNode<T> y = null;
248 SplayTreeNode<T> x = tree;
249
250 // 查找z的插入位置
251 while (x != null) {
252 y = x;
253 cmp = z.key.compareTo(x.key);
254 if (cmp < 0)
255 x = x.left;
256 else if (cmp > 0)
257 x = x.right;
258 else {
259 System.out.printf("不允许插入相同节点(%d)!\n", z.key);
260 z=null;
261 return tree;
262 }
263 }
264
265 if (y==null)
266 tree = z;
267 else {
268 cmp = z.key.compareTo(y.key);
269 if (cmp < 0)
270 y.left = z;
271 else
272 y.right = z;
273 }
274
275 return tree;
276 }
277
278 public void insert(T key) {
279 SplayTreeNode<T> z=new SplayTreeNode<T>(key,null,null);
280
281 // 如果新建结点失败,则返回。
282 if ((z=new SplayTreeNode<T>(key,null,null)) == null)
283 return ;
284
285 // 插入节点
286 mRoot = insert(mRoot, z);
287 // 将节点(key)旋转为根节点
288 mRoot = splay(mRoot, key);
289 }
290
291 /*
292 * 删除结点(z),并返回被删除的结点
293 *
294 * 参数说明:
295 * bst 伸展树
296 * z 删除的结点
297 */
298 private SplayTreeNode<T> remove(SplayTreeNode<T> tree, T key) {
299 SplayTreeNode<T> x;
300
301 if (tree == null)
302 return null;
303
304 // 查找键值为key的节点,找不到的话直接返回。
305 if (search(tree, key) == null)
306 return tree;
307
308 // 将key对应的节点旋转为根节点。
309 tree = splay(tree, key);
310
311 if (tree.left != null) {
312 // 将"tree的前驱节点"旋转为根节点
313 x = splay(tree.left, key);
314 // 移除tree节点
315 x.right = tree.right;
316 }
317 else
318 x = tree.right;
319
320 tree = null;
321
322 return x;
323 }
324
325 public void remove(T key) {
326 mRoot = remove(mRoot, key);
327 }
328
329 /*
330 * 销毁伸展树
331 */
332 private void destroy(SplayTreeNode<T> tree) {
333 if (tree==null)
334 return ;
335
336 if (tree.left != null)
337 destroy(tree.left);
338 if (tree.right != null)
339 destroy(tree.right);
340
341 tree=null;
342 }
343
344 public void clear() {
345 destroy(mRoot);
346 mRoot = null;
347 }
348
349 /*
350 * 打印"伸展树"
351 *
352 * key -- 节点的键值
353 * direction -- 0,表示该节点是根节点;
354 * -1,表示该节点是它的父结点的左孩子;
355 * 1,表示该节点是它的父结点的右孩子。
356 */
357 private void print(SplayTreeNode<T> tree, T key, int direction) {
358
359 if(tree != null) {
360
361 if(direction==0) // tree是根节点
362 System.out.printf("%2d is root\n", tree.key);
363 else // tree是分支节点
364 System.out.printf("%2d is %2d's %6s child\n", tree.key, key, direction==1?"right" : "left");
365
366 print(tree.left, tree.key, -1);
367 print(tree.right,tree.key, 1);
368 }
369 }
370
371 public void print() {
372 if (mRoot != null)
373 print(mRoot, mRoot.key, 0);
374 }
375 }
伸展树的测试程序(SplayTreeTest.java)

1 /**
2 * Java 语言: 伸展树
3 *
4 * @author skywang
5 * @date 2014/02/03
6 */
7 public class SplayTreeTest {
8
9 private static final int arr[] = {10,50,40,30,20,60};
10
11 public static void main(String[] args) {
12 int i, ilen;
13 SplayTree<Integer> tree=new SplayTree<Integer>();
14
15 System.out.print("== 依次添加: ");
16 ilen = arr.length;
17 for(i=0; i<ilen; i++) {
18 System.out.print(arr[i]+" ");
19 tree.insert(arr[i]);
20 }
21
22 System.out.print("\n== 前序遍历: ");
23 tree.preOrder();
24
25 System.out.print("\n== 中序遍历: ");
26 tree.inOrder();
27
28 System.out.print("\n== 后序遍历: ");
29 tree.postOrder();
30 System.out.println();
31
32 System.out.println("== 最小值: "+ tree.minimum());
33 System.out.println("== 最大值: "+ tree.maximum());
34 System.out.println("== 树的详细信息: ");
35 tree.print();
36
37 i = 30;
38 System.out.printf("\n== 旋转节点(%d)为根节点\n", i);
39 tree.splay(i);
40 System.out.printf("== 树的详细信息: \n");
41 tree.print();
42
43 // 销毁二叉树
44 tree.clear();
45 }
46 }
在二叉查找树的Java实现中,使用了泛型,也就意味着它支持任意类型;但是该类型必须要实现Comparable接口。
伸展树的Java测试程序
伸展树的测试程序运行结果如下:
== 依次添加: 10 50 40 30 20 60 == 前序遍历: 60 30 20 10 50 40 == 中序遍历: 10 20 30 40 50 60 == 后序遍历: 10 20 40 50 30 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
测试程序的主要流程是:新建伸展树,然后向伸展树中依次插入10,50,40,30,20,60。插入完毕这些数据之后,伸展树的节点是60;此时,再旋转节点,使得30成为根节点。
依次插入10,50,40,30,20,60示意图如下:

将30旋转为根节点的示意图如下:

来源:https://www.cnblogs.com/skywang12345/p/3604286.html
