我们知道AVL树为了保持严格的平衡,所以在数据插入上会呈现过多的旋转,影响了插入和删除的性能,此时AVL的一个变种
伸展树(Splay)就应运而生了,我们知道万事万物都遵循一个“八二原则“,也就是说80%的人只会用到20%的数据,比如说我们
的“QQ输入法”,平常打的字也就那么多,或许还没有20%呢。
一:伸展树
1:思想
伸展树的原理就是这样的一个”八二原则”,比如我要查询树中的“节点7”,如果我们是AVL的思路,每次都查询“节点7”,那么当这
棵树中的节点越来越多的情况下就会呈现下旋,所以复杂度只会递增,伸展树的想法就是在第一次查询时树里面会经过一阵痉挛把
“节点7”顶成“根节点”,操作类似AVL的双旋转,比如下图:
当我们再次查询同样的”数字7“时,直接在根节点处O(1)取出,当然这算是一个最理想的情况,有时痉挛过度,会出现糟糕的”链表“,
也就退化了到O(N),所以伸展树讲究的是”摊还时间“,意思就是说在”连续的一系列操作中的平均时间“,当然可以保证是log(N)。
2:伸展方式
不知道大家可否记得,在AVL中的旋转要分4个情况,同样伸展树中的伸展需要考虑6种情况,当然不考虑镜像的话也就是3种情况,
从树的伸展方向上来说有“自下而上”和“自上而下"的两种方式,考虑到代码实现简洁,我还是说下后者。
<1> 自上而下的伸展
这种伸展方式会把树切成三份,L树,M树,R树,考虑的情况有:单旋转,“一字型”旋转,“之字形”旋转。
①: 单旋转
从图中我们可以看到,要将“节点2”插入到根上,需要将接近于“节点2”的数插入到根上,也就是这里的“节点7”,首先树被分成了3份,
初始情况,L和R树是“空节点”,M是整棵树,现在需要我们一步一步拆分,当我们将“节点2”试插入到“节点7”的左孩子时,发现“节点7”
就是父节点,满足“单旋转”情况,然后我们将整棵树放到“R树”中的left节点上,M此时是一个逻辑上的空节点,然后我们将R树追加到
M树中。L树追加到M的左子树中,最后我们将“节点2”插入到根节点上。说这么多有点拗口,伸展树比较难懂,需要大家仔细品味一下。
②: 一字型
一字型旋转方式与我们AVL中的“单旋转”类似,首先同样我们切成了三份,当我们"预插入20时”,发现20的“父节点”是根的右孩子,
而我们要插入的数字又在父节点的右边,此时满足”一字型“旋转,我们将7,10两个节点按照”右右情况”旋转,旋转后“节点10"的
左孩子放入到L树的right节点,"节点10”作为中间树M,最后将20插入根节点。
③: 之字形
之字形有点类似AVL中的“双旋转”,不过人家采取的策略是不一样的,当我们试插入“节点9”,同样发现“父节点”是根的右儿子,并且
“节点9”要插入到父节点的内侧,根据规则,需要将“父节点10”作为M树中的根节点,“节点7”作为L树中的right节点,然后M拼接L和R,
最后将节点9插入到根上。
3:基本操作
①:节点定义
我们还是采用普通二叉树中的节点定义,也就没有了AVL那么烦人的高度信息。
1 public class BinaryNode<T> 2 { 3 // Constructors 4 public BinaryNode(T theElement) : this(theElement, null, null) { } 5 6 public BinaryNode(T theElement, BinaryNode<T> lt, BinaryNode<T> rt) 7 { 8 element = theElement; 9 left = lt;10 right = rt;11 }12 13 public T element;14 15 public BinaryNode<T> left;16 17 public BinaryNode<T> right;18 }
②:伸展
这里为了编写代码方便,我采用的是逻辑nullNode节点,具体伸展逻辑大家可以看上面的图。
1 #region 伸展 2 /// <summary> 3 /// 伸展 4 /// </summary> 5 /// <param name="Key"></param> 6 /// <param name="tree"></param> 7 /// <returns></returns> 8 public BinaryNode<T> Splay(T Key, BinaryNode<T> tree) 9 {10 BinaryNode<T> leftTreeMax, rightTreeMin;11 12 header.left = header.right = nullNode;13 14 leftTreeMax = rightTreeMin = header;15 16 nullNode.element = Key;17 18 while (true)19 {20 int compareResult = Key.CompareTo(tree.element);21 22 if (compareResult < 0)23 {24 //如果成立,说明是”一字型“旋转25 if (Key.CompareTo(tree.left.element) < 0)26 tree = rotateWithLeftChild(tree);27 28 if (tree.left == nullNode)29 break;30 31 //动态的将中间树的”当前节点“追加到 R 树中,同时备份在header中32 rightTreeMin.left = tree;33 34 rightTreeMin = tree;35 36 tree = tree.left;37 }38 else if (compareResult > 0)39 {40 //如果成立,说明是”一字型“旋转41 if (Key.CompareTo(tree.right.element) > 0)42 tree = rotateWithRightChild(tree);43 44 if (tree.right == nullNode)45 break;46 47 //动态的将中间树的”当前节点“追加到 L 树中,同时备份在header中48 leftTreeMax.right = tree;49 50 leftTreeMax = tree;51 52 tree = tree.right;53 }54 else55 {56 break;57 }58 }59 60 /* 剥到最后一层,来最后一次切分 */61 //把中间树的左孩子给“左树”62 leftTreeMax.right = tree.left;63 64 //把中间树的右孩子给“右树”65 rightTreeMin.left = tree.right;66 67 /* 合并操作 */68 //将头节点的左树作为中间树的左孩子69 tree.left = header.right;70 71 //将头结点的右树作为中间树的右孩子72 tree.right = header.left;73 74 return tree;75 }76 #endregion
③:插入
插入操作关键在于我们要找到接近于”要插入点“的节点,然后顶成“根节点”,也就是上面三分图中的最后一分。
1 #region 插入 2 /// <summary> 3 /// 插入 4 /// </summary> 5 /// <param name="Key"></param> 6 public void Insert(T Key) 7 { 8 if (newNode == null) 9 newNode = new BinaryNode<T>(default(T));10 11 newNode.element = Key;12 13 if (root == nullNode)14 {15 newNode.left = newNode.right = nullNode;16 17 root = newNode;18 }19 else20 {21 root = Splay(Key, root);22 23 int compareResult = Key.CompareTo(root.element);24 25 if (compareResult < 0)26 {27 newNode.left = root.left;28 29 newNode.right = root;30 31 root.left = nullNode;32 33 root = newNode;34 }35 else36 if (compareResult > 0)37 {38 newNode.right = root.right;39 40 newNode.left = root;41 42 root.right = nullNode;43 44 root = newNode;45 }46 else47 return;48 }49 50 newNode = null;51 }52 #endregion
④:删除
删除操作也要将节点伸展到根上,然后进行删除,逻辑很简单。
1 #region 删除 2 /// <summary> 3 /// 删除 4 /// </summary> 5 /// <param name="Key"></param> 6 public void Remove(T Key) 7 { 8 BinaryNode<T> newTree; 9 10 //将删除结点顶到根节点11 root = Splay(Key, root);12 13 //不等于说明没有找到14 if (root.element.CompareTo(Key) != 0)15 return;16 17 //如果左边为空,则直接用root的右孩子接上去18 if (root.left == nullNode)19 {20 newTree = root.right;21 }22 else23 {24 newTree = root.left;25 26 newTree = Splay(Key, newTree);27 28 newTree.right = root.right;29 }30 root = newTree;31 }32 #endregion
总的运行代码如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace DataStructSplay 7 { 8 public class BinaryNode<T> 9 { 10 public BinaryNode(T theElement) : this(theElement, null, null) { } 11 12 public BinaryNode(T theElement, BinaryNode<T> lt, BinaryNode<T> rt) 13 { 14 element = theElement; 15 left = lt; 16 right = rt; 17 } 18 19 public T element; 20 21 public BinaryNode<T> left; 22 23 public BinaryNode<T> right; 24 } 25 26 public class SplayTree<T> where T : IComparable 27 { 28 public BinaryNode<T> root; 29 30 public BinaryNode<T> nullNode; 31 32 public BinaryNode<T> header = new BinaryNode<T>(default(T)); 33 34 public BinaryNode<T> newNode; 35 36 public SplayTree() 37 { 38 nullNode = new BinaryNode<T>(default(T)); 39 40 nullNode.left = nullNode.right = nullNode; 41 42 root = nullNode; 43 } 44 45 #region 插入 46 /// <summary> 47 /// 插入 48 /// </summary> 49 /// <param name="Key"></param> 50 public void Insert(T Key) 51 { 52 if (newNode == null) 53 newNode = new BinaryNode<T>(default(T)); 54 55 newNode.element = Key; 56 57 if (root == nullNode) 58 { 59 newNode.left = newNode.right = nullNode; 60 61 root = newNode; 62 } 63 else 64 { 65 root = Splay(Key, root); 66 67 int compareResult = Key.CompareTo(root.element); 68 69 if (compareResult < 0) 70 { 71 newNode.left = root.left; 72 73 newNode.right = root; 74 75 root.left = nullNode; 76 77 root = newNode; 78 } 79 else 80 if (compareResult > 0) 81 { 82 newNode.right = root.right; 83 84 newNode.left = root; 85 86 root.right = nullNode; 87 88 root = newNode; 89 } 90 else 91 return; 92 } 93 94 newNode = null; 95 } 96 #endregion 97 98 #region 是否包含 99 /// <summary>100 /// 是否包含101 /// </summary>102 /// <param name="Key"></param>103 /// <returns></returns>104 public bool Contains(T Key)105 {106 if (isEmpty())107 return false;108 109 root = Splay(Key, root);110 111 return root.element.CompareTo(Key) == 0;112 }113 #endregion114 115 #region 判断是否为空116 /// <summary>117 /// 判断是否为空118 /// </summary>119 /// <returns></returns>120 public bool isEmpty()121 {122 return root == nullNode;123 }124 #endregion125 126 #region 伸展127 /// <summary>128 /// 伸展129 /// </summary>130 /// <param name="Key"></param>131 /// <param name="tree"></param>132 /// <returns></returns>133 public BinaryNode<T> Splay(T Key, BinaryNode<T> tree)134 {135 BinaryNode<T> leftTreeMax, rightTreeMin;136 137 header.left = header.right = nullNode;138 139 leftTreeMax = rightTreeMin = header;140 141 nullNode.element = Key;142 143 while (true)144 {145 int compareResult = Key.CompareTo(tree.element);146 147 if (compareResult < 0)148 {149 //如果成立,说明是”一字型“旋转150 if (Key.CompareTo(tree.left.element) < 0)151 tree = rotateWithLeftChild(tree);152 153 if (tree.left == nullNode)154 break;155 156 //动态的将中间树的”当前节点“追加到 R 树中,同时备份在header中157 rightTreeMin.left = tree;158 159 rightTreeMin = tree;160 161 tree = tree.left;162 }163 else if (compareResult > 0)164 {165 //如果成立,说明是”一字型“旋转166 if (Key.CompareTo(tree.right.element) > 0)167 tree = rotateWithRightChild(tree);168 169 if (tree.right == nullNode)170 break;171 172 //动态的将中间树的”当前节点“追加到 L 树中,同时备份在header中173 leftTreeMax.right = tree;174 175 leftTreeMax = tree;176 177 tree = tree.right;178 }179 else180 {181 break;182 }183 }184 185 /* 剥到最后一层,来最后一次切分 */186 //把中间树的左孩子给“左树”187 leftTreeMax.right = tree.left;188 189 //把中间树的右孩子给“右树”190 rightTreeMin.left = tree.right;191 192 /* 合并操作 */193 //将头节点的左树作为中间树的左孩子194 tree.left = header.right;195 196 //将头结点的右树作为中间树的右孩子197 tree.right = header.left;198 199 return tree;200 }201 #endregion202 203 #region 删除204 /// <summary>205 /// 删除206 /// </summary>207 /// <param name="Key"></param>208 public void Remove(T Key)209 {210 BinaryNode<T> newTree;211 212 //将删除结点顶到根节点213 root = Splay(Key, root);214 215 //不等于说明没有找到216 if (root.element.CompareTo(Key) != 0)217 return;218 219 //如果左边为空,则直接用root的右孩子接上去220 if (root.left == nullNode)221 {222 newTree = root.right;223 }224 else225 {226 newTree = root.left;227 228 newTree = Splay(Key, newTree);229 230 newTree.right = root.right;231 }232 root = newTree;233 }234 #endregion235 236 #region 右旋转237 /// <summary>238 /// 右旋转239 /// </summary>240 /// <param name="k1"></param>241 /// <returns></returns>242 public BinaryNode<T> rotateWithRightChild(BinaryNode<T> k1)243 {244 BinaryNode<T> k2 = k1.right;245 k1.right = k2.left;246 k2.left = k1;247 return k2;248 }249 #endregion250 251 #region 左旋转252 /// <summary>253 /// 左旋转254 /// </summary>255 /// <param name="k2"></param>256 /// <returns></returns>257 public BinaryNode<T> rotateWithLeftChild(BinaryNode<T> k2)258 {259 BinaryNode<T> k1 = k2.left;260 k2.left = k1.right;261 k1.right = k2;262 return k1;263 }264 #endregion265 }266 }
伸展树可以总结成一幅图: