大家好,今天和大家聊一個新的資料結構,叫做Treap。
Treap本質上也是一顆BST(平衡二叉搜尋樹),和我們之前介紹的SBT是一樣的。但是Treap維持平衡的方法和SBT不太一樣,有些許區別,相比來說呢,Treap的原理還要再簡單一些,所以之前在競賽當中不允許使用STL的時候,我們通常都會手寫一棵Treap來代替。
Treap的基本原理
既然是平衡二叉搜尋樹,關鍵點就在於平衡,那麼重點自然是如何維護樹的平衡。
在Treap當中,維護平衡非常簡單,只有一句話,就是通過維護小頂堆的形式來維持樹的平衡。Treap也正是因此得名,因為它是Tree和Heap的結合體。
我們來看下Treap當中節點的結構:
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插入元素的操作。唯一的問題是如何維持樹的平衡。
我們前文說了,我們是通過維持堆的性質來保持平衡的,那麼自然又會有一個新的問題。為什麼維持堆的性質可以保證平衡呢?
答案很簡單,因為我們在插入的時候,需要對每一個插入的Node隨機附上一個priority。堆就是用來維護這個priority的,保證樹根一定擁有最小的priority。正是由於這個priority是隨機的,我們可以保證整棵樹蛻化成線性的概率降到無窮低。
當我們插入元素之後發現破壞了堆的性質,那麼我們需要通過旋轉操作來維護。舉個簡單的例子,在下圖當中,如果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當中,大家感興趣可以點選閱讀原文檢視,程式碼裡都有詳細的註釋,大家應該都能看明白。
今天的文章就到這裡,衷心祝願大家每天都有所收穫。如果還喜歡今天的內容的話,請來一個三連支援吧~(點贊、關注、轉發)