Treap:结合堆和二叉树的搜索树
大家好,今天我们来讨论一种新的数据结构,称为TReap。
TReap本质上也是一种平衡二叉搜索树(BST),与之前介绍的SBT类似。不过,TReap维持平衡的方式与SBT略有不同,相对而言,TReap的原理更为简单。因此,在竞赛中不允许使用STL时,我们通常手动实现TReap以替代其他数据结构。
TReap的基本原理
作为平衡二叉搜索树,TReap的关键在于如何保持树的平衡。TReap通过维护小顶堆的方式来实现平衡,这也是其名称的由来,因为它是树(Tree)与堆(Heap)的结合。
让我们来看一下TReap中节点的结构:
class TReapNode(TReEnode): TReEnode: TReap树的节点类。参数:key: 节点的键,作为字典的键;value: 节点的值,作为字典的值;priority: 节点的优先级,专门用于TReap结构,描述节点在TReap中的优先级;leftChild: 节点的左孩子;rightChild: 节点的右孩子;father: 节点的父亲,用于在需要删除或旋转节点时标记父节点的地址。
def __init__(self, key=None, value=None, leftChild=None, rightChild=None, father=None, priority=None): super().__init__(key, value, leftChild, rightChild, 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是我抽象出来的通用节点结构,包含key、value、leftChild、rightChild和father。TReapNode是在此基础上增加了一个priority属性。
增加priority属性的原因在于维护堆的性质,从而保持树的平衡。具体操作方法将在后文中详细介绍。
TReap的增删改查
插入
TReap的插入操作十分简单,实际上就是普通BST的插入过程。唯一的问题在于如何保持树的平衡。
如前所述,我们通过维持堆的性质来保持平衡,但这也引出了一个新的问题:为什么维持堆的性质可以确保平衡呢?
答案很简单,因为在插入时,我们会随机为每个新节点附上一个priority。堆的性质确保根节点的优先级一定是最小的。由于这个priority是随机的,因此可以保证整棵树退化为线性结构的概率极低。
在插入元素后,如果发现破坏了堆的性质,我们需要通过旋转操作来修复。举个简单的例子,假设B节点的priority比D小,为了保持堆的性质,我们需要交换B和D的位置。直接交换会导致BST的性质被破坏,因此我们采用旋转操作。
在旋转后,B和D的位置互换,且旋转后A和E的priority仍然大于D,因此整棵树依然维持了性质。右旋的情况也是类似的,交换左孩子和父亲需进行右旋,而交换右孩子和父亲则需进行左旋。
整个插入过程实际上是基础BST插入的过程,添加了旋转判断。插入时,我们通过比较当前节点的值决定插入左侧还是右侧。需要注意的是,在插入完成后,我们增加了维护逻辑,检查插入是否破坏了堆的性质。可能有人会问,为什么只维护一次?因为在递归回溯的过程中,维护逻辑会在树的每一层执行,因此可以确保从插入的位置一直维护到树根。
查询
查询操作非常简单,与BST的查询过程无异,没有任何变化。
删除
删除操作稍微复杂一些,因为需要维护优先级,不过逻辑并不难理解,关键是要保持堆的性质。
有两种情况非常简单:第一,删除的节点是叶子节点,这样直接删除不会影响其他节点;第二,删除的是链节点,即只有一个孩子节点,那么只需要将其孩子节点转移给父节点,即可保持堆和BST的性质不变。
对于这两种情况之外的节点,则无法直接删除,因为会影响堆的性质。此时,可以先将要删除的节点旋转为叶子节点或链节点,然后再进行删除。在这个过程中,需要比较它的两个孩子的优先级,以确保堆的性质不被破坏。
修改
修改操作也非常简单,只需查找到对应的节点并修改其value即可。
旋转
旋转操作的代码逻辑与之前在SBT中介绍的旋转操作相似,代码也基本一致:
def reset_child(self, node, child, left_or_right=’left’): 重置父节点的孩子,因为在Python中所有实例都是通过引用传递,因此我们需要将节点设置为其父节点的孩子。
def rotate_left(self, node, father, left_or_right): TReap的左旋操作。
def rotate_right(self, node, father, left_or_right): TReap的右旋操作。
需要注意的是,由于Python存储的都是引用,因此在旋转操作后必须重新覆盖父节点中的值,才能使修改生效。否则,我们修改了节点的引用,但父节点中仍存储的是旧的地址,修改将不会生效。
后记
到此为止,TReap的基本原理已经介绍完毕。除了基本操作外,TReap还有其他一些操作,例如可以将其分割成两个TReap,或将两个TReap合并为一个,还可以查找第K大的元素等。这些额外操作我用得不多,因此不再详细介绍,感兴趣的朋友可以进一步了解。
TReap这种数据结构在实际应用中较少使用,主要还是在竞赛场景中。我们学习它主要是为了提升和锻炼我们的数据结构能力以及代码实现能力。TReap的最大优点在于实现简单,操作不复杂,但由于是通过随机的priority来控制树的平衡,因此显然无法做到完美平衡,只能避免最坏情况,无法保证进入最佳情况。不过对于二叉树而言,树深的微小差距并不会造成太大影响。因此,TReap的性能并不差,属于性价比非常高的数据结构。
最后,我将完整的代码放在了paste中,感兴趣的朋友可以点击阅读原文查看,代码中都有详细注释,相信大家能够理解。
[[[IMG_1]]]
[[[IMG_2]]]
[[[IMG_3]]]
