Red-Black Trees: Querying
Table of Contents
The Imports
from bowling.data_structures.red_black_tree import tree as rb_tree
The Forester
This is the class to hold the tree and answer questions about it. Most of the methods (except for the red-black-height) are the same as the Binary Search Tree with the main exception being that we need to use the NIL
object instead of None
. This means that nothing in the tree
module can use the Forester
directly or we'll end up with a circular reference.
class Forester:
"""The forester manages the tree
Args:
tree: tree to manage
enforce_properties: check that the black-heights are the same
"""
def __init__(self, tree: rb_tree.RedBlackTree=None,
enforce_properties: bool=False) -> None:
self.tree = tree
self.enforce_properties = enforce_properties
return
Search
The first thing we'll look at is searching for a Node in a Binary Search Tree. Because of the way the tree is structured the search looks a lot like a Binary Search where we start at the root of the tree and then work our way downwards, picking the next child to follow based on whether the key we're searching for is greater than the right child's key or less than the left child's key. If it's equal then we've found the node we want.
The Implementation
I'll add the iterative version of the Tree Search as a method for our Query class.
def search(self, key) -> rb_tree.Node:
"""Searches the tree for the node with the matching key
Args:
key: node's key to search for
Returns:
the node with the key or None if it isn't in the tree
"""
node = self.tree.root
while node is not rb_tree.LEAF and key != node.key:
if key < node.key:
node = node.left
else:
node = node.right
return node
Testing
# pypi
from expects import be, be_none, equal, expect
from bowling.data_structures.red_black_tree import tree as rb_tree
from bowling.data_structures.red_black_tree.forester import Forester
root = rb_tree.Node(10)
tree = rb_tree.RedBlackTree(root)
query = Forester(tree)
output = query.search(5)
expect(output).to(be(rb_tree.NIL))
expect(query.search(10)).to(equal(root))
five = rb_tree.Node(5)
root.left = five
expect(query.search(5)).to(be(five))
root.right = fifteen = rb_tree.Node(15)
expect(query.search(15)).to(be(fifteen))
Miminum and Maximum
Mimimum
def min(self, node: rb_tree.Node=None) -> rb_tree.Node:
"""Returns the node with the smallest key
Args:
node: a node to use as the starting root
"""
if node is None:
node = self.tree.root
while node.left is not rb_tree.LEAF:
node = node.left
return node
Testing
root = rb_tree.Node(10)
tree = rb_tree.RedBlackTree(root)
query = Forester(tree)
root.left = rb_tree.Node(5)
root.left.left = rb_tree.Node(2)
root.right = rb_tree.Node(15)
root.right.right = rb_tree.Node(17)
root.right.left = rb_tree.Node(11)
expect(query.min()).to(equal(rb_tree.Node(2)))
root.left.left.left = rb_tree.Node(1)
expect(query.min()).to(equal(rb_tree.Node(1)))
expect(query.min(tree.root.right)).to(equal(rb_tree.Node(11)))
Maximum
def max(self, root: rb_tree.Node=None) -> rb_tree.Node:
"""Returns the node with the largest key
Args:
root: subtree root to start at
Returns:
node with the largest key in tree/subtree
"""
if root is None:
root = self.tree.root
while root.right is not rb_tree.LEAF:
root = root.right
return root
Testing
root = rb_tree.Node(10)
tree = rb_tree.RedBlackTree(root)
query = Forester(tree)
root.left = rb_tree.Node(5)
root.left.left = rb_tree.Node(2)
root.right = rb_tree.Node(15)
expect(query.max()).to(equal(rb_tree.Node(15)))
root.right.right = rb_tree.Node(17)
expect(query.max()).to(equal(rb_tree.Node(17)))
expect(query.min()).to(equal(rb_tree.Node(2)))
expect(query.max(tree.root.left)).to(equal(rb_tree.Node(5)))
Height
The height of the Binary Search Tree is the number of edges from the root of the tree to the furthest node. The algorithms we're looking at here don't use them but I'm going to use height to look at how the order you insert nodes in the tree affects the height.
@property
def height(self) -> int:
"""The length of the longest path starting at the root
Returns:
number of edges from root to furthest leaf
"""
return self.tree_height(self.tree.root)
def tree_height(self, node: rb_tree.Node=None) -> int:
"""The length of the longest path starting at the node
Args:
the node to start the measurement from
Returns:
number of edges from root to furthest leaf
"""
if node is rb_tree.LEAF:
return -1
left = self.tree_height(node.left) + 1
right = self.tree_height(node.right) + 1
return max(left, right)
tree = rb_tree.RedBlackTree()
query = Forester(tree)
expect(query.height).to(equal(-1))
root = rb_tree.Node(10)
tree.root = root
expect(query.height).to(equal(0))
root.left = rb_tree.Node(8)
expect(query.height).to(equal(1))
root.right = rb_tree.Node(12)
expect(query.height).to(equal(1))
root.left.left = rb_tree.Node(4)
expect(query.height).to(equal(2))
root.left.left.left = rb_tree.Node(2)
expect(query.height).to(equal(3))
Black Height
@property
def black_height(self) -> int:
"""The number of black nodes below the root
"""
return self.find_black_height(self.tree.root)
def find_black_height(self, node: rb_tree.Node=None) -> int:
"""Find the number of black nodes below a node
Note:
This assumes that the starting node is black. In the cases where it's red
it will be one more than the true height
Args:
node: base node to use
"""
if node is rb_tree.LEAF:
return 0
add_for_color = 1 if node.is_black else 0
left = self.find_black_height(node.left) + add_for_color
right = self.find_black_height(node.right) + add_for_color
if self.enforce_properties:
assert left == right, f"Black Height: Left={left} Right={right}"
return max((left, right))
Testing
tree = rb_tree.RedBlackTree()
forester = Forester(tree)
root = rb_tree.Node(26, color=rb_tree.Color.BLACK)
tree.root = root
expect(forester.black_height).to(equal(1))
root.left = rb_tree.Node(17, color=rb_tree.Color.RED)
expect(forester.black_height).to(equal(1))
root.left.left = rb_tree.Node(14, color=rb_tree.Color.BLACK)
expect(forester.black_height).to(equal(2))
root.left.left.left = rb_tree.Node(10, color=rb_tree.Color.RED)
expect(forester.black_height).to(equal(2))
root.left.left.right = rb_tree.Node(16, color=rb_tree.Color.BLACK)
expect(forester.black_height).to(equal(3))
Testing
from bowling.data_structures.red_black_tree.tree import Node, RedBlackTree
from bowling.data_structures.red_black_tree.arborist import Arborist
from bowling.data_structures.red_black_tree.forester import Forester