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 else 55 { 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 }
③:插入
插入操作關鍵在於我們要找到接近於”要插入點“的節點,然後頂成“根節點”,也就是上面三分圖中的最後一分。
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 else 20 { 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 else 36 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 else 47 return; 48 } 49 50 newNode = null; 51 }
④:刪除
刪除操作也要將節點伸展到根上,然後進行刪除,邏輯很簡單。
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 else 23 { 24 newTree = root.left; 25 26 newTree = Splay(Key, newTree); 27 28 newTree.right = root.right; 29 } 30 root = newTree; 31 }