Interval Trees

We know that a tree is often a good way to represent an ordered set of values. It can also be a good way to order a set of ranges as well.


January 01, 2000
URL:http://www.drdobbs.com/cpp/interval-trees/184401179

January 2000/Interval Trees


Introduction

Programmers frequently need to store and retrieve values that describe a limited numerical range. A range is specified by a value pair, l and h such that l <= h, representing an interval [l, h]. The interval endpoints typically lie on the real number line, but in applications they are represented as values of floating-point, integer, character, or some other ordered type (such as date or time). To iterate over those entities with intervals containing a particular value, you can use a simple array-like container. But if you need more efficient access, you need a more sophisticated data structure.

For example, consider a large meteorological database consisting of the daily temperature ranges at various observation stations around the world. Figure 1 shows a possible implementation of the observation class that represents a record in the database. The member functions low and high return the temperature values representing the daily low and high temperatures, respectively. The id and date member functions return the station identifier and date of observation respectively. From the thousands of observations, you need to extract those that contain a given temperature value, t (i.e., low() <= t <= high()).

An array-based storage scheme (possibly implemented using an STL vector container) and a brute-force search through the entire database is acceptable only if a single extraction is to be performed or if the number of elements is small. For example, if you know all the temperature values of interest in advance, you need to make only one pass through the array. However, if you can interactively specify different temperature values at different times, the brute-force search technique becomes impractical because every element in the array must be examined during each extraction operation. If you sort the array in ascending order of the low-temperature values, you can terminate the sequential search when you reach the object whose low value is greater than t. Unfortunately, this technique becomes increasingly ineffective as t increases, because fewer observations are eliminated.

In this article, I present an STL-like container class based on the interval-tree data structure for storing interval objects, and STL-like iterator classes that efficiently access just those elements whose intervals contain a given query value. Furthermore, I leverage existing STL classes and algorithms to minimize the amount of new code I needed to write for this implementation.

Interval Tree Data Structure

Figure 2 illustrates the interval tree built from the following list of labeled intervals:

a=[75, 76], b=[75, 79], c=[75, 84],
d=[76, 80], e=[79, 80], f=[79, 83],
g=[79, 91], h=[80, 84], i=[80, 90],
j=[83, 84], k=[84, 85], l=[91, 92],
m=[91, 92]

Conceptually, the interval tree is based on the binary tree constructed from the sorted set X of unique end-point values of all the intervals:

X = {75, 76, 79, 80, 83,
     84, 85, 90, 91, 92}

Each node in the tree consists of a discriminant value and two lists, ascending low (AL) and descending high (DH), that contain those intervals encompassing the discriminant value. Both AL and DH of a node contain the same intervals; however, AL is sorted in ascending order of the lower endpoints of its constituent intervals and DH is sorted in descending order of the upper endpoints.

To construct the root of the interval tree, you select the median member of X. This forms the root's discriminant value. In Figure 2, the discriminant for the root is 83 at offset 4. Next, you select the intervals that contain the discriminant value and insert them into the root's AL and DH lists. Here:

AL = {c, f, g, h, i, j}
DH = {g, i, j, h, c, f}

The left subtree from the root is constructed recursively in a similar manner by considering the members X to the left of (less than) 83 and the remaining unassigned intervals. The right subtree is also recursively constructed by considering the members of X to the right of (greater than) 83 and the intervals that remain unassigned after the construction of the root and left subtree. It is important to note that an interval is assigned only once to any node. However, there is no restriction on the number of identical intervals that can be stored in the tree (e.g., intervals l and m in Figure 2).

A subtree is considered to be empty if all of its nodes have empty AL (and consequently DH) lists. Since an empty subtree provides no additional information, you may eliminate it from the interval tree in order to conserve memory. In Figure 2, nodes with empty AL and DH lists (N, Q, T, U, and W) are shown as hollow circles. Any empty node that can be eliminated is connected to its parent using a dashed line (e.g. N, Q, T, and W). It is possible for nodes with empty AL and DH lists to still be a part of the tree (such as U) because it has non-empty child nodes.

