可旋轉Treap(樹堆)總結


樹堆,在數據結構中也稱Treap,是指有一個隨機附加域滿足堆的性質的二叉搜索樹,其結構相當於以隨機數據插入的二叉搜索樹。其基本操作的期望時間復雜度為O(logn)。相對於其他的平衡二叉搜索樹,Treap的特點是實現簡單,且能基本實現隨機平衡的結構。

在深入了解Treap之前,我們先來了解一下BST。


BST(Binary-search tree),即二分搜索樹,是一棵二叉樹,且滿足性質:若每個節點都有一個key值,則對於每個根節點,均滿足key[leftson]<key[root]<key[rightson]。換句話說,即滿足樹的先序遍歷等於樹的中序遍歷。如下圖即為一顆BST:

其特點可以幫助我們快速查找樹中的某些元素。舉個例子,如果我們要查找2元素,那么先與6比較,比6小,那么進入6的左子樹,再與5進行比較,比5小,進入5的左子樹,我們就成功地找到了2。這比我們暴力查找要快得多。但是缺點也很顯著。比如說,如果輸入數據滿足單調遞增性質,那么我們在建樹時便會將其建成一條鏈,從而導致其算法的復雜度退化。當然,特定情況下,我們可以借助random—shuffle函數將數據打亂,但是一般情況下,BST便有着很大的局限性。因此我們需要一種更高級的數據結構來克服這種局限性,便是我們的Treap。

Treap=Tree+heap,顧名思義,Treap便是一種樹與堆的結合體。總的來說,就是在維護BST性質的同時,給與每個節點一個隨機的random值,同時保證random值滿足小根堆的性質。這樣,便可以輕而易舉的防止復雜度的退化。

在了解了Treap的原理之后,我們便可以嘗試用代碼來實現其功能。下面以洛谷-P3369普通平衡樹為例,一道典型並且操作齊全的模版題,其主要要求我們完成6個操作:

  1. 插入x數

  2. 刪除x數(若有多個相同的數,因只刪除一個)

  3. 查詢x數的排名(排名定義為比當前數小的數的個數+1。若有多個相同的數,因輸出最小的排名)

  4. 查詢排名為x的數

  5. 求x的前驅(前驅定義為小於x,且最大的數)

  6. 求x的后繼(后繼定義為大於x,且最小的數)

首先便是隨機值的實現。由於<stdlib>中的rand()函數速度較慢且局限性較大,在數據結構中不太適用,所以在這里建議rand函數的功能用手寫來實現。

隨機函數:

int rand()  
{
int seed=12345; return seed=(int)seed*482711LL%2147483647; }

稍微優化后可以變為這樣(雖然沒什么用):

inline int rand()
{
static int seed=12345; return seed=(int)seed*482711LL%2147483647; }

然后還需要一個函數來更新每個節點的信息,也是十分的淺顯易懂:

void update(int p)
{
    size[p]=size[l[p]]+size[r[p]]+ct[p];
}

在我們維護Treap的過程中,子樹大小的維護也時時刻刻都是有必要的,在每個函數中都應該有體現,具體維護方式如下:

對於旋轉,我們要在旋轉后對子節點和根節點分別重新計算其子樹的大小。 

對於插入,在尋找插入的位置時,每經過一個節點,都要先使以它為根的子樹的大小增加 1,再遞歸進入子樹查找。 

對於刪除,在尋找待刪除節點,遞歸返回時要把所有的經過的節點的子樹的大小減少 1。要注意的是,刪除之前一定要保證待刪除節點存在於 Treap 中。

維護子樹的大小也是Treap的一個關鍵部分。

那么現在便來到了最關鍵也是Treap中最核心的一步:如何維護堆的性質,即如何在Treap中插入元素(ins)。

對於Treap中的每個元素,為保證我們堆的性質,插入操作便分為了兩種操作:左旋(lturn),右旋(rturn)。下面重點講解這兩種操作。

 下面畫了一個圖以便理解:

以上圖為例,我們可以看到,從左到右便是右旋的過程,使得根節點由u變為了x。由於a仍比x小,所以x的左子樹仍為a,u比x大,所以為x的右子樹,但對於b,大於x小於u,所以應在x的右子樹,u的左子樹,同理,c應在u的右子樹,旋轉完畢。這便是右旋的過程。

理解了右旋的過程之后,我們也可以較為輕松的寫出右旋的代碼,為了方便,加了個小小的傳引用:

void rturn(int &k)
{
    int t=l[k];//記錄左兒子
    l[k]=r[t];
    r[t]=k;//旋轉的過程
    size[t]=size[k];//size的轉換
    update(k);//更新k
    k=t;
}

左旋轉的過程就是上圖從右到作的過程,代碼實現也同理:

void lturn(int &k)
{
    int t=r[k];//記錄右兒子
    r[k]=l[t];
    l[t]=k;//旋轉
    size[t]=size[k];///size轉換
    update(k);//更新k
    k=t;
}

了解這兩種操作后,插入元素就變得得心應手了,先把要插入的點插入到一個葉子上,然后跟維護堆一樣,如果當前節點的優先級比根大就旋轉,如果當前節點是根的左兒子就右旋,如果當前節點是根的右兒子就左旋。

依然舉兩個例子來幫助理解:

如圖所示,我們需要把D和F元素插入到Treap中,對於D,先將其放在一個葉子節點,然后與其父親相比較發現比父親小卻在父親的右子樹上,所以我們需要對D進行右旋操作,同理,F元素經過一次次的比較,一次次的旋轉,最終也可以到達如圖所示的位置。

至此,我們已經基本解決了對於Treap的插入操作。

代碼如下:

void ins(int &p,int x)
{
    if (p==0)
    {
        p=++sz;
        size[p]=ct[p]=1,v[p]=x,rnd[p]=rand();
        return;
    }
    size[p]++;
    if (v[p]==x) ct[p]++;
    else if (x>v[p])
    {
        ins(r[p],x);
        if (rnd[r[p]]<rnd[p]) lturn(p);
    }
    else
    {
        ins(l[p],x);
        if (rnd[l[p]]<rnd[p]) rturn(p);
    }
}

接下來是刪除操作(del),刪除操作算是Treap中最難理解的操作了吧(主要因為代碼長╮(╯▽╰)╭)。本可以通過兩種方式來達成刪除操作,但對於初學者來講,這里推薦並主要講解其中一種方式。

注意到Treap的性質,即必須滿足堆的性質,所以對於Treap,我們也可以用刪除堆的方式,借助旋轉操作,加以解決。

如果該節點的左子節點的key小於右子節點的key,右旋該節點,使該節點降為右子樹的根節點,然后訪問右子樹的根節點,遞歸地操作下去;反之同理。實質上即為讓key小的節點有限旋到上面,保證堆的性質,進而進行刪除操作。

刪除操作比較難以理解,希望通過代碼可以加深對其的認識。

代碼實現:

void del(int &p,int x)
{
    if (p==0) return;
    if (v[p]==x)
    {
        if (ct[p]>1) ct[p]--,size[p]--;
        else
        {
            if (l[p]==0||r[p]==0) p=l[p]+r[p];
            else if (rnd[l[p]]<rnd[r[p]]) rturn(p),del(p,x);
            else lturn(p),del(p,x); 
        }
    }
    else if (x>v[p]) size[p]--,del(r[p],x);
    else size[p]--,del(l[p],x);
}

解決完刪除操作后,查找(query)操作便顯得較為簡單,按照一般樹上問題解決方式統計即可,這里不多贅述,其中query1代表查詢x數的排名,query2代表查詢排名為x的數。

代碼實現:

int query1(int p,int x)
{
    if (p==0) return 0;
    if (v[p]==x) return size[l[p]]+1;
    if (x>v[p]) return size[l[p]]+ct[p]+query1(r[p],x);
    else return query1(l[p],x);
}
int query2(int p,int x)
 {
    if (p==0) return 0;
     if (x<=size[l[p]]) return query2(l[p],x);
     x-=size[l[p]];
     if (x<=ct[p]) return v[p];
     x-=ct[p];
      return query2(r[p],x);
 }

