数据结构与算法专题——第十题 输入法跳不过的坎-伸展树
我們知道AVL樹為了保持嚴(yán)格的平衡,所以在數(shù)據(jù)插入上會呈現(xiàn)過多的旋轉(zhuǎn),影響了插入和刪除的性能,此時AVL的一個變種伸展樹(Splay)就應(yīng)運而生了,我們知道萬事萬物都遵循一個“八二原則“,也就是說80%的人只會用到20%的數(shù)據(jù),比如說我們的“QQ輸入法”,平常打的字也就那么多,或許還沒有20%呢。
一:伸展樹
1:思想
伸展樹的原理就是這樣的一個”八二原則”,比如我要查詢樹中的“節(jié)點7”,如果我們是AVL的思路,每次都查詢“節(jié)點7”,那么當(dāng)這棵樹中的節(jié)點越來越多的情況下就會呈現(xiàn)下旋,所以復(fù)雜度只會遞增,伸展樹的想法就是在第一次查詢時樹里面會經(jīng)過一陣痙攣把“節(jié)點7”頂成“根節(jié)點”,操作類似AVL的雙旋轉(zhuǎn),比如下圖:
當(dāng)我們再次查詢同樣的”數(shù)字7“時,直接在根節(jié)點處O(1)取出,當(dāng)然這算是一個最理想的情況,有時痙攣過度,會出現(xiàn)糟糕的”鏈表“,也就退化了到O(N),所以伸展樹講究的是”攤還時間“,意思就是說在”連續(xù)的一系列操作中的平均時間“,當(dāng)然可以保證是log(N)。
2:伸展方式
不知道大家可否記得,在AVL中的旋轉(zhuǎn)要分4個情況,同樣伸展樹中的伸展需要考慮6種情況,當(dāng)然不考慮鏡像的話也就是3種情況,從樹的伸展方向上來說有“自下而上”和“自上而下"的兩種方式,考慮到代碼實現(xiàn)簡潔,我還是說下后者。
1) 自上而下的伸展
這種伸展方式會把樹切成三份,L樹,M樹,R樹,考慮的情況有:單旋轉(zhuǎn),“一字型”旋轉(zhuǎn),“之字形”旋轉(zhuǎn)。
單旋轉(zhuǎn)
從圖中我們可以看到,要將“節(jié)點2”插入到根上,需要將接近于“節(jié)點2”的數(shù)插入到根上,也就是這里的“節(jié)點7”,首先樹被分成了3份,初始情況,L和R樹是“空節(jié)點”,M是整棵樹,現(xiàn)在需要我們一步一步拆分,當(dāng)我們將“節(jié)點2”試插入到“節(jié)點7”的左孩子時,發(fā)現(xiàn)“節(jié)點7”就是父節(jié)點,滿足“單旋轉(zhuǎn)”情況,然后我們將整棵樹放到“R樹”中的left節(jié)點上,M此時是一個邏輯上的空節(jié)點,然后我們將R樹追加到M樹中。L樹追加到M的左子樹中,最后我們將“節(jié)點2”插入到根節(jié)點上。說這么多有點拗口,伸展樹比較難懂,需要大家仔細(xì)品味一下。
一字型
一字型旋轉(zhuǎn)方式與我們AVL中的“單旋轉(zhuǎn)”類似,首先同樣我們切成了三份,當(dāng)我們"預(yù)插入20時”,發(fā)現(xiàn)20的“父節(jié)點”是根的右孩子,而我們要插入的數(shù)字又在父節(jié)點的右邊,此時滿足”一字型“旋轉(zhuǎn),我們將7,10兩個節(jié)點按照”右右情況”旋轉(zhuǎn),旋轉(zhuǎn)后“節(jié)點10"的左孩子放入到L樹的right節(jié)點,"節(jié)點10”作為中間樹M,最后將20插入根節(jié)點。
之字形
之字形有點類似AVL中的“雙旋轉(zhuǎn)”,不過人家采取的策略是不一樣的,當(dāng)我們試插入“節(jié)點9”,同樣發(fā)現(xiàn)“父節(jié)點”是根的右兒子,并且“節(jié)點9”要插入到父節(jié)點的內(nèi)側(cè),根據(jù)規(guī)則,需要將“父節(jié)點10”作為M樹中的根節(jié)點,“節(jié)點7”作為L樹中的right節(jié)點,然后M拼接L和R,最后將節(jié)點9插入到根上。
3:基本操作
1) 節(jié)點定義
我們還是采用普通二叉樹中的節(jié)點定義,也就沒有了AVL那么煩人的高度信息。
public?class?BinaryNode<T>{//?Constructorspublic?BinaryNode(T?theElement)?:?this(theElement,?null,?null)?{?}public?BinaryNode(T?theElement,?BinaryNode<T>?lt,?BinaryNode<T>?rt){element?=?theElement;left?=?lt;right?=?rt;}public?T?element;public?BinaryNode<T>?left;public?BinaryNode<T>?right;}2) 伸展
這里為了編寫代碼方便,我采用的是邏輯nullNode節(jié)點,具體伸展邏輯大家可以看上面的圖。
#region?伸展///?<summary>///?伸展///?</summary>///?<param?name="Key"></param>///?<param?name="tree"></param>///?<returns></returns>public?BinaryNode<T>?Splay(T?Key,?BinaryNode<T>?tree){BinaryNode<T>?leftTreeMax,?rightTreeMin;header.left?=?header.right?=?nullNode;leftTreeMax?=?rightTreeMin?=?header;nullNode.element?=?Key;while?(true){int?compareResult?=?Key.CompareTo(tree.element);if?(compareResult?<?0){//如果成立,說明是”一字型“旋轉(zhuǎn)if?(Key.CompareTo(tree.left.element)?<?0)tree?=?rotateWithLeftChild(tree);if?(tree.left?==?nullNode)break;//動態(tài)的將中間樹的”當(dāng)前節(jié)點“追加到?R?樹中,同時備份在header中rightTreeMin.left?=?tree;rightTreeMin?=?tree;tree?=?tree.left;}else?if?(compareResult?>?0){//如果成立,說明是”一字型“旋轉(zhuǎn)if?(Key.CompareTo(tree.right.element)?>?0)tree?=?rotateWithRightChild(tree);if?(tree.right?==?nullNode)break;//動態(tài)的將中間樹的”當(dāng)前節(jié)點“追加到?L?樹中,同時備份在header中l(wèi)eftTreeMax.right?=?tree;leftTreeMax?=?tree;tree?=?tree.right;}else{break;}}/*?剝到最后一層,來最后一次切分?*///把中間樹的左孩子給“左樹”leftTreeMax.right?=?tree.left;//把中間樹的右孩子給“右樹”rightTreeMin.left?=?tree.right;/*?合并操作?*///將頭節(jié)點的左樹作為中間樹的左孩子tree.left?=?header.right;//將頭結(jié)點的右樹作為中間樹的右孩子tree.right?=?header.left;return?tree;}#endregion3) 插入
插入操作關(guān)鍵在于我們要找到接近于”要插入點“的節(jié)點,然后頂成“根節(jié)點”,也就是上面三分圖中的最后一分。
#region?插入///?<summary>///?插入///?</summary>///?<param?name="Key"></param>public?void?Insert(T?Key){if?(newNode?==?null)newNode?=?new?BinaryNode<T>(default(T));newNode.element?=?Key;if?(root?==?nullNode){newNode.left?=?newNode.right?=?nullNode;root?=?newNode;}else{root?=?Splay(Key,?root);int?compareResult?=?Key.CompareTo(root.element);if?(compareResult?<?0){newNode.left?=?root.left;newNode.right?=?root;root.left?=?nullNode;root?=?newNode;}elseif?(compareResult?>?0){newNode.right?=?root.right;newNode.left?=?root;root.right?=?nullNode;root?=?newNode;}elsereturn;}newNode?=?null;}#endregion4) 刪除
刪除操作也要將節(jié)點伸展到根上,然后進行刪除,邏輯很簡單。
#region?刪除///?<summary>///?刪除///?</summary>///?<param?name="Key"></param>public?void?Remove(T?Key){BinaryNode<T>?newTree;//將刪除結(jié)點頂?shù)礁?jié)點root?=?Splay(Key,?root);//不等于說明沒有找到if?(root.element.CompareTo(Key)?!=?0)return;//如果左邊為空,則直接用root的右孩子接上去if?(root.left?==?nullNode){newTree?=?root.right;}else{newTree?=?root.left;newTree?=?Splay(Key,?newTree);newTree.right?=?root.right;}root?=?newTree;}#endregion伸展樹可以總結(jié)成一幅圖:
總結(jié)
以上是生活随笔為你收集整理的数据结构与算法专题——第十题 输入法跳不过的坎-伸展树的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 不宜过分炒作第三代半导体材料弯道超车
- 下一篇: MassTransit Get Star