你们好,今天和你们聊一个新的数据结构,叫作Treap。node
Treap本质上也是一颗BST(平衡二叉搜索树),和咱们以前介绍的SBT是同样的。可是Treap维持平衡的方法和SBT不太同样,有些许区别,相比来讲呢,Treap的原理还要再简单一些,因此以前在竞赛当中不容许使用STL的时候,咱们一般都会手写一棵Treap来代替。web
既然是平衡二叉搜索树,关键点就在于平衡,那么重点天然是如何维护树的平衡。数据结构
在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的插入元素的操做,其实插入元素的操做很是简单,就是普通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当中,你们感兴趣能够点击阅读原文查看,代码里都有详细的注释,你们应该都能看明白。
今天的文章就到这里,衷心祝愿你们天天都有所收获。若是还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发)