最后,我們來處理一下前驅與后繼的問題。前驅定義為小於x,且最大的數,后繼定義為大於x,且最小的數,也較為簡單,過程中維護一下max和min即可輕易地解決。

該部分代碼:

int findfront(int p,int x)
{
    if (p==0) return -inf;
    if (v[p]<x) return max(v[p],findfront(r[p],x));
    else if (v[p]>=x) return findfront(l[p],x);
}
int findback(int p,int x)
{
    if (p==0) return inf;
    if (v[p]<=x) return findback(r[p],x);
    else return min(v[p],findback(l[p],x));
}

至此,Treap中的所有操作都已經解決。將這些操作拼接串聯起來,便構成了Treap的基本框架,完整模版如下(以普通平衡樹為例):

#include <stdio.h>
#include <algorithm>
#include <stdlib.h>
using namespace std;
#define inf 300000030
int l[100100],r[100100],v[100100],size[100100],rnd[100100],ct[100100];
int sz;
void update(int p)
{
    size[p]=size[l[p]]+size[r[p]]+ct[p];
}
void lturn(int &k)
{
    int t=r[k];
    r[k]=l[t];
    l[t]=k;
    size[t]=size[k];
    update(k);
    k=t;
}
void rturn(int &k)
{
    int t=l[k];
    l[k]=r[t];
    r[t]=k;
    size[t]=size[k];
    update(k);
    k=t;
}
void ins(int &p,int x)
{
    if (p==0)
    {
        p=++sz;
        size[p]=ct[p]=1,v[p]=x,rnd[p]=rand();
        return;
    }
    size[p]++;
    if (v[p]==x) ct[p]++;
    else if (x>v[p])
    {
        ins(r[p],x);
        if (rnd[r[p]]<rnd[p]) lturn(p);
    }
    else
    {
        ins(l[p],x);
        if (rnd[l[p]]<rnd[p]) rturn(p);
    }
}
void del(int &p,int x)
{
    if (p==0) return;
    if (v[p]==x)
    {
        if (ct[p]>1) ct[p]--,size[p]--;
        else
        {
            if (l[p]==0||r[p]==0) p=l[p]+r[p];
            else if (rnd[l[p]]<rnd[r[p]]) rturn(p),del(p,x);
            else lturn(p),del(p,x); 
        }
    }
    else if (x>v[p]) size[p]--,del(r[p],x);
    else size[p]--,del(l[p],x);
}
int query1(int p,int x)
{
    if (p==0) return 0;
    if (v[p]==x) return size[l[p]]+1;
    if (x>v[p]) return size[l[p]]+ct[p]+query1(r[p],x);
    else return query1(l[p],x);
}
int query2(int p,int x)
 {
    if (p==0) return 0;
     if (x<=size[l[p]]) return query2(l[p],x);
     x-=size[l[p]];
     if (x<=ct[p]) return v[p];
     x-=ct[p];
      return query2(r[p],x);
 }
 int findfront(int p,int x)
{
    if (p==0) return -inf;
    if (v[p]<x) return max(v[p],findfront(r[p],x));
    else if (v[p]>=x) return findfront(l[p],x);
}
int findback(int p,int x)
{
    if (p==0) return inf;
    if (v[p]<=x) return findback(r[p],x);
    else return min(v[p],findback(l[p],x));
}
int ss;
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int flag,x;
        scanf("%d%d",&flag,&x);
        if (flag==1) ins(ss,x);
        if (flag==2) del(ss,x);
        if (flag==3) printf("%d\n",query1(ss,x));
        if (flag==4) printf("%d\n",query2(ss,x)); 
        if (flag==5) printf("%d\n",findfront(ss,x));
        if (flag==6) printf("%d\n",findback(ss,x));
    }
}
 


免責聲明!

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



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