Splay學習筆記(保姆級)

記錄我對於Splay的學習和理解。node

首先介紹Splay學習

伸展樹(Splay Tree),也叫分裂樹,是一種二叉排序樹,它能在O(log n)內完成插入、查找和刪除操做。它由丹尼爾·斯立特Daniel Sleator 和 羅伯特·恩卓·塔揚Robert Endre Tarjan 在1985年發明的。 
spa

在伸展樹上的通常操做都基於伸展操做:假設想要對一個二叉查找樹執行一系列的查找操做,爲了使整個查找時間更小,被查頻率高的那些條目就應當常常處於靠近樹根的位置。因而想到設計一個簡單方法, 在每次查找以後對樹進行重構,把被查找的條目搬移到離樹根近一些的地方。伸展樹應運而生。伸展樹是一種自調整形式的二叉查找樹,它會沿着從某個節點到樹根之間的路徑,經過一系列的旋轉把這個節點搬移到樹根去。設計

它的優點在於不須要記錄用於平衡樹的冗餘信息。(源自百度百科)code

好了,最好的理解就是先畫一幅圖來表示orm

 

 這就是一個簡單的平衡樹,知足左兒子比父親小,右兒子比父親大,而且任何一個子樹也都是平衡樹。但這個平衡樹可能會出現比較極端的狀況變成一條鏈,即2 3 4 5 6,爲了解決這個問題咱們能夠用到Splay。具體原理即咱們能夠用修改結點的順序來避免這種狀況,好比上面這幅圖能夠旋轉結點2變爲:blog

這就是將2旋轉(rotate)。咱們能夠發現旋轉以後它仍是一個平衡樹,那麼咱們再旋轉一下結點2排序

 

 

經過這三幅圖咱們能夠發現旋轉的一些規律了:get

1.旋轉的結點位置變到它的父結點的位置,父結點到該結點原來相對的位置(即原來是左兒子則父結點到右兒子的位置,反之亦然)。it

2.父結點的另外一個兒子不變,原旋轉的結點的位置變爲旋轉的結點的相對的兒子(即原來是左兒子,則它的右兒子變爲父結點的左兒子)。

用代碼表示會更加直觀。(root記錄根結點,sum記錄結點的數量)

 

