1. 程式人生 > >Treap(樹堆)圖解與實現

Treap(樹堆)圖解與實現

前面我們介紹了AVL樹,伸展樹,它們都是二叉搜尋樹,二叉搜尋樹的主要問題就是其結構與資料相關,樹的深度可能會很大,Treap樹就是一種解決二叉搜尋樹可能深度過大的另一種資料結構。

Treap=Tree+Heap。Treap本身是一棵二叉搜尋樹,它的左子樹和右子樹也分別是一個Treap,和一般的二叉搜尋樹不同的是,Treap紀錄一個額外的資料,就是優先順序。Treap在以關鍵碼構成二叉搜尋樹的同時,還滿足堆的性質。這些優先順序是是在結點插入時,隨機賦予的,Treap根據這些優先順序滿足堆的性質。這樣的話,Treap是有一個隨機附加域滿足堆的性質的二叉搜尋樹,其結構相當於以隨機資料插入的二叉搜尋樹

。其基本操作的期望時間複雜度為O(logn)。相對於其他的平衡二叉搜尋樹,Treap的特點是實現簡單,且能基本實現隨機平衡的結構。

Treap維護堆性質的方法只用到了旋轉,只需要兩種旋轉,程式設計複雜度比Splay要小一些。

這裡我們還是先給出treap的結構定義:

typedef struct TreapNode* Tree;
typedef int ElementType;
struct TreapNode
{
    ElementType val; //結點值
    int priority;   //優先順序
    Tree lchild;
    Tree rchild;
    TreapNode(int val=0,int priority=0) //預設建構函式
    {
        lchild=rchild=NULL;
        this->val=val;
        this->priority=priority;
    }
};

可以看到這個和前面的AVL樹以及伸展樹基本都是一樣的,這裡去掉了父節點了,因為父節點的存在只是為了方便我們的操作,這裡將不用到父節點統一的進行各項操作。

treap的搜尋和一般的BST樹是一致的,這裡我們就跳過吧,直接說插入操作。和二叉搜尋樹的插入一樣,先把要插入的點插入到一個葉子上,然後跟維護堆一樣,如果當前節點的優先順序比它父節點優先順序小就旋轉,如果當前節點是其父節點的左兒子就右旋,如果當前節點是其父節點的右兒子就左旋。(簡單點說把優先順序小的往上提)

注:這裡我們都調整為最小堆的形式。

一、旋轉

如果知道AVL樹或者伸展樹的旋轉方式,那麼treap的旋轉就顯得很簡單了,這裡我們還是用圖來展示一下過程:

(1)左旋轉

黑色的數字是優先順序,當插入數值為7優先順序為20的結點後,違反了最小堆的定義,所以這裡需要進行左旋轉,將結點7進行提升一個層次,當然這個左旋轉方式有很多種,如果把結點7看做操作物件,那麼就需要知道它的父節點以及祖父結點,但是我們在定義中又去掉了父節點指標,這樣在獲取父節點的是否似乎變得困難了,不妨換個角度,如果我們結點7的父節點為物件,那麼只需要知道結點3的父節點就可以操作了,這樣的話我們可以通過引用傳引數,就可以不用父節點指標,直接進行旋轉操作,直接看程式碼吧

//左旋轉
void left_rotate(Tree &node)
{
    Tree temp=node->rchild;
    node->rchild=temp->lchild;
    temp->lchild=node;
    node=temp;
}


引數node為需要調整結點的父節點,在圖示中也就是結點3,對比圖看就明白了,注意這裡是指標的引用。

(2)右旋轉

同左旋轉本質是一樣的。

//右旋轉
void right_rotate(Tree &node)
{
    Tree temp=node->lchild;
    node->lchild=temp->rchild;
    temp->rchild=node;
    node=temp;
}

二、插入

先來看看圖解:

插入值為18,優先順序為20的結點後,違反了最小堆的定義,因此要進行調整,把優先順序小的往上提,也就是小的優先順序插入的是右子樹,那麼需要進行左旋轉,這裡進行一次旋轉過後就OK了。

同樣,這種情況左旋轉,旋轉後發現還是不滿足最小堆的定義,並且小優先順序的結點在左子樹,所以還需要進行右旋轉,如下圖所示:

右旋後,很遺憾還是不行,還需要左旋:

OK,終於完成,插入一個數據,也許要進行多次旋轉,不過也僅僅是左旋或者右旋而已,相對AVL樹來說,簡單很多了。

下面來看看插入的程式碼

//插入函式
bool insert(Tree &root,ElementType val=0,int priority=0)
{
    Tree node=new TreapNode(val,priority);
    return insert_val(root, node);
}
//內部插入函式
//引數:根結點,需插入的結點
bool insert_val(Tree &root,Tree &node)
{
    if (!root)
    {
        root=node; //插入
        return true;
    }
    else if(root->val>node->val)
    {
       bool flag=insert_val(root->lchild, node);
        if (root->priority>node->priority) //檢查是否需要調整
            right_rotate(root);
        return flag;
    }
    else if(root->val<node->val)
    {
        bool flag=insert_val(root->rchild, node);
        if (root->priority>node->priority) //檢查是否需要調整
            left_rotate(root);
        return flag;
    }
    //已經含有該元素,釋放結點
    delete node;
    return false;

}


對比圖解,應該很容易就明白啦。

三、刪除

(1)用二叉搜尋樹的方式刪除

在前面的AVL樹以及伸展樹的刪除結點中,我們都是採用的BST樹中刪除結點的辦法,也就是對於葉子結點或者只有一個孩子的結點的刪除方式是直接刪除,否則找替代元素,化繁為簡。但是如果treap也是採用這樣的方式我們看可不可以呢,首先我們肯定能正確的刪除結點,但是我們刪除後我們可能需要進行調整,因為也許優先順序不在滿足要求,可能需要向下調整,也可能需要向下調整,這樣呢,比較麻煩,這裡我們講用堆的方式刪除

(2)用堆的方式刪除

因為Treap滿足堆性質,所以只需要把要刪除的節點旋轉到葉節點上,然後直接刪除就可以了,具體的方法:如果該節點的左子節點的優先順序小於右子節點的優先順序,右旋該節點,使該節點降為右子樹的根節點,然後訪問右子樹的根節點,繼續操作;反之,左旋該節點,使該節點降為左子樹的根節點,然後訪問左子樹的根節點,繼續操作,直到變成可以直接刪除的節點。(即:讓小優先順序的結點旋到上面,滿足最小堆的性質)。

下面圖解展示:

刪除結點11的右孩子的優先順序更小,因此需要左旋,旋轉後如圖所示,這個時候,結點11還未到最簡單的情況,需要再次旋轉:

這次是其左孩子優先順序較低,需要右旋。當然旋轉後,還是還要繼續的。。。

到這裡,我們喜歡的情況出現了,需要刪除的結點11只有左孩子,這樣我們重接左孩子就可以了。

對於刪除操作應該是明白了吧,下面看程式碼:

//刪除函式
bool remove(Tree &root,ElementType val)
{
    if (root==NULL)
        return false;
    else if (root->val>val)
        return remove(root->lchild,val);
    else if(root->val<val)
        return remove(root->rchild,val);
    else //找到執行刪除處理
    {

        Tree *node=&root;
        while ((*node)->lchild && (*node)->rchild)  //從該結點開始往下調整
        {
            if ((*node)->lchild->priority<(*node)->rchild->priority) //比較其左右孩子優先順序
            {
                right_rotate(*node); //右旋轉
                node=&((*node)->rchild); //更新傳入引數,進入下一層
            }
            else
            {
                left_rotate(*node); //左旋轉
                node=&((*node)->lchild); //更新傳入引數,進入下一層
            }
        }
        
        //最後調整到(或者本來是)葉子結點,或者只有一個孩子的情況,可以直接刪除了
        if ((*node)->lchild==NULL)
            (*node)=(*node)->rchild;
        else if((*node)->rchild==NULL)
            (*node)=(*node)->lchild;
        return true;
    }
}


OK,treap到這裡就講完了,treap的程式設計複雜度比起AVL,和伸展樹來說要小很多,這是隻是簡單的對treap進行了實現,對於treap的很多應用還需要學習學習,擴充套件思路。

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
typedef struct TreapNode* Tree;
typedef int ElementType;
struct TreapNode
{
    ElementType val; //結點值
    int priority;   //優先順序
    Tree lchild;
    Tree rchild;
    TreapNode(int val=0,int priority=0) //預設建構函式
    {
        lchild=rchild=NULL;
        this->val=val;
        this->priority=priority;
    }
};
Tree search(Tree &,ElementType);
void right_rotate(Tree &);
void left_rotate(Tree &);
bool insert_val(Tree &,Tree &);
bool insert(Tree &,ElementType,int);
bool remove(Tree &,ElementType);


//查詢函式
Tree search(Tree &root,ElementType val)
{
    if (!root)
        return NULL;
    else if (root->val>val)
       return search(root->lchild,val);
    else if(root->val<val)
      return  search(root->rchild,val);
    return root;
}
//插入函式
bool insert(Tree &root,ElementType val=0,int priority=0)
{
    Tree node=new TreapNode(val,priority);
    return insert_val(root, node);
}
//內部插入函式
//引數:根結點,需插入的結點
bool insert_val(Tree &root,Tree &node)
{
    if (!root)
    {
        root=node; //插入
        return true;
    }
    else if(root->val>node->val)
    {
       bool flag=insert_val(root->lchild, node);
        if (root->priority>node->priority) //檢查是否需要調整
            right_rotate(root);
        return flag;
    }
    else if(root->val<node->val)
    {
        bool flag=insert_val(root->rchild, node);
        if (root->priority>node->priority) //檢查是否需要調整
            left_rotate(root);
        return flag;
    }
    //已經含有該元素,釋放結點
    delete node;
    return false;

}
//右旋轉
void right_rotate(Tree &node)
{
    Tree temp=node->lchild;
    node->lchild=temp->rchild;
    temp->rchild=node;
    node=temp;
}
//左旋轉
void left_rotate(Tree &node)
{
    Tree temp=node->rchild;
    node->rchild=temp->lchild;
    temp->lchild=node;
    node=temp;
}

//刪除函式
bool remove(Tree &root,ElementType val)
{
    if (root==NULL)
        return false;
    else if (root->val>val)
        return remove(root->lchild,val);
    else if(root->val<val)
        return remove(root->rchild,val);
    else //找到執行刪除處理
    {

        Tree *node=&root;
        while ((*node)->lchild && (*node)->rchild)  //從該結點開始往下調整
        {
            if ((*node)->lchild->priority<(*node)->rchild->priority) //比較其左右孩子優先順序
            {
                right_rotate(*node); //右旋轉
                node=&((*node)->rchild); //更新傳入引數,進入下一層
            }
            else
            {
                left_rotate(*node); //左旋轉
                node=&((*node)->lchild); //更新傳入引數,進入下一層
            }
        }
        
        //最後調整到(或者本來是)葉子結點,或者只有一個孩子的情況,可以直接刪除了
        if ((*node)->lchild==NULL)
            (*node)=(*node)->rchild;
        else if((*node)->rchild==NULL)
            (*node)=(*node)->lchild;
        return true;
    }
}


//前序
void PreOrder(Tree root)
{
    if (root==NULL)
        return;
    printf("%d ",root->val);
    PreOrder(root->lchild);
    PreOrder(root->rchild);
}
//中序
void InOrder(Tree root)
{
    if (root==NULL)
        return;
    InOrder(root->lchild);
    printf("%d ",root->val);
    InOrder(root->rchild);
}
int main()
{
    Tree root=NULL;

    insert(root,11,6);
    insert(root,7,13);
    insert(root,14,14);
    insert(root,3,18);
    insert(root,9,22);
    insert(root,18,20);
    insert(root,16,26);
    insert(root,15,30);
    insert(root,17,12);
    printf("插入:\n");
    printf("前序:");
    PreOrder(root);
    printf("\n");
    printf("中序:");
    InOrder(root);
    printf("\n");
    printf("刪除:\n");
    remove(root,11);
    printf("前序:");
    PreOrder(root);
    printf("\n");
    printf("中序:");
    InOrder(root);
    printf("\n");
    return 0;
}