Skip to main content

Interval Tree

Description

Interval tree is a data structure that can be used to solve interval query problems.

  • Query: returns the number of overlaps with the query interval

Complexity:

O(logn)O(\log n) for query, O(nlogn)O(n \log n) for build

An interval tree stores a set of one-dimensional intervals and, given a query point qq, reports all intervals that contain qq in output-sensitive time. The classical construction picks the median of all endpoint coordinates as the root's split value, stores the intervals that cross the median at the root (once sorted by left endpoint and once by right endpoint), and recursively builds left and right subtrees from the intervals that lie strictly to one side of the median.

A stabbing query at qq compares qq with the split value: if qq is smaller, every interval at the root with left endpoint q\le q is a hit and we recurse left; if qq is larger, every interval with right endpoint q\ge q is a hit and we recurse right. The sorted lists at the root are scanned only until the first miss, so each recursion level reports at most as many extra intervals as it outputs.

Codes

warning

The class-based implementation below does not use the self-balancing property of the interval tree. Worst case time complexity is O(n2)O(n^2).


class Interval:
"""
Structure to represent an interval
"""
def __init__(self, low: int, high: int):
"""
Initialize the interval
:param low: the low value of the interval
:param high: the high value of the interval
"""
self.low = low
self.high = high

class Node:
"""
Structure to represent a node in Interval Search Tree
"""
def __init__(self, i: Interval):
"""
Initialize the node
:param i: the interval of the node
"""
self.i = i
# the maximum value of the interval
self.max = i.high
self.left = None
self.right = None
# internal property maintained along with the interval tree
# sections belows could be removed if not needed
self.interval_count = 1

def newNode(i: Interval) -> Node:
"""
Create a new Interval Search Tree Node
:param i: the interval of the node
:return: the new node
"""
temp = Node(Interval(i.low, i.high))
return temp

def insert(root: Node, i: Interval) -> Node:
"""
Insert the interval into the interval tree
This is similar to BST Insert.
Here the low value of interval is used to maintain BST property
:param root: the root of the interval tree
:param i: the interval to insert
:return: the node that the interval is inserted into
"""
# Base case: Tree is empty, new node becomes root
if root is None:
return newNode(i)

# Get low value of interval at root
l = root.i.low

# If root's low value is smaller,
# then new interval goes to left subtree
if i.low < l:
root.left = insert(root.left, i)

# Else, new node goes to right subtree.
else:
root.right = insert(root.right, i)

# Update the max value of this ancestor if needed
if root.max < i.high:
root.max = i.high

# additional property to maintain the interval tree, expected to be O(1)
# sections belows could be removed if not needed
root.interval_count += 1

return root

def isOverlapping(i1: Interval, i2: Interval) -> bool:
"""
Check if two intervals overlap
:param i1: the first interval
:param i2: the second interval
:return: True if the intervals overlap, otherwise False
"""
if i1.low <= i2.high and i2.low <= i1.high:
return True
return False

def overlapSearch(root: Node, i: Interval) -> Interval:
"""
Search for the interval that overlaps with the query interval
:param root: the root of the interval tree
:param i: the interval to search for
:return: the node that overlaps with the interval, otherwise None
"""
# Base Case, tree is empty
if root is None:
return None

# If given interval overlaps with root
if isOverlapping(root.i, i) is True:
return root.i

# If left child of root is present and max of left child is
# greater than or equal to given interval, then i may
# overlap with an interval is left subtree
if root.left is not None and root.left.max >= i.low:
return overlapSearch(root.left, i)

# Else interval can only overlap with right subtree
return overlapSearch(root.right, i)

def inorder(root: Node):
"""
Inorder traversal of the interval tree
:param root: the root of the interval tree
"""
if root is None:
return
inorder(root.left)
print("[" + str(root.i.low) + ", " + str(root.i.high) + "]" + " max = " + str(root.max))
inorder(root.right)

def isContaining(i1: Interval, i2: Interval) -> bool:
"""
Check if i2 is contained by i1
:param i1: the first interval
:param i2: the second interval
:return: True if i2 is contained by i1, otherwise False
"""
if i1.low <= i2.low and i1.high >= i2.high:
return True
return False

def containSearch(root: Node, i: Interval) -> Interval:
"""
Search for the interval that containing the query interval
:param root: the root of the interval tree
:param i: the interval to search for
:return: the interval that containing the query interval, otherwise None
"""
if root is None:
return None
if isContaining(root.i, i) is True:
return root.i

Example

Loading Python runner...

Description

Run time analysis

Build is O(nlogn)O(n \log n) and each stabbing query runs in O(logn+m)O(\log n + m) where mm is the number of reported intervals. The depth of the tree is O(logn)O(\log n), and at each level the scan over the sorted cross list stops as soon as it encounters a non-reported interval.

Space analysis

O(n)O(n). Every interval is stored in exactly one node — the highest node whose median it crosses — and each such interval appears twice in that node's two sorted lists.

Proof of correctness

Fix a query point qq. An interval [a,b][a, b] contains qq iff aqba \le q \le b. Each interval is stored at the unique node whose median mm lies in [a,b][a, b]. If q<mq < m then q<bq < b is automatic, so the interval contains qq iff aqa \le q; the scan of by_lo in increasing left-endpoint order reports exactly these intervals and stops at the first a>qa > q. The symmetric argument handles q>mq > m, and q=mq = m reports every crossing interval. Every qualifying interval whose storing node is on the root-to-leaf path towards qq is reported, and intervals on other branches cannot contain qq because they lie entirely on the opposite side of some median. See de Berg et al., Chapter 10, for the full treatment.

Extensions

Applications

References

  • de Berg, Cheong, van Kreveld, Overmars. Computational Geometry: Algorithms and Applications, 3rd ed., Chapter 10 (More Geometric Data Structures).
  • Edelsbrunner, H. "Dynamic data structures for orthogonal intersection queries." Technical report, 1980.
  • cp-algorithms — Segment Tree
  • GeeksforGeeks — Interval Tree