伸展樹


伸展樹(Splay Tree)樹平衡二叉查找樹的一種,具有二叉查找樹的所有性質。在性能上又比普通的二叉查找樹有所改進:普通的二叉查找樹在最壞情況下的查找操作的時間復雜度為O(n)(當二叉樹退化成一條鏈的時候),而伸展樹在任何情況下的平攤時間復雜度均為 O(log2n).

特性

  1. 和普通的二叉查找樹相比,具有任何情況下、任何操作的平攤O(log2n)的復雜度,時間性能上更好
  2. 和一般的平衡二叉樹比如 紅黑樹、AVL樹相比,維護更少的節點額外信息,空間性能更優,同時編程復雜度更低
  3. 在很多情況下,對於查找操作,后面的查詢和之前的查詢有很大的相關性。這樣每次查詢操作將被查到的節點旋轉到樹的根節點位置,這樣下次查詢操作可以很快的完成
  4. 可以完成對區間的查詢、修改、刪除等操作,可以實現線段樹和樹狀數組的所有功能 
     

旋轉

    伸展樹實現O(log2n)量級的平攤復雜度依靠每次對伸展樹進行查詢、修改、刪除操作之后,都進行旋轉操作 Splay(x, root),該操作將節點x旋轉到樹的根部。 
    伸展樹的旋轉有六種類型,如果去掉鏡像的重復,則為三種:zig(zag)、zig-zig(zag-zag)、zig-zag(zag-zig)。

1 自底向上的方式進行旋轉

    這種方式需要每個節點存放其父節點的指

1.1 zig旋轉

zig旋轉 
    如圖所示,x節點的父節點為y,x為y的左子節點,且y節點為根。則只需要對x節點進行一次右旋(zig操作),使之成為y的父節點,就可以使x成為伸展樹的根節點。

1.2 zig-zig旋轉

zig-zig旋轉 
    如上圖所示,x節點的父節點y,y的父節點z,三者在一字型鏈上。此時,先對y節點和z節點進行zig旋轉,然后再對x節點和y節點進行zig旋轉,最后變為右圖所示,x成為y和z的祖先節點。

1.3 zig-zag旋轉

zig-zag旋轉 
    如上圖所示,x節點的父節點y,y的父節點z,三者在之字型鏈上。此時,先對x節點和y節點進行zig旋轉,然后再對x節點和y節點進行zag旋轉,最后變為右圖所示,x成為y和z的祖先節點。

2 自頂向下的方式進行旋轉

    這種方式不需要節點存儲其父節點的指針。當我們沿着樹向下搜索某個節點x時,將搜索路徑上的節點及其子樹移走。構建兩棵臨時的樹——左樹和右樹。沒有被移走的節點構成的樹稱為中樹。

(1) 當前節點x是中樹的根 
(2) 左樹L保存小於x的節點 
(3) 右樹R保存大於x的節點

    開始時候,x是樹T的根,左樹L和右樹R都為空。三種旋轉操作:

2.1 zig旋轉

zig旋轉 
    如圖所示,x節點的子節點y就是我們要找的節點,則只需要對y節點進行一次右旋(zig操作),使之成為x的父節點,就可以使y成為伸展樹的根節點。將y作為中樹的根,同時,x節點移動到右樹R中,顯然右樹上的節點都大於所要查找的節點。

2.2 zig-zig旋轉

zig-zig旋轉 
    如上圖所示,x節點的左子節點y,y的左子節點z,三者在一字型鏈上,且要查找的節點位於z節點為根的子樹中。此時,對x節點和y節點進行zig,然后對z和y進行zig,使z成為中樹的根,同時將y及其子樹掛載到右樹R上。

2.3 zig-zag旋轉

zig-zag旋轉 
    如上圖所示,x節點的左子節點y,y的右子節點z,三者在之字型鏈上,且需要查找的元素位於以z為根的子樹上。此時,先對x節點和y節點進行zig旋轉,將x及其右子樹掛載到右樹R上,此時y成為中樹的根節點;然后再對z節點和y節點進行zag旋轉,使得z成為中樹的根節點。

2.4 合並

合並 
    最后,找到節點或者遇到空節點之后,需要對左、中、右樹進行合並。如圖所示,將左樹掛載到中樹的最左下方(滿足遍歷順序要求),將右樹掛載到中樹的最右下方(滿足遍歷順序要求)。

 

父節點向左到左子節點-> zig

父節點向右到右子節點->zag

舉例說明旋轉操作

Original

zig-zag (double rotation)

zig-zig

zig (single rotation at root)

伸展樹的基本操作

    利用Splay操作,可以在伸展樹上進行如下操作: 
(1) Find(x, S) 判斷x是否在伸展樹S表示的有序集中 
    首先按照普通的二叉查找樹查找算法進行查找,如果找到元素x,則執行Splay(x, S)操作將x旋轉到樹根的位置。

(2) Insert(x, S) 將元素x插入到樹中 
    首先按照普通的二叉查找樹插入算法進行插入,然后執行Splay(x, S)

(3) Delete(x, S) 將元素x從伸展樹S所表示的有序集中刪除 
    首先按照普通的二叉查找樹查找算法找到x的位置。如果x沒有孩子或只有一個孩子,則直接將x刪除,並通過Splay操作,將x節點的父節點調整到伸展樹的根節點處。否則,向下查找x的后繼節點y,用y替代x的位置,然后執行Splay(y, S),將y調整為伸展樹的根

(4) Join(S1, S2) 將兩個伸展樹S1, S2合並為一個伸展樹。其中S1的所有元素小於S2中的所有元素。 
join 
    首先按照普通的二叉查找樹查找算法找到S1中最大元素x,然后執行Splay(x, S1)將x旋轉到S1的根部,此時S1中的所有元素必然在x的左子樹上,x的右子樹為空,則可以將S2掛載到x的右子樹位置。

(5) Split(x, S) 以x為界,將伸展樹S分離為兩棵伸展樹S1和S2,其中S1中所有元素都小於x,S2中所有元素都大於x。 
Split

    首先執行Find(x, S),將元素x調整為伸展樹的根節點,則x的左子樹就是S1,右子樹就是S2.

伸展樹Splay(x,S)實現(c++)

1 自底向上的旋轉方式
struct TreeNode{
    int data;
    TreeNode* left;
    TreeNode* right;
    TreeNode* parent;
    TreeNode(int d) :
        data(d), left(NULL), right(NULL), parent(NULL){};
};

TreeNode* gTreeRoot;
void Rotate(TreeNode* x, bool left_rotate){ //旋轉x節點(將x節點 按照 left_rotate 指示 繞着其父節點y 進行左旋或者右旋
    TreeNode* y = x->parent;
    if (y == NULL){
        return;
    }

    if (left_rotate){
        y->right = x->left;
        if (!x->left){
            x->left->parent = y;
        }
    }
    else{
        y->left = x->right;
        if (!x->right){
            x->right->parent = y;
        }
    }

    x->parent = y->parent;
    if (!y->parent){
        if (y == y->parent->left){
            y->parent->left = x;
        }
        else{
            y->parent->right = x;
        }
    }

    if (y == gTreeRoot){ //全局的根節點
        gTreeRoot = x;
    }
}

//將節點x通過不斷的Rotate操作,直到x成為f的子節點
void Splay(TreeNode* x, TreeNode* f){
    TreeNode* y = x->parent, *z = NULL;
    while (y != f){
        z = y->parent;
        if (z == f){
            Rotate(x, x == y->right);
        }
        else{
            if (!(x == y->left ^ y == z->left)){ //一字型 旋轉 zig-zig
                Rotate(y, y == z->right);
                Rotate(x, x == y->right);
            }
            else{ //之字型旋轉 zig-zag
                Rotate(x, x == y->right);

                //注意,上一步的rotate操作,x的地址沒有發生改變,但是x地址指向的結構體中的各個域被修改為經過旋轉之后的結構
                //所有,這里直接使用x即可
                Rotate(x, x == z->right);                 
            }
        }
    }
}

 2 自頂向下的方式旋轉

struct TreeNode{
    int data;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int d = 0) :
        data(d), left(NULL), right(NULL){};
};

TreeNode* Splay(int i, TreeNode* t){
    TreeNode N, *l, *r, *y; // l, t, r 分別為左樹、中樹、右樹
    if (t == NULL){
        return;
    }
    l = r = &N;            
    while (true){
        if (i < t->data){
            if (t->left == NULL){ //碰到空節點,結束
                break;
            }
            if (i < t->left->data){    //需要進行右旋
                y = t->left;
                t->left = y->right;
                y->right = t;
                t = y;
                if (t->left == NULL){
                    break;
                }
            }
            r->left = t;    //掛載到右樹,最小的位置
            r = t;
            t = t->left; //將 z 升為中樹的根節點
        }
        else if (i > t->data){
            if (t->right == NULL){
                break;
            }
            if (i > t->right->data){ //需要進行左旋
                y = t->right;
                t->right = y->left;
                y->left = t;
            }
            l->right = t;    //掛載到左樹,最大的位置
            l = t;
            t = t->right;  //將z升為中樹的根節點
        }
        else{
            break;
        }
    }
    l->right = t->left;  //將左、中、右樹進行合並
    r->left = t->right;
    t->left = N.right;
    t->right = N.left;
    return t;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM