【資料結構】【平衡樹】淺析樹堆Treap
阿新 • • 發佈:2018-12-18
【Treap】
【Treap淺析】
Treap作為二叉排序樹處理演算法之一,首先得清楚二叉排序樹是什麼。對於一棵樹的任意一節點,若該節點的左子樹的所有節點的關鍵字都小於該節點的關鍵字,且該節點的柚子樹的所有節點的關鍵字都大於該節點的關鍵字,則這棵樹是一棵二叉排序樹。
Treap在每一個節點中有兩個最關鍵的元素——weight和value。
value是在建立這個新節點時賦予該節點的數值大小,Treap利用每個節點的value值將整棵樹維持成一個二叉排序樹。
weight是在建立這個新節點時賦予該節點的偽隨機數,Treap利用每個節點的weight值將整棵樹維持成一個最小堆(或最大堆) 。
看似不相容的二叉排序樹和堆的結合卻正是Treap的關鍵之處。如果用遞增(或遞減)數列只是對單純的二叉排序樹進行插入操作的話可以想見會變成這種情況:
此時一棵二叉搜尋樹就退化成了數鏈,每次對樹上進行查詢都會退化成O(N)。而利用隨機數的隨機性把二叉樹維護成一個堆,就可以儘量使一條不嚴格數鏈維護成一棵偽平衡樹。
那Treap是如何同時維護堆和二叉搜尋樹的呢?這就涉及到旋轉操作。
Treap涉及兩種旋轉操作:左旋和右旋。這裡用最小堆舉例子(懶得用電腦畫圖(逃:
利用程式碼看一下左、右旋程式碼的實現:
1.左旋:
1 void lt(int &k){2 int tmp = t[k].r; 3 t[k].r = t[tmp].l; 4 t[tmp].l = k; 5 t[tmp].size = t[k].size; 6 t[k].size = t[t[k].l].size + t[t[k].r].size + t[k].cnt; 7 k = tmp; 8 }
2.右旋:
1 void rt(int &k){ 2 int tmp = t[k].l; 3 t[k].l = t[tmp].r; 4 t[tmp].r = k; 5 t[tmp].size = t[k].size;6 t[k].size = t[t[k].r].size + t[t[k].l].size + t[k].cnt; 7 k = tmp; 8 }
除了旋轉之外還有許多其他的操作,結合程式碼看一下:
插入節點:
1 void Insert(int &k,int num){ 2 if(!k){//如果這是一個沒有被建立過的新的節點就建立一個新的節點 3 k = ++size; 4 t[k].wt = rand();//為該新節點賦予一個隨機值weight 5 t[k].val = num;//為該節點賦予一個數值大小value 6 t[k].cnt = t[k].size = 1; 7 return ; 8 } 9 ++t[k].size;//插入了一個新節點,所以路徑上每個結點的子樹大小++ 10 if(num == t[k].val){//如果這個數值被新增到樹中過,就把那個節點處的cnt++ 11 ++t[k].cnt; 12 return ; 13 } 14 if(num < t[k].val){//如果要被插入的數值比當前結點數值小,向左子樹尋找 15 Insert(t[k].l,num); 16 if(t[t[k].l].wt < t[k].wt)//如果左右孩子其中一個不滿足最小堆,即其中一個小於父親節點,則通過旋轉使其滿足最小堆 17 rt(k); 18 } 19 else{//如果要被插入的數值比當前結點數值大,向右子樹尋找 20 Insert(t[k].r,num); 21 if(t[t[k].r].wt < t[k].wt)//維護最小堆 22 lt(k); 23 }
刪除節點:
1 void Delete(int &k,int num){ 2 if(!k) 3 return ; 4 if(t[k].val == num){ 5 if(t[k].cnt > 1){//如果某個數值被插入2次及以上,在節點處cnt--即可。 6 --t[k].cnt; 7 --t[k].size; 8 return ; 9 } 10 else if(!(t[k].l * t[k].r)){//如果那個數值只被插入了一次且左右孩子至少有一個為空,則直接把不為空的孩子接到父親節點上。 11 k = t[k].l + t[k].r; 12 return ; 13 } 14 else{//如果兩個孩子都不為空,則把兩個孩子中weight較小的旋轉到父親節點,把原來的要刪除的節點一直旋轉成葉子節點或只有一個非空孩子的節點。 15 if(t[t[k].l].wt <= t[t[k].r].wt){ 16 rt(k); 17 Delete(k,num); 18 } 19 else{ 20 lt(k); 21 Delete(k,num); 22 } 23 } 24 } 25 else{ 26 --t[k].size;//經過的路徑size全都-- 27 if(num < t[k].val)//判斷接下來尋找num位置的走向 28 Delete(t[k].l,num); 29 else 30 Delete(t[k].r,num); 31 } 32 }
求序列中某一數值在數列中的排名:
1 int Rank(int &k,int num){ 2 if(!k) 3 return 0; 4 if(t[k].val == num) 5 return t[t[k].l].size + 1;//若與當前結點相等,左節點size+1就是排名,防止t[k].cnt影響排名 6 else if(t[k].val > num) 7 return Rank(t[k].l,num); 8 else 9 return Rank(t[k].r,num) + t[t[k].l].size + t[k].cnt;//遞迴到右子樹返回的是在右子樹之內的排名,要加上父節點的cnt和左孩子的size 10 }
求序列中第k大數值:
1 int Search(int &k,int num){ 2 if(!k) 3 return 0; 4 if(num <= t[t[k].l].size) 5 return Search(t[k].l,num); 6 else if(num <= t[t[k].l].size + t[k].cnt)//如果排名屬於(左孩子size,左孩子size + 當前結點cnt]區間內,則第k大數就是該節點的數值大小。 7 return t[k].val; 8 else 9 return Search(t[k].r,num - t[t[k].l].size - t[k].cnt);//進行右子樹的遞迴,排名需剪掉左子樹的size和當前結點cnt,這樣才能保證在右子樹找到正確結果。 10 }
求前序及後序:
1 int Pre(int &k,int num){ 2 if(!k) 3 return -2147483648;//返回極小值避免對下方max函式產生影響 4 if(t[k].val >= num) 5 return Pre(t[k].l,num); 6 else 7 return max(t[k].val,Pre(t[k].r,num));//如果當前結點數值小於num,則在該節點右子樹尋找比該節點數值更大的節點,後序同理 8 } 9 int Nex(int &k,int num){ 10 if(!k) 11 return 2147483647; 12 if(t[k].val <= num) 13 return Nex(t[k].r,num); 14 else 15 return min(t[k].val,Nex(t[k].l,num)); 16 }