概要
前面分別通過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旋轉為根節點的示意圖如下:

