伸展树(Splay Tree)是特殊的二叉查找树。它的特殊是指,它除了本身是棵二叉查找树之外,它还具备一个特点: 当某个节点被访问时,伸展树会通过旋转使该节点成为树根。这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点。

Java数据结构与算法解析(八)——伸展树 Java数据结构与算法解析(八)——伸展树(java 树 数据结构)  Java 数据结构 算法解析 第1张

伸展树简介

伸展树(Splay Tree)是特殊的二叉查找树。

它的特殊是指,它除了本身是棵二叉查找树之外,它还具备一个特点: 当某个节点被访问时,伸展树会通过旋转使该节点成为树根。这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点。

特性

  1. 和普通的二叉查找树相比,具有任何情况下、任何操作的平摊O(log2n)的复杂度,时间性能上更好
  2. 和一般的平衡二叉树比如 红黑树、AVL树相比,维护更少的节点额外信息,空间性能更优,同时编程复杂度更低
  3. 在很多情况下,对于查找操作,后面的查询和之前的查询有很大的相关性。这样每次查询操作将被查到的节点旋转到树的根节点位置,这样下次查询操作可以很快的完成
  4. 可以完成对区间的查询、修改、删除等操作,可以实现线段树和树状数组的所有功能

旋转

伸展树实现O(log2n)量级的平摊复杂度依靠每次对伸展树进行查询、修改、删除操作之后,都进行旋转操作 Splay(x, root),该操作将节点x旋转到树的根部。

伸展树的旋转有六种类型,如果去掉镜像的重复,则为三种:zig(zag)、zig-zig(zag-zag)、zig-zag(zag-zig)。

1 自底向上的方式进行旋转

1.1 zig旋转

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第2张

如图所示,x节点的父节点为y,x为y的左子节点,且y节点为根。则只需要对x节点进行一次右旋(zig操作),使之成为y的父节点,就可以使x成为伸展树的根节点。

1.2 zig-zig旋转

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第3张

如上图所示,x节点的父节点y,y的父节点z,三者在一字型链上。此时,先对y节点和z节点进行zig旋转,然后再对x节点和y节点进行zig旋转,最后变为右图所示,x成为y和z的祖先节点。

1.3 zig-zag旋转

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第4张

如上图所示,x节点的父节点y,y的父节点z,三者在之字型链上。此时,先对x节点和y节点进行zig旋转,然后再对x节点和y节点进行zag旋转,最后变为右图所示,x成为y和z的祖先节点。

2 自顶向下的方式进行旋转

这种方式不需要节点存储其父节点的引用。当我们沿着树向下搜索某个节点x时,将搜索路径上的节点及其子树移走。构建两棵临时的树——左树和右树。没有被移走的节点构成的树称为中树。

  1. 当前节点x是中树的根
  2. 左树L保存小于x的节点
  3. 右树R保存大于x的节点

开始时候,x是树T的根,左树L和右树R都为空。三种旋转操作:

2.1 zig旋转

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第5张

如图所示,x节点的子节点y就是我们要找的节点,则只需要对y节点进行一次右旋(zig操作),使之成为x的父节点,就可以使y成为伸展树的根节点。将y作为中树的根,同时,x节点移动到右树R中,显然右树上的节点都大于所要查找的节点。

2.2 zig-zig旋转

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第6张

如上图所示,x节点的左子节点y,y的左子节点z,三者在一字型链上,且要查找的节点位于z节点为根的子树中。此时,对x节点和y节点进行zig,然后对z和y进行zig,使z成为中树的根,同时将y及其子树挂载到右树R上。

2.3 zig-zag旋转

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第7张

如上图所示,x节点的左子节点y,y的右子节点z,三者在之字型链上,且需要查找的元素位于以z为根的子树上。此时,先对x节点和y节点进行zig旋转,将x及其右子树挂载到右树R上,此时y成为中树的根节点;然后再对z节点和y节点进行zag旋转,使得z成为中树的根节点。

