Treap——堆和二叉樹的完美結合,性價比極值的搜索樹

你們好,今天和你們聊一個新的數據結構,叫作Treap。node

Treap本質上也是一顆BST(平衡二叉搜索樹),和咱們以前介紹的SBT是同樣的。可是Treap維持平衡的方法和SBT不太同樣,有些許區別,相比來講呢,Treap的原理還要再簡單一些,因此以前在競賽當中不容許使用STL的時候,咱們一般都會手寫一棵Treap來代替。web

Treap的基本原理

既然是平衡二叉搜索樹,關鍵點就在於平衡,那麼重點天然是如何維護樹的平衡。數據結構

在Treap當中,維護平衡很是簡單,只有一句話,就是經過維護小頂堆的形式來維持樹的平衡。Treap也正是所以得名,由於它是Tree和Heap的結合體。編輯器

咱們來看下Treap當中節點的結構:ide

class TreapNode(TreeNode):
    """
    TreeNode: The node class of treap tree.
    Paramters: 
        key: The key of node, can be treated as the key of dictionary
        value: The value of node, can be treated as the value of dictionary
        priority: The priority of node, specially for treap structure, describe the priority of the node in the treap. 
        lchild: The left child of node
        rchild: The right child of node
        father: The parent of node, incase that we need to remove or rotate the node in the treap, so we need father parameter to mark the address of the parent
    """

    def __init__(self, key=None, value=None, lchild=None, rchild=None, father=None, priority=None):
        super().__init__(key, value, lchild, rchild, father)
        self._priority = priority

    @property
    def priority(self):
        return self._priority

    @priority.setter
    def priority(self, priority):
        self._priority = priority

    def __str__(self):
        return 'key={}, value={}'.format(self.key, self.value)

這裏的TreeNode是我抽象出來的樹結構通用的Node,當中包含key、value、lchild、rchild和father。TreapNode其實就是在此基礎上增長了一個priority屬性。性能

之因此要增長這個priority屬性是爲了維護它堆的性質,經過維護這個堆的性質來保持樹的平衡。具體的操做方法,請往下看。學習

Treap的增刪改查

插入

首先來說Treap的插入元素的操做,其實插入元素的操做很是簡單,就是普通BST插入元素的操做。惟一的問題是如何維持樹的平衡。flex

咱們前文說了,咱們是經過維持堆的性質來保持平衡的,那麼天然又會有一個新的問題。爲何維持堆的性質能夠保證平衡呢?url

答案很簡單,由於咱們在插入的時候,須要對每個插入的Node隨機附上一個priority。堆就是用來維護這個priority的,保證樹根必定擁有最小的priority。正是因爲這個priority是隨機的,咱們能夠保證整棵樹蛻化成線性的機率降到無窮低spa

當咱們插入元素以後發現破壞了堆的性質,那麼咱們須要經過旋轉操做來維護。舉個簡單的例子,在下圖當中,若是B節點的priority比D要小,爲了保證堆的性質,須要將B和D進行互換。因爲直接互換會破壞BST的性質,因此咱們採起旋轉的操做。

旋轉以後咱們發現B和D互換了位置,而且旋轉以後的A和E的priority都是大於D的,因此旋轉以後咱們整棵樹依然維持了性質。

右旋的狀況也是同樣的,其實咱們觀察一下會發現,要交換左孩子和父親須要右旋,若是是要交換右孩子和父親,則須要左旋

整個插入的操做其實就是基礎的BST插入過程,加上旋轉的判斷。

    def _insert(self, node, father, new_node, left_or_right='left'):
        """
        Inside implement of insert node.
        Implement in recursion.
        Since the parameter passed in Python is reference, so when we add node, we need to assign the node to its father, otherwise the reference will lose outside the function.
        When we add node, we need to compare its key with its father's key to make sure it's the lchild or rchild of its father.
        """

        if node is None:
            if new_node.key < father.key:
                father.lchild = new_node
            else:
                father.rchild = new_node
            new_node.father = father
            return
        if new_node.key < node.key:
            self._insert(node.lchild, node, new_node, 'left')
            # maintain
            if node.lchild.priority < node.priority:
                self.rotate_right(node, father, left_or_right)
        else:
            self._insert(node.rchild, node, new_node, 'right')
            # maintain
            if node.rchild.priority < node.priority:
                self.rotate_left(node, father, left_or_right)

前面的邏輯就是BST的插入,也就是和當前節點比大小,決定插入在左邊仍是右邊。注意一下,這裏咱們在插入完成以後,增長了maintain的邏輯,其實也就是比較一下,剛剛進行的插入是否破壞了堆的性質。可能有些同窗要問我了,這裏爲何只maintain了一次?有可能插入的priority很是小,須要一直旋轉到樹根不是嗎?

的確如此,可是不要忘了,咱們這裏的maintain邏輯並不是只調用一次。隨着整個遞歸的回溯,在樹上的每一層它其實都會執行一次maintain邏輯。因此是能夠保證從插入的地方一直維護到樹根的。

查詢

查詢很簡單,不用多說,就是BST的查詢操做,沒有任何變化。

    def _query(self, node, key, backup=None):
        if node is None:
            return backup
        if key < node.key:
            return self._query(node.lchild, key, backup)
        elif key > node.key:
            return self._query(node.rchild, key, backup)
        return node

    def query(self, key, backup=None):
        """
        Return the result of query a specific node, if not exists return None
        """

        return self._query(self.root, key, backup)

刪除

刪除的操做稍微麻煩了一些,因爲涉及到了優先級的維護,不過邏輯也不難理解,只須要牢記須要保證堆的性質便可。

首先,有兩種狀況很是簡單,一種是要刪除的節點是葉子節點,這個都很容易想明白,刪除它不會影響任何其餘節點,直接刪除便可。第二種狀況是鏈節點,也就是說它只有一個孩子,那麼刪除它也不會引發變化,只須要將它的孩子過繼給它的父親,整個堆和BST的性質也不會受到影響。

對於這兩種狀況以外,咱們就沒辦法直接刪除了,由於必然會影響堆的性質。這裏有一個很巧妙的作法,就是能夠先將要刪除的節點旋轉,將它旋轉成葉子節點或者是鏈節點,再進行刪除

在這個過程中,咱們須要比較一下它兩個孩子的優先級,確保堆的性質不會受到破壞。

    def _delete_node(self, node, father, key, child='left'):
        """
        Implement function of delete node.
        Defined as a private function that only can be called inside.
        """

        if node is None:
            return
        if key < node.key:
            self._delete_node(node.lchild, node, key)
        elif key > node.key:
            self._delete_node(node.rchild, node, key, 'right')
        else:
            # 若是是鏈節點,葉子節點的狀況也包括了
            if node.lchild is None:
                self.reset_child(father, node.rchild, child)
            elif node.rchild is None:
                self.reset_child(father, node.lchild, child)
            else:
                # 根據兩個孩子的priority決定是左旋仍是右旋
                if node.lchild.priority < node.rchild.priority:
                    node = self.rotate_right(node, father, child)
                    self._delete_node(node.rchild, node, key, 'right')
                else:
                    node = self.rotate_left(node, father, child)
                    self._delete_node(node.lchild, node, key)

                    
    def delete(self, key):
        """
        Interface of delete method face outside.
        """

        self._delete_node(self.root, None, key, 'left')

修改

修改的操做也很是簡單,咱們直接查找到對應的節點,修改它的value便可。

旋轉

咱們也貼一下旋轉操做的代碼,其實這裏的邏輯和以前SBT當中介紹的旋轉操做是同樣的,代碼也基本相同:

    def reset_child(self, node, child, left_or_right='left'):
        """
        Reset the child of father, since in Python all the instances passed by reference, so we need to set the node as a child of its father node.
        """

        if node is None:
            self.root = child
            self.root.father = None
            return
        if left_or_right == 'left':
            node.lchild = child
        else:
            node.rchild = child
        if child is not None:
            child.father = node


 def rotate_left(self, node, father, left_or_right):
        """
        Left rotate operation of Treap.
        Example: 

                D
              /   \
             A      B
                   / \
                  E   C

        After rotate:

                B
               / \
              D   C
             / \
            A   E 
        """

        rchild = node.rchild
        node.rchild = rchild.lchild
        if rchild.lchild is not None:
            rchild.lchild.father = node
        rchild.lchild = node
        node.father = rchild
        self.reset_child(father, rchild, left_or_right)
        return rchild

    def rotate_right(self, node, father, left_or_right):
        """
        Right rotate operation of Treap.
        Example: 

                D
              /   \
             A     B
            / \
           E   C

        After rotate:

                A
               / \
              E   D
                 / \
                C   B 
        """

        lchild = node.lchild
        node.lchild = lchild.rchild
        if lchild.rchild is not None:
            lchild.rchild.father = node
        lchild.rchild = node
        node.father = lchild
        self.reset_child(father, lchild, left_or_right)
        return lchild

這裏惟一要注意的是,因爲Python當中存儲的都是引用,因此咱們在旋轉操做以後必需要從新覆蓋一下父節點當中當中的值纔會生效。負責咱們修改了node的引用,可是father當中仍是存儲的舊的地址,同樣沒有生效。

後記

基本上到這裏整個Treap的原理就介紹完了,固然除了咱們剛纔介紹的基本操做以外,Treap還有一些其餘的操做。好比能夠split成兩個Treap,也能夠由兩個Treap合併成一個。還能夠查找第K大的元素,等等。這些額外的操做,我用得也很少,就很少介紹了,你們感興趣能夠去了解一下。

Treap這個數據結構在實際當中幾乎沒有用到過,通常仍是以競賽場景爲主,咱們學習它主要就是爲了提高和鍛鍊咱們的數據結構能力以及代碼實現能力。Treap它的最大優勢就是實現簡單,沒有太多複雜的操做,可是咱們前面也說了,它是經過隨機的priority來控制樹的平衡的,那麼它顯然沒法作到完美平衡,只能作到不落入最壞的狀況,可是沒法保證能夠進入最好的狀況。不過對於二叉樹來講,樹深的一點差距相差並不大。因此Treap的性能倒也沒有那麼差勁,屬於一個性價比很是高的數據結構。

最後,仍是老規矩,我把完整的代碼放在了paste當中,你們感興趣能夠點擊閱讀原文查看,代碼裏都有詳細的註釋,你們應該都能看明白。

今天的文章就到這裏,衷心祝願你們天天都有所收穫。若是還喜歡今天的內容的話,請來一個三連支持吧~(點贊、關注、轉發

相關文章
相關標籤/搜索