All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.brein.time.timeintervals.indexes.IntervalTree Maven / Gradle / Ivy

package com.brein.time.timeintervals.indexes;

import com.brein.time.exceptions.FailedIO;
import com.brein.time.exceptions.IllegalConfiguration;
import com.brein.time.timeintervals.filters.IntervalFilter;
import com.brein.time.timeintervals.intervals.IInterval;
import org.apache.log4j.Logger;

import java.io.Externalizable;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@SuppressWarnings("NullableProblems")
public class IntervalTree implements Collection, Externalizable {
    private static final Logger LOGGER = Logger.getLogger(IntervalTree.class);

    private transient IntervalTreeConfiguration configuration = null;
    private IntervalTreeNode root = null;

    private long size = 0L;

    public Collection find(final IInterval query) {
        return find(query, this.configuration.getIntervalFilter());
    }

    public Collection find(final IInterval query, final IntervalFilter filter) {
        if (this.root == null) {
            return Collections.emptyList();
        } else {
            return _find(this.root, query, filter);
        }
    }

    /**
     * Returns the current root of the tree. It is not recommended to store the result of this method in any
     * other than a local variable. The root of the tree will change whenever it is rebalanced.
     *
     * @return the current root of the tree
     *
     * @see #balance()
     */
    protected IntervalTreeNode getRoot() {
        return root;
    }

    protected Collection _find(final IntervalTreeNode node,
                                          final IInterval query,
                                          final IntervalFilter filter) {
        if (node == null) {
            return Collections.emptyList();
        }

        // check if the current node overlaps
        final int cmpNode = node.compareTo(query);
        if (cmpNode == 0) {
            return node.find(query, filter);
        } else if (cmpNode < 0) {
            return _find(node.getRight(), query, filter);
        } else {
            return _find(node.getLeft(), query, filter);
        }
    }

    public Stream overlapStream(final IInterval query) {
        if (this.root == null) {
            return Stream.empty();
        } else {
            return _overlap(this.root, query);
        }
    }

    public Collection overlap(final IInterval query) {
        if (this.root == null) {
            return Collections.emptyList();
        } else {
            return _overlap(this.root, query).collect(Collectors.toList());
        }
    }

    protected Stream _overlap(final IntervalTreeNode node, final IInterval query) {

        if (node == null) {
            return Stream.empty();
        }

        // we create three streams:
        //  1. the one of the current node
        //  2. the one coming from the left
        //  3. the one coming form the right
        final Stream nodeStream;
        final Stream leftNodeStream;
        final Stream rightNodeStream;

        if (node.compare(node.getStart(), query.getNormEnd()) <= 0 &&
                node.compare(node.getEnd(), query.getNormStart()) >= 0) {
            nodeStream = node.getIntervals().stream();
        } else {
            nodeStream = Stream.empty();
        }

        if (node.hasLeft() && node.compare(node.getLeft().getMax(), query.getNormStart()) >= 0) {
            leftNodeStream = this._overlap(node.getLeft(), query);
        } else {
            leftNodeStream = Stream.empty();
        }

        rightNodeStream = this._overlap(node.getRight(), query);

        return Stream.of(leftNodeStream, nodeStream, rightNodeStream).flatMap(s -> s);
    }

    public IntervalTree insert(final IInterval interval) {
        add(interval);
        return this;
    }

    protected IntervalTreeNode _add(final IntervalTreeNode node,
                                    final IInterval interval,
                                    final AtomicBoolean changed) {
        if (node == null) {
            changed.set(true);
            return createNode(interval);
        }

        final IntervalTreeNodeChildType childType;
        final int cmpNode = node.compareTo(interval);
        if (cmpNode == 0) {
            changed.set(node.addInterval(interval));
            return node;
        } else if (cmpNode < 0) {
            childType = IntervalTreeNodeChildType.RIGHT;
        } else {
            childType = IntervalTreeNodeChildType.LEFT;
        }

        // add the node to the child and replace the returned value
        node.setChild(_add(node.getChild(childType), interval, changed), childType);

        // the node may have changed, thus we may have to re-balance
        return isAutoBalancing() ? balance(node) : node;
    }

    protected IntervalTreeNode createNode(final IInterval interval) {
        final IntervalTreeNode node = new IntervalTreeNode();
        node.setConfiguration(this.configuration);
        node.init(interval);
        node.addInterval(interval);

        return node;
    }

    public void balance() {
        this.nodeIterator().forEachRemaining(node -> {
            if (node.isLeaf()) {
                // nothing to do
            } else if (node.isRoot()) {
                this.root = balance(node);
            } else {
                node.setLeft(balance(node.getLeft()));
                node.setRight(balance(node.getRight()));
            }
        });
    }

    public boolean isBalanced() {
        return isBalanced(this.root);
    }

