可旋轉Treap 樹堆 總結

2022-07-14 22:12:22 字數 4466 閱讀 3674

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

在深入了解treap之前,我們先來了解一下bst。

bst(binary-search tree),即二分搜尋樹,是一棵二叉樹,且滿足性質:若每個節點都有乙個key值,則對於每個根節點,均滿足key[leftson]

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

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

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

插入x數

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

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

查詢排名為x的數

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

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

首先便是隨機值的實現。由於中的rand()函式速度較慢且侷限性較大,在資料結構中不太適用,所以在這裡建議rand函式的功能用手寫來實現。

隨機函式:

int

rand()

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

inline int

rand()

然後還需要乙個函式來更新每個節點的資訊,也是十分的淺顯易懂:

void update(int

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)

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

void lturn(int &k)

了解這兩種操作後,插入元素就變得得心應手了,先把要插入的點插入到乙個葉子上,然後跟維護堆一樣,如果當前節點的優先順序比根大就旋轉,如果當前節點是根的左兒子就右旋,如果當前節點是根的右兒子就左旋。依然舉兩個例子來幫助理解:

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

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

**如下:

void ins(int &p,int

x) size[p]++;

if (v[p]==x) ct[p]++;

else

if (x>v[p])

else

}

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

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

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

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

**實現:

void del(int &p,int

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)

int query2(int p,int

x)

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

該部分**:

int findfront(int p,int

x)

int findback(int p,int

x)

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

#include #include 

#include

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)void lturn(int &k)

void rturn(int &k)

void ins(int &p,int

x) size[p]++;

if (v[p]==x) ct[p]++;

else

if (x>v[p])

else

}void del(int &p,int

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)int query2(int p,intx)

int findfront(int p,int

x)int findback(int p,int

x)int

ss;int

main()

}

Treap樹堆非旋轉演算法

核心操作 與合併 split 有兩種 按權值 與按位置 排名 如果按權值 split 那麼分出來兩棵樹的第一棵樹上的每個數的大小都小於 或者小於等於,視具體情況 x。如果按位置 split 那麼分出來兩棵樹的第一棵樹上恰好有x個結點。按權值 inline void split int k,int l...

Treap(樹堆)詳解

本篇隨筆詳細講解一下一種隨機化資料結構 樹堆 treap 首先給乙個字串等式 treap tree heap 所以 treap 樹堆其實就是樹 堆。樹是二叉查詢樹 bst 堆是二叉堆,大根堆小根堆都可以。關於 bst 的相關知識,請看官走這邊 bst詳解 樹堆既是一棵二叉查詢樹,也是乙個二叉堆。但是...

可並堆之左偏樹總結

左偏樹,顧名思義,是左邊的結點權值較大的樹形資料結構。主要用於兩個優先佇列的快速合併,是可並堆的一種實現方式。定義與性質 外結點 乙個結點的右子結點為空就為外結點 距離 結點一直向右,直到外結點所經歷的步數,每個結點距離等於右兒子的距離 1。左偏樹的父親結點的優先順序高於兒子結點 父親結點的左子節點...