Packages

Implements an interval tree using an augmented self-balancing AVL tree with an interval as the data field and a max value tracking the interval high value in the subtree rooted at that node

Current section

Files

Jump to
interval_tree lib interval tree.ex
Raw

lib/interval/tree.ex

defmodule Interval.Tree do
@moduledoc """
Module stores interval data in a tree structure called an interval tree
The underlying tree implements a self-balancing AVL tree ensuring
all insert operations are O(log n) and that tree height is always O(log n)
The tree node contains the interval data which
contains the start and finish times
We also keep track of the max time for that node's subtree. This
is helpful when we compute the overlap search
The left and right child nodes of the current node are stored in the
left and right fields respectively
Thanks to geeksforgeeks.org and the CLR algorithms textbook
for Interval and AVL Tree descriptions and implementations
http://www.geeksforgeeks.org/interval-tree/
http://www.geeksforgeeks.org/avl-tree-set-1-insertion/
https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree
Implemented operations are traverse, search, and insert
"""
alias Interval.Tree, as: Tree
alias Interval.Node, as: Node
# Define interval tree struct
defstruct size: 0, root: nil
def new, do: %Tree{}
##############################################################################
# Traverse - O(n)
@doc "Traverse implements an inorder traversal"
def traverse(%Tree{root: node}) do
list = do_traverse(node, [])
# Since we prepended to the list for O(1),
# we now need to reverse to ensure correct order
Enum.reverse(list)
end
# Private helper functions which assist in the recursion
# Matches case where node is nil
defp do_traverse(nil, list), do: list
# Matches case where node is non-empty
defp do_traverse(%Node{data: interval, left: left, right: right}, list)
when is_list(list) do
# Recurse left
list = do_traverse(left, list)
# Print current interval node
list = List.flatten(["#{inspect(interval)}"], list)
# Recurse right
_list = do_traverse(right, list)
end
##############################################################################
# Search - O(min(n, k log n)) where k is the number of overlapping intervals
@doc """
Search whether a given interval key overlaps
with any interval nodes, returns ALL overlaps
"""
def search(%Tree{root: node}, %Interval{} = key) do
do_search(node, key, MapSet.new())
end
# Private helper functions which assist in the recursion
# search base case matching nil node
def do_search(nil, %Interval{}, acc), do: acc
def do_search(
%Node{data: %Interval{} = t1, left: t1_left, right: t1_right},
%Interval{} = t2,
acc
) do
# check if given interval key overlaps with current interval node
acc =
case Interval.overlap?(t1, t2) do
true -> MapSet.put(acc, t1)
false -> acc
end
# recurse left and right
# NOTE: the classic overlap condition is
# (t1.start < t2.finish and t1.finish > t2.start)
# Given that the left child exists and its max is greater than
# the interval key's start, then the key may overlap with an interval
# node in the left subtree, search left!
# (notice this is half the classic overlap condition)
acc =
cond do
t1_left != nil and t1_left.max > t2.start ->
do_search(t1_left, t2, acc)
true ->
acc
end
# If we have an "overlap" with the current node's start and the right's
# aggregate max finish, then search the right subtree
# (notice this pretty well resembles the classic overlap condition with the
# difference being the aggregate max term)
acc =
cond do
t1_right != nil and t1.start < t2.finish and t1_right.max > t2.start ->
do_search(t1_right, t2, acc)
true ->
acc
end
acc
end
##############################################################################
# Insert - O(log n)
@doc """
Public insert method, inserts interval value into the interval key using
the low interval node value to maintain sorted order
"""
def insert(%Tree{root: node} = tree, %Interval{} = value) do
node = do_insert(node, value)
# Update the passed back tree with the updated size
%Tree{tree | root: node, size: tree.size + 1}
end
# Private helper functions which assist in the recursion
# Base Case - empty tree - pattern match on empty node
defp do_insert(nil, %Interval{} = interval) do
%Node{data: interval, max: interval.finish, height: 1}
end
# Non-empty, traverse to left
defp do_insert(
%Node{data: %Interval{start: low}, left: l, right: r} = n,
%Interval{start: start_key} = interval
)
when start_key < low do
# Perform normal BST insertion
l = do_insert(l, interval)
# update the node with the updated left child,
# also update height and max interval
n =
%Node{n | left: l, height: max_height(l, r) + 1}
|> update_max_interval
# ensure balance is maintained
_node = balance(n, start_key)
end
# Non-empty, traverse to right
defp do_insert(
%Node{data: %Interval{start: low}, left: l, right: r} = n,
%Interval{start: start_key} = interval
)
when start_key >= low do
# recurse with right subtree
r = do_insert(r, interval)
# update the node with updated right child,
# also update height and max interval
n =
%Node{n | right: r, height: max_height(l, r) + 1}
|> update_max_interval
# ensure balance is maintained
_node = balance(n, start_key)
end
##############################################################################
# AVL balance and rotation helpers
defp balance(%Node{left: l, right: r} = node, low_key)
when is_integer(low_key) do
# Using height delta we determine
# if we need to balance the tree at this node
delta = height_delta(node)
# 4 cases to handle a node imbalance
_node =
cond do
# Case 1, Left Left
# Since the delta is greater than 1, the left subtree is higher
# and since low_key is less than y's start_key it was inserted on its left
# Hence - left left
# z y
# / \ / \
# y T4 Right Rotate (z) x z
# / \ - - - - - - - - -> / \ / \
# x T3 T1 T2 T3 T4
# / \
# T1 T2
delta > 1 and l != nil and l.data != nil and low_key < l.data.start ->
right_rotate(node)
# Case 2, Right Right
# Since the delta is less than -1, the right subtree is higher
# and since the low_key is greater than y's start_key it was inserted on the right
# Hence - right right
# z y
# / \ / \
# T1 y Left Rotate(z) z x
# / \ - - - - - - - -> / \ / \
# T2 x T1 T2 T3 T4
# / \
# T3 T4
delta < -1 and r != nil and r.data != nil and low_key >= r.data.start ->
left_rotate(node)
# Case 3, Left Right
# Since the delta is greater than 1, the left subtree is higher
# Since the low_key is greater than in this case y's start key, the node
# was inserted on y's right subtree
# Hence - left right
# z z x
# / \ / \ / \
# y T4 Left Rotate (y) x T4 Right Rotate(z) y z
# / \ - - - - - - - - -> / \ - - - - - - - -> / \ / \
# T1 x y T3 T1 T2 T3 T4
# / \ / \
# T2 T3 T1 T2
delta > 1 and l != nil and l.data != nil and low_key >= l.data.start ->
%Node{node | left: left_rotate(l)} |> right_rotate
# Case 4, Right Left
# Since the delta is less than -1, the right subtree is higher
# Since the low_key is less than y's start key, the node
# was inserted on y's left subtree
# Hence - right left
# z z x
# / \ / \ / \
# T1 y Right Rotate (y) T1 x Left Rotate(z) z y
# / \ - - - - - - - - -> / \ - - - - - - - -> / \ / \
# x T4 T2 y T1 T2 T3 T4
# / \ / \
# T2 T3 T3 T4
delta < -1 and r != nil and r.data != nil and low_key < r.data.start ->
%Node{node | right: right_rotate(r)} |> left_rotate
# Default case
true ->
node
end
end
_ = """
Right rotate subtree rooted at z. See following diagram
We rotate z (the old root) to the right leaving y as the new root
T1, T2, T3 and T4 are subtrees.
z y
/ \ / \
y T4 Right Rotate (z) x z
/ \ - - - - - - - - -> / \ / \
x T3 T1 T2 T3 T4
/ \
T1 T2
"""
defp right_rotate(%Node{left: %Node{left: x, right: t3} = y, right: t4} = z) do
# Perform rotation, update heights and max interval
z =
%Node{z | left: t3, height: max_height(t3, t4) + 1}
|> update_max_interval
_y =
%Node{y | right: z, height: max_height(x, z) + 1}
|> update_max_interval
end
_ = """
Left rotate subtree rooted at z. See following diagram
We rotate z (the old root) to the left leaving y as the new root
z y
/ \ / \
T1 y Left Rotate(z) z x
/ \ - - - - - - - -> / \ / \
T2 x T1 T2 T3 T4
/ \
T3 T4
"""
defp left_rotate(%Node{left: t1, right: %Node{left: t2, right: x} = y} = z) do
# Perform rotation, update heights and max interval
z =
%Node{z | right: t2, height: max_height(t1, t2) + 1}
|> update_max_interval
_y =
%Node{y | left: z, height: max_height(z, x) + 1}
|> update_max_interval
end
##############################################################################
# Max interval flag helpers
# Update max interval
defp update_max_interval(%Node{data: interval, left: left, right: right} = node) do
max = Kernel.max(do_max(left), do_max(right)) |> Kernel.max(interval.finish)
Kernel.put_in(node.max, max)
end
defp do_max(nil), do: 0
defp do_max(%Node{max: max}), do: max
##############################################################################
# Height helpers
# Update max tree height
defp max_height(left, right) do
Kernel.max(do_height(left), do_height(right))
end
defp height_delta(nil), do: 0
defp height_delta(%Node{left: l, right: r}), do: do_height(l) - do_height(r)
defp do_height(nil), do: 0
defp do_height(%Node{height: height}), do: height
@doc "Provides dump of tree info to be used in Inspect protocol implementation"
def info(%Tree{} = tree) do
{tree.size, tree.root}
end
##############################################################################
# Inspect Protocol implementation -- custom behavior when inspect is invoked
# Allows users to inspect this module type in a controlled manner
defimpl Inspect do
import Inspect.Algebra
def inspect(t, opts) do
info = Inspect.Tuple.inspect(Tree.info(t), opts)
concat(["#IntervalTree<", info, ">"])
end
end
end