package jannovar.interval; import java.util.ArrayList; import java.util.List; /** * Implements an Interval Tree. * <P> * The construction of the interval tree proceeds in several phases. * <ol> * <li>The 2n endpoints are sorted for the n intervals and the median point of * the endpoints is calculated. * <li>The intervals are divided into three categories. * <ol> * <li>Intervals that cross the median are stored in the intervals list. * <li>Intervals completely to the left of the median are stored in the leftNode * list. * <li>Intervals completely to the right are stored in the rightNode list. * </ol> * <li>Each interval in the intervals list is sorted in two lists: leftorder * (sorted by increasing left endpoints) and rightorder (sorted by decreasing * right endpoints). * </ol> * Note that a Node object may have non-null right and left children while the * Node itself has not associated intervals, depending on the relationship of * the median value to the values of the intervals. * <P> * To search in the interval tree, the following procedure is used. * <ol> * <li>If the highpoint of the interval is smaller than the median, the right * subtree is eliminated. Otherwise, the search method is called recursively. * <li>If the lowpoint of the interval is larger than the median, the left * subtree is eliminated. Otherwise, the search method is called recursively. * <li>Always searches the intervals stored at current node using the two sorted * lists leftorder and rightorder. * <ol> * <li>If the highpoint is smaller than the median, search the leftorder list * and output all intervals until there is one with a left endpoint larger than * the lowpoint. * <li>If the lowpoint is larger than the median, search the rightorder list and * output all intervals until there is one with a right endpoint smaller than * the highpoint. * </ol> * </ol> * <P> * A slight modification of the search algorithm is made to search for the left * and right neighbors of search queries that do not overlap with any intervals. * We make essentially a binary-tree type search using the medians of the nodes. * Each time we pass a node, we check whether we can update the neihgbors. One * issue is that we have to make special compensation when we update using a * node with no intervals on its own. * <P> * The construction of an Interval Tree enables a fast search of overlapping * intervals. * * @author Christopher Dommaschenz, Radostina Misirkova, Nadine Taube, Gizem * Top, Peter Robinson * @version 0.12 (13 June, 2013) * @param <T> */ public class IntervalTree<T> implements java.io.Serializable { /** * A version numberused during deserialization to verify that the sender and * receiver of a serialized object have loaded classes for that object that * are compatible with respect to serialization. */ private static final long serialVersionUID = 1L; /** The root node of the entire interval tree. */ private Node<T> root; /** * All intervals of the entire tree (pointers to these intervals are also * used in the nodes). */ private List<Interval<T>> intervals; /** * A Comparator that is used to sort intervals by their left endpoint in * ascending order. */ private LeftComparator leftcomp = null; /** * A Comparator that is used to sort intervals by their right endpoint in * descending order. */ private RightComparator rightcomp = null; /** * The left neighbor of the current query position (non-overlapping interval * to the left of the query that is the closest of all intervals). */ private Interval<T> leftNeighbor = null; /** * The right neighbor of the current query position (non-overlapping * interval to the right of the query that is the closest of all intervals). */ private Interval<T> rightNeighbor = null; /** The default constructor should not be used and is declared private. */ private IntervalTree() { } /** * Tree constructor. * * @param intervals * A list that contains the intervals */ public IntervalTree(List<Interval<T>> intervals) { /* sets the root and calls the node constructor with list */ initializeComparators(); this.root = new Node<T>(intervals, this.leftcomp, this.rightcomp); this.intervals = intervals; } /** * A helper method intended to be used by the Constructors of this class to * initialize the static Comparator objects that will be used to sort * intervals. */ private void initializeComparators() { if (this.leftcomp == null) { this.leftcomp = new LeftComparator(); } if (this.rightcomp == null) { this.rightcomp = new RightComparator(); } // Node.setLeftComparator(leftcomp); // Node.setRightComparator(rightcomp); } /** * Search function which calls the method searchInterval to find intervals. * <P> * As a side-effect of the search, the variables {@link #rightNeighbor} and * {@link #leftNeighbor} are set. If this method returns an empty list, then * these variables contain the intervals that are the closest neighbors to * the left and the right to the query position. * * @param low * The lower element of the interval * @param high * The higher element of the interval * @return result, which is an ArrayList containg the found intervals */ public ArrayList<T> search(int low, int high) { ArrayList<Interval<T>> result = new ArrayList<Interval<T>>(); /* reset */ this.leftNeighbor = null; this.rightNeighbor = null; // System.out.println("Search for (" + low + "," + high + ")"); // debugPrint(this.root); searchInterval(root, result, low, high); ArrayList<T> obtlst = new ArrayList<T>(); for (Interval<T> it : result) { obtlst.add(it.getValue()); } /* Search for neighbors if there are no hits */ if (obtlst.isEmpty()) searchInbetween(low); return obtlst; } /** * In cases where we do not find an intersection, i.e., when a call to * {@link #search} reveals an emptylist because none of the items overlaps * with the search coordinates, then the search function automatically calls * {@link #searchInbetween}, which sets the variables {@link #rightNeighbor} * and {@link #leftNeighbor}, which can then be called by this function and * {@link #getLeftNeighbor}. * * @return the right neighbor of the current search query, if no overlapping * interval was found. */ public T getRightNeighbor() { if (rightNeighbor == null) return null; else return rightNeighbor.getValue(); } /** * In cases where we do not find an intersection, i.e., when a call to * {@link #search} reveals an emptylist because none of the items overlaps * with the search coordinates, then the search function automatically calls * {@link #searchInbetween}, which sets the variables {@link #rightNeighbor} * and {@link #leftNeighbor}, which can then be called by this function and * {@link #getRightNeighbor}. * * @return the left neighbor of the current search query, if no overlapping * interval was found. */ public T getLeftNeighbor() { if (leftNeighbor == null) return null; else return leftNeighbor.getValue(); } /** * This function checks if {@code item}, which is passed to the function as * an argument, is a better left neighbor for position x than the current * value of {@link #leftNeighbor}. It first checks if the currentValue of * leftNeighbor is null, in which case we simply assign a value. Because of * the way we traverse the interval tree, this value is guaranteed to be * valid. We then check whether the item is actually located to the left of * x (if not, we just return). Finally, we check if the "high" (rightmost) * value of the interval represented by item is closer to x than the value * of {@link #leftNeighbor}. If this is the case, then item is a better left * neighbor, and we assign it to the class variable {@link #leftNeighbor}. * <P> * Note that this function is intended to be used by the function * {@link #searchInbetween} if there is no interval that overlaps the * original search query. * * @param item * The new candidate left neighbor * @param x * the search position */ private void switchLeftNeighborIfBetter(Interval<T> item, int x) { if (item == null) return; // System.out.println("TOP: switchLefttNeighborIfBetter item=" + item + // " current leftN=" + leftNeighbor); if (this.leftNeighbor == null) { this.leftNeighbor = item; return; } /* 1) Return if item is not to left of x */ if (item.getHigh() >= x) return; /* 2) if item is closer to x than current leftNeighbor, replace it */ if (item.getHigh() > this.leftNeighbor.getHigh()) { this.leftNeighbor = item; } } /** * This function checks if {@code item}, which is passed to the function as * an argument, is a better right neighbor for position x than the current * value of {@link #rightNeighbor}. If the current value of * {@link #rightNeighbor} is null, we assign it the value of {@code item} * (this is guaranteed to be a valid value). Otherwise, we check whether * item is to the right of {@link #rightNeighbor} (if not, just return). * Finally, we check whether item is closer to {@code x} than the current * rightneighbor, and if so, we update the value of {@link #rightNeighbor} * to {@code item}.# * * @param item * The new candidate rightt neighbor * @param x * the search position */ private void switchRightNeighborIfBetter(Interval<T> item, int x) { if (item == null) return; // System.out.println("TOP: switchRightNeighborIfBetter item=" + item + // " current rightN=" + rightNeighbor); if (this.rightNeighbor == null) { this.rightNeighbor = item; return; } /* 1) Return if item is not to right of x */ if (item.getLow() <= x) return; /* 2) if item is closer to x than current leftNeighbor, replace it */ if (item.getLow() < this.rightNeighbor.getLow()) { this.rightNeighbor = item; } } /** * This function is called if no inverval is found to overlap with the * search query. If this method is called, we know that we are "in between" * two intervals (or at the extreme right or left end of the search space). * * @param x * The lower range of the oringal search query (it doesnt matter * whether we take the lower or the upper range, since both do * not overlap). */ private void searchInbetween(int x) { Node<T> n = this.root; while (n != null) { if (x < n.getMedian()) { /* * First up date the right neighbor (most left to date that is * right of query) . */ if (n.hasInterval()) { Interval<T> item = n.getLeftmostItem(); switchRightNeighborIfBetter(item, x); } else { Node<T> rd = n.getLeftmostDescendentOfRightChild(); Interval<T> item = rd.getLeftmostItem(); switchRightNeighborIfBetter(item, x); } /* Now continue to navigate the interval tree */ n = n.getLeft(); } else { /* * First up date the left neighbor (most right to date that is * left of query) . */ if (n.hasInterval()) { Interval<T> item = n.getRightmostItem(); switchLeftNeighborIfBetter(item, x); } else { /* * If the current node has no intervals on its own, then it * is possible that its right child has a better right * neighbor than the current right neighbor. */ Node<T> rd = n.getRightmostDescendentOfLeftChild(); Interval<T> item = rd.getRightmostItem(); switchLeftNeighborIfBetter(item, x); } /* Now continue to navigate the interval tree */ n = n.getRight(); } } } /** * Searches for intervals in the interval tree. * * @param n * A node of the interval tree * @param result * An ArrayList containg the found intervals * @param ilow * The lower element of the search interval * @param ihigh * The higher element of the search interval */ private void searchInterval(Node<T> n, ArrayList<Interval<T>> result, int ilow, int ihigh) { /* ends if the node n is empty */ if (n == null) { return; } /* * if ilow is smaller than the median of n the left side of the tree is * searched */ if (ilow < n.getMedian()) { /* as long as the iterator i is smaller than the size of leftorder */ int size = n.leftorder.size(); for (int i = 0; i < size; i++) { /* * breaks if the lowpoint at position i is bigger than the * wanted high point */ if (n.leftorder.get(i).getLow() > ihigh) { break; } /* adds the interval at position i of leftorder to result */ result.add(n.leftorder.get(i)); } /* * if ihigh is bigger than the median of n the right side of the * tree is searched */ } else if (ihigh > n.getMedian()) { /* as long as the iterator i is smaller than the size of rightorder */ int size = n.rightorder.size(); for (int i = 0; i < size; i++) { /* * breaks if the highpoint at position i is smaller than the * wanted low point */ if (n.rightorder.get(i).getHigh() < ilow) { break; } /* adds the interval at position i of rightorder to result */ result.add(n.rightorder.get(i)); } } /* * if the query is to the left of the median and the leftNode is not * empty the searchInterval method is called recursively */ if (ilow < n.getMedian() && n.getLeft() != null) { searchInterval(n.getLeft(), result, ilow, ihigh); } /* * if the query is to the right of the median and the rightNode is not * empty the searchInterval method is called recursively */ if (ihigh > n.getMedian() && n.getRight() != null) { searchInterval(n.getRight(), result, ilow, ihigh); } } /** * This is intended to be used to print out the interval tree for debugging * purposes. * * @param n * current {@link Node} */ public void debugPrint(Node<T> n) { System.out.println("IntervalTree<T> starting at " + n.toString()); n.debugPrint(null, 0); System.out.println("LeftNeighbor:" + leftNeighbor); System.out.println("RightNeighbor:" + rightNeighbor); } public void debugPrint() { debugPrint(this.root); } }