Splay伸展樹學習筆記


Splay伸展樹

有篇Splay入門必看文章 —— CSDN鏈接

 

經典引文

 

空間效率:O(n)
時間效率:O(log n)插入、查找、刪除
創造者:Daniel Sleator 和 Robert Tarjan
優點:每次查詢會調整樹的結構,使被查詢頻率高的條目更靠近樹根。

Tree Rotation


 
樹的旋轉是splay的基礎,對於二叉查找樹來說,樹的旋轉不破壞查找樹的結構。
 

Splaying

 
Splaying是Splay Tree中的基本操作,為了讓被查詢的條目更接近樹根,Splay Tree使用了樹的旋轉操作,同時保證二叉排序樹的性質不變。
Splaying的操作受以下三種因素影響:
  • 節點x是父節點p的左孩子還是右孩子
  • 節點p是不是根節點,如果不是
  • 節點p是父節點g的左孩子還是右孩子
同時有三種基本操作:
 

Zig Step


當p為根節點時,進行zip step操作。
當x是p的左孩子時,對x右旋;
當x是p的右孩子時,對x左旋。
 

Zig-Zig Step

當p不是根節點,且x和p同為左孩子或右孩子時進行Zig-Zig操作。
當x和p同為左孩子時,依次將p和x右旋;
當x和p同為右孩子時,依次將p和x左旋。
 
 

Zig-Zag Step

當p不是根節點,且x和p不同為左孩子或右孩子時,進行Zig-Zag操作。
當p為左孩子,x為右孩子時,將x左旋后再右旋。
當p為右孩子,x為左孩子時,將x右旋后再左旋。
 
 

應用

 
Splay Tree可以方便的解決一些區間問題,根據不同形狀二叉樹先序遍歷結果不變的特性,可以將區間按順序建二叉查找樹。
每次自下而上的一套splay都可以將x移動到根節點的位置,利用這個特性,可以方便的利用Lazy的思想進行區間操作。
對於每個節點記錄size,代表子樹中節點的數目,這樣就可以很方便地查找區間中的第k小或第k大元素。
對於一段要處理的區間[x, y],首先splay x-1到root,再splay y+1到root的右孩子,這時root的右孩子的左孩子對應子樹就是整個區間。
這樣,大部分區間問題都可以很方便的解決,操作同樣也適用於一個或多個條目的添加或刪除,和區間的移動。

 

自學筆記

 

今天開始自己動手寫Splay。身邊的小伙伴大多是用下標和數組來維系各個結點的聯系,但是我還是一如既往的喜歡C++的指針(❤ ω ❤)。

 

結點以一個struct結構體的形式存在。

struct node {
    int value;
    node *father;
    node *son[2];
    
    node (int v = 0, node *f = NULL) {
        value = v;
        father = f;
        son[0] = NULL;
        son[1] = NULL;
    }
};

其中,son[0]代表左兒子,son[1]代表右兒子。

 

用一個函數來判斷子節點是父節點的哪個兒子

inline bool son(node *f, node *s) {
    return f->son[1] == s;
}

返回值就是son[]數組的下標,這個函數很方便。

 

最關鍵的是旋轉操作,有別於常見的zig,zag旋轉,我喜歡用一個函數實現其兩者的功能,即rotate(x)代表將x旋轉到其父節點的位置上。

inline void rotate(node *t) {
    node *f = t->father;
    node *g = f->father;
    bool a = son(f, t), b = !a;
    f->son[a] = t->son[b];
    if (t->son[b] != NULL)
        t->son[b]->father = f;
    t->son[b] = f;
    f->father = t;
    t->father = g;
    if (g != NULL)
        g->son[son(g, f)] = t;
    else
        root = t;
}

函數會自行判斷x實在父節點的左兒子上還是右兒子上,並自動左旋或右旋。這里要注意改變祖父結點的兒子指針,以及結點的父親指針,切忌馬虎漏掉。同時還要事先做好特判,放止訪問非法地址。這里用指針相較於用下標的一個好處就是,如果你不小心訪問了NULL即空指針,也是下標黨常用的0下標,指針寫法一定會RE,而下標寫法可能就不會崩潰,因而不易發現錯誤,導致一些較為復雜而智障的錯誤。

 

然后是核心函數——Splay函數,貌似也有人叫Spaly的樣子,然而我並沒有考證什么。Splay(x,y)用於將x結點旋轉到y結點的某個兒子上。特別地,Splay(x,NIL)代表將x旋轉到根節點的位置上。根節點的父親一般是NIL或0。

inline void spaly(node *t, node *p) {
    while (t->father != p) {
        node *f = t->father;
        node *g = f->father;
        if (g == p)
            rotate(t);
        else {
            if (son(g, f) ^ son(f, t))
                rotate(t), rotate(t);
            else
                rotate(f), rotate(t);
        }
    }
}

這里值得注意的是兩種雙旋。如果t(該節點),f(父親節點),g(祖父節點)形成了一條單向的鏈,即[右→右]或[左→左]這樣子,那么就先對父親結點進行rotate操作,再對該節點進行rotate操作;否則就對該節點連續進行兩次rotate操作。據稱單旋無神犇,雙旋O(logN),這句話我也沒有考證,個人表示不想做什么太多的探究,畢竟Splay的復雜度本來就挺玄學的了,而且專門卡單旋Splay的題也沒怎么聽說過。對了,這個雙旋操作和AVL的雙旋是不是有那么幾分相似啊,雖然還是不太一樣的吧,好吧其實也不怎么像╮(╯-╰)╭。

 

接下來談談插入操作。插入操作就和普通的二叉搜索樹類似,先找到合適的葉子結點,然后在空着的son[]上新建結點,把值放入。不同的是需要把新建的結點Splay到根節點位置,復雜度需要,不要問為什么。

inline void insert(int val) {
    if (root == NULL)
        root = new node(val, NULL);
    for (node *t = root; t; t = t->son[val >= t->value]) {
        if (t->value == val) { spaly(t, NULL); return; }
        if (t->son[val >= t->value] == NULL)
            t->son[val >= t->value] = new node(val, t);
    }
}

注意,這個插入函數實現的是非重集合。

 

與之對應的就是刪除操作,相對的復雜一些。刪除一個元素,需要先在樹中找到這個結點,然后把這個結點Splay到根節點位置,開始分類討論。如果這個結點沒有左兒子(左子樹),直接把右兒子放在根的位置上即可;否則的話就需要想方設法合並左右子樹:在左子樹種找到最靠右(最大)的結點,把它旋轉到根節點的兒子上,此時它一定沒有右兒子,因為根節點的左子樹中不存在任何一個元素比它更大,那么把根節點的右子樹接在這個結點的右兒子上即可。

inline void erase(int val) {
    node *t = root;
    for ( ; t; ) {
        if (t->value == val)
            break;
        t = t->son[val > t->value];
    }
    if (t != NULL) {
        spaly(t, NULL);
        if (t->son[0] == NULL) {
            root = t->son[1];
            if (root != NULL)
                root->father = NULL;
        }
        else {
            node *p = t->son[0];
            while (p->son[1] != NULL)
                p = p->son[1];
            spaly(p, t); root = p;
            root->father = NULL;
            p->son[1] = t->son[1];
            if (p->son[1] != NULL)
                p->son[1]->father = p;
        }
    }
}

相較於insert()確實復雜了不少。

 

以上就是Splay的框架了,是Splay必不可少的部分,在此基礎上可以加入許多新的功能。

 

例如,手動實現垃圾回收,這樣新建結點的常數會小很多,畢竟C++的new是很慢的。

node tree[siz], *stk[siz]; int top;

inline node *newnode(int v, node *f) {
    node *ret = stk[--top];
    ret->size = 1;
    ret->value = v;
    ret->father = f;
    ret->son[0] = NULL;
    ret->son[1] = NULL;
    ret->reverse = false;
    return ret;
}

inline void freenode(node *t) {
    stk[top++] = t;
}

 

有的時候需要我們維護子樹大小。

inline int size(node *t) {
    return t == NULL ? 0 : t->size;
}

inline void update(node *t) {
    t->size = 1;
    t->size += size(t->son[0]);
    t->size += size(t->son[1]);
}

簡單,安全。

 

維護區間翻轉的時候需要用到打標記的方式。

inline bool tag(node *t) {
    return t == NULL ? false : t->reverse;
}

inline void reverse(node *t) {
    if (t != NULL)
        t->reverse ^= true;
}

inline void pushdown(node *t) {
    if (tag(t)) {
        std::swap(t->son[0], t->son[1]);
        reverse(t->son[0]);
        reverse(t->son[1]);
        t->reverse ^= true;
    }
}

 

還有更為簡潔的rotate函數。

inline void connect(node *f, node *s, bool k) {
    if (f == NULL)
        root = s;
    else
        f->son[k] = s;
    if (s != NULL)
        s->father = f;
}

inline void rotate(node *t) {
    node *f = t->father;
    node *g = f->father;
    bool a = son(f, t), b = !a;
    connect(f, t->son[b], a);
    connect(g, t, son(g, f));
    connect(t, f, b);
    update(f);
    update(t);
}

 

@Author: YouSiki


免責聲明!

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



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