Add Treap Revisited post
This commit is contained in:
commit
cc4440c946
146
content/posts/2024-07-27-treap-revisited/index.md
Normal file
146
content/posts/2024-07-27-treap-revisited/index.md
Normal file
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
title: "Treap, revisited"
|
||||
date: 2024-07-27T14:12:27+01:00
|
||||
draft: false # I don't care for draft mode, git has branches for that
|
||||
description: "An even simpler BST"
|
||||
tags:
|
||||
- algorithms
|
||||
- data structures
|
||||
- python
|
||||
categories:
|
||||
- programming
|
||||
series:
|
||||
- Cool algorithms
|
||||
favorite: false
|
||||
disable_feed: false
|
||||
---
|
||||
|
||||
My [last post]({{< relref "../2024-07-20-treap/index.md" >}}) about the _Treap_
|
||||
showed an implementation using tree rotations, as is commonly done with [AVL
|
||||
Trees][avl] and [Red Black Trees][rb].
|
||||
|
||||
But the _Treap_ lends itself well to a simple and elegant implementation with no
|
||||
tree rotations. This makes it especially easy to implement the removal of a key,
|
||||
rather than the fiddly process of deletion using tree rotations.
|
||||
|
||||
[avl]: https://en.wikipedia.org/wiki/AVL_tree
|
||||
[rb]: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree
|
||||
|
||||
<!--more-->
|
||||
|
||||
## Implementation
|
||||
|
||||
All operations on the tree will be implemented in terms of two fundamental
|
||||
operations: `split` and `merge`.
|
||||
|
||||
We'll be reusing the same structures as in the last post, so let's skip straight
|
||||
to implementing those fundaments, and building on them for `insert` and
|
||||
`delete`.
|
||||
|
||||
### Split
|
||||
|
||||
Splitting a tree means taking a key, and getting the following output:
|
||||
|
||||
* a `left` node, root of the tree of all keys lower than the input.
|
||||
* an extracted `node` which corresponds to the input `key`.
|
||||
* a `right` node, root of the tree of all keys higher than the input.
|
||||
|
||||
```python
|
||||
type OptionalNode[K, V] = Node[K, V] | None
|
||||
|
||||
class SplitResult(NamedTuple):
|
||||
left: OptionalNode
|
||||
node: OptionalNode
|
||||
right: OptionalNode
|
||||
|
||||
def split(root: OptionalNode[K, V], key: K) -> SplitResult:
|
||||
# Base case, empty tree
|
||||
if root is None:
|
||||
return SplitResult(None, None, None)
|
||||
# If we found the key, simply extract left and right
|
||||
if root.key == key:
|
||||
left, right = root.left, root.right
|
||||
root.left, root.right = None, None
|
||||
return SplitResult(left, root, right)
|
||||
# Otherwise, recurse on the corresponding side of the tree
|
||||
if root.key < key:
|
||||
left, node, right = split(root.right, key)
|
||||
root.right = left
|
||||
return SplitResult(root, node, right)
|
||||
if key < root.key:
|
||||
left, node, right = split(root.left, key)
|
||||
root.left = right
|
||||
return SplitResult(left, node, root)
|
||||
raise RuntimeError("Unreachable")
|
||||
```
|
||||
|
||||
### Merge
|
||||
|
||||
Merging a `left` and `right` tree means (cheaply) building a new tree containing
|
||||
both of them. A pre-condition for merging is that the `left` tree is composed
|
||||
entirely of nodes that are lower than any key in `right` (i.e: as in `left` and
|
||||
`right` after a `split`).
|
||||
|
||||
```python
|
||||
def merge(
|
||||
left: OptionalNode[K, V],
|
||||
right: OptionalNode[K, V],
|
||||
) -> OptionalNode[K, V]:
|
||||
# Base cases, left or right being empty
|
||||
if left is None:
|
||||
return right
|
||||
if right is None:
|
||||
return left
|
||||
# Left has higher priority, it must become the root node
|
||||
if left.priority >= right.priority:
|
||||
# We recursively reconstruct its right sub-tree
|
||||
left.right = merge(left.right, right)
|
||||
return left
|
||||
# Right has higher priority, it must become the root node
|
||||
if left.priority < right.priority:
|
||||
# We recursively reconstruct its left sub-tree
|
||||
right.left = merge(left, right.left)
|
||||
return right
|
||||
raise RuntimeError("Unreachable")
|
||||
```
|
||||
|
||||
### Insertion
|
||||
|
||||
Inserting a node into the tree is done in two steps:
|
||||
|
||||
1. `split` the tree to isolate the middle insertion point
|
||||
2. `merge` it back up to form a full tree with the inserted key
|
||||
|
||||
```python
|
||||
def insert(self, key: K, value: V) -> bool:
|
||||
# `left` and `right` come before/after the key
|
||||
left, node, right = split(self._root, key)
|
||||
was_updated: bool
|
||||
# Create the node, or update its value, if the key was already in the tree
|
||||
if node is None:
|
||||
node = Node(key, value)
|
||||
was_updated = False
|
||||
else:
|
||||
node.value = value
|
||||
was_updated = True
|
||||
# Rebuild the tree with a couple of merge operations
|
||||
self._root = merge(left, merge(node, right))
|
||||
# Signal whether the key was already in the key
|
||||
return was_updated
|
||||
```
|
||||
|
||||
### Removal
|
||||
|
||||
Removing a key from the tree is similar to inserting a new key, and forgetting
|
||||
to insert it back: simply `split` the tree and `merge` it back without the
|
||||
extracted middle node.
|
||||
|
||||
```python
|
||||
def remove(self, key: K) -> bool:
|
||||
# `node` contains the key, or `None` if the key wasn't in the tree
|
||||
left, node, right = split(self._root, key)
|
||||
# Put the tree back together, without the extract node
|
||||
self._root = merge(left, right)
|
||||
# Signal whether `key` was mapped in the tree
|
||||
return node is not None
|
||||
```
|
Loading…
Reference in a new issue