diff --git a/src/main/java/com/github/btrekkie/red_black_node/RedBlackNode.java b/src/main/java/com/github/btrekkie/red_black_node/RedBlackNode.java new file mode 100644 index 000000000..7777b7361 --- /dev/null +++ b/src/main/java/com/github/btrekkie/red_black_node/RedBlackNode.java @@ -0,0 +1,1369 @@ +package com.github.btrekkie.red_black_node; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * A node in a red-black tree ( https://en.wikipedia.org/wiki/Red%E2%80%93black_tree ). Compared to a class like Java's + * TreeMap, RedBlackNode is a low-level data structure. The internals of a node are exposed as public fields, allowing + * clients to directly observe and manipulate the structure of the tree. This gives clients flexibility, although it + * also enables them to violate the red-black or BST properties. The RedBlackNode class provides methods for performing + * various standard operations, such as insertion and removal. + * + * Unlike most implementations of binary search trees, RedBlackNode supports arbitrary augmentation. By subclassing + * RedBlackNode, clients can add arbitrary data and augmentation information to each node. For example, if we were to + * use a RedBlackNode subclass to implement a sorted set, the subclass would have a field storing an element in the set. + * If we wanted to keep track of the number of non-leaf nodes in each subtree, we would store this as a "size" field and + * override augment() to update this field. All RedBlackNode methods (such as "insert" and remove()) call augment() as + * necessary to correctly maintain the augmentation information, unless otherwise indicated. + * + * The values of the tree are stored in the non-leaf nodes. RedBlackNode does not support use cases where values must be + * stored in the leaf nodes. It is recommended that all of the leaf nodes in a given tree be the same (black) + * RedBlackNode instance, to save space. The root of an empty tree is a leaf node, as opposed to null. + * + * For reference, a red-black tree is a binary search tree satisfying the following properties: + * + * - Every node is colored red or black. + * - The leaf nodes, which are dummy nodes that do not store any values, are colored black. + * - The root is black. + * - Both children of each red node are black. + * - Every path from the root to a leaf contains the same number of black nodes. + * + * @param The type of node in the tree. For example, we might have + * "class FooNode extends RedBlackNode>". + * @author Bill Jacobs + */ +public abstract class RedBlackNode> implements Comparable { + /** A Comparator that compares Comparable elements using their natural order. */ + private static final Comparator> NATURAL_ORDER = new Comparator>() { + @Override + public int compare(Comparable value1, Comparable value2) { + return value1.compareTo(value2); + } + }; + + /** The parent of this node, if any. "parent" is null if this is a leaf node. */ + public N parent; + + /** The left child of this node. "left" is null if this is a leaf node. */ + public N left; + + /** The right child of this node. "right" is null if this is a leaf node. */ + public N right; + + /** Whether the node is colored red, as opposed to black. */ + public boolean isRed; + + /** + * Sets any augmentation information about the subtree rooted at this node that is stored in this node. For + * example, if we augment each node by subtree size (the number of non-leaf nodes in the subtree), this method would + * set the size field of this node to be equal to the size field of the left child plus the size field of the right + * child plus one. + * + * "Augmentation information" is information that we can compute about a subtree rooted at some node, preferably + * based only on the augmentation information in the node's two children and the information in the node. Examples + * of augmentation information are the sum of the values in a subtree and the number of non-leaf nodes in a subtree. + * Augmentation information may not depend on the colors of the nodes. + * + * This method returns whether the augmentation information in any of the ancestors of this node might have been + * affected by changes in this subtree since the last call to augment(). In the usual case, where the augmentation + * information depends only on the information in this node and the augmentation information in its immediate + * children, this is equivalent to whether the augmentation information changed as a result of this call to + * augment(). For example, in the case of subtree size, this returns whether the value of the size field prior to + * calling augment() differed from the size field of the left child plus the size field of the right child plus one. + * False positives are permitted. The return value is unspecified if we have not called augment() on this node + * before. + * + * This method may assume that this is not a leaf node. It may not assume that the augmentation information stored + * in any of the tree's nodes is correct. However, if the augmentation information stored in all of the node's + * descendants is correct, then the augmentation information stored in this node must be correct after calling + * augment(). + */ + public boolean augment() { + return false; + } + + /** + * Throws a RuntimeException if we detect that this node locally violates any invariants specific to this subclass + * of RedBlackNode. For example, if this stores the size of the subtree rooted at this node, this should throw a + * RuntimeException if the size field of this is not equal to the size field of the left child plus the size field + * of the right child plus one. Note that we may call this on a leaf node. + * + * assertSubtreeIsValid() calls assertNodeIsValid() on each node, or at least starts to do so until it detects a + * problem. assertNodeIsValid() should assume the node is in a tree that satisfies all properties common to all + * red-black trees, as assertSubtreeIsValid() is responsible for such checks. assertNodeIsValid() should be + * "downward-looking", i.e. it should ignore any information in "parent", and it should be "local", i.e. it should + * only check a constant number of descendants. To include "global" checks, such as verifying the BST property + * concerning ordering, override assertSubtreeIsValid(). assertOrderIsValid is useful for checking the BST + * property. + */ + public void assertNodeIsValid() { + + } + + /** Returns whether this is a leaf node. */ + public boolean isLeaf() { + return left == null; + } + + /** Returns the root of the tree that contains this node. */ + public N root() { + @SuppressWarnings("unchecked") + N node = (N)this; + while (node.parent != null) { + node = node.parent; + } + return node; + } + + /** Returns the first node in the subtree rooted at this node, if any. */ + public N min() { + if (isLeaf()) { + return null; + } + @SuppressWarnings("unchecked") + N node = (N)this; + while (!node.left.isLeaf()) { + node = node.left; + } + return node; + } + + /** Returns the last node in the subtree rooted at this node, if any. */ + public N max() { + if (isLeaf()) { + return null; + } + @SuppressWarnings("unchecked") + N node = (N)this; + while (!node.right.isLeaf()) { + node = node.right; + } + return node; + } + + /** Returns the node immediately before this in the tree that contains this node, if any. */ + public N predecessor() { + if (!left.isLeaf()) { + N node; + for (node = left; !node.right.isLeaf(); node = node.right); + return node; + } else if (parent == null) { + return null; + } else { + @SuppressWarnings("unchecked") + N node = (N)this; + while (node.parent != null && node.parent.left == node) { + node = node.parent; + } + return node.parent; + } + } + + /** Returns the node immediately after this in the tree that contains this node, if any. */ + public N successor() { + if (!right.isLeaf()) { + N node; + for (node = right; !node.left.isLeaf(); node = node.left); + return node; + } else if (parent == null) { + return null; + } else { + @SuppressWarnings("unchecked") + N node = (N)this; + while (node.parent != null && node.parent.right == node) { + node = node.parent; + } + return node.parent; + } + } + + /** + * Performs a left rotation about this node. This method assumes that !isLeaf() && !right.isLeaf(). It calls + * augment() on this node and on its resulting parent. However, it does not call augment() on any of the resulting + * parent's ancestors, because that is normally the responsibility of the caller. + * @return The return value from calling augment() on the resulting parent. + */ + public boolean rotateLeft() { + if (isLeaf() || right.isLeaf()) { + throw new IllegalArgumentException("The node or its right child is a leaf"); + } + N newParent = right; + right = newParent.left; + @SuppressWarnings("unchecked") + N nThis = (N)this; + if (!right.isLeaf()) { + right.parent = nThis; + } + newParent.parent = parent; + parent = newParent; + newParent.left = nThis; + if (newParent.parent != null) { + if (newParent.parent.left == this) { + newParent.parent.left = newParent; + } else { + newParent.parent.right = newParent; + } + } + augment(); + return newParent.augment(); + } + + /** + * Performs a right rotation about this node. This method assumes that !isLeaf() && !left.isLeaf(). It calls + * augment() on this node and on its resulting parent. However, it does not call augment() on any of the resulting + * parent's ancestors, because that is normally the responsibility of the caller. + * @return The return value from calling augment() on the resulting parent. + */ + public boolean rotateRight() { + if (isLeaf() || left.isLeaf()) { + throw new IllegalArgumentException("The node or its left child is a leaf"); + } + N newParent = left; + left = newParent.right; + @SuppressWarnings("unchecked") + N nThis = (N)this; + if (!left.isLeaf()) { + left.parent = nThis; + } + newParent.parent = parent; + parent = newParent; + newParent.right = nThis; + if (newParent.parent != null) { + if (newParent.parent.left == this) { + newParent.parent.left = newParent; + } else { + newParent.parent.right = newParent; + } + } + augment(); + return newParent.augment(); + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). This method is more efficient than fixInsertion if + * "augment" is false or augment() might return false. + * @param augment Whether to set the augmentation information for "node" and its ancestors, by calling augment(). + */ + public void fixInsertionWithoutGettingRoot(boolean augment) { + if (!isRed) { + throw new IllegalArgumentException("The node must be red"); + } + boolean changed = augment; + if (augment) { + augment(); + } + + RedBlackNode node = this; + while (node.parent != null && node.parent.isRed) { + N parent = node.parent; + N grandparent = parent.parent; + if (grandparent.left.isRed && grandparent.right.isRed) { + grandparent.left.isRed = false; + grandparent.right.isRed = false; + grandparent.isRed = true; + + if (changed) { + changed = parent.augment(); + if (changed) { + changed = grandparent.augment(); + } + } + node = grandparent; + } else { + if (parent.left == node) { + if (grandparent.right == parent) { + parent.rotateRight(); + node = parent; + parent = node.parent; + } + } else if (grandparent.left == parent) { + parent.rotateLeft(); + node = parent; + parent = node.parent; + } + + if (parent.left == node) { + boolean grandparentChanged = grandparent.rotateRight(); + if (augment) { + changed = grandparentChanged; + } + } else { + boolean grandparentChanged = grandparent.rotateLeft(); + if (augment) { + changed = grandparentChanged; + } + } + + parent.isRed = false; + grandparent.isRed = true; + node = parent; + break; + } + } + + if (node.parent == null) { + node.isRed = false; + } + if (changed) { + for (node = node.parent; node != null; node = node.parent) { + if (!node.augment()) { + break; + } + } + } + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). This method is more efficient than fixInsertion() if + * augment() might return false. + */ + public void fixInsertionWithoutGettingRoot() { + fixInsertionWithoutGettingRoot(true); + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). + * @param augment Whether to set the augmentation information for "node" and its ancestors, by calling augment(). + * @return The root of the resulting tree. + */ + public N fixInsertion(boolean augment) { + fixInsertionWithoutGettingRoot(augment); + return root(); + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). + * @return The root of the resulting tree. + */ + public N fixInsertion() { + fixInsertionWithoutGettingRoot(true); + return root(); + } + + /** Returns a Comparator that compares instances of N using their natural order, as in N.compareTo. */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private Comparator naturalOrder() { + Comparator comparator = (Comparator)NATURAL_ORDER; + return (Comparator)comparator; + } + + /** + * Inserts the specified node into the tree rooted at this node. Assumes this is the root. We treat newNode as a + * solitary node that does not belong to any tree, and we ignore its initial "parent", "left", "right", and isRed + * fields. + * + * If it is not efficient or convenient to find the location for a node using a Comparator, then you should manually + * add the node to the appropriate location, color it red, and call fixInsertion(). + * + * @param newNode The node to insert. + * @param allowDuplicates Whether to insert newNode if there is an equal node in the tree. To check whether we + * inserted newNode, check whether newNode.parent is null and the return value differs from newNode. + * @param comparator A comparator indicating where to put the node. If this is null, we use the nodes' natural + * order, as in N.compareTo. If you are passing null, then you must override the compareTo method, because the + * default implementation requires the nodes to already be in the same tree. + * @return The root of the resulting tree. + */ + public N insert(N newNode, boolean allowDuplicates, Comparator comparator) { + if (parent != null) { + throw new IllegalArgumentException("This is not the root of a tree"); + } + @SuppressWarnings("unchecked") + N nThis = (N)this; + if (isLeaf()) { + newNode.isRed = false; + newNode.left = nThis; + newNode.right = nThis; + newNode.parent = null; + newNode.augment(); + return newNode; + } + if (comparator == null) { + comparator = naturalOrder(); + } + + N node = nThis; + int comparison; + while (true) { + comparison = comparator.compare(newNode, node); + if (comparison < 0) { + if (!node.left.isLeaf()) { + node = node.left; + } else { + newNode.left = node.left; + newNode.right = node.left; + node.left = newNode; + newNode.parent = node; + break; + } + } else if (comparison > 0 || allowDuplicates) { + if (!node.right.isLeaf()) { + node = node.right; + } else { + newNode.left = node.right; + newNode.right = node.right; + node.right = newNode; + newNode.parent = node; + break; + } + } else { + newNode.parent = null; + return nThis; + } + } + newNode.isRed = true; + return newNode.fixInsertion(); + } + + /** + * Moves this node to its successor's former position in the tree and vice versa, i.e. sets the "left", "right", + * "parent", and isRed fields of each. This method assumes that this is not a leaf node. + * @return The node with which we swapped. + */ + private N swapWithSuccessor() { + N replacement = successor(); + boolean oldReplacementIsRed = replacement.isRed; + N oldReplacementLeft = replacement.left; + N oldReplacementRight = replacement.right; + N oldReplacementParent = replacement.parent; + + replacement.isRed = isRed; + replacement.left = left; + replacement.right = right; + replacement.parent = parent; + if (parent != null) { + if (parent.left == this) { + parent.left = replacement; + } else { + parent.right = replacement; + } + } + + @SuppressWarnings("unchecked") + N nThis = (N)this; + isRed = oldReplacementIsRed; + left = oldReplacementLeft; + right = oldReplacementRight; + if (oldReplacementParent == this) { + parent = replacement; + parent.right = nThis; + } else { + parent = oldReplacementParent; + parent.left = nThis; + } + + replacement.right.parent = replacement; + if (!replacement.left.isLeaf()) { + replacement.left.parent = replacement; + } + if (!right.isLeaf()) { + right.parent = nThis; + } + return replacement; + } + + /** + * Performs red-black deletion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that all paths from the root to a leaf that pass through the sibling of this node have + * one fewer black node than all other root-to-leaf paths. This method assumes that this is not a leaf node. + */ + private void fixSiblingDeletion() { + RedBlackNode sibling = this; + boolean changed = true; + boolean haveAugmentedParent = false; + boolean haveAugmentedGrandparent = false; + while (true) { + N parent = sibling.parent; + if (sibling.isRed) { + parent.isRed = true; + sibling.isRed = false; + if (parent.left == sibling) { + changed = parent.rotateRight(); + sibling = parent.left; + } else { + changed = parent.rotateLeft(); + sibling = parent.right; + } + haveAugmentedParent = true; + haveAugmentedGrandparent = true; + } else if (!sibling.left.isRed && !sibling.right.isRed) { + sibling.isRed = true; + if (parent.isRed) { + parent.isRed = false; + break; + } else { + if (changed && !haveAugmentedParent) { + changed = parent.augment(); + } + N grandparent = parent.parent; + if (grandparent == null) { + break; + } else if (grandparent.left == parent) { + sibling = grandparent.right; + } else { + sibling = grandparent.left; + } + haveAugmentedParent = haveAugmentedGrandparent; + haveAugmentedGrandparent = false; + } + } else { + if (sibling == parent.left) { + if (!sibling.left.isRed) { + sibling.rotateLeft(); + sibling = sibling.parent; + } + } else if (!sibling.right.isRed) { + sibling.rotateRight(); + sibling = sibling.parent; + } + sibling.isRed = parent.isRed; + parent.isRed = false; + if (sibling == parent.left) { + sibling.left.isRed = false; + changed = parent.rotateRight(); + } else { + sibling.right.isRed = false; + changed = parent.rotateLeft(); + } + haveAugmentedParent = haveAugmentedGrandparent; + haveAugmentedGrandparent = false; + break; + } + } + + // Update augmentation info + N parent = sibling.parent; + if (changed && parent != null) { + if (!haveAugmentedParent) { + changed = parent.augment(); + } + if (changed && parent.parent != null) { + parent = parent.parent; + if (!haveAugmentedGrandparent) { + changed = parent.augment(); + } + if (changed) { + for (parent = parent.parent; parent != null; parent = parent.parent) { + if (!parent.augment()) { + break; + } + } + } + } + } + } + + /** + * Removes this node from the tree that contains it. The effect of this method on the fields of this node is + * unspecified. This method assumes that this is not a leaf node. This method is more efficient than remove() if + * augment() might return false. + * + * If the node has two children, we begin by moving the node's successor to its former position, by changing the + * successor's "left", "right", "parent", and isRed fields. + */ + public void removeWithoutGettingRoot() { + if (isLeaf()) { + throw new IllegalArgumentException("Attempted to remove a leaf node"); + } + N replacement; + if (left.isLeaf() || right.isLeaf()) { + replacement = null; + } else { + replacement = swapWithSuccessor(); + } + + N child; + if (!left.isLeaf()) { + child = left; + } else if (!right.isLeaf()) { + child = right; + } else { + child = null; + } + + if (child != null) { + // Replace this node with its child + child.parent = parent; + if (parent != null) { + if (parent.left == this) { + parent.left = child; + } else { + parent.right = child; + } + } + child.isRed = false; + + if (child.parent != null) { + N parent; + for (parent = child.parent; parent != null; parent = parent.parent) { + if (!parent.augment()) { + break; + } + } + } + } else if (parent != null) { + // Replace this node with a leaf node + N leaf = left; + N parent = this.parent; + N sibling; + if (parent.left == this) { + parent.left = leaf; + sibling = parent.right; + } else { + parent.right = leaf; + sibling = parent.left; + } + + if (!isRed) { + RedBlackNode siblingNode = sibling; + siblingNode.fixSiblingDeletion(); + } else { + while (parent != null) { + if (!parent.augment()) { + break; + } + parent = parent.parent; + } + } + } + + if (replacement != null) { + replacement.augment(); + for (N parent = replacement.parent; parent != null; parent = parent.parent) { + if (!parent.augment()) { + break; + } + } + } + + // Clear any previously existing links, so that we're more likely to encounter an exception if we attempt to + // access the removed node + parent = null; + left = null; + right = null; + isRed = true; + } + + /** + * Removes this node from the tree that contains it. The effect of this method on the fields of this node is + * unspecified. This method assumes that this is not a leaf node. + * + * If the node has two children, we begin by moving the node's successor to its former position, by changing the + * successor's "left", "right", "parent", and isRed fields. + * + * @return The root of the resulting tree. + */ + public N remove() { + if (isLeaf()) { + throw new IllegalArgumentException("Attempted to remove a leaf node"); + } + + // Find an arbitrary non-leaf node in the tree other than this node + N node; + if (parent != null) { + node = parent; + } else if (!left.isLeaf()) { + node = left; + } else if (!right.isLeaf()) { + node = right; + } else { + return left; + } + + removeWithoutGettingRoot(); + return node.root(); + } + + /** + * Returns the root of a perfectly height-balanced subtree containing the next "size" (non-leaf) nodes from + * "iterator", in iteration order. This method is responsible for setting the "left", "right", "parent", and isRed + * fields of the nodes, and calling augment() as appropriate. It ignores the initial values of the "left", "right", + * "parent", and isRed fields. + * @param iterator The nodes. + * @param size The number of nodes. + * @param height The "height" of the subtree's root node above the deepest leaf in the tree that contains it. Since + * insertion fixup is slow if there are too many red nodes and deleteion fixup is slow if there are too few red + * nodes, we compromise and have red nodes at every fourth level. We color a node red iff its "height" is equal + * to 1 mod 4. + * @param leaf The leaf node. + * @return The root of the subtree. + */ + private static > N createTree( + Iterator iterator, int size, int height, N leaf) { + if (size == 0) { + return leaf; + } else { + N left = createTree(iterator, (size - 1) / 2, height - 1, leaf); + N node = iterator.next(); + N right = createTree(iterator, size / 2, height - 1, leaf); + + node.isRed = height % 4 == 1; + node.left = left; + node.right = right; + if (!left.isLeaf()) { + left.parent = node; + } + if (!right.isLeaf()) { + right.parent = node; + } + + node.augment(); + return node; + } + } + + /** + * Returns the root of a perfectly height-balanced tree containing the specified nodes, in iteration order. This + * method is responsible for setting the "left", "right", "parent", and isRed fields of the nodes (excluding + * "leaf"), and calling augment() as appropriate. It ignores the initial values of the "left", "right", "parent", + * and isRed fields. + * @param nodes The nodes. + * @param leaf The leaf node. + * @return The root of the tree. + */ + public static > N createTree(Collection nodes, N leaf) { + int size = nodes.size(); + if (size == 0) { + return leaf; + } + + int height = 0; + for (int subtreeSize = size; subtreeSize > 0; subtreeSize /= 2) { + height++; + } + + N node = createTree(nodes.iterator(), size, height, leaf); + node.parent = null; + node.isRed = false; + return node; + } + + /** + * Concatenates to the end of the tree rooted at this node. To be precise, given that all of the nodes in this + * precede the node "pivot", which precedes all of the nodes in "last", this returns the root of a tree containing + * all of these nodes. This method destroys the trees rooted at "this" and "last". We treat "pivot" as a solitary + * node that does not belong to any tree, and we ignore its initial "parent", "left", "right", and isRed fields. + * This method assumes that this node and "last" are the roots of their respective trees. + * + * This method takes O(log N) time. It is more efficient than inserting "pivot" and then calling concatenate(last). + * It is considerably more efficient than inserting "pivot" and all of the nodes in "last". + */ + public N concatenate(N last, N pivot) { + // If the black height of "first", where first = this, is less than or equal to that of "last", starting at the + // root of "last", we keep going left until we reach a black node whose black height is equal to that of + // "first". Then, we make "pivot" the parent of that node and of "first", coloring it red, and perform + // insertion fixup on the pivot. If the black height of "first" is greater than that of "last", we do the + // mirror image of the above. + + if (parent != null) { + throw new IllegalArgumentException("This is not the root of a tree"); + } + if (last.parent != null) { + throw new IllegalArgumentException("\"last\" is not the root of a tree"); + } + + // Compute the black height of the trees + int firstBlackHeight = 0; + @SuppressWarnings("unchecked") + N first = (N)this; + for (N node = first; node != null; node = node.right) { + if (!node.isRed) { + firstBlackHeight++; + } + } + int lastBlackHeight = 0; + for (N node = last; node != null; node = node.right) { + if (!node.isRed) { + lastBlackHeight++; + } + } + + // Identify the children and parent of pivot + N firstChild = first; + N lastChild = last; + N parent; + if (firstBlackHeight <= lastBlackHeight) { + parent = null; + int blackHeight = lastBlackHeight; + while (blackHeight > firstBlackHeight) { + if (!lastChild.isRed) { + blackHeight--; + } + parent = lastChild; + lastChild = lastChild.left; + } + if (lastChild.isRed) { + parent = lastChild; + lastChild = lastChild.left; + } + } else { + parent = null; + int blackHeight = firstBlackHeight; + while (blackHeight > lastBlackHeight) { + if (!firstChild.isRed) { + blackHeight--; + } + parent = firstChild; + firstChild = firstChild.right; + } + if (firstChild.isRed) { + parent = firstChild; + firstChild = firstChild.right; + } + } + + // Add "pivot" to the tree + pivot.isRed = true; + pivot.parent = parent; + if (parent != null) { + if (firstBlackHeight < lastBlackHeight) { + parent.left = pivot; + } else { + parent.right = pivot; + } + } + pivot.left = firstChild; + if (!firstChild.isLeaf()) { + firstChild.parent = pivot; + } + pivot.right = lastChild; + if (!lastChild.isLeaf()) { + lastChild.parent = pivot; + } + + // Perform insertion fixup + return pivot.fixInsertion(); + } + + /** + * Concatenates the tree rooted at "last" to the end of the tree rooted at this node. To be precise, given that all + * of the nodes in this precede all of the nodes in "last", this returns the root of a tree containing all of these + * nodes. This method destroys the trees rooted at "this" and "last". It assumes that this node and "last" are the + * roots of their respective trees. This method takes O(log N) time. It is considerably more efficient than + * inserting all of the nodes in "last". + */ + public N concatenate(N last) { + if (parent != null || last.parent != null) { + throw new IllegalArgumentException("The node is not the root of a tree"); + } + if (isLeaf()) { + return last; + } else if (last.isLeaf()) { + @SuppressWarnings("unchecked") + N nThis = (N)this; + return nThis; + } else { + N node = last.min(); + last = node.remove(); + return concatenate(last, node); + } + } + + /** + * Splits the tree rooted at this node into two trees, so that the first element of the return value is the root of + * a tree consisting of the nodes that were before the specified node, and the second element of the return value is + * the root of a tree consisting of the nodes that were equal to or after the specified node. This method is + * destructive, meaning it does not preserve the original tree. It assumes that this node is the root and is in the + * same tree as splitNode. It takes O(log N) time. It is considerably more efficient than removing all of the + * nodes at or after splitNode and then creating a new tree from those nodes. + * @param The node at which to split the tree. + * @return An array consisting of the resulting trees. + */ + public N[] split(N splitNode) { + // To split the tree, we accumulate a pre-split tree and a post-split tree. We walk down the tree toward the + // position where we are splitting. Whenever we go left, we concatenate the right subtree with the post-split + // tree, and whenever we go right, we concatenate the pre-split tree with the left subtree. We use the + // concatenation algorithm described in concatenate(Object, Object). For the pivot, we use the last node where + // we went left in the case of a left move, and the last node where we went right in the case of a right move. + // + // The method uses the following variables: + // + // node: The current node in our walk down the tree. + // first: A node on the right spine of the pre-split tree. At the beginning of each iteration, it is the black + // node with the same black height as "node". If the pre-split tree is empty, this is null instead. + // firstParent: The parent of "first". If the pre-split tree is empty, this is null. Otherwise, this is the + // same as first.parent, unless first.isLeaf(). + // firstPivot: The node where we last went right, i.e. the next node to use as a pivot when concatenating with + // the pre-split tree. + // advanceFirst: Whether to set "first" to be its next black descendant at the end of the loop. + // last, lastParent, lastPivot, advanceLast: Analogous to "first", firstParent, firstPivot, and advanceFirst, + // but for the post-split tree. + if (parent != null) { + throw new IllegalArgumentException("This is not the root of a tree"); + } + if (isLeaf() || splitNode.isLeaf()) { + throw new IllegalArgumentException("The root or the split node is a leaf"); + } + + // Create an array containing the path from the root to splitNode + int depth = 1; + N parent; + for (parent = splitNode; parent.parent != null; parent = parent.parent) { + depth++; + } + if (parent != this) { + throw new IllegalArgumentException("The split node does not belong to this tree"); + } + RedBlackNode[] path = new RedBlackNode[depth]; + for (parent = splitNode; parent != null; parent = parent.parent) { + depth--; + path[depth] = parent; + } + + @SuppressWarnings("unchecked") + N node = (N)this; + N first = null; + N firstParent = null; + N last = null; + N lastParent = null; + N firstPivot = null; + N lastPivot = null; + while (!node.isLeaf()) { + boolean advanceFirst = !node.isRed && firstPivot != null; + boolean advanceLast = !node.isRed && lastPivot != null; + if ((depth + 1 < path.length && path[depth + 1] == node.left) || depth + 1 == path.length) { + // Left move + if (lastPivot == null) { + // The post-split tree is empty + last = node.right; + last.parent = null; + if (last.isRed) { + last.isRed = false; + lastParent = last; + last = last.left; + } + } else { + // Concatenate node.right and the post-split tree + if (node.right.isRed) { + node.right.isRed = false; + } else if (!node.isRed) { + lastParent = last; + last = last.left; + if (last.isRed) { + lastParent = last; + last = last.left; + } + advanceLast = false; + } + lastPivot.isRed = true; + lastPivot.parent = lastParent; + if (lastParent != null) { + lastParent.left = lastPivot; + } + lastPivot.left = node.right; + if (!lastPivot.left.isLeaf()) { + lastPivot.left.parent = lastPivot; + } + lastPivot.right = last; + if (!last.isLeaf()) { + last.parent = lastPivot; + } + last = lastPivot.left; + lastParent = lastPivot; + lastPivot.fixInsertionWithoutGettingRoot(false); + } + lastPivot = node; + node = node.left; + } else { + // Right move + if (firstPivot == null) { + // The pre-split tree is empty + first = node.left; + first.parent = null; + if (first.isRed) { + first.isRed = false; + firstParent = first; + first = first.right; + } + } else { + // Concatenate the post-split tree and node.left + if (node.left.isRed) { + node.left.isRed = false; + } else if (!node.isRed) { + firstParent = first; + first = first.right; + if (first.isRed) { + firstParent = first; + first = first.right; + } + advanceFirst = false; + } + firstPivot.isRed = true; + firstPivot.parent = firstParent; + if (firstParent != null) { + firstParent.right = firstPivot; + } + firstPivot.right = node.left; + if (!firstPivot.right.isLeaf()) { + firstPivot.right.parent = firstPivot; + } + firstPivot.left = first; + if (!first.isLeaf()) { + first.parent = firstPivot; + } + first = firstPivot.right; + firstParent = firstPivot; + firstPivot.fixInsertionWithoutGettingRoot(false); + } + firstPivot = node; + node = node.right; + } + + depth++; + + // Update "first" and "last" to be the nodes at the proper black height + if (advanceFirst) { + firstParent = first; + first = first.right; + if (first.isRed) { + firstParent = first; + first = first.right; + } + } + if (advanceLast) { + lastParent = last; + last = last.left; + if (last.isRed) { + lastParent = last; + last = last.left; + } + } + } + + // Add firstPivot to the pre-split tree + N leaf = node; + if (first == null) { + first = leaf; + } else { + firstPivot.isRed = true; + firstPivot.parent = firstParent; + if (firstParent != null) { + firstParent.right = firstPivot; + } + firstPivot.left = leaf; + firstPivot.right = leaf; + firstPivot.fixInsertionWithoutGettingRoot(false); + for (first = firstPivot; first.parent != null; first = first.parent) { + first.augment(); + } + first.augment(); + } + + // Add lastPivot to the post-split tree + lastPivot.isRed = true; + lastPivot.parent = lastParent; + if (lastParent != null) { + lastParent.left = lastPivot; + } + lastPivot.left = leaf; + lastPivot.right = leaf; + lastPivot.fixInsertionWithoutGettingRoot(false); + for (last = lastPivot; last.parent != null; last = last.parent) { + last.augment(); + } + last.augment(); + + @SuppressWarnings("unchecked") + N[] result = (N[])Array.newInstance(getClass(), 2); + result[0] = first; + result[1] = last; + return result; + } + + /** + * Returns the lowest common ancestor of this node and "other" - the node that is an ancestor of both and is not the + * parent of a node that is an ancestor of both. Assumes that this is in the same tree as "other". Assumes that + * neither "this" nor "other" is a leaf node. This method may return "this" or "other". + * + * Note that while it is possible to compute the lowest common ancestor in O(P) time, where P is the length of the + * path from this node to "other", the "lca" method is not guaranteed to take O(P) time. If your application + * requires this, then you should write your own lowest common ancestor method. + */ + public N lca(N other) { + if (isLeaf() || other.isLeaf()) { + throw new IllegalArgumentException("One of the nodes is a leaf node"); + } + + // Compute the depth of each node + int depth = 0; + for (N parent = this.parent; parent != null; parent = parent.parent) { + depth++; + } + int otherDepth = 0; + for (N parent = other.parent; parent != null; parent = parent.parent) { + otherDepth++; + } + + // Go up to nodes of the same depth + @SuppressWarnings("unchecked") + N parent = (N)this; + N otherParent = other; + if (depth <= otherDepth) { + for (int i = otherDepth; i > depth; i--) { + otherParent = otherParent.parent; + } + } else { + for (int i = depth; i > otherDepth; i--) { + parent = parent.parent; + } + } + + // Find the LCA + while (parent != otherParent) { + parent = parent.parent; + otherParent = otherParent.parent; + } + if (parent != null) { + return parent; + } else { + throw new IllegalArgumentException("The nodes do not belong to the same tree"); + } + } + + /** + * Returns an integer comparing the position of this node in the tree that contains it with that of "other". Returns + * a negative number if this is earlier, a positive number if this is later, and 0 if this is at the same position. + * Assumes that this is in the same tree as "other". Assumes that neither "this" nor "other" is a leaf node. + * + * The base class's implementation takes O(log N) time. If a RedBlackNode subclass stores a value used to order the + * nodes, then it could override compareTo to compare the nodes' values, which would take O(1) time. + * + * Note that while it is possible to compare the positions of two nodes in O(P) time, where P is the length of the + * path from this node to "other", the default implementation of compareTo is not guaranteed to take O(P) time. If + * your application requires this, then you should write your own comparison method. + */ + @Override + public int compareTo(N other) { + if (isLeaf() || other.isLeaf()) { + throw new IllegalArgumentException("One of the nodes is a leaf node"); + } + + // The algorithm operates as follows: compare the depth of this node to that of "other". If the depth of + // "other" is greater, keep moving up from "other" until we find the ancestor at the same depth. Then, keep + // moving up from "this" and from that node until we reach the lowest common ancestor. The node that arrived + // from the left child of the common ancestor is earlier. The algorithm is analogous if the depth of "other" is + // not greater. + if (this == other) { + return 0; + } + + // Compute the depth of each node + int depth = 0; + RedBlackNode parent; + for (parent = this; parent.parent != null; parent = parent.parent) { + depth++; + } + int otherDepth = 0; + N otherParent; + for (otherParent = other; otherParent.parent != null; otherParent = otherParent.parent) { + otherDepth++; + } + + // Go up to nodes of the same depth + if (depth < otherDepth) { + otherParent = other; + for (int i = otherDepth - 1; i > depth; i--) { + otherParent = otherParent.parent; + } + if (otherParent.parent != this) { + otherParent = otherParent.parent; + } else if (left == otherParent) { + return 1; + } else { + return -1; + } + parent = this; + } else if (depth > otherDepth) { + parent = this; + for (int i = depth - 1; i > otherDepth; i--) { + parent = parent.parent; + } + if (parent.parent != other) { + parent = parent.parent; + } else if (other.left == parent) { + return -1; + } else { + return 1; + } + otherParent = other; + } else { + parent = this; + otherParent = other; + } + + // Keep going up until we reach the lowest common ancestor + while (parent.parent != otherParent.parent) { + parent = parent.parent; + otherParent = otherParent.parent; + } + if (parent.parent == null) { + throw new IllegalArgumentException("The nodes do not belong to the same tree"); + } + if (parent.parent.left == parent) { + return -1; + } else { + return 1; + } + } + + /** Throws a RuntimeException if the RedBlackNode fields of this are not correct for a leaf node. */ + private void assertIsValidLeaf() { + if (left != null || right != null || parent != null || isRed) { + throw new RuntimeException("A leaf node's \"left\", \"right\", \"parent\", or isRed field is incorrect"); + } + } + + /** + * Throws a RuntimeException if the subtree rooted at this node does not satisfy the red-black properties, excluding + * the requirement that the root be black, or it contains a repeated node other than a leaf node. + * @param blackHeight The required number of black nodes in each path from this to a leaf node, including this and + * the leaf node. + * @param visited The nodes we have reached thus far, other than leaf nodes. This method adds the non-leaf nodes in + * the subtree rooted at this node to "visited". + */ + private void assertSubtreeIsValidRedBlack(int blackHeight, Set> visited) { + @SuppressWarnings("unchecked") + N nThis = (N)this; + if (left == null || right == null) { + assertIsValidLeaf(); + if (blackHeight != 1) { + throw new RuntimeException("Not all root-to-leaf paths have the same number of black nodes"); + } + return; + } else if (!visited.add(new Reference(nThis))) { + throw new RuntimeException("The tree contains a repeated non-leaf node"); + } else { + int childBlackHeight; + if (isRed) { + if ((!left.isLeaf() && left.isRed) || (!right.isLeaf() && right.isRed)) { + throw new RuntimeException("A red node has a red child"); + } + childBlackHeight = blackHeight; + } else if (blackHeight == 0) { + throw new RuntimeException("Not all root-to-leaf paths have the same number of black nodes"); + } else { + childBlackHeight = blackHeight - 1; + } + + if (!left.isLeaf() && left.parent != this) { + throw new RuntimeException("left.parent != this"); + } + if (!right.isLeaf() && right.parent != this) { + throw new RuntimeException("right.parent != this"); + } + RedBlackNode leftNode = left; + RedBlackNode rightNode = right; + leftNode.assertSubtreeIsValidRedBlack(childBlackHeight, visited); + rightNode.assertSubtreeIsValidRedBlack(childBlackHeight, visited); + } + } + + /** Calls assertNodeIsValid() on every node in the subtree rooted at this node. */ + private void assertNodesAreValid() { + assertNodeIsValid(); + if (left != null) { + RedBlackNode leftNode = left; + RedBlackNode rightNode = right; + leftNode.assertNodesAreValid(); + rightNode.assertNodesAreValid(); + } + } + + /** + * Throws a RuntimeException if the subtree rooted at this node is not a valid red-black tree, e.g. if a red node + * has a red child or it contains a non-leaf node "node" for which node.left.parent != node. (If parent != null, + * it's okay if isRed is true.) This method is useful for debugging. See also assertSubtreeIsValid(). + */ + public void assertSubtreeIsValidRedBlack() { + if (isLeaf()) { + assertIsValidLeaf(); + } else { + if (parent == null && isRed) { + throw new RuntimeException("The root is red"); + } + + // Compute the black height of the tree + Set> nodes = new HashSet>(); + int blackHeight = 0; + @SuppressWarnings("unchecked") + N node = (N)this; + while (node != null) { + if (!nodes.add(new Reference(node))) { + throw new RuntimeException("The tree contains a repeated non-leaf node"); + } + if (!node.isRed) { + blackHeight++; + } + node = node.left; + } + + assertSubtreeIsValidRedBlack(blackHeight, new HashSet>()); + } + } + + /** + * Throws a RuntimeException if we detect a problem with the subtree rooted at this node, such as a red child of a + * red node or a non-leaf descendant "node" for which node.left.parent != node. This method is useful for + * debugging. RedBlackNode subclasses may want to override assertSubtreeIsValid() to call assertOrderIsValid. + */ + public void assertSubtreeIsValid() { + assertSubtreeIsValidRedBlack(); + assertNodesAreValid(); + } + + /** + * Throws a RuntimeException if the nodes in the subtree rooted at this node are not in the specified order or they + * do not lie in the specified range. Assumes that the subtree rooted at this node is a valid binary tree, i.e. it + * has no repeated nodes other than leaf nodes. + * @param comparator A comparator indicating how the nodes should be ordered. + * @param start The lower limit for nodes in the subtree, if any. + * @param end The upper limit for nodes in the subtree, if any. + */ + private void assertOrderIsValid(Comparator comparator, N start, N end) { + if (!isLeaf()) { + @SuppressWarnings("unchecked") + N nThis = (N)this; + if (start != null && comparator.compare(nThis, start) < 0) { + throw new RuntimeException("The nodes are not ordered correctly"); + } + if (end != null && comparator.compare(nThis, end) > 0) { + throw new RuntimeException("The nodes are not ordered correctly"); + } + RedBlackNode leftNode = left; + RedBlackNode rightNode = right; + leftNode.assertOrderIsValid(comparator, start, nThis); + rightNode.assertOrderIsValid(comparator, nThis, end); + } + } + + /** + * Throws a RuntimeException if the nodes in the subtree rooted at this node are not in the specified order. + * Assumes that this is a valid binary tree, i.e. there are no repeated nodes other than leaf nodes. This method is + * useful for debugging. RedBlackNode subclasses may want to override assertSubtreeIsValid() to call + * assertOrderIsValid. + * @param comparator A comparator indicating how the nodes should be ordered. If this is null, we use the nodes' + * natural order, as in N.compareTo. + */ + public void assertOrderIsValid(Comparator comparator) { + if (comparator == null) { + comparator = naturalOrder(); + } + assertOrderIsValid(comparator, null, null); + } +} diff --git a/src/main/java/com/github/btrekkie/red_black_node/Reference.java b/src/main/java/com/github/btrekkie/red_black_node/Reference.java new file mode 100644 index 000000000..843053820 --- /dev/null +++ b/src/main/java/com/github/btrekkie/red_black_node/Reference.java @@ -0,0 +1,28 @@ +package com.github.btrekkie.red_black_node; + +/** + * Wraps a value using reference equality. In other words, two references are equal only if their values are the same + * object instance, as in ==. + * @param  The type of value. + */ +class Reference { + /** The value this wraps. */ + private final T value; + + public Reference(T value) { + this.value = value; + } + + public boolean equals(Object obj) { + if (!(obj instanceof Reference)) { + return false; + } + Reference reference = (Reference)obj; + return value == reference.value; + } + + @Override + public int hashCode() { + return System.identityHashCode(value); + } +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java new file mode 100644 index 000000000..8b5102471 --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java @@ -0,0 +1,40 @@ +package com.github.btrekkie.arbitrary_order_collection; + +import java.util.Comparator; + +/** + * Provides objects ordered in an arbitrary, but consistent fashion, through the createValue() method. To determine the + * relative order of two values, use ArbitraryOrderValue.compareTo. We may only compare values on which we have not + * called "remove" that were created in the same ArbitraryOrderCollection instance. Note that despite the name, + * ArbitraryOrderCollection does not implement Collection. + */ +/* We implement an ArbitraryOrderCollection using a red-black tree. We order the nodes arbitrarily. + */ +public class ArbitraryOrderCollection { + /** The Comparator for ordering ArbitraryOrderNodes. */ + private static final Comparator NODE_COMPARATOR = new Comparator() { + @Override + public int compare(ArbitraryOrderNode node1, ArbitraryOrderNode node2) { + return 0; + } + }; + + /** The root node of the tree. */ + private ArbitraryOrderNode root = new ArbitraryOrderNode(); + + /** Adds and returns a new value for ordering. */ + public ArbitraryOrderValue createValue() { + ArbitraryOrderNode node = new ArbitraryOrderNode(); + root = root.insert(node, true, NODE_COMPARATOR); + return new ArbitraryOrderValue(node); + } + + /** + * Removes the specified value from this collection. Assumes we obtained the value by calling createValue() on this + * instance of ArbitraryOrderCollection. After calling "remove" on a value, we may no longer compare it to other + * values. + */ + public void remove(ArbitraryOrderValue value) { + root = value.node.remove(); + } +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java new file mode 100644 index 000000000..b5d28d9be --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java @@ -0,0 +1,8 @@ +package com.github.btrekkie.arbitrary_order_collection; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** A node in an ArbitraryOrderCollection tree. See ArbitraryOrderCollection. */ +class ArbitraryOrderNode extends RedBlackNode { + +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java new file mode 100644 index 000000000..c12b995ad --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java @@ -0,0 +1,19 @@ +package com.github.btrekkie.arbitrary_order_collection; + +/** + * A value in an ArbitraryOrderCollection. To determine the relative order of two values in the same collection, call + * compareTo. + */ +public class ArbitraryOrderValue implements Comparable { + /** The node that establishes this value's relative position. */ + final ArbitraryOrderNode node; + + ArbitraryOrderValue(ArbitraryOrderNode node) { + this.node = node; + } + + @Override + public int compareTo(ArbitraryOrderValue other) { + return node.compareTo(other.node); + } +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java new file mode 100644 index 000000000..9d855d17d --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java @@ -0,0 +1,72 @@ +package com.github.btrekkie.arbitrary_order_collection.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import com.github.btrekkie.arbitrary_order_collection.ArbitraryOrderCollection; +import com.github.btrekkie.arbitrary_order_collection.ArbitraryOrderValue; + +public class ArbitraryOrderCollectionTest { + /** Tests ArbitraryOrderCollection. */ + @Test + public void test() { + ArbitraryOrderCollection collection = new ArbitraryOrderCollection(); + List values1 = new ArrayList(5); + ArbitraryOrderValue value = collection.createValue(); + assertEquals(0, value.compareTo(value)); + values1.add(value); + for (int i = 0; i < 4; i++) { + values1.add(collection.createValue()); + } + Collections.sort(values1); + List values2 = new ArrayList(10); + for (int i = 0; i < 10; i++) { + value = collection.createValue(); + values2.add(value); + } + for (int i = 0; i < 5; i++) { + collection.remove(values2.get(2 * i)); + } + assertEquals(0, values1.get(0).compareTo(values1.get(0))); + assertTrue(values1.get(0).compareTo(values1.get(1)) < 0); + assertTrue(values1.get(1).compareTo(values1.get(0)) > 0); + assertTrue(values1.get(4).compareTo(values1.get(2)) > 0); + assertTrue(values1.get(0).compareTo(values1.get(4)) < 0); + + collection = new ArbitraryOrderCollection(); + values1 = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + value = collection.createValue(); + values1.add(value); + } + for (int i = 0; i < 500; i++) { + collection.remove(values1.get(2 * i)); + } + values2 = new ArrayList(500); + for (int i = 0; i < 500; i++) { + values2.add(values1.get(2 * i + 1)); + } + for (int i = 0; i < 500; i++) { + values2.get(0).compareTo(values2.get(i)); + } + Collections.sort(values2); + for (int i = 0; i < 500; i++) { + collection.createValue(); + } + for (int i = 0; i < 499; i++) { + assertTrue(values2.get(i).compareTo(values2.get(i + 1)) < 0); + assertTrue(values2.get(i + 1).compareTo(values2.get(i)) > 0); + } + for (int i = 1; i < 500; i++) { + assertEquals(0, values2.get(i).compareTo(values2.get(i))); + assertTrue(values2.get(0).compareTo(values2.get(i)) < 0); + assertTrue(values2.get(i).compareTo(values2.get(0)) > 0); + } + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/IntervalTree.java b/src/test/java/com/github/btrekkie/interval_tree/IntervalTree.java new file mode 100644 index 000000000..d5e88624e --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/IntervalTree.java @@ -0,0 +1,60 @@ +package com.github.btrekkie.interval_tree; + +/** + * An interval tree data structure, which supports adding or removing an interval and finding an arbitrary interval that + * contains a specified value. + */ +/* The interval tree is ordered in ascending order of the start an interval, with ties broken by the end of the + * interval. Each node is augmented with the maximum ending value of an interval in the subtree rooted at the node. + */ +public class IntervalTree { + /** The root node of the tree. */ + private IntervalTreeNode root = IntervalTreeNode.LEAF; + + /** Adds the specified interval to this. */ + public void addInterval(IntervalTreeInterval interval) { + root = root.insert(new IntervalTreeNode(interval), true, null); + } + + /** + * Removes the specified interval from this, if it is present. + * @param interval The interval. + * @return Whether the interval was present. + */ + public boolean removeInterval(IntervalTreeInterval interval) { + IntervalTreeNode node = root; + while (!node.isLeaf()) { + if (interval.start < node.interval.start) { + node = node.left; + } else if (interval.start > node.interval.start) { + node = node.right; + } else if (interval.end < node.interval.end) { + node = node.left; + } else if (interval.end > node.interval.end) { + node = node.right; + } else { + root = node.remove(); + return true; + } + } + return false; + } + + /** + * Returns an aribtrary IntervalTreeInterval in this that contains the specified value. Returns null if there is no + * such interval. + */ + public IntervalTreeInterval findInterval(double value) { + IntervalTreeNode node = root; + while (!node.isLeaf()) { + if (value >= node.interval.start && value <= node.interval.end) { + return node.interval; + } else if (value <= node.left.maxEnd) { + node = node.left; + } else { + node = node.right; + } + } + return null; + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeInterval.java b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeInterval.java new file mode 100644 index 000000000..e9f38b9d9 --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeInterval.java @@ -0,0 +1,28 @@ +package com.github.btrekkie.interval_tree; + +/** + * An inclusive range of values [start, end]. Two intervals are equal if they have the same starting and ending values. + */ +public class IntervalTreeInterval { + /** The smallest value in the range. */ + public final double start; + + /** The largest value in the range. */ + public final double end; + + public IntervalTreeInterval(double start, double end) { + if (start > end) { + throw new IllegalArgumentException("The end of the range must be at most the start"); + } + this.start = start; + this.end = end; + } + + public boolean equals(Object obj) { + if (!(obj instanceof IntervalTreeInterval)) { + return false; + } + IntervalTreeInterval interval = (IntervalTreeInterval)obj; + return start == interval.start && end == interval.end; + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeNode.java b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeNode.java new file mode 100644 index 000000000..d75004e77 --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeNode.java @@ -0,0 +1,68 @@ +package com.github.btrekkie.interval_tree; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** + * A node in an IntervalTree. See the comments for the implementation of IntervalTree. Its compareTo method orders + * nodes as suggested in the comments for the implementation of IntervalTree. + */ +class IntervalTreeNode extends RedBlackNode { + /** The dummy leaf node. */ + public static final IntervalTreeNode LEAF = new IntervalTreeNode(); + + /** The interval stored in this node. */ + public IntervalTreeInterval interval; + + /** The maximum ending value of an interval in the subtree rooted at this node. */ + public double maxEnd; + + public IntervalTreeNode(IntervalTreeInterval interval) { + this.interval = interval; + maxEnd = interval.end; + } + + /** Constructs a new dummy leaf node. */ + private IntervalTreeNode() { + interval = null; + maxEnd = Double.NEGATIVE_INFINITY; + } + + @Override + public boolean augment() { + double newMaxEnd = Math.max(interval.end, Math.max(left.maxEnd, right.maxEnd)); + if (newMaxEnd == maxEnd) { + return false; + } else { + maxEnd = newMaxEnd; + return true; + } + } + + @Override + public void assertNodeIsValid() { + double expectedMaxEnd; + if (isLeaf()) { + expectedMaxEnd = Double.NEGATIVE_INFINITY; + } else { + expectedMaxEnd = Math.max(interval.end, Math.max(left.maxEnd, right.maxEnd)); + } + if (maxEnd != expectedMaxEnd) { + throw new RuntimeException("The node's maxEnd does not match that of the children"); + } + } + + @Override + public int compareTo(IntervalTreeNode other) { + if (interval.start != interval.end) { + return Double.compare(interval.start, other.interval.start); + } else { + return Double.compare(interval.end, other.interval.end); + } + } + + @Override + public void assertSubtreeIsValid() { + super.assertSubtreeIsValid(); + assertOrderIsValid(null); + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java b/src/test/java/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java new file mode 100644 index 000000000..cf0dbdfa6 --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java @@ -0,0 +1,48 @@ +package com.github.btrekkie.interval_tree.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.github.btrekkie.interval_tree.IntervalTree; +import com.github.btrekkie.interval_tree.IntervalTreeInterval; + +public class IntervalTreeTest { + /** Tests IntervalTree. */ + @Test + public void test() { + IntervalTree tree = new IntervalTree(); + assertNull(tree.findInterval(0.5)); + assertNull(tree.findInterval(-1)); + tree.addInterval(new IntervalTreeInterval(5, 7)); + tree.addInterval(new IntervalTreeInterval(42, 48)); + tree.addInterval(new IntervalTreeInterval(-1, 2)); + tree.addInterval(new IntervalTreeInterval(6, 12)); + tree.addInterval(new IntervalTreeInterval(21, 23)); + assertTrue(tree.removeInterval(new IntervalTreeInterval(-1, 2))); + assertFalse(tree.removeInterval(new IntervalTreeInterval(-1, 2))); + tree.addInterval(new IntervalTreeInterval(-6, -2)); + assertEquals(new IntervalTreeInterval(6, 12), tree.findInterval(8)); + assertNull(tree.findInterval(0)); + assertEquals(new IntervalTreeInterval(21, 23), tree.findInterval(21)); + assertEquals(new IntervalTreeInterval(42, 48), tree.findInterval(48)); + IntervalTreeInterval interval = tree.findInterval(6.5); + assertTrue(new IntervalTreeInterval(5, 7).equals(interval) || new IntervalTreeInterval(6, 12).equals(interval)); + + tree = new IntervalTree(); + for (int i = 0; i < 500; i++) { + tree.addInterval(new IntervalTreeInterval(2 * i, 2 * i + 1)); + } + for (int i = 0; i < 250; i++) { + tree.removeInterval(new IntervalTreeInterval(4 * i + 2, 4 * i + 3)); + } + assertNull(tree.findInterval(123.5)); + assertEquals(new IntervalTreeInterval(124, 125), tree.findInterval(124.5)); + assertEquals(new IntervalTreeInterval(776, 777), tree.findInterval(776)); + assertEquals(new IntervalTreeInterval(0, 1), tree.findInterval(0.5)); + assertEquals(new IntervalTreeInterval(996, 997), tree.findInterval(997)); + } +} diff --git a/src/test/java/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java b/src/test/java/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java new file mode 100644 index 000000000..4d571aeb1 --- /dev/null +++ b/src/test/java/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java @@ -0,0 +1,171 @@ +package com.github.btrekkie.red_black_node.test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Comparator; + +import org.junit.Test; + +/** + * Tests RedBlackNode. Most of the testing for RedBlackNode takes place in TreeListTest, IntervalTreeTest, + * SubArrayMinTest, and ArbitraryOrderCollectionTest, which test realistic use cases of RedBlackNode. TreeListTest + * tests most of the RedBlackNode methods, while IntervalTreeTest tests the "insert" method, SubArrayMinTest tests + * "lca", ArbitraryOrderCollectionTest tests compareTo, and RedBlackNodeTest tests assertSubtreeIsValid() and + * assertOrderIsValid. + */ +public class RedBlackNodeTest { + /** + * Returns whether the subtree rooted at the specified node is valid, as in TestRedBlackNode.assertSubtreeIsValid(). + */ + private boolean isSubtreeValid(TestRedBlackNode node) { + try { + node.assertSubtreeIsValid(); + return true; + } catch (RuntimeException exception) { + return false; + } + } + + /** + * Returns whether the nodes in the subtree rooted at the specified node are ordered correctly, as in + * TestRedBlackNode.assertOrderIsValid. + * @param comparator A comparator indicating how the nodes should be ordered. If this is null, we use the nodes' + * natural ordering, as in TestRedBlackNode.compare. + */ + private boolean isOrderValid(TestRedBlackNode node, Comparator comparator) { + try { + node.assertOrderIsValid(null); + return true; + } catch (RuntimeException exception) { + return false; + } + } + + /** Tests RedBlackNode.assertSubtreeIsValid() and RedBlackNode.assertOrderIsValid. */ + @Test + public void testAssertIsValid() { + // Create a perfectly balanced tree of height 3 + TestRedBlackNode node0 = new TestRedBlackNode(0); + TestRedBlackNode node1 = new TestRedBlackNode(1); + TestRedBlackNode node2 = new TestRedBlackNode(2); + TestRedBlackNode node3 = new TestRedBlackNode(3); + TestRedBlackNode node4 = new TestRedBlackNode(4); + TestRedBlackNode node5 = new TestRedBlackNode(5); + TestRedBlackNode node6 = new TestRedBlackNode(6); + node0.parent = node1; + node0.left = TestRedBlackNode.LEAF; + node0.right = TestRedBlackNode.LEAF; + node1.parent = node3; + node1.left = node0; + node1.right = node2; + node1.isRed = true; + node2.parent = node1; + node2.left = TestRedBlackNode.LEAF; + node2.right = TestRedBlackNode.LEAF; + node3.left = node1; + node3.right = node5; + node4.parent = node5; + node4.left = TestRedBlackNode.LEAF; + node4.right = TestRedBlackNode.LEAF; + node5.parent = node3; + node5.left = node4; + node5.right = node6; + node5.isRed = true; + node6.parent = node5; + node6.left = TestRedBlackNode.LEAF; + node6.right = TestRedBlackNode.LEAF; + + node3.left = node3; + node3.right = node3; + node3.parent = node3; + assertFalse(isSubtreeValid(node3)); + node3.left = node1; + node3.right = node5; + node3.parent = null; + + node0.parent = node3; + assertFalse(isSubtreeValid(node3)); + node0.parent = node1; + + node1.right = node0; + assertFalse(isSubtreeValid(node3)); + node1.right = node2; + + node5.isRed = false; + assertFalse(isSubtreeValid(node3)); + assertTrue(isSubtreeValid(node5)); + node5.isRed = true; + + node3.isRed = true; + assertFalse(isSubtreeValid(node3)); + assertTrue(isSubtreeValid(node5)); + node3.isRed = false; + + node0.isRed = true; + node2.isRed = true; + node4.isRed = true; + node6.isRed = true; + assertFalse(isSubtreeValid(node3)); + node0.isRed = false; + node2.isRed = false; + node4.isRed = false; + node6.isRed = false; + + TestRedBlackNode.LEAF.isRed = true; + assertFalse(isSubtreeValid(node3)); + TestRedBlackNode.LEAF.isRed = false; + + TestRedBlackNode.LEAF.isValid = false; + assertFalse(isSubtreeValid(node3)); + assertFalse(isSubtreeValid(TestRedBlackNode.LEAF)); + TestRedBlackNode.LEAF.isValid = true; + + node1.isValid = false; + assertFalse(isSubtreeValid(node3)); + node1.isValid = true; + + node3.value = 2; + node2.value = 3; + assertFalse(isOrderValid(node3, null)); + assertFalse( + isOrderValid(node3, new Comparator() { + @Override + public int compare(TestRedBlackNode node1, TestRedBlackNode node2) { + return node1.value - node2.value; + } + })); + node3.value = 3; + node2.value = 2; + + node2.value = 4; + node4.value = 2; + assertFalse(isOrderValid(node3, null)); + node2.value = 2; + node4.value = 4; + + node0.value = 1; + node1.value = 0; + assertFalse(isOrderValid(node3, null)); + node0.value = 0; + node1.value = 1; + + // Do all of the assertions for which the tree is supposed to be valid at the end, to make sure we didn't make a + // mistake undoing any of the modifications + assertTrue(isSubtreeValid(node3)); + assertTrue(isSubtreeValid(node1)); + assertTrue(isSubtreeValid(node0)); + assertTrue(isSubtreeValid(TestRedBlackNode.LEAF)); + assertTrue(isOrderValid(node3, null)); + assertTrue(isOrderValid(node1, null)); + assertTrue(isOrderValid(node0, null)); + assertTrue(isOrderValid(TestRedBlackNode.LEAF, null)); + assertTrue( + isOrderValid(node3, new Comparator() { + @Override + public int compare(TestRedBlackNode node1, TestRedBlackNode node2) { + return node1.value - node2.value; + } + })); + } +} diff --git a/src/test/java/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java b/src/test/java/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java new file mode 100644 index 000000000..8e3b78cd8 --- /dev/null +++ b/src/test/java/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java @@ -0,0 +1,36 @@ +package com.github.btrekkie.red_black_node.test; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** A RedBlackNode for RedBlackNodeTest. */ +class TestRedBlackNode extends RedBlackNode { + /** The dummy leaf node. */ + public static final TestRedBlackNode LEAF = new TestRedBlackNode(); + + /** The value stored in this node. "value" is unspecified if this is a leaf node. */ + public int value; + + /** Whether this node is considered valid, as in assertNodeIsValid(). */ + public boolean isValid = true; + + public TestRedBlackNode(int value) { + this.value = value; + } + + /** Constructs a new dummy leaf node. */ + private TestRedBlackNode() { + + } + + @Override + public void assertNodeIsValid() { + if (!isValid) { + throw new RuntimeException("isValid is false"); + } + } + + @Override + public int compareTo(TestRedBlackNode other) { + return value - other.value; + } +} diff --git a/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMin.java b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMin.java new file mode 100644 index 000000000..63634c9f8 --- /dev/null +++ b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMin.java @@ -0,0 +1,91 @@ +package com.github.btrekkie.sub_array_min; + +/** A list of integers. SubArrayMin provides the ability to quickly determine the minimum value in a given sublist. */ +/* We implement SubArrayMin using a red-black tree augmented by subtree size and minimum value. Using the subtree size + * augmentation, we can find the node at a given index. + */ +public class SubArrayMin { + /** The root node. */ + private SubArrayMinNode root = SubArrayMinNode.LEAF; + + /** Appends the specified value to the end of the list. */ + public void add(int value) { + SubArrayMinNode newNode = new SubArrayMinNode(value); + newNode.left = SubArrayMinNode.LEAF; + newNode.right = SubArrayMinNode.LEAF; + if (root.isLeaf()) { + root = newNode; + newNode.augment(); + } else { + SubArrayMinNode node = root.max(); + node.right = newNode; + newNode.parent = node; + newNode.isRed = true; + root = newNode.fixInsertion(); + } + } + + /** Returns the node for the element with the specified index. Assumes "index" is in the range [0, root.size). */ + private SubArrayMinNode getNode(int index) { + if (index < 0 || index >= root.size) { + throw new IndexOutOfBoundsException("Index " + index + " is not in the range [0, " + root.size + ")"); + } + int rank = index; + SubArrayMinNode node = root; + while (rank != node.left.size) { + if (rank < node.left.size) { + node = node.left; + } else { + rank -= node.left.size + 1; + node = node.right; + } + } + return node; + } + + /** + * Returns the minimum value in the subarray starting at index startIndex and ending at index endIndex - 1, + * inclusive. Assumes startIndex < endIndex, and assumes this contains indices startIndex and endIndex - 1. + */ + public int min(int startIndex, int endIndex) { + if (startIndex >= endIndex) { + throw new IllegalArgumentException("The start index must be less than the end index"); + } + SubArrayMinNode start = getNode(startIndex); + SubArrayMinNode end = getNode(endIndex - 1); + SubArrayMinNode lca = start.lca(end); + + int min = Math.min(lca.value, Math.min(start.value, end.value)); + if (start != lca) { + if (start.right.min < min) { + min = start.right.min; + } + for (SubArrayMinNode node = start; node.parent != lca; node = node.parent) { + if (node.parent.left == node) { + if (node.parent.value < min) { + min = node.parent.value; + } + if (node.parent.right.min < min) { + min = node.parent.right.min; + } + } + } + } + if (end != lca) { + if (end.left.min < min) { + min = end.left.min; + } + for (SubArrayMinNode node = end; node.parent != lca; node = node.parent) { + if (node.parent.right == node) { + if (node.parent.value < min) { + min = node.parent.value; + } + if (node.parent.left.min < min) { + min = node.parent.left.min; + } + } + } + } + return min; + } +} diff --git a/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMinNode.java b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMinNode.java new file mode 100644 index 000000000..1839b7220 --- /dev/null +++ b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMinNode.java @@ -0,0 +1,56 @@ +package com.github.btrekkie.sub_array_min; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** A node in a SubArrayMin object. See the comments for the implementation of that class. */ +class SubArrayMinNode extends RedBlackNode { + /** The dummy leaf node. */ + public static final SubArrayMinNode LEAF = new SubArrayMinNode(); + + /** The element stored in the node. The value is unspecified if this is a leaf node. */ + public final int value; + + /** The number of elements in the subtree rooted at this node. */ + public int size; + + /** The minimum element in the subtree rooted at this node. This is Integer.MAX_VALUE if this is a leaf node. */ + public int min; + + public SubArrayMinNode(int value) { + this.value = value; + } + + private SubArrayMinNode() { + value = 0; + min = Integer.MAX_VALUE; + } + + @Override + public boolean augment() { + int newSize = left.size + right.size + 1; + int newMin = Math.min(value, Math.min(left.min, right.min)); + if (newSize == size && newMin == min) { + return false; + } else { + size = newSize; + min = newMin; + return true; + } + } + + @Override + public void assertNodeIsValid() { + int expectedSize; + int expectedMin; + if (isLeaf()) { + expectedSize = 0; + expectedMin = Integer.MAX_VALUE; + } else { + expectedSize = left.size + right.size + 1; + expectedMin = Math.min(value, Math.min(left.min, right.min)); + } + if (size != expectedSize || min != expectedMin) { + throw new RuntimeException("The node's size or minimum value does not match that of the children"); + } + } +} diff --git a/src/test/java/com/github/btrekkie/sub_array_min/test/SubArrayMinTest.java b/src/test/java/com/github/btrekkie/sub_array_min/test/SubArrayMinTest.java new file mode 100644 index 000000000..5daa7e053 --- /dev/null +++ b/src/test/java/com/github/btrekkie/sub_array_min/test/SubArrayMinTest.java @@ -0,0 +1,40 @@ +package com.github.btrekkie.sub_array_min.test; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.github.btrekkie.sub_array_min.SubArrayMin; + +public class SubArrayMinTest { + /** Tests SubArrayMin. */ + @Test + public void test() { + SubArrayMin sam = new SubArrayMin(); + sam.add(12); + sam.add(42); + sam.add(-3); + sam.add(16); + sam.add(5); + sam.add(8); + sam.add(4); + assertEquals(-3, sam.min(0, 7)); + assertEquals(12, sam.min(0, 2)); + assertEquals(-3, sam.min(2, 4)); + assertEquals(12, sam.min(0, 1)); + assertEquals(5, sam.min(3, 6)); + assertEquals(4, sam.min(4, 7)); + + sam = new SubArrayMin(); + for (int i = 0; i < 1000; i++) { + sam.add(-Integer.bitCount(i)); + } + assertEquals(0, sam.min(0, 1)); + assertEquals(-4, sam.min(0, 30)); + assertEquals(-9, sam.min(0, 1000)); + assertEquals(-9, sam.min(123, 777)); + assertEquals(-8, sam.min(777, 888)); + assertEquals(-6, sam.min(777, 788)); + assertEquals(-9, sam.min(900, 1000)); + } +}