2.4 合并

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第8张

最后,找到节点或者遇到空节点之后,需要对左、中、右树进行合并。如图所示,将左树挂载到中树的最左下方(满足遍历顺序要求),将右树挂载到中树的最右下方(满足遍历顺序要求)。

伸展树的实现

1.节点

  1. publicclassSplayTree<TextendsComparable<T>>{
  2. privateSplayTreeNode<T>mRoot;//根结点
  3. publicclassSplayTreeNode<TextendsComparable<T>>{
  4. Tkey;//关键字(键值)
  5. SplayTreeNode<T>left;//左孩子
  6. SplayTreeNode<T>right;//右孩子
  7. publicSplayTreeNode(){
  8. this.left=null;
  9. this.right=null;
  10. }
  11. publicSplayTreeNode(Tkey,SplayTreeNode<T>left,SplayTreeNode<T>right){
  12. this.key=key;
  13. this.left=left;
  14. this.right=right;
  15. }
  16. }
  17. }

SplayTree是伸展树,而SplayTreeNode是伸展树节点。在此,我将SplayTreeNode定义为SplayTree的内部类。在伸展树SplayTree中包含了伸展树的根节点mRoot。SplayTreeNode包括的几个组成元素:

  1. key – 是关键字,是用来对伸展树的节点进行排序的。
  2. left – 是左孩子。
  3. right – 是右孩子。

2.旋转

  1. /*
  2. *旋转key对应的节点为根节点,并返回根节点。
  3. *
  4. *注意:
  5. *(a):伸展树中存在"键值为key的节点"
  6. *将"键值为key的节点"旋转为根节点。
  7. *(b):伸展树中不存在"键值为key的节点",并且key<tree.key
  8. *b-1"键值为key的节点"的前驱节点存在的话,将"键值为key的节点"的前驱节点旋转为根节点。
  9. *b-2"键值为key的节点"的前驱节点存在的话,则意味着,key比树中任何键值都小,那么此时,将最小节点旋转为根节点。
  10. *(c):伸展树中不存在"键值为key的节点",并且key>tree.key
  11. *c-1"键值为key的节点"的后继节点存在的话,将"键值为key的节点"的后继节点旋转为根节点。
  12. *c-2"键值为key的节点"的后继节点不存在的话,则意味着,key比树中任何键值都大,那么此时,将最大节点旋转为根节点。
  13. */
  14. privateSplayTreeNode<T>splay(SplayTreeNode<T>tree,Tkey){
  15. if(tree==null)
  16. returntree;
  17. SplayTreeNode<T>N=newSplayTreeNode<T>();
  18. SplayTreeNode<T>l=N;
  19. SplayTreeNode<T>r=N;
  20. SplayTreeNode<T>c;
  21. for(;;){
  22. intcmp=key.compareTo(tree.key);
  23. if(cmp<0){
  24. if(key.compareTo(tree.left.key)<0){
  25. c=tree.left;/*rotateright*/
  26. tree.left=c.right;
  27. c.right=tree;
  28. tree=c;
  29. if(tree.left==null)
  30. break;
  31. }
  32. r.left=tree;/*linkright*/
  33. r=tree;
  34. tree=tree.left;
  35. }elseif(cmp>0){
  36. if(tree.right==null)
  37. break;
  38. if(key.compareTo(tree.right.key)>0){
  39. c=tree.right;/*rotateleft*/
  40. tree.right=c.left;
  41. c.left=tree;
  42. tree=c;
  43. if(tree.right==null)
  44. break;
  45. }
  46. l.right=tree;/*linkleft*/
  47. l=tree;
  48. tree=tree.right;
  49. }else{
  50. break;
  51. }
  52. }
  53. l.right=tree.left;/*assemble*/
  54. r.left=tree.right;
  55. tree.left=N.right;
  56. tree.right=N.left;
  57. returntree;
  58. }
  59. publicvoidsplay(Tkey){
  60. mRoot=splay(mRoot,key);
  61. }

上面的代码的作用:将”键值为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步。

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第9张

01, 右旋

对应代码中的”rotate right”部分

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第10张

02, 右链接

对应代码中的”link right”部分

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第11张

03.组合

对应代码中的”assemble”部分

 Java数据结构与算法解析(八)——伸展树(java 树 数据结构) Java 数据结构 算法解析 第12张

提示:如果在上面的伸展树中查找”70”,则正好与”示例1”对称,而对应的操作则分别是”rotate left”, “link left”和”assemble”。

其它的情况,例如”查找15是b-1的情况,查找5是b-2的情况”等等,这些都比较简单,大家可以自己分析。

3.插入

  1. /**
  2. *将结点插入到伸展树中,并返回根节点
  3. *@paramtree伸展树的根节点
  4. *@paramz插入的结点
  5. *@return
  6. */
  7. privateSplayTreeNode<T>insert(SplayTreeNode<T>tree,SplayTreeNode<T>z){
  8. intcmp;
  9. SplayTreeNode<T>y=null;
  10. SplayTreeNode<T>x=tree;
  11. //查找z的插入位置
  12. while(x!=null){
  13. y=x;
  14. cmp=z.key.compareTo(x.key);
  15. if(cmp<0)
  16. x=x.left;
  17. elseif(cmp>0)
  18. x=x.right;
  19. else{
  20. System.out.printf("不允许插入相同节点(%d)!\n",z.key);
  21. z=null;
  22. returntree;
  23. }
  24. }
  25. if(y==null)
  26. tree=z;
  27. else{
  28. cmp=z.key.compareTo(y.key);
  29. if(cmp<0)
  30. y.left=z;
  31. else
  32. y.right=z;
  33. }
  34. returntree;
  35. }
  36. publicvoidinsert(Tkey){
  37. SplayTreeNode<T>z=newSplayTreeNode<T>(key,null,null);
  38. //如果新建结点失败,则返回。
  39. if((z=newSplayTreeNode<T>(key,null,null))==null)
  40. return;
  41. //插入节点
  42. mRoot=insert(mRoot,z);
  43. //将节点(key)旋转为根节点
  44. mRoot=splay(mRoot,key);
  45. }

insert(key)是提供给外部的接口,它的作用是新建节点(节点的键值为key),并将节点插入到伸展树中;然后,将该节点旋转为根节点。

insert(tree, z)是内部接口,它的作用是将节点z插入到tree中。insert(tree, z)在将z插入到tree中时,仅仅只将tree当作是一棵二叉查找树,而且不允许插入相同节点。

4.删除

  1. /**
  2. *
  3. *@paramtree伸展树
  4. *@paramkey删除的结点
  5. *@return
  6. */
  7. privateSplayTreeNode<T>remove(SplayTreeNode<T>tree,Tkey){
  8. SplayTreeNode<T>x;
  9. if(tree==null)
  10. returnnull;
  11. //查找键值为key的节点,找不到的话直接返回。
  12. if(search(tree,key)==null)
  13. returntree;
  14. //将key对应的节点旋转为根节点。
  15. tree=splay(tree,key);
  16. if(tree.left!=null){
  17. //将"tree的前驱节点"旋转为根节点
  18. x=splay(tree.left,key);
  19. //移除tree节点
  20. x.right=tree.right;
  21. }
  22. else
  23. x=tree.right;
  24. tree=null;
  25. returnx;
  26. }
  27. publicvoidremove(Tkey){
  28. mRoot=remove(mRoot,key);
  29. }

remove(key)是外部接口,remove(tree, key)是内部接口。

remove(tree, key)的作用是:删除伸展树中键值为key的节点。

它会先在伸展树中查找键值为key的节点。若没有找到的话,则直接返回。若找到的话,则将该节点旋转为根节点,然后再删除该节点。

伸展树实现完整代码

  1. publicclassSplayTree<TextendsComparable<T>>{
  2. privateSplayTreeNode<T>mRoot;//根结点
  3. publicclassSplayTreeNode<TextendsComparable<T>>{
  4. Tkey;//关键字(键值)
  5. SplayTreeNode<T>left;//左孩子
  6. SplayTreeNode<T>right;//右孩子
  7. publicSplayTreeNode(){
  8. this.left=null;
  9. this.right=null;
  10. }
  11. publicSplayTreeNode(Tkey,SplayTreeNode<T>left,SplayTreeNode<T>right){
  12. this.key=key;
  13. this.left=left;
  14. this.right=right;
  15. }
  16. }
  17. /*
  18. *旋转key对应的节点为根节点,并返回根节点。
  19. *
  20. *注意:
  21. *(a):伸展树中存在"键值为key的节点"
  22. *将"键值为key的节点"旋转为根节点。
  23. *(b):伸展树中不存在"键值为key的节点",并且key<tree.key
  24. *b-1"键值为key的节点"的前驱节点存在的话,将"键值为key的节点"的前驱节点旋转为根节点。
  25. *b-2"键值为key的节点"的前驱节点存在的话,则意味着,key比树中任何键值都小,那么此时,将最小节点旋转为根节点。
  26. *(c):伸展树中不存在"键值为key的节点",并且key>tree.key
  27. *c-1"键值为key的节点"的后继节点存在的话,将"键值为key的节点"的后继节点旋转为根节点。
  28. *c-2"键值为key的节点"的后继节点不存在的话,则意味着,key比树中任何键值都大,那么此时,将最大节点旋转为根节点。
  29. */
  30. privateSplayTreeNode<T>splay(SplayTreeNode<T>tree,Tkey){
  31. if(tree==null)
  32. returntree;
  33. SplayTreeNode<T>N=newSplayTreeNode<T>();
  34. SplayTreeNode<T>l=N;
  35. SplayTreeNode<T>r=N;
  36. SplayTreeNode<T>c;
  37. for(;;){
  38. intcmp=key.compareTo(tree.key);
  39. if(cmp<0){
  40. if(key.compareTo(tree.left.key)<0){
  41. c=tree.left;/*rotateright*/
  42. tree.left=c.right;
  43. c.right=tree;
  44. tree=c;
  45. if(tree.left==null)
  46. break;
  47. }
  48. r.left=tree;/*linkright*/
  49. r=tree;
  50. tree=tree.left;
  51. }elseif(cmp>0){
  52. if(tree.right==null)
  53. break;
  54. if(key.compareTo(tree.right.key)>0){
  55. c=tree.right;/*rotateleft*/
  56. tree.right=c.left;
  57. c.left=tree;
  58. tree=c;
  59. if(tree.right==null)
  60. break;
  61. }
  62. l.right=tree;/*linkleft*/
  63. l=tree;
  64. tree=tree.right;
  65. }else{
  66. break;
  67. }
  68. }
  69. l.right=tree.left;/*assemble*/
  70. r.left=tree.right;
  71. tree.left=N.right;
  72. tree.right=N.left;
  73. returntree;
  74. }
  75. publicvoidsplay(Tkey){
  76. mRoot=splay(mRoot,key);
  77. }
  78. /**
  79. *将结点插入到伸展树中,并返回根节点
  80. *@paramtree伸展树的根节点
  81. *@paramz插入的结点
  82. *@return
  83. */
  84. privateSplayTreeNode<T>insert(SplayTreeNode<T>tree,SplayTreeNode<T>z){
  85. intcmp;
  86. SplayTreeNode<T>y=null;
  87. SplayTreeNode<T>x=tree;
  88. //查找z的插入位置
  89. while(x!=null){
  90. y=x;
  91. cmp=z.key.compareTo(x.key);
  92. if(cmp<0)
  93. x=x.left;
  94. elseif(cmp>0)
  95. x=x.right;
  96. else{
  97. System.out.printf("不允许插入相同节点(%d)!\n",z.key);
  98. z=null;
  99. returntree;
  100. }
  101. }
  102. if(y==null)
  103. tree=z;
  104. else{
  105. cmp=z.key.compareTo(y.key);
  106. if(cmp<0)
  107. y.left=z;
  108. else
  109. y.right=z;
  110. }
  111. returntree;
  112. }
  113. publicvoidinsert(Tkey){
  114. SplayTreeNode<T>z=newSplayTreeNode<T>(key,null,null);
  115. //如果新建结点失败,则返回。
  116. if((z=newSplayTreeNode<T>(key,null,null))==null)
  117. return;
  118. //插入节点
  119. mRoot=insert(mRoot,z);
  120. //将节点(key)旋转为根节点
  121. mRoot=splay(mRoot,key);
  122. }
  123. /*
  124. *删除结点(z),并返回被删除的结点
  125. *
  126. *参数说明:
  127. *bst伸展树
  128. *z删除的结点
  129. */
  130. /**
  131. *
  132. *@paramtree伸展树
  133. *@paramkey删除的结点
  134. *@return
  135. */
  136. privateSplayTreeNode<T>remove(SplayTreeNode<T>tree,Tkey){
  137. SplayTreeNode<T>x;
  138. if(tree==null)
  139. returnnull;
  140. //查找键值为key的节点,找不到的话直接返回。
  141. if(search(tree,key)==null)
  142. returntree;
  143. //将key对应的节点旋转为根节点。
  144. tree=splay(tree,key);
  145. if(tree.left!=null){
  146. //将"tree的前驱节点"旋转为根节点
  147. x=splay(tree.left,key);
  148. //移除tree节点
  149. x.right=tree.right;
  150. }
  151. else
  152. x=tree.right;
  153. tree=null;
  154. returnx;
  155. }
  156. publicvoidremove(Tkey){
  157. mRoot=remove(mRoot,key);
  158. }
  159. /*
  160. *(递归实现)查找"伸展树x"中键值为key的节点
  161. */
  162. privateSplayTreeNode<T>search(SplayTreeNode<T>x,Tkey){
  163. if(x==null)
  164. returnx;
  165. intcmp=key.compareTo(x.key);
  166. if(cmp<0)
  167. returnsearch(x.left,key);
  168. elseif(cmp>0)
  169. returnsearch(x.right,key);
  170. else
  171. returnx;
  172. }
  173. publicSplayTreeNode<T>search(Tkey){
  174. returnsearch(mRoot,key);
  175. }
  176. /*
  177. *查找最小结点:返回tree为根结点的伸展树的最小结点。
  178. */
  179. privateSplayTreeNode<T>minimum(SplayTreeNode<T>tree){
  180. if(tree==null)
  181. returnnull;
  182. while(tree.left!=null)
  183. tree=tree.left;
  184. returntree;
  185. }
  186. publicTminimum(){
  187. SplayTreeNode<T>p=minimum(mRoot);
  188. if(p!=null)
  189. returnp.key;
  190. returnnull;
  191. }
  192. /*
  193. *查找最大结点:返回tree为根结点的伸展树的最大结点。
  194. */
  195. privateSplayTreeNode<T>maximum(SplayTreeNode<T>tree){
  196. if(tree==null)
  197. returnnull;
  198. while(tree.right!=null)
  199. tree=tree.right;
  200. returntree;
  201. }
  202. publicTmaximum(){
  203. SplayTreeNode<T>p=maximum(mRoot);
  204. if(p!=null)
  205. returnp.key;
  206. returnnull;
  207. }
转载请说明出处
知优网 » Java数据结构与算法解析(八)——伸展树(java 树 数据结构)

发表评论

您需要后才能发表评论