    @SuppressWarnings("SimplifiableIfStatement")
    protected boolean isBalanced(final IntervalTreeNode node) {
        if (node == null) {
            return true;
        }
        return Math.abs(determineBalance(node)) <= 1L &&
                isBalanced(node.getLeft()) && isBalanced(node.getRight());
    }

    protected IntervalTreeNode balance(final IntervalTreeNode node) {

        // check the balance
        final long balance = determineBalance(node);
        if (Math.abs(balance) <= 1) {
            return node;
        }

        // find the deepest unbalanced sub-tree (there may be more down the road)

        // validate the different four cases four unbalanced tree's
        final long balanceLeft = balance > 1L ? determineBalance(node.getLeft()) : 0L;
        final long balanceRight = balance < -1L ? determineBalance(node.getRight()) : 0L;

        // Left Left Case
        if (balance > 1 && balanceLeft >= 0) {
            return rightRotate(node);
        }
        // Right Right Case
        else if (balance < -1 && balanceRight <= 0) {
            return leftRotate(node);
        }
        // Left Right Case
        else if (balance > 1 && balanceLeft < 0) {
            node.setLeft(leftRotate(node.getLeft()));
            return rightRotate(node);
        }
        // Right Left Case
        else if (balance < -1 && balanceRight > 0) {
            node.setRight(rightRotate(node.getRight()));
            return leftRotate(node);
        }
        // any other Case, no changes - should never happen
        else {
            LOGGER.warn(String.format("Balancing node '%s' reached an unexpected state (b: %d, l: %d, r: %d)",
                    node, balance, balanceLeft, balanceRight));
            LOGGER.warn(this);
            return node;
        }
    }

    public IntervalTree delete(final IInterval interval) {
        remove(interval);
        return this;
    }

    protected IntervalTreeNode _remove(final IntervalTreeNode node,
                                       final IInterval interval,
                                       final AtomicBoolean changed) {
        if (node == null) {
            changed.set(false);
            return null;
        }

        final IntervalTreeNodeChildType childType;
        final int cmpNode = node.compareTo(interval);
        if (cmpNode == 0) {
            if (node.removeInterval(interval)) {
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Removed interval '" + interval + "' from node '" + node + "'.");
                }

                changed.set(true);
                return removeEmptyNode(node);
            } else {
                changed.set(false);
                return node;
            }
        } else if (cmpNode < 0) {
            childType = IntervalTreeNodeChildType.RIGHT;
        } else {
            childType = IntervalTreeNodeChildType.LEFT;
        }

        // add the node to the child and replace the returned value
        node.setChild(_remove(node.getChild(childType), interval, changed), childType);

