I've implemented an AA Tree (a type of self-balancing binary search tree) using Numba, a just-in-time compiler for Python. This implementation includes rank and select operations, which are useful for order statistics.
The AA Tree is designed to maintain balance for fast operations while being simpler to implement than other self-balancing trees like Red-Black trees. I chose to use Numba to potentially improve performance, especially for the rank and select operations.
Here's my implementation:
# Order Statistics Tree based on AA Tree
from numba import njit
from numba import uint32, deferred_type, optional, uint16
from numba.experimental import jitclass
from collections import OrderedDict
node_type = deferred_type()
spec = OrderedDict()
spec['data'] = uint32
spec['left'] = optional(node_type)
spec['right'] = optional(node_type)
spec['level'] = uint16
spec['size'] = uint16
spec['left_size'] = uint16
@jitclass(spec)
class NumbaNodeAA:
def __init__(self, data: int):
self.data = data
self.left = None
self.right = None
self.level = 1
self.size = 1
self.left_size = 0
node_type.define(NumbaNodeAA.class_type.instance_type)
class AAOSTN:
def __init__(self):
self.root = None
def insert(self, data: int):
self.root = self._insert(self.root, data)
def _insert(self, node: NumbaNodeAA, data: int):
if node is None:
return NumbaNodeAA(data)
if data < node.data:
node.left = self._insert(node.left, data)
elif data > node.data:
node.right = self._insert(node.right, data)
else:
return node
node = self._skew(node)
node = self._split(node)
node.size = self._update_size_static(node)
return node
def _skew(self, node: NumbaNodeAA):
if node is None or node.left is None:
return node
if node.level == node.left.level:
left = node.left
node.left = left.right
left.right = node
node.size = self._update_size_static(node)
left.size = self._update_size_static(left)
return left
return node
def _split(self, node: NumbaNodeAA):
if node is None or node.right is None or node.right.right is None:
return node
if node.level == node.right.right.level:
right = node.right
node.right = right.left
right.left = node
right.level += 1
node.size = self._update_size_static(node)
right.size = self._update_size_static(right)
return right
return node
@staticmethod
@njit
def _update_size_static(node: NumbaNodeAA):
if node is None:
return 0
left_size = node.left.size if node.left is not None else 0
right_size = node.right.size if node.right is not None else 0
node.left_size = left_size
node.size = 1 + left_size + right_size
return node.size
@staticmethod
@njit(fastmath=True)
def _select_static(node: NumbaNodeAA, k: int):
while node is not None:
if k < node.left_size:
node = node.left
elif k > node.left_size:
k -= node.left_size + 1
node = node.right
else:
return node.data
return -1
@staticmethod
@njit(fastmath=True)
def _rank_static(node: NumbaNodeAA, data: int):
rank = 0
while node is not None:
if data < node.data:
node = node.left
elif data > node.data:
rank += node.left_size + 1
node = node.right
else:
return rank + node.left_size
return -1
def select(self, k: uint32):
return self._select_static(self.root, k)
def rank(self, data: uint32):
return self._rank_static(self.root, data)
def delete(self, data: int):
self.root = self._delete(self.root, data)
def _delete(self, node: NumbaNodeAA, data: int):
if node is None:
return None
if data < node.data:
node.left = self._delete(node.left, data)
elif data > node.data:
node.right = self._delete(node.right, data)
else:
if node.left is None or node.right is None:
return node.left if node.left is not None else node.right
else:
successor = self._find_min(node.right)
node.data = successor.data
node.right = self._delete(node.right, successor.data)
node = self._decrease_level(node)
node = self._skew(node)
if node.right is not None:
node.right = self._skew(node.right)
if node.right is not None and node.right.right is not None:
node.right.right = self._skew(node.right.right)
node = self._split(node)
if node.right is not None:
node.right = self._split(node.right)
node.size = self._update_size_static(node)
return node
def _decrease_level(self, node: NumbaNodeAA):
left_level = node.left.level if node.left is not None else 0
right_level = node.right.level if node.right is not None else 0
level = min(left_level, right_level) + 1
if level < node.level:
node.level = level
if node.right is not None and node.right.level > level:
node.right.level = level
return node
def _find_min(self, node: NumbaNodeAA):
current = node
while current.left is not None:
current = current.left
return current
def inorder(self):
stack = []
current = self.root
while stack or current is not None:
if current is not None:
stack.append(current)
current = current.left
else:
current = stack.pop()
print(current.data, end=' ')
current = current.right
print()
The AAOSTN class represents the AA Tree. Key methods include:
- insert: Adds a new node to the tree
- delete: Removes a node from the tree
- select: Finds the k-th smallest element in the tree
- rank: Finds the rank of a given element in the tree
The tree maintains its balance using the skew and split operations, which are characteristic of AA Trees.
I'm particularly interested in feedback on:
- Overall code structure and organization
- Effective use of Numba (I'm new to using it)
- Correctness of the AA Tree implementation
- Performance considerations, especially for insertions
- Python best practices and any potential improvements
While the rank and select operations seem to perform well, I've noticed that insertions are slower than I expected. Any insights into this would be appreciated, but I welcome all forms of feedback to improve this code.
Benchmark plots: