Java数据结构与算法解析——伸展树
<h2>伸展树简介</h2> <p>伸展树(Splay Tree)是特殊的二叉查找树。</p> <p>它的特殊是指,它除了本身是棵二叉查找树之外,它还具备一个特点: 当某个节点被访问时,伸展树会通过旋转使该节点成为树根。这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点。</p> <h2>特性</h2> <p>1.和普通的二叉查找树相比,具有任何情况下、任何操作的平摊O(log2n)的复杂度,时间性能上更好</p> <p>2.和一般的平衡二叉树比如 红黑树、AVL树相比,维护更少的节点额外信息,空间性能更优,同时编程复杂度更低</p> <p>3.在很多情况下,对于查找操作,后面的查询和之前的查询有很大的相关性。这样每次查询操作将被查到的节点旋转到树的根节点位置,这样下次查询操作可以很快的完成</p> <p>4.可以完成对区间的查询、修改、删除等操作,可以实现线段树和树状数组的所有功能</p> <h2>旋转</h2> <p>伸展树实现O(log2n)量级的平摊复杂度依靠每次对伸展树进行查询、修改、删除操作之后,都进行旋转操作 Splay(x, root),该操作将节点x旋转到树的根部。</p> <p>伸展树的旋转有六种类型,如果去掉镜像的重复,则为三种:zig(zag)、zig-zig(zag-zag)、zig-zag(zag-zig)。</p> <p>1 自底向上的方式进行旋转</p> <p>1.1 zig旋转 <img src="https://simg.open-open.com/show/14986cfd4b87403ffb2e0266190ffa8e.png"></p> <p>如图所示,x节点的父节点为y,x为y的左子节点,且y节点为根。则只需要对x节点进行一次右旋(zig操作),使之成为y的父节点,就可以使x成为伸展树的根节点。</p> <p>1.2 zig-zig旋转 <img src="https://simg.open-open.com/show/60b7e321117c0172893de8fb8e7d7cb0.png"></p> <p>如上图所示,x节点的父节点y,y的父节点z,三者在一字型链上。此时,先对y节点和z节点进行zig旋转,然后再对x节点和y节点进行zig旋转,最后变为右图所示,x成为y和z的祖先节点。</p> <p>1.3 zig-zag旋转 <img src="https://simg.open-open.com/show/92b9fbef1b25e4241cd1f36c66be25b1.png"></p> <p>如上图所示,x节点的父节点y,y的父节点z,三者在之字型链上。此时,先对x节点和y节点进行zig旋转,然后再对x节点和y节点进行zag旋转,最后变为右图所示,x成为y和z的祖先节点。</p> <p>2 自顶向下的方式进行旋转这种方式不需要节点存储其父节点的引用。当我们沿着树向下搜索某个节点x时,将搜索路径上的节点及其子树移走。构建两棵临时的树——左树和右树。没有被移走的节点构成的树称为中树。</p> <p>(1) 当前节点x是中树的根</p> <p>(2) 左树L保存小于x的节点</p> <p>(3) 右树R保存大于x的节点</p> <p>开始时候,x是树T的根,左树L和右树R都为空。三种旋转操作:</p> <p><strong>2.1 zig旋转</strong> <img src="https://simg.open-open.com/show/c7727891171f757fc243621bb05edb55.png"></p> <p>如图所示,x节点的子节点y就是我们要找的节点,则只需要对y节点进行一次右旋(zig操作),使之成为x的父节点,就可以使y成为伸展树的根节点。将y作为中树的根,同时,x节点移动到右树R中,显然右树上的节点都大于所要查找的节点。</p> <p>2.2 zig-zig旋转 <img src="https://simg.open-open.com/show/d6947ce14b6d35e308c156a2631afd64.png"> 如上图所示,x节点的左子节点y,y的左子节点z,三者在一字型链上,且要查找的节点位于z节点为根的子树中。此时,对x节点和y节点进行zig,然后对z和y进行zig,使z成为中树的根,同时将y及其子树挂载到右树R上。</p> <p>2.3 zig-zag旋转</p> <p><img src="https://simg.open-open.com/show/7b19fcebf0f6f91ad51e1715a2946e47.png"></p> <p>如上图所示,x节点的左子节点y,y的右子节点z,三者在之字型链上,且需要查找的元素位于以z为根的子树上。此时,先对x节点和y节点进行zig旋转,将x及其右子树挂载到右树R上,此时y成为中树的根节点;然后再对z节点和y节点进行zag旋转,使得z成为中树的根节点。</p> <p>2.4 合并 <img src="https://simg.open-open.com/show/c2da63e45a1e5b709b4b96f1108a211b.png"></p> <p>最后,找到节点或者遇到空节点之后,需要对左、中、右树进行合并。如图所示,将左树挂载到中树的最左下方(满足遍历顺序要求),将右树挂载到中树的最右下方(满足遍历顺序要求)。</p> <h2>伸展树的实现</h2> <h2>1.节点</h2> <pre> <code class="language-java">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; } } }</code></pre> <p>SplayTree是伸展树,而SplayTreeNode是伸展树节点。在此,我将SplayTreeNode定义为SplayTree的内部类。在伸展树SplayTree中包含了伸展树的根节点mRoot。SplayTreeNode包括的几个组成元素:</p> <p>(1) key – 是关键字,是用来对伸展树的节点进行排序的。</p> <p>(2) left – 是左孩子。</p> <p>(3) right – 是右孩子。</p> <h2>2.旋转</h2> <pre> <code class="language-java">/* * 旋转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 (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); } }</code></pre> <p>上面的代码的作用:将”键值为key的节点”旋转为根节点,并返回根节点。它的处理情况共包括:</p> <p>(a):伸展树中存在”键值为key的节点”。</p> <p>将”键值为key的节点”旋转为根节点。</p> <p>(b):伸展树中不存在”键值为key的节点”,并且key < tree->key。</p> <p>b-1) “键值为key的节点”的前驱节点存在的话,将”键值为key的节点”的前驱节点旋转为根节点。</p> <p>b-2) “键值为key的节点”的前驱节点存在的话,则意味着,key比树中任何键值都小,那么此时,将最小节点旋转为根节点。</p> <p>(c):伸展树中不存在”键值为key的节点”,并且key > tree->key。</p> <p>c-1) “键值为key的节点”的后继节点存在的话,将”键值为key的节点”的后继节点旋转为根节点。</p> <p>c-2) “键值为key的节点”的后继节点不存在的话,则意味着,key比树中任何键值都大,那么此时,将最大节点旋转为根节点。</p> <p>下面列举个例子分别对a进行说明。</p> <p>在下面的伸展树中查找10,,共包括”右旋” –> “右链接” –> “组合”这3步。</p> <p><img src="https://simg.open-open.com/show/50de184e0dc19d2ee3ecf31c158a0a2d.jpg"></p> <p>01, 右旋</p> <p>对应代码中的”rotate right”部分</p> <p><img src="https://simg.open-open.com/show/324f0dd304d42e781e87bc2069616164.jpg"></p> <p>02, 右链接</p> <p>对应代码中的”link right”部分</p> <p><img src="https://simg.open-open.com/show/9f01384161c74924244958eacaf544c2.jpg"></p> <p>03.组合</p> <p>对应代码中的”assemble”部分</p> <p><img src="https://simg.open-open.com/show/41c8a96a73bd865658716018073cd87c.jpg"></p> <p>提示:如果在上面的伸展树中查找”70”,则正好与”示例1”对称,而对应的操作则分别是”rotate left”, “link left”和”assemble”。</p> <p>其它的情况,例如”查找15是b-1的情况,查找5是b-2的情况”等等,这些都比较简单,大家可以自己分析。</p> <h2>3.插入</h2> <pre> <code class="language-java">/** * 将结点插入到伸展树中,并返回根节点 * @param tree 伸展树的根节点 * @param z 插入的结点 * @return */ 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); }</code></pre> <p>insert(key)是提供给外部的接口,它的作用是新建节点(节点的键值为key),并将节点插入到伸展树中;然后,将该节点旋转为根节点。</p> <p>insert(tree, z)是内部接口,它的作用是将节点z插入到tree中。insert(tree, z)在将z插入到tree中时,仅仅只将tree当作是一棵二叉查找树,而且不允许插入相同节点。</p> <h2>4.删除</h2> <pre> <code class="language-java">/** * * @param tree 伸展树 * @param key 删除的结点 * @return */ 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); }</code></pre> <p>remove(key)是外部接口,remove(tree, key)是内部接口。</p> <p>remove(tree, key)的作用是:删除伸展树中键值为key的节点。</p> <p>它会先在伸展树中查找键值为key的节点。若没有找到的话,则直接返回。若找到的话,则将该节点旋转为根节点,然后再删除该节点。</p> <p>伸展树实现完整代码</p> <pre> <code class="language-java">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; } } /* * 旋转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 (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); } /** * 将结点插入到伸展树中,并返回根节点 * @param tree 伸展树的根节点 * @param z 插入的结点 * @return */ 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); } /* * 删除结点(z),并返回被删除的结点 * * 参数说明: * bst 伸展树 * z 删除的结点 */ /** * * @param tree 伸展树 * @param key 删除的结点 * @return */ 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); } /* * (递归实现)查找"伸展树x"中键值为key的节点 */ private SplayTreeNode<T> search(SplayTreeNode<T> x, T key) { if (x==null) return x; int cmp = key.compareTo(x.key); if (cmp < 0) return search(x.left, key); else if (cmp > 0) return search(x.right, key); else return x; } public SplayTreeNode<T> search(T key) { return search(mRoot, key); } /* * 查找最小结点:返回tree为根结点的伸展树的最小结点。 */ private SplayTreeNode<T> minimum(SplayTreeNode<T> tree) { if (tree == null) return null; while(tree.left != null) tree = tree.left; return tree; } public T minimum() { SplayTreeNode<T> p = minimum(mRoot); if (p != null) return p.key; return null; } /* * 查找最大结点:返回tree为根结点的伸展树的最大结点。 */ private SplayTreeNode<T> maximum(SplayTreeNode<T> tree) { if (tree == null) return null; while(tree.right != null) tree = tree.right; return tree; } public T maximum() { SplayTreeNode<T> p = maximum(mRoot); if (p != null) return p.key; return null; } }</code></pre> <p> </p> <p>来自:http://blog.csdn.net/u012124438/article/details/78067998</p> <p> </p>
本文由用户 GreOConnor 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!