        return isAutoBalancing() ? balance(node) : node;
    }

    /**
     * Removes an empty node (i.e., does not contain any intervals). When removing an empty node, the resulting tree
     * must be rebalanced (if activated, see {@link #isAutoBalancing()}). The method returns the re-organized, balanced
     * sub-tree. The returned sub-tree can be at most better (in height) by one - if the tree was balanced before.
     * 

* - http://www.mathcs.emory.edu/~cheung/Courses/323/Syllabus/Trees/AVL-delete.html
* - http://quiz.geeksforgeeks.org/binary-search-tree-set-2-delete/ * * @param node the node which contained the removed interval * * @return the new root to be used in replacement for the passed {@code node} */ protected IntervalTreeNode removeEmptyNode(final IntervalTreeNode node) { if (!node.isEmpty()) { return node; } /* * There is a different between the new root (rootNode) of the sub-tree and the position where the * action really took place (i.e., the parent of the node, which was modified). To understand the difference, * it is best to look at: * * - http://www.mathcs.emory.edu/~cheung/Courses/323/Syllabus/Trees/AVL-delete.html */ final IntervalTreeNode rootNode; final IntervalTreeNode actionNode; // the empty not is a leaf, just remove it if (node.isLeaf()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing leaf '" + node + "' from tree."); } rootNode = null; actionNode = null; // the action-node would be node.getParent(), but will be balanced in the _remove } // we have an empty node, which just have one parent, so we just keep that one parent else if (node.isSingleParent()) { rootNode = node.getSingleChild(); actionNode = null; // the action-node would be node.getParent(), but will be balanced in the _remove if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing node '" + node + "' and replacing with '" + rootNode + "' from tree."); } } // we have two sub-trees else { rootNode = findLeftLeaf(node.getRight()); actionNode = node == rootNode.getParent() ? null : rootNode.getParent(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing node '" + node + "' and replacing with smallest '" + rootNode + "' from tree."); } final IntervalTreeNodeContext nodeCtx = node.detach(); final IntervalTreeNodeChildType replacementChildType = rootNode.determineChildType(); final IntervalTreeNodeContext replacementCtx = rootNode.detach(); // if the replacementCtx has children, we have to move them to the old parent (can only have one) if (replacementCtx.isSingleParent()) { replacementCtx.getParent().setChild(replacementCtx.getSingleChild(), replacementChildType); } // it may be that the node had the smallestNode as child, in that case we have to keep the old child if (nodeCtx.getLeft() == rootNode) { rootNode.setLeft(replacementCtx.getLeft()); rootNode.setRight(nodeCtx.getRight()); rootNode.setParent(nodeCtx.getParent()); } else if (nodeCtx.getRight() == rootNode) { rootNode.setRight(replacementCtx.getRight()); rootNode.setLeft(nodeCtx.getLeft()); rootNode.setParent(nodeCtx.getParent()); } else { rootNode.setContext(nodeCtx); } } // if we replace the root, we have to let the if (rootNode != null && node.isRoot()) { rootNode.setParent(null); rootNode.setLevel(0L); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("rootNode: " + rootNode); LOGGER.trace("actionNode: " + actionNode); } if (!isAutoBalancing()) { return rootNode; } else if (rootNode == null) { return null; } else if (actionNode == null) { return balance(rootNode); } else { /* * In this case, we have to follow up from the actionNode up to the rootNode * and make sure everything is balanced within this area. */ IntervalTreeNodeChildType childType = actionNode.determineChildType(); IntervalTreeNode n = actionNode.getParent(); do { final IntervalTreeNode child = n.getChild(childType); n.setChild(balance(child), childType); if (n == rootNode) { break; } childType = n.determineChildType(); n = n.getParent(); } while (true); return balance(rootNode); } } // Get Balance factor of node N protected long determineBalance(final IntervalTreeNode node) { if (node == null) { return 0L; } return (node.hasLeft() ? node.getLeft().getHeight() : 0) - (node.hasRight() ? node.getRight().getHeight() : 0); } protected IntervalTreeNode leftRotate(final IntervalTreeNode node) { final IntervalTreeNode right = node.getRight(); final IntervalTreeNodeContext rightCtx = right.detach(); final IntervalTreeNodeContext nodeCtx = node.detach(); node.setLeft(nodeCtx.getLeft()); node.setRight(rightCtx.getLeft()); right.setLeft(node); right.setRight(rightCtx.getRight()); return right; } protected IntervalTreeNode rightRotate(final IntervalTreeNode node) { final IntervalTreeNode left = node.getLeft(); final IntervalTreeNodeContext leftCtx = left.detach(); final IntervalTreeNodeContext nodeCtx = node.detach(); node.setLeft(leftCtx.getRight()); node.setRight(nodeCtx.getRight()); left.setRight(node); left.setLeft(leftCtx.getLeft()); return left; } protected IntervalTreeNode findLeftLeaf(final IntervalTreeNode startNode) { if (startNode == null) { return null; } IntervalTreeNode node = startNode; while (node.getLeft() != null) { node = node.getLeft(); } return node; } protected PositionedNode findLeftLeaf(final PositionedNode posNode) { if (posNode == null || posNode.getNode() == null) { return null; } IntervalTreeNode node = posNode.getNode(); long offset = 0; while (node.getLeft() != null) { node = node.getLeft(); offset++; } return PositionedNode.moveLeft(node, posNode, offset); } @Override public int size() { return size < Integer.MAX_VALUE ? Long.valueOf(size).intValue() : Integer.MAX_VALUE; } @Override public boolean isEmpty() { return this.root == null; } @Override @SuppressWarnings("SimplifiableIfStatement") public boolean contains(final Object o) { if (o instanceof IInterval) { return !find(IInterval.class.cast(o)).isEmpty(); } else { return false; } } @Override public Iterator iterator() { final Iterator outerNodeIt = nodeIterator(); return new Iterator() { private Iterator nodeCollectionIt = null; private IInterval next = findNext(); @Override public boolean hasNext() { return this.next != null; } protected IInterval findNext() { if (this.nodeCollectionIt != null && this.nodeCollectionIt.hasNext()) { // nothing to do, next will return something } else if (outerNodeIt.hasNext()) { this.nodeCollectionIt = outerNodeIt.next().iterator(); } else { return null; } return this.nodeCollectionIt.next(); } @Override public IInterval next() { if (!hasNext()) { throw new NoSuchElementException(); } final IInterval result = this.next; this.next = findNext(); return result; } }; } @Override public Object[] toArray() { if (this.size() == 0L) { return new IInterval[0]; } final IInterval[] intervals = new IInterval[size()]; final AtomicInteger pos = new AtomicInteger(0); iterator().forEachRemaining(i -> intervals[pos.getAndIncrement()] = i); return intervals; } @Override @SuppressWarnings("unchecked") public T[] toArray(final T[] arr) { final T[] intervals; if (arr.length < this.size) { intervals = (T[]) Array.newInstance(arr.getClass().getComponentType(), size()); } else { intervals = arr; } final AtomicInteger pos = new AtomicInteger(0); iterator().forEachRemaining(i -> intervals[pos.getAndIncrement()] = (T) i); for (int i = intervals.length; i < size(); i++) { intervals[i] = null; } return intervals; } @Override public boolean add(final IInterval interval) { final AtomicBoolean changed = new AtomicBoolean(false); this.root = _add(this.root, interval, changed); this.size += changed.get() ? 1 : 0; return changed.get(); } @Override @SuppressWarnings("SimplifiableIfStatement") public boolean remove(final Object o) { final IInterval interval; if (o instanceof IInterval) { interval = IInterval.class.cast(o); } else { return false; } final AtomicBoolean changed = new AtomicBoolean(false); this.root = _remove(this.root, interval, changed); this.size -= changed.get() ? 1 : 0; return changed.get(); } @Override public boolean containsAll(final Collection c) { for (final Object o : c) { if (!contains(o)) { return false; } } return true; } @Override public boolean addAll(final Collection c) { final AtomicBoolean changed = new AtomicBoolean(false); c.forEach(interval -> changed.compareAndSet(false, add(interval))); return changed.get(); } @Override public boolean removeAll(final Collection c) { final AtomicBoolean changed = new AtomicBoolean(false); c.forEach(interval -> changed.compareAndSet(false, remove(interval))); return changed.get(); } @Override public boolean retainAll(final Collection c) { final List contained = c.stream() .filter(this::contains) .map(IInterval.class::cast) .collect(Collectors.toList()); // if there is nothing to be removed, we can return false if (contained.size() == this.size()) { return false; } clear(); addAll(contained); return true; } @Override public void clear() { this.root = null; this.size = 0; } public Iterator nodeIterator() { final IntervalTreeNode outerFirstNext = findLeftLeaf(this.root); return new Iterator() { private IntervalTreeNode next = outerFirstNext; @Override public boolean hasNext() { return this.next != null; } @Override public IntervalTreeNode next() { if (!hasNext()) { throw new NoSuchElementException(); } final IntervalTreeNode result = this.next; if (this.next.getRight() != null) { this.next = findLeftLeaf(this.next.getRight()); return result; } else { while (true) { final IntervalTreeNode parent = this.next.getParent(); if (parent == null) { this.next = null; return result; } else if (parent.getLeft() == this.next) { this.next = parent; return result; } else { this.next = parent; } } } } }; } public Iterator positionIterator() { final PositionedNode outerFirstNext = findLeftLeaf(new PositionedNode(this.root, 0L, 0L)); return new Iterator() { private PositionedNode next = outerFirstNext; @Override public boolean hasNext() { return next != null; } @Override public PositionedNode next() { if (!hasNext()) { throw new NoSuchElementException(); } final PositionedNode result = this.next; final IntervalTreeNode right = this.next.getNode().getRight(); if (right == null) { while (true) { final IntervalTreeNode node = this.next.getNode(); final IntervalTreeNode parent = node.getParent(); if (parent == null) { this.next = null; return result; } final PositionedNode posParent = PositionedNode.moveUp(parent, this.next, 1L); if (parent.getLeft() == node) { this.next = posParent; return result; } else { this.next = posParent; } } } else { final PositionedNode posRight = PositionedNode.moveRight(right, this.next, 1L); this.next = findLeftLeaf(posRight); return result; } } }; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); toString("", this.root, sb, true); return sb.toString(); } private void toString(final String prefix, final IntervalTreeNode node, final StringBuilder sb, final boolean tail) { sb.append(prefix) .append(tail ? "└── " : "├── ") .append(node) .append(System.lineSeparator()); if (node == null || node.isLeaf()) { return; } final String newPrefix = prefix + (tail ? " " : "│ "); toString(newPrefix, node.getLeft(), sb, false); toString(newPrefix, node.getRight(), sb, true); } @Override public void writeExternal(final ObjectOutput out) throws IOException { out.writeLong(this.size); if (this.root != null) { this.root.writeExternal(out); } } @Override public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException { this.size = in.readLong(); if (this.size > 0) { this.root = new IntervalTreeNode(); this.root.setConfiguration(this.configuration); this.root.readExternal(in); } } public boolean isAutoBalancing() { return this.configuration.isAutoBalancing(); } public IntervalTreeConfiguration getConfiguration() { return configuration; } public void setConfiguration(final IntervalTreeConfiguration configuration) { if (this.root != null) { throw new IllegalConfiguration("The configuration cannot be changed once the tree is created."); } this.configuration = configuration; } public void saveToFile(final File file) throws FailedIO { IntervalTreeBuilder.saveToFile(file, this); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy