Splay伸展樹學習筆記

Splay伸展樹

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

 

經典引文

 

空間效率: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[]數組的下標,這個函數很方便。spa

 

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

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。code

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的雙旋是否是有那麼幾分類似啊,雖然仍是不太同樣的吧,好吧其實也不怎麼像╮(╯-╰)╭。blog

 

接下來談談插入操做。插入操做就和普通的二叉搜索樹相似,先找到合適的葉子結點,而後在空着的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

相關文章
相關標籤/搜索