Given a query value, q, you report all intervals that contain this value by first examining the discriminant at the root node. If q is equal to the discriminant, then you simply report all the intervals in the node's AL list and end the traversal process. If q is less than the discriminant, you report the intervals in the node's AL list in sequence, starting at the beginning of the list, until you reach an interval whose lower endpoint is greater than q. You then recursively apply this procedure to the left subtree. If q is greater than the discriminant, you report all the intervals in the node's DH list in sequence, starting at the beginning of the list, until you reach an interval whose higher endpoint is less than q. You then recursively apply this procedure to the right subtree.

Implementation

Since the tree construction process is time consuming and complex, I avoid dynamically maintaining the interval tree when intervals are being added, modified, and/or deleted. Instead, the interval tree operates in two distinct modes. During the initialization mode, you can use the tree container like the standard vector container. Once you have inserted the intervals, you call the container's construct method. This puts the container in the query mode. If you modify any of the low/high values when the container is in this mode, you may destroy the integrity of the interval tree. Therefore, you should call the container's destruct method to revert back to the initialization mode before making any changes to the intervals stored in the container.

Figure 1 shows the key implementation features of an interval class. The interval class must define a type range_type that represents the data type of the endpoints of the range. This data type must have the following operators defined over it:

Furthermore, the interval class must also define member functions low and high which return the low and high endpoints, respectively, of the interval. The return type of these functions must also be range_type. The built-in scalar and floating-point data types in C++ meet all the requirements of the range_type type.

Figure 3 shows the relevant portions of the implementation of the interval tree template class, itree. This class is based on, and takes the same template parameters as, std::vector. Template parameter T represents the interval class whose instances will be stored in the container. I redefine T and T::range_type as invl_type and range_type, respectively, to enhance code clarity. Since, the container will look and behave like an STL vector during the initialization mode, I define invl_vec as std::vector<invl_type> and the private data member invals of type invl_vec.

Vector invals provides the underlying vector container functionality for the interval tree container. The al and dh data members are vectors containing pointers to intervals. I used these vectors to maintain the AL and DH lists for the interval tree's nodes. Each node needs only an index indicating where its lists begin and the number of items in the lists (see Figure 3). Since the number of items in the AL and DH lists is the same in a node, the node needs only one start and one size member.

To force the interval tree class to behave like a standard vector of intervals, I duplicate the vector container's public type definitions and member functions within the interval tree container class. The duplicated member functions call the corresponding functions in vector ivals. The reason for this extra level of forwarding is to force the non-const vector functions to fail if the interval tree is in query mode, preventing the accidental modification of the tree structure. Since I use the assert macro to perform the run-time check, and the functions are inlined, the overhead of the extra function call will be optimized away by the compiler for production-level code. Of course this technique for avoiding inadvertent modification of the intervals is not foolproof and only works during the application's development phase.

Figure 4 illustrates how an application uses the interval tree container class. Any of the container's const or non-const accessor functions can be called when it is in the initialization mode. However, once you have constructed the interval tree, only the const member functions may be called via a const reference to the interval tree container.

The interval tree construction is performed by the construct method. In this function, I first call extract_values to build a sorted vector of unique interval end-point values. Next, I call the recursive function construct_tree to build the interval tree, starting at the root. The flags parameter to construct_tree tracks the intervals that have been assigned to a node and should not be considered further.

The num_al_dh parameter maintains a running count of the number of entries in the al and dh vector pair that have been occupied so far. It is used to populate the start member of the current node. The start and end parameters are the starting and ending (inclusive) indices of the endpoint values that should be considered when determining the discriminant for the current node.

The construct_tree function is a direct implementation of the recursive interval tree construction process described above. I skip over any interval that has already been assigned to a node, locate the discriminant value from the values parameter, and add the pointers to the intervals that contain the discriminant of the node's AL and DH lists. An interval whose high endpoint is less than or greater than the node's discriminant will be in the left or right subtree of the node, respectively. I maintain indicators for whether such intervals exist using the continue_left and continue_right flags. If the existence of any intervals in the right and/or left subtrees is not indicated, I prune the tree by assigning a null pointer to the corresponding subtree's pointer.

The member function deconstruct calls the recursive function delete_node to delete the nodes of the interval tree. However, the vector containing the intervals is not disturbed.