struct node
{
    int ch[2], ff, val, size, cnt;//左右兒子 父結點 數值大小 樹的大小 val的數量 
}tree[N];
int root, sum;
void pushup(int x) { tree[x].size = tree[tree[x].ch[0]].size + tree[tree[x].ch[1]].size + tree[x].cnt;//size爲左兒子的size加右兒子的size加cnt } inline void rotate(int x)//x爲要旋轉的結點 { int y = tree[x].ff, z = tree[y].ff;//y爲x的父結點 z爲x的祖父結點 int k = tree[y].ch[1] == x;//k表明x是y的哪一個兒子 0爲左兒子 1爲右兒子 tree[y].ff = x;//y的父親變爲x tree[x].ff = z;//x的父親變爲z tree[tree[x].ch[!k]].ff = y; //x的原來的與x的位置相對的那個兒子的父親變爲y tree[z].ch[tree[z].ch[1] == y] = x;//z的原來的y的位置變爲x tree[y].ch[k] = tree[x].ch[!k];//y的原來x的位置變爲x的與x相對的那個兒子變爲y的兒子(我在說什麼) tree[x].ch[!k] = y;//x的原來的與x的位置相對的那個兒子變爲y pushup(x), pushup(y);//更新 }

 

 

這就是一個基本的旋轉操做,是否是很是的簡單(?)。

接下來就能夠看看關於旋轉的一些問題了了,假如咱們只旋轉一個點的話會發現一條長鏈始終都會存在,如何解決呢?咱們能夠旋轉Y,但如何旋轉,這個時候須要討論的狀況比較多,可是在看了一個大佬的講解以後有一種更加簡單的寫法。

 

void Splay(int x, int goal)//將x旋轉爲goal的兒子,goal爲0的時候即將x旋轉爲根結點
{
    int y, z;
    while (tree[x].ff != goal)
    {
        y = tree[x].ff;//父結點
        z = tree[y].ff;//祖父結點
        if (z != goal)
        {
            (tree[z].ch[0] == y) ^ (tree[y].ch[0] == x) ? rotate(x) : rotate(y);//假如x和y分別是y和z的同一個兒子則旋轉y不然旋轉x
        }
        rotate(x);//最後必須旋轉x
    }
    if (goal == 0)//即x爲根結點
    {
        root = x;
    }
}

 

這樣寫就十分簡潔(感謝大佬)

而後是Find操做,若是理解了平衡樹的話應該能獨立寫出find,即利用左兒子小右兒子大的性質,作到相似於二分查找。咱們能夠將要Find的數字旋轉到根結點,這樣操做會更加方便,也能夠直接return找到的位置。爲了方便看懂下面代碼會寫更復雜一點(順帶一提,要查出x的排行便可在find以後利用子樹的size求出)

inline void find(int x)//x爲要尋找的數值
{
    int u = root;//u爲根結點
    while (true)
    {
        if (tree[u].ch[x > tree[u].val])//避免樹裏面不存在x
        {
            break;
        }
        if (tree[u].val > x)//假如val大於x則x在u的左兒子
        {
            u = tree[u].ch[0];
        }
        if (tree[u].val < x)//假如val小於x則x在u的右兒子
        {
            u = tree[u].ch[1];
        }
        if (tree[u].val == x)//找到x
        {
            break;
        }
    }
    Splay(u, 0);//把查找到的位置旋轉到根結點
}

接下來是insert,相似於find操做,但與find不一樣的是假如找到其父結點以後不存在兒子的話能夠生成一個新兒子。

inline void insert(int x)//插入x
{
    int u = root, ff = 0;
    while (tree[u].val != x && u)//若u爲0則表明不存在
    {
        ff = u;//記錄父結點
        u = tree[u].ch[tree[u].val < x];
    }
    if (u != 0)//即本就存在u
    {
        tree[u].cnt++;//數量加一
    }
    else
    {
        sum++;
        if (ff)//假如ff不爲0
        {
            tree[ff].ch[tree[ff].val < x] = sum;
        }
        tree[sum].ff = ff;
        tree[sum].cnt = 1;
        tree[sum].size = 1;
        tree[sum].val = x;
    }
    Splay(sum, 0);//把該點旋轉爲根結點,同時能pushup
}

 寫完insert以後咱們能夠再瞭解一下前驅和後繼,利用find將x移動到根節點上,前驅即在x的左子樹,後繼在x的右子樹,還需考慮到x不存在的狀況。具體可見代碼註釋(最後的if else能夠簡化在一塊兒,爲方便理解分開寫)

inline int Next(int x, int k)//k=0表明前驅,k=1表明後繼
{
    find(x);
    if (tree[root].val > x&& k == 1)//假如x不存在且要找的是後繼
    {
        return root;//此時根結點即知足要求
    }
    if (tree[root].val < x && k == 0)//假如x不存在且要找的是前驅
    {
        return root;//此時根結點即知足要求
    }
    if (k)//找後繼
    {
        int u = tree[root].ch[1];//令u爲x的右子樹,即大於x
        while (tree[u].ch[0])//要找的大於x的最小的數即找出左子樹的值
        {
            u = tree[u].ch[0];//左子樹必定小於右子樹
        }
        return u;
    }
    else//找前驅
    {
        int u = tree[root].ch[0];//同理先令u爲x的左子樹
        while (tree[u].ch[1])//找最大值在右子樹找
        {
            u = tree[u].ch[1];//右子樹必定大於左子樹
        }
        return u;
    }
}

在寫出Next以後咱們能夠拓展一下Next的應用,假如咱們要刪除x,find(x)再刪除的話是很麻煩的,由於還會牽連到x的子樹,但咱們有沒有辦法將x的子樹全都去掉呢?利用Next和平衡樹的性質是能夠的,倘若咱們將x的後繼變爲根結點,再將x的前驅變爲x的後繼的左兒子的話,此時x就爲x的前驅的右兒子且絕對沒有兒子。此時咱們就能夠直接將其刪除(同理也能夠將x的前驅變爲根結點,能夠本身畫一下)。注意考慮到x的數量,具體也能夠根據問題來修改。具體見代碼:

inline void Delete(int x)
{
    int last = Next(x, 0);//找到x的前驅
    int next = Next(x, 1);//找到x的後繼
    Splay(next, 0);//將x的後繼變爲根結點
    Splay(last, next);//將x的前驅變爲x的後繼的左兒子
    int del = tree[last].ch[1];//del爲x的位置
    if (tree[del].cnt > 1)//若是x的數量大於一
    {
        tree[del].cnt--;
        Splay(del, 0);//將del移動到根結點同時能夠更新樹
    }
    else
    {
        tree[last].ch[1] = 0;//直接刪除
        Splay(last, 0);//將last移動到根結點同時更新樹
    }
}

最後咱們能夠嘗試求出kth,注意是求出第k小的而不是第k大= =,kth能夠利用size來求出,根據排名和size能夠判斷x是在左子樹仍是右子樹或者結點上,一路循環便可

inline int kth(int x)
{
    int u = root;
    if (tree[u].size < x)//即x大於樹的大小此時不存在
    {
        return 0;
    }
    while (true)
    {
        if (tree[tree[u].ch[0]].size + tree[u].cnt >= x)//假如x在左子樹或根結點上
        {
            if (tree[tree[u].ch[0]].size >= x)//假如左子樹的大小大於x
            {
                u = tree[u].ch[0];//即x在左子樹裏面找
            }
            else//即x在根結點上
            {
                return tree[u].val;
            }
        }
        else//x在右子樹上
        {
            x -= tree[tree[u].ch[0]].size + tree[u].cnt;
            u = tree[u].ch[1];
        }
    }
}

這些是根據洛谷P3369學習的一些基本操做,還有一些操做後續填坑。

相關文章
相關標籤/搜索