数据结构(二) --- 伸展树(Splay Tree)

     文章图片和代码来自邓俊辉老师课件

概述

        伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的。(出处百度百科)

        它的操作就是将访问到的元素放在根节点处。主要的操作就是 zip 和 zag

08A1-4 逐层伸展.mp4_20190130_202558.485

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

splaytree

 

算法分析

双层伸展

08A2-1 伸展树:双层伸展 双层伸展.mp4_20190130_203507.032

      双层伸展的作用是提升了树平均的访问性能。构思的精髓 : 向上追溯两层,而非一层。 右下角是伸展树需要处理的四种情况。具体的处理是怎么样的呢?

      双层伸展主要在 zig-zip 和 zag-zag 的情况下发挥作用,例如要使v 升到根节点,双层伸展要求我们使用先对祖父节点zig,然后再做一次zip ,即是下部分三幅图演示的那样。下面我们看一下使用这种方法真的可以提升性能吗。

 

08A2-3 子孙同侧.mp4_20190130_205041.810

 

分摊性能

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

 

08A2-6 分摊性能.mp4_20190130_210026.435

 

算法实现

       代码是根绝邓老师的提供的代码用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后续的使用。

 

综合评价

08A3-7 综合评价.mp4_20190201_154122.152

       不能保证单次最坏情况的出现的原因是,假如我们一开始要找的那个点就在最底下,那么就可能达到了最坏的情况。

 

参考资料

  • 邓俊辉老师的数据结构课程
posted @ 2019-02-01 15:46  float123  阅读(971)  评论(0编辑  收藏  举报