4.5 KiB
title | date | draft | description | tags | categories | series | favorite | disable_feed | graphviz | |||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Treap | 2024-07-20T14:12:27+01:00 | false | A simpler BST |
|
|
|
false | false | true |
The Treap is a mix between a Binary Search Tree and a Heap.
Like a Binary Search Tree, it keeps an ordered set of keys in the shape of a tree, allowing for binary search traversal.
Like a Heap, it associates each node with a priority, making sure that a parent's priority is always higher than any of its children.
What does it do?
By randomizing the priority value of each key at insertion time, we ensure a high likelihook that the tree stays roughly balanced, avoiding degenerating to unbalanced O(N) height.
Here's a sample tree created by inserting integers from 0 to 250 into the tree:
{{< graphviz file="treap.gv" />}}
Implementation
I'll be keeping the theme for this [series] by using Python to implement the
Treap. This leads to somewhat annoying code to handle the left
/right
nodes
which is easier to do in C, using pointers.
[series]: {{< ref "/series/cool-algorithms/" >}}
Representation
Creating a new Treap
is easy: the tree starts off empty, waiting for new nodes
to insert.
Each Node
must keep track of the key
, the mapped value
, and the node's
priority
(which is assigned randomly). Finally it must also allow for storing
two children (left
and right
).
class Node[K, V]:
key: K
value: V
priority: float
left: Node[K, V] | None
righg: Node[K, V] | None
def __init__(self, key: K, value: V):
# Store key and value, like a normal BST node
self.key = key
self.value = value
# Priority is derived randomly
self.priority = random()
self.left = None
self.right = None
class Treap[K, V]:
_root: Node[K, V] | None
def __init__(self):
# The tree starts out empty
self._root = None
Search
Searching the tree is the same as in any other Binary Search Tree.
def get(self, key: K) -> T | None:
node = self._root
# The usual BST traversal
while node is not None:
if node.key == key:
return node.value
elif node.key < key:
node = node.right
else:
node = node.left
return None
Insertion
To insert a new key
into the tree, we identify which leaf position it should
be inserted at. We then generate the node's priority, insert it at this
position, and rotate the node upwards until the heap property is respected.
type ChildField = Literal["left, right"]
def insert(self, key: K, value: V) -> bool:
# Empty treap base-case
if self._root is None:
self._root = Node(key, value)
# Signal that we're not overwriting the value
return False
# Keep track of the parent chain for rotation after insertion
parents = []
node = self._root
while node is not None:
# Insert a pre-existing key
if node.key == key:
node.value = value
return True
# Go down the tree, keep track of the path through the tree
field = "left" if key < node.key else "right"
parents.append((node, field))
node = getattr(node, field)
# Key wasn't found, we're inserting a new node
child = Node(key, value)
parent, field = parents[-1]
setattr(parent, field, child)
# Rotate the new node up until we respect the decreasing priority property
self._rotate_up(child, parents)
# Key wasn't found, signal that we inserted a new node
return False
def _rotate_up(
self,
node: Node[K, V],
parents: list[tuple[Node[K, V], ChildField]],
) -> None:
while parents:
parent, field = parents.pop()
# If the parent has higher priority, we're done rotating
if parent.priority >= node.priority:
break
# Check for grand-parent/root of tree edge-case
if parents:
# Update grand-parent to point to the new rotated node
grand_parent, field = parents[-1]
setattr(grand_parent, field, node)
else:
# Point the root to the new rotated node
self._root = node
other_field = "left" if field == "right" else "right"
# Rotate the node up
setattr(parent, field, getattr(node, other_field))
setattr(node, other_field, parent)