Query Iterator

The query iterator class query_iterator, derived from the standard forward iterator, is shown in Figure 5. Since it is a forward iterator, reverse and random iteration is disallowed. I maintain the current state of the retrieval process (defined by the current node, query value, and the position of the iterator in the node's AL and DH lists) in the private data members of query_iterator.

I provide a public default constructor for query_iterator that creates an uninitialized iterator. In order to create an initialized iterator, you must call the itree's qbegin method, and provide a query value. In the qbegin function, I use the iterator class's private constructor (the itree class is as a friend of query_iterator) to create a partially initialized iterator. I complete the initialization of the iterator by calling its init_node member function. The init_node function locates the node closest to the root that contains intervals that contain the query value, and sets the position of the iterator to the beginning of the node's AL and DH lists.

In the iterator's increment method, I update the iterator's position in the AL and DH lists so that the dereferencing operators report the next valid interval. If the next valid interval is the interval pointed to by the subsequent entry in the node's AL or DH list, I simply increment the iterator's index member. However, if the interval is in a subtree, I set the interval's cur_node member to the appropriate child node, and call the iterator's init_node method. If there are no more intervals containing the query value, I assign a null pointer to the cur_node member and zero to the index member.

In the functions for the iterator comparison operators (operator== and operator!=), I compare the value, cur_node, and index members of the two iterators. Therefore, in qend, I return an iterator with the cur_node member set to a null pointer and the index member set to zero in order to make it easy to test for the end of a query iteration process.

Enhancements

I have made several compromises in order to keep this implementation simple and small. The most significant is not updating the interval tree dynamically. However, the mode-based implementation has not been an inconvenience because most of my programs perform insertions and queries in bursts. If your applications call for interleaved update and query operations, this implementation may not have satisfactory performance. If your application utilizes a stable set of intervals, you may need to execute the time-consuming tree construction code only once, by implementing code that streams the constructed interval tree to a file. Finally, I have used the assert macro extensively to prevent the inadvertent modification of intervals when the container was in query mode. You may prefer to implement a more robust error-handling scheme by throwing exceptions at the appropriate places in the code.

For further information regarding interval trees and an application, please see the paper "Speeding Up Isosurface Extraction Using Interval Trees," by Paolo Cignoni et al., IEEE Transactions on Visualization and Computer Graphics, Volume 3(2), April-June 1997.

Yogi Dandass is a researcher at Mississippi State University's High Performance Computing Laboratory and is also working towards a Ph.D. degree in Computer Science. He designs software libraries in C++ for numerical computing, artificial intelligence, and real-time communication middleware. He can be reached at [email protected].

January 2000/Interval Trees/Figure 1

Figure 1: An observation class for recording high and low temperature readings

#include <time.h>

class observation {
public:
   typedef float range_type;

   observation(range_type rHigh, range_type rLow, short sID, 
      time_t DateTime) 
      : m_rHigh(rHigh), m_rLow(rLow), m_sID(sID), 
        m_tmDate(DateTime) {}
   float low() const { return m_rLow; }
   float high() const { return m_rHigh; }
   short id() const { return m_sID; }
   time_t date() const { return m_tmDate; }

private:
   range_type  m_rHigh, m_rLow;
   short       m_sID;
   time_t      m_tmDate;
};
January 2000/Interval Trees/Figure 2

Figure 2: An interval tree

January 2000/Interval Trees/Figure 3

Figure 3: The interval tree template class itree

#ifndef ITREE_H_
#define ITREE_H_
#include <vector>
#include <set>
#include <assert.h>
#include <algorithm>
#include <iterator>

template<class T, class A = std::allocator<T> > class itree 
{
public:
   typedef T                                 invl_type;
   typedef T::range_type                     range_type;
   typedef std::vector<invl_type, A>         invl_vec;

   typedef invl_vec::iterator                iterator;
   typedef invl_vec::const_iterator          const_iterator;
   // other types derived from invl_vec omitted
   ...

private:
   typedef struct node_tag       // Interval tree nodes
   {
      range_type      discrim;  // discriminant
      int             start;    // starting offset in AL and DH
      int             size;     // number of entris in AL and DH
      struct node_tag *left;    // left subtree
      struct node_tag *right;   // right subtree
   } node;

public:
   ... // query iterator definition omitted
   ... // functions forwarded to std::vector omitted
   // Interval tree specific functionality follows

   itree() : root(NULL) {}
   ~itree() { deconstruct(); }

   void deconstruct(void) // reverts initialization mode
   { delete_tree(root);  root = NULL; }

   void delete_tree(node *cur_node) {  // delete nodes in tree
      if (cur_node != NULL) {
         delete_tree(cur_node->left);    // left tree 
                                         //  recursively
         delete_tree(cur_node->right);   // right tree 
                                         //  recursively
         delete cur_node;
     }
   }

   bool constructed() const { return root != NULL; }

   // build the interval tree structure and
   // put the container into query mode
   itree const& construct(void) {
      std::vector<range_type> values;
      std::vector<bool>       flags(invls.size(), false);
      int                     num_al_dh = 0;

      extract_values(values);   
      al.resize(invls.size());
      dh.resize(invls.size());
      root = construct_tree(values, flags, num_al_dh, 0, 
                            values.size() - 1);
      return *this;
   }
   ... // extract_values function definition omitted
   // recursively construct the tree
   node* 
   construct_tree(const std::vector<range_type>& values, 
      std::vector<bool>& flags, int& num_al_dh,
      int start, int end) {
      int         discrim_pos;
      range_type  discrim;
      int         list_start, list_size;
      node        *root, *left, *right;
      bool        continue_left = false;
      bool        continue_right = false;

      root = left = right = NULL;
      if (start > end)
         return root;
      discrim_pos = (start + end) / 2;
      discrim = values[discrim_pos];
 
      list_start = num_al_dh;
      list_size = 0;
      for (int i = 0; i < invls.size(); i++) {
         if (flags[i])
            continue;

         if ((invls[i].low() <= discrim) && 
             (invls[i].high() >= discrim)) {

            al[num_al_dh] = &(invls[i]);
            dh[num_al_dh] = &(invls[i]);

            num_al_dh++;
            list_size++;
            flags[i] = true;
         }

         if ((invls[i].low() < discrim) && 
             (invls[i].high() < discrim))
            continue_left = true;

          if ((invls[i].low() > discrim) && 
              (invls[i].high() > discrim))

            continue_right = true;
      }

      // see if left and/or right subtree needs to be built
      if (continue_left && (start <= (discrim_pos - 1)))
         left = construct_tree(values, flags, num_al_dh, start, 
                   discrim_pos - 1);

      if (continue_right && ((discrim_pos + 1) <= end))
         right = construct_tree(values, flags, num_al_dh, 
                    discrim_pos + 1, end);

      // this node is needed only if thre are entries in the 
      // AL/DH list or the left or right subtree exists.
      if ((list_size > 0) || (left != NULL) || (right != NULL)) {
         std::sort(&(al[list_start]), 
            &(al[list_start + list_size]), comp_for_al);
         std::sort(&(dh[list_start]), 
            &(dh[list_start + list_size]), comp_for_dh);

         root = new node;
         root->left = left;
         root->right = right;
         root->discrim = discrim;
         root->start = list_start;
         root->size = list_size;
      }
      return root;
   }

   ... // comparison functions for sorting AL and DL omitted

   query_iterator qbegin(range_type x) { 
      assert(constructed());
      query_iterator it(root, 0, x, &al, &dh); 
      it.init_node(); return it; 
   }

   query_iterator qend(range_type x) { 
      assert(constructed());
      query_iterator  it(NULL, 0, x, NULL, NULL); 
      return it; 
   }

private:
   invl_vec                invls;  // vector of intervals
   std::vector<invl_type*> al, dh; // vectors of interval ptrs
   node                    *root;  // Interval tree root node
};
#endif    // ITREE_H_
January 2000/Interval Trees/Figure 4

Figure 4: Sample use of an interval tree container

#include <limits.h>
#include <iostream>
#include "observation.h"
#include "itree.h"
#define MAX_OBS       1000
#define QUERY_VALUE   1.0

int main() {
   itree<observation> Tree;    // The interval tree

   // Add some intervals to the tree
   for (short s = 0; s < MAX_OBS; ++s) {
      time_t                  tmTime;
      observation::range_type rHigh, rLow;

      rLow = (float)rand() / RAND_MAX * 70.0;
      rHigh = rLow + (float)rand() / RAND_MAX * 40.0;
      time(&tmTime);

      observation obs(rHigh, rLow, s, tmTime);
      Tree.push_back(obs);  // use a vector insertion function
   }

   // Put the tree in query mode
   itree<observation> const &ConstTree = Tree.construct();

   // Use the const iterator to access all intervals sequentially
   std::cout << "Brute-force results\n";
   itree<observation>::const_iterator citer;
   for(citer = ConstTree.begin(); 
      citer != ConstTree.end(); ++citer)
      if ((citer->low() <= QUERY_VALUE) && 
          (citer->high() >= QUERY_VALUE))
         std::cout << "ID: " << citer->id()
                   << ", Low: " << citer->low() 
                   << ", High: " << citer->high() << std::endl;

   // Use the query iterator to access intervals
   std::cout << "Query results\n";
   itree<observation>::query_iterator iter;
   for (iter = Tree.qbegin(QUERY_VALUE); 
      iter != Tree.qend(QUERY_VALUE); ++iter)

      std::cout << "ID: " << iter->id() 
                << ", Low: " << iter->low() 
                << ", High: " << iter->high() << std::endl;

   return 0;
}
January 2000/Interval Trees/Figure 5

Figure 5: The query_iterator class

template<class T, class A = std::allocator<T> > class itree 
{
public:
   class query_iterator : public 
      std::iterator<std::forward_iterator_tag, itree> {
   public:
      ... // constructors and operator*() omitted 

      query_iterator& operator++(void)  // prefix 
      { Increment(); return *this; }  

      query_iterator operator++(int)  // postfix
      { query_iterator it(*this); Increment(); return it; }

      const T* operator->() const { 
         if (cur_node->discrim >= value)
            return (*p_al)[cur_node->start + index]; 
         else
           return (*p_dh)[cur_node->start + index]; 
      }

      bool operator!=(query_iterator& it) const 
      { return !(*this == it); }

      bool operator==(query_iterator& it) const { 
         return (value == it.value) && 
            (cur_node == it.cur_node) && (index == it.index);
      }

   private:
      node*                   cur_node;
      int                     index;
      std::vector<invl_type*> *p_al, *p_dh;
      range_type              value;

      query_iterator(node* cur_node, int index, range_type value,
         std::vector<invl_type*> *p_al, 
         std::vector<invl_type*> *p_dh) 
         : cur_node(cur_node), index(index), value(value), 
           p_al(p_al), p_dh(p_dh) {} // private constructor

      void Increment(void) {
         index++;
         if (index == cur_node->size) {
            if (cur_node->discrim == value) { // finished!
               cur_node = NULL;
               index = 0;
               return;
            }
            else if (cur_node->discrim > value)
               cur_node = cur_node->left;
            else
               cur_node = cur_node->right;
            init_node();
            return;
         }
         if (cur_node->discrim > value) {
            if ((*p_al)[cur_node->start + index]->low() <= value)
               return;
            else
               cur_node = cur_node->left;
         }
         else if (cur_node->discrim < value) {
            if((*p_dh)[cur_node->start + index]->high() >= value)
               return;
            else
               cur_node = cur_node->right;
         }
         else  //(cur_node->discrim == value)
            return;
         init_node();
         return;
      }

      void init_node(void) {
         index = 0;        
         while (cur_node != NULL) {
            if (value < cur_node->discrim) {
               if ((cur_node->size != 0) &&
                   ((*p_al)[cur_node->start]->low() <= value))
                  return;
               else
                  cur_node = cur_node->left;
            }
            else if (value > cur_node->discrim) {
               if ((cur_node->size != 0) && 
                   ((*p_dh)[cur_node->start]->high() >= value))
                  return;
               else
                  cur_node = cur_node->right;
            }
            else //(value == cur_node->discrim)
               return;
         }
      }

      friend itree;  // allow itree to use private constructor
   };    // end of query_iterator definition
   ... // remiander of itree definition omitted
};  // end of itree definition

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.