From 389f8ea4e2a17583650a8515fede34b558127d33 Mon Sep 17 00:00:00 2001 From: Bill Jacobs Date: Mon, 23 May 2016 13:51:21 -0700 Subject: [PATCH] Initial commit This adds the initial contents of the repository. --- .classpath | 7 + .project | 17 + .settings/org.eclipse.jdt.core.prefs | 11 + .settings/org.eclipse.jdt.ui.prefs | 60 + README.md | 60 +- .../ArbitraryOrderCollection.java | 40 + .../ArbitraryOrderNode.java | 8 + .../ArbitraryOrderValue.java | 19 + .../test/ArbitraryOrderCollectionTest.java | 72 + .../btrekkie/interval_tree/IntervalTree.java | 60 + .../interval_tree/IntervalTreeInterval.java | 28 + .../interval_tree/IntervalTreeNode.java | 68 + .../interval_tree/test/IntervalTreeTest.java | 48 + .../btrekkie/red_black_node/RedBlackNode.java | 1224 +++++++++++++++++ .../btrekkie/red_black_node/Reference.java | 28 + .../red_black_node/test/RedBlackNodeTest.java | 171 +++ .../red_black_node/test/TestRedBlackNode.java | 36 + .../github/btrekkie/tree_list/TreeList.java | 461 +++++++ .../btrekkie/tree_list/TreeListNode.java | 40 + .../btrekkie/tree_list/test/TreeListTest.java | 551 ++++++++ 20 files changed, 3008 insertions(+), 1 deletion(-) create mode 100644 .classpath create mode 100644 .project create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 .settings/org.eclipse.jdt.ui.prefs create mode 100644 src/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java create mode 100644 src/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java create mode 100644 src/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java create mode 100644 src/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java create mode 100644 src/com/github/btrekkie/interval_tree/IntervalTree.java create mode 100644 src/com/github/btrekkie/interval_tree/IntervalTreeInterval.java create mode 100644 src/com/github/btrekkie/interval_tree/IntervalTreeNode.java create mode 100644 src/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java create mode 100644 src/com/github/btrekkie/red_black_node/RedBlackNode.java create mode 100644 src/com/github/btrekkie/red_black_node/Reference.java create mode 100644 src/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java create mode 100644 src/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java create mode 100644 src/com/github/btrekkie/tree_list/TreeList.java create mode 100644 src/com/github/btrekkie/tree_list/TreeListNode.java create mode 100644 src/com/github/btrekkie/tree_list/test/TreeListTest.java diff --git a/.classpath b/.classpath new file mode 100644 index 000000000..72c2ba61a --- /dev/null +++ b/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.project b/.project new file mode 100644 index 000000000..ec61c803b --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + DataStructures + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..7341ab168 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..a15aefcfd --- /dev/null +++ b/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,60 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=false +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_type_arguments=false diff --git a/README.md b/README.md index bc2c15b8e..0883fb752 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,60 @@ # RedBlackNode -`RedBlackNode` is a Java implementation of red-black trees. By subclassing `RedBlackNode`, clients can add arbitrary data and augmentation information to each node. (self-balancing binary search tree, self-balancing BST, augment, augmented) +`RedBlackNode` is a Java implementation of red-black trees. By subclassing +`RedBlackNode`, clients can add arbitrary data and augmentation information to +each node. (self-balancing binary search tree, self-balancing BST, augment, +augmented) + +# Features +* Supports min, max, root, predecessor, successor, insert, remove, rotate, + split, concatenate, create balanced tree, and compare operations. The running + time of each operation has optimal big O bounds. +* Supports arbitrary augmentation by overriding `augment()`. Examples of + augmentation are the number of non-leaf nodes in a subtree and the sum of the + values in a subtree. +* The parent and child links and the color are public fields. This gives + clients flexibility. However, it is possible for a client to violate the + red-black or BST properties. +* "Assert is valid" methods allow clients to check for errors in the structure + or contents of a red-black tree. This is useful for debugging. +* As a bonus (a proof of concept and a test case), this includes the `TreeList` + class, a `List` implementation backed by a red-black tree augmented by subtree + size. +* Tested in Java 6.0 and 7.0. It might also work in Java 5.0. + +# Limitations: +* Augmentations that depend on information stored in a node's ancestors are not + (easily) supported. For example, augmenting each node with the number of + nodes in the left subtree is not (easily and efficiently) supported, because + in order to perform a right rotation, we would need to use the parent's + augmentation information. However, `RedBlackNode` supports augmenting each + node with the number of nodes in the subtree, which is basically equivalent. +* The running time of each operation has optimal big O bounds. However, beyond + this, no special effort has been made to optimize performance. + +# Example +
+/** Red-black tree augmented by the sum of the values in the subtree. */
+public class SumNode extends RedBlackNode {
+    public int value;
+    public int sum;
+
+    public SumNode(int value) {
+        this.value = value;
+    }
+
+    @Override
+    public boolean augment() {
+        int newSum = value + left.sum + right.sum;
+        if (newSum == sum) {
+            return false;
+        } else {
+            sum = newSum;
+            return true;
+        }
+    }
+}
+
+ +# Documentation +For more detailed instructions, check the source code to see the full API and +Javadoc documentation. diff --git a/src/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java b/src/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java new file mode 100644 index 000000000..8b5102471 --- /dev/null +++ b/src/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/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java b/src/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java new file mode 100644 index 000000000..b5d28d9be --- /dev/null +++ b/src/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/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java b/src/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java new file mode 100644 index 000000000..c12b995ad --- /dev/null +++ b/src/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/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java b/src/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java new file mode 100644 index 000000000..9d855d17d --- /dev/null +++ b/src/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/com/github/btrekkie/interval_tree/IntervalTree.java b/src/com/github/btrekkie/interval_tree/IntervalTree.java new file mode 100644 index 000000000..d5e88624e --- /dev/null +++ b/src/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/com/github/btrekkie/interval_tree/IntervalTreeInterval.java b/src/com/github/btrekkie/interval_tree/IntervalTreeInterval.java new file mode 100644 index 000000000..e9f38b9d9 --- /dev/null +++ b/src/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/com/github/btrekkie/interval_tree/IntervalTreeNode.java b/src/com/github/btrekkie/interval_tree/IntervalTreeNode.java new file mode 100644 index 000000000..d75004e77 --- /dev/null +++ b/src/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/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java b/src/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java new file mode 100644 index 000000000..cf0dbdfa6 --- /dev/null +++ b/src/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/com/github/btrekkie/red_black_node/RedBlackNode.java b/src/com/github/btrekkie/red_black_node/RedBlackNode.java new file mode 100644 index 000000000..8393fafeb --- /dev/null +++ b/src/com/github/btrekkie/red_black_node/RedBlackNode.java @@ -0,0 +1,1224 @@ +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 ). The RedBlackNode class provides + * methods for performing various standard operations. The leaf nodes in a tree are dummy nodes colored black that do + * not contain any values. It is recommended that all of the leaf nodes in a given tree be the same RedBlackNode + * instance, to save space. The root of an empty tree is a leaf node, as opposed to null. + * + * Subclasses may add arbitrary information to the node. For example, if we were to use a RedBlackNode subclass to + * implement a sorted set, the subclass should have a field storing an element in the set. Subclasses can augment the + * tree with arbitrary information by overriding augment(). + * + * The internals of the node are exposed publicly, so clients can access or alter a node arbitrarily. This gives + * clients flexibility. It is possible for a client to violate the red-black or BST properties. + * + * @param The type of node in the tree. For example, we might have "class FooNode extends + * RedBlackNode>". + */ +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 subtree + * 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 !right.isLeaf(). It calls augment() on this + * node and on its resulting parent. + * @return The return value from calling augment() on the resulting parent. + */ + public boolean rotateLeft() { + if (right.isLeaf()) { + throw new IllegalArgumentException("The 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 !left.isLeaf(). It calls augment() on this + * node and on its resulting parent. + * @return The return value from calling augment() on the resulting parent. + */ + public boolean rotateRight() { + if (left.isLeaf()) { + throw new IllegalArgumentException("The 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. 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(). + */ + public void fixInsertion(boolean augment) { + if (!isRed) { + throw new IllegalArgumentException("The node must be red"); + } + boolean changed; + if (augment) { + changed = augment(); + } else { + changed = false; + } + + 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. The method performs any rotations by calling rotateLeft() and + * rotateRight(). + */ + public void fixInsertion() { + fixInsertion(true); + } + + /** Returns a Comparator that compares instances of N using their natural order, as in N.compare. */ + private Comparator naturalOrder() { + @SuppressWarnings("unchecked") + Comparator comparator = (Comparator)NATURAL_ORDER; + return 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 for a subclass to find the location for a node using a Comparator, then it + * 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.compare. + * @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; + newNode.fixInsertion(); + return root(); + } + + /** + * 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. + * @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. + */ + private void fixSiblingDeletion() { + RedBlackNode sibling = this; + boolean changed = true; + 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; + } + } else if (!sibling.left.isRed && !sibling.right.isRed) { + sibling.isRed = true; + if (parent.isRed) { + parent.isRed = false; + break; + } else { + if (changed) { + changed = parent.augment(); + } + N grandparent = parent.parent; + if (grandparent == null) { + break; + } else if (grandparent.left == parent) { + sibling = grandparent.right; + } else { + sibling = grandparent.left; + } + } + } 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(); + } + break; + } + } + + if (changed) { + for (N parent = sibling.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 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 its + * "left", "right", "parent", and "isBlack" fields. + */ + public void removeWithoutGettingRoot() { + 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) { + child.parent = parent; + if (parent != null) { + if (parent.left == this) { + parent.left = child; + } else { + parent.right = child; + } + } + parent = null; + 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) { + 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; + } + this.parent = null; + 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; + } + } + } + } + + /** + * Removes this node from the tree that contains it. The effect of this method on the fields of this node is + * unspecified. + * + * If the node has two children, we begin by moving the node's successor to its former position, by changing its + * "left", "right", "parent", and "isBlack" fields. + * + * @return The root of the resulting tree. + */ + public N remove() { + // 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" 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. + * @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, 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.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; + while (lastBlackHeight > firstBlackHeight) { + if (!lastChild.isRed) { + lastBlackHeight--; + } + parent = lastChild; + lastChild = lastChild.left; + } + if (lastChild.isRed) { + parent = lastChild; + lastChild = lastChild.left; + } + } else { + parent = null; + while (firstBlackHeight > lastBlackHeight) { + if (!firstChild.isRed) { + firstBlackHeight--; + } + 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 (parent.left == lastChild) { + 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 + pivot.fixInsertion(); + + return pivot.root(); + } + + /** + * 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 (isLeaf()) { + return last; + } else if (parent != null) { + throw new IllegalArgumentException("This is not the root of a tree"); + } 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 assumes + * that this node is the root. It takes O(log N) time. It is considerably more efficient than removing all of the + * elements after splitNode and then creating a new tree from those nodes. + * @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, advanceFirst: 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"); + } + + // 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"); + } + @SuppressWarnings("unchecked") + N[] path = (N[])Array.newInstance(getClass(), 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.fixInsertion(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.fixInsertion(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.fixInsertion(false); + for (first = firstPivot; first.parent != null; first = first.parent) { + first.augment(); + } + first.augment(); + } + + // Add lastPivot to the post-split tree + if (last == null) { + last = leaf; + } else { + lastPivot.isRed = true; + lastPivot.parent = lastParent; + if (lastParent != null) { + lastParent.left = lastPivot; + } + lastPivot.left = leaf; + lastPivot.right = leaf; + lastPivot.fixInsertion(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 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". + * + * 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. + */ + public int compareTo(N other) { + // 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 this is a repeated node other than a leaf node or the subtree rooted at this node + * does not satisfy the red-black properties, excluding the requirement that the root be black. + * @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) { + throw new RuntimeException("A red node has a red child"); + } + if (!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.compare. + */ + public void assertOrderIsValid(Comparator comparator) { + if (comparator == null) { + comparator = naturalOrder(); + } + assertOrderIsValid(comparator, null, null); + } +} diff --git a/src/com/github/btrekkie/red_black_node/Reference.java b/src/com/github/btrekkie/red_black_node/Reference.java new file mode 100644 index 000000000..843053820 --- /dev/null +++ b/src/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/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java b/src/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java new file mode 100644 index 000000000..6059745aa --- /dev/null +++ b/src/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, and + * ArbitraryOrderCollectionTest, which test realistic use cases of RedBlackNode. TreeListTest tests most of the + * RedBlackNode methods, while IntervalTreeTest tests non-structural augmentation and the "insert" method, + * 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/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java b/src/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java new file mode 100644 index 000000000..8e3b78cd8 --- /dev/null +++ b/src/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/com/github/btrekkie/tree_list/TreeList.java b/src/com/github/btrekkie/tree_list/TreeList.java new file mode 100644 index 000000000..b5de0ccbc --- /dev/null +++ b/src/com/github/btrekkie/tree_list/TreeList.java @@ -0,0 +1,461 @@ +package com.github.btrekkie.tree_list; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** + * Implements a list using a self-balancing binary search tree augmented by subtree size. The benefit of this compared + * to ArrayList or LinkedList is that it supports both decent random access and quickly adding to or removing from the + * middle of the list. Operations have the following running times: + * + * size(): O(1) + * get, set, add, remove: O(log N) + * addAll: O(log N + P), where P is the number of elements we add + * iterator(): O(log N + P + M log N), where P is the number of elements over which we iterate and M is the number of + * elements we remove + * listIterator: O(log N + P + M log N + R log N), where P is the number of times we iterate over or set an element, M + * is the number of elements we add or remove, and R is the number of times we change the direction of iteration + * clear(): O(1), excluding garbage collection + * subList.clear(): O(log N), excluding garbage collection + * Constructor: O(N) + * + * This class is similar to an Apache Commons Collections class by the same name. I speculate that the Apache class is + * faster than this (by a constant factor) for most operations. However, this class's implementations of addAll and + * subList.clear() are asymptotically faster than the Apache class's implementations. + */ +public class TreeList extends AbstractList { + /** The dummy leaf node. */ + private final TreeListNode leaf = new TreeListNode(null); + + /** The root node of the tree. */ + private TreeListNode root; + + /** Constructs a new empty TreeList. */ + public TreeList() { + root = leaf; + } + + /** Constructs a new TreeList containing the specified values, in iteration order. */ + public TreeList(Collection values) { + root = createTree(values); + } + + /** Returns the root of a perfectly height-balanced tree containing the specified values, in iteration order. */ + private TreeListNode createTree(Collection values) { + List> nodes = new ArrayList>(values.size()); + for (T value : values) { + nodes.add(new TreeListNode(value)); + } + return RedBlackNode.>createTree(nodes, leaf); + } + + /** + * Returns the node for get(index). Raises an IndexOutOfBoundsException if "index" is not in the range [0, size()). + */ + private TreeListNode 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; + TreeListNode 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; + } + + @Override + public T get(int index) { + return getNode(index).value; + } + + @Override + public int size() { + return root.size; + } + + @Override + public boolean isEmpty() { + return root == leaf; + } + + @Override + public T set(int index, T value) { + modCount++; + TreeListNode node = getNode(index); + T oldValue = node.value; + node.value = value; + return oldValue; + } + + @Override + public void add(int index, T value) { + if (index < 0 || index > root.size) { + throw new IndexOutOfBoundsException("Index " + index + " is not in the range [0, " + root.size + "]"); + } + modCount++; + + TreeListNode newNode = new TreeListNode(value); + newNode.left = leaf; + newNode.right = leaf; + if (root.isLeaf()) { + root = newNode; + newNode.isRed = false; + return; + } + newNode.isRed = true; + if (index < root.size) { + TreeListNode node = getNode(index); + if (node.left.isLeaf()) { + node.left = newNode; + newNode.parent = node; + } else { + node = node.predecessor(); + node.right = newNode; + newNode.parent = node; + } + } else { + TreeListNode node; + node = root.max(); + node.right = newNode; + newNode.parent = node; + } + + newNode.fixInsertion(); + while (root.parent != null) { + root = root.parent; + } + } + + @Override + public T remove(int index) { + TreeListNode node = getNode(index); + modCount++; + T value = node.value; + root = node.remove(); + return value; + } + + @Override + public boolean addAll(int index, Collection values) { + if (index < 0 || index > root.size) { + throw new IndexOutOfBoundsException("Index " + index + " is not in the range [0, " + root.size + "]"); + } + modCount++; + if (values.isEmpty()) { + return false; + } else { + if (index >= root.size) { + root = root.concatenate(createTree(values)); + } else { + TreeListNode[] split = root.split(getNode(index)); + root = split[0].concatenate(createTree(values)).concatenate(split[1]); + } + return true; + } + } + + @Override + public boolean addAll(Collection values) { + modCount++; + if (values.isEmpty()) { + return false; + } else { + root = root.concatenate(createTree(values)); + return true; + } + } + + @Override + protected void removeRange(int startIndex, int endIndex) { + if (startIndex != endIndex) { + modCount++; + TreeListNode last; + if (endIndex == root.size) { + last = leaf; + } else { + TreeListNode[] split = root.split(getNode(endIndex)); + root = split[0]; + last = split[1]; + } + TreeListNode first = root.split(getNode(startIndex))[0]; + root = first.concatenate(last); + } + } + + @Override + public void clear() { + modCount++; + root = leaf; + } + + /** The class for TreeList.iterator(). */ + private class TreeListIterator implements Iterator { + /** The value of TreeList.this.modCount we require to continue iteration without concurrent modification. */ + private int modCount = TreeList.this.modCount; + + /** + * The node containing the last element next() returned. This is null if we have yet to call next() or we have + * called remove() since the last call to next(). + */ + private TreeListNode node; + + /** The node containing next(). This is null if we have reached the end of the list. */ + private TreeListNode nextNode; + + /** Whether we have (successfully) called next(). */ + private boolean haveCalledNext; + + private TreeListIterator() { + if (root.isLeaf()) { + nextNode = null; + } else { + nextNode = root.min(); + } + } + + @Override + public boolean hasNext() { + return nextNode != null; + } + + @Override + public T next() { + if (nextNode == null) { + throw new NoSuchElementException("Reached the end of the list"); + } else if (TreeList.this.modCount != modCount) { + throw new ConcurrentModificationException(); + } + haveCalledNext = true; + node = nextNode; + nextNode = nextNode.successor(); + return node.value; + } + + @Override + public void remove() { + if (node == null) { + if (!haveCalledNext) { + throw new IllegalStateException("Must call next() before calling remove()"); + } else { + throw new IllegalStateException("Already removed this element"); + } + } else if (TreeList.this.modCount != modCount) { + throw new ConcurrentModificationException(); + } + root = node.remove(); + node = null; + TreeList.this.modCount++; + modCount = TreeList.this.modCount; + } + } + + @Override + public Iterator iterator() { + return new TreeListIterator(); + } + + /** The class for TreeList.listIterator. */ + private class TreeListListIterator implements ListIterator { + /** The value of TreeList.this.modCount we require to continue iteration without concurrent modification. */ + private int modCount = TreeList.this.modCount; + + /** The current return value for nextIndex(). */ + private int nextIndex; + + /** The node for next(), or null if hasNext() is false. */ + private TreeListNode nextNode; + + /** The node for previous(), or null if hasPrevious() is false. */ + private TreeListNode prevNode; + + /** Whether we have called next() or previous(). */ + private boolean haveCalledNextOrPrevious; + + /** Whether we (successfully) called next() more recently than previous(). */ + private boolean justCalledNext; + + /** + * Whether we have (successfully) called remove() or "add" since the last (successful) call to next() or + * previous(). + */ + private boolean haveModified; + + /** + * Constructs a new TreeListListIterator. + * @param index The starting index, as in the "index" argument to listIterator. This method assumes that + * 0 <= index < root.size. + */ + private TreeListListIterator(int index) { + nextIndex = index; + if (index > 0) { + prevNode = getNode(index - 1); + nextNode = prevNode.successor(); + } else { + prevNode = null; + if (root.size > 0) { + nextNode = root.min(); + } else { + nextNode = null; + } + } + } + + @Override + public boolean hasNext() { + if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + return nextNode != null; + } + + @Override + public T next() { + if (nextNode == null) { + throw new NoSuchElementException("Reached the end of the list"); + } else if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + haveCalledNextOrPrevious = true; + justCalledNext = true; + haveModified = false; + nextIndex++; + prevNode = nextNode; + nextNode = nextNode.successor(); + return prevNode.value; + } + + @Override + public int nextIndex() { + if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + return nextIndex; + } + + @Override + public boolean hasPrevious() { + if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + return prevNode != null; + } + + @Override + public T previous() { + if (prevNode == null) { + throw new NoSuchElementException("Reached the beginning of the list"); + } else if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + haveCalledNextOrPrevious = true; + justCalledNext = false; + haveModified = false; + nextIndex--; + nextNode = prevNode; + prevNode = prevNode.predecessor(); + return nextNode.value; + } + + @Override + public int previousIndex() { + return nextIndex - 1; + } + + @Override + public void set(T value) { + if (!haveCalledNextOrPrevious) { + throw new IllegalStateException("Must call next() or previous() before calling \"set\""); + } else if (haveModified) { + throw new IllegalStateException("Already modified the list at this position"); + } else if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + if (justCalledNext) { + prevNode.value = value; + } else { + nextNode.value = value; + } + TreeList.this.modCount++; + modCount = TreeList.this.modCount; + } + + @Override + public void add(T value) { + if (haveModified) { + throw new IllegalStateException("Already modified the list at this position"); + } else if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + + // Create the new node + TreeListNode newNode = new TreeListNode(value);; + newNode.left = leaf; + newNode.right = leaf; + newNode.isRed = true; + + // Insert newNode. There is guaranteed to be a leaf child of prevNode or nextNode where we can insert it. + if (nextNode != null && nextNode.left.isLeaf()) { + nextNode.left = newNode; + newNode.parent = nextNode; + } else if (prevNode != null) { + prevNode.right = newNode; + newNode.parent = prevNode; + } else { + root = newNode; + } + + prevNode = newNode; + newNode.fixInsertion(); + nextIndex++; + haveModified = true; + TreeList.this.modCount++; + modCount = TreeList.this.modCount; + } + + @Override + public void remove() { + if (!haveCalledNextOrPrevious) { + throw new IllegalStateException("Must call next() or previous() before calling remove()"); + } else if (haveModified) { + throw new IllegalStateException("Already modified the list at this position"); + } else if (modCount != TreeList.this.modCount) { + throw new ConcurrentModificationException(); + } + + if (justCalledNext) { + TreeListNode predecessor = prevNode.predecessor(); + root = prevNode.remove(); + prevNode = predecessor; + } else { + TreeListNode successor = nextNode.successor(); + root = nextNode.remove(); + nextNode = successor; + } + + haveModified = true; + TreeList.this.modCount++; + modCount = TreeList.this.modCount; + } + } + + @Override + public ListIterator listIterator(int index) { + if (index < 0 || index > root.size) { + throw new IndexOutOfBoundsException("Index " + index + " is not in the range [0, " + root.size + "]"); + } + return new TreeListListIterator(index); + } +} diff --git a/src/com/github/btrekkie/tree_list/TreeListNode.java b/src/com/github/btrekkie/tree_list/TreeListNode.java new file mode 100644 index 000000000..a16637e81 --- /dev/null +++ b/src/com/github/btrekkie/tree_list/TreeListNode.java @@ -0,0 +1,40 @@ +package com.github.btrekkie.tree_list; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** A node in a TreeList. See the comments for TreeList. */ +class TreeListNode extends RedBlackNode> { + /** The element stored in the node. The value is unspecified if this is a leaf node. */ + public T value; + + /** The number of elements in the subtree rooted at this node, not counting leaf nodes, as in TreeList.leaf. */ + public int size; + + public TreeListNode(T value) { + this.value = value; + } + + @Override + public boolean augment() { + int newSize = left.size + right.size + 1; + if (newSize == size) { + return false; + } else { + size = newSize; + return true; + } + } + + @Override + public void assertNodeIsValid() { + int expectedSize; + if (isLeaf()) { + expectedSize = 0; + } else { + expectedSize = left.size + right.size + 1; + } + if (size != expectedSize) { + throw new RuntimeException("The node's size does not match that of the children"); + } + } +} diff --git a/src/com/github/btrekkie/tree_list/test/TreeListTest.java b/src/com/github/btrekkie/tree_list/test/TreeListTest.java new file mode 100644 index 000000000..f59a86f0b --- /dev/null +++ b/src/com/github/btrekkie/tree_list/test/TreeListTest.java @@ -0,0 +1,551 @@ +package com.github.btrekkie.tree_list.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +import org.junit.Test; + +import com.github.btrekkie.tree_list.TreeList; + +public class TreeListTest { + /** Tests TreeList.add. */ + @Test + public void testAdd() { + List list = new TreeList(); + assertEquals(Collections.emptyList(), list); + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + + list.add(4); + list.add(17); + list.add(-4); + list.add(null); + assertEquals(Arrays.asList(4, 17, -4, null), list); + assertEquals(4, list.size()); + assertFalse(list.isEmpty()); + assertEquals(-4, list.get(2).intValue()); + + boolean threwException; + try { + list.get(-3); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + try { + list.get(9); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + try { + list.add(-3, 5); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + try { + list.add(9, 10); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + + list.add(1, 6); + list.add(0, -1); + list.add(6, 42); + assertEquals(Arrays.asList(-1, 4, 6, 17, -4, null, 42), list); + assertEquals(7, list.size()); + assertEquals(6, list.get(2).intValue()); + assertEquals(null, list.get(5)); + + list = new TreeList(); + for (int i = 0; i < 200; i++) { + list.add(i + 300); + } + for (int i = 0; i < 300; i++) { + list.add(i, i); + } + for (int i = 499; i >= 0; i--) { + list.add(500, i + 500); + } + List expected = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + expected.add(i); + } + assertEquals(expected, list); + assertEquals(1000, list.size()); + assertFalse(list.isEmpty()); + assertEquals(777, list.get(777).intValue()); + assertEquals(123, list.get(123).intValue()); + assertEquals(0, list.get(0).intValue()); + assertEquals(999, list.get(999).intValue()); + } + + /** Tests TreeList.remove. */ + @Test + public void testRemove() { + List list = new TreeList(); + list.add(17); + list.add(-5); + list.add(null); + list.add(0); + list.add(1, 16); + assertEquals(null, list.remove(3)); + assertEquals(17, list.remove(0).intValue()); + assertEquals(0, list.remove(2).intValue()); + assertEquals(Arrays.asList(16, -5), list); + assertEquals(2, list.size()); + assertFalse(list.isEmpty()); + assertEquals(16, list.get(0).intValue()); + assertEquals(-5, list.get(1).intValue()); + + boolean threwException; + try { + list.remove(-3); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + try { + list.remove(9); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + + list = new TreeList(); + for (int i = 0; i < 1000; i++) { + list.add(i); + } + for (int i = 0; i < 250; i++) { + assertEquals(2 * i + 501, list.remove(501).intValue()); + assertEquals(2 * i + 1, list.remove(i + 1).intValue()); + } + List expected = new ArrayList(500); + for (int i = 0; i < 500; i++) { + expected.add(2 * i); + } + assertEquals(expected, list); + assertEquals(500, list.size()); + assertFalse(list.isEmpty()); + assertEquals(0, list.get(0).intValue()); + assertEquals(998, list.get(499).intValue()); + assertEquals(84, list.get(42).intValue()); + assertEquals(602, list.get(301).intValue()); + } + + /** Tests TreeList.set.*/ + @Test + public void testSet() { + List list = new TreeList(); + list.addAll(Arrays.asList(5, 17, 42, -6, null, 3, null)); + list.set(3, 12); + list.set(0, 6); + list.set(6, 88); + boolean threwException; + try { + list.set(7, 2); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + try { + list.set(-1, 4); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + assertEquals(Arrays.asList(6, 17, 42, 12, null, 3, 88), list); + assertEquals(7, list.size()); + assertFalse(list.isEmpty()); + assertEquals(42, list.get(2).intValue()); + assertEquals(6, list.get(0).intValue()); + assertEquals(88, list.get(6).intValue()); + + list = new TreeList(); + for (int i = 0; i < 1000; i++) { + list.add(999 - i); + } + for (int i = 0; i < 500; i++) { + list.set(i, i); + list.set(i + 500, i + 500); + } + List expected = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + expected.add(i); + } + assertEquals(expected, list); + assertEquals(1000, list.size()); + assertFalse(list.isEmpty()); + assertEquals(123, list.get(123).intValue()); + assertEquals(777, list.get(777).intValue()); + assertEquals(0, list.get(0).intValue()); + assertEquals(999, list.get(999).intValue()); + } + + /** Tests TreeList.addAll. */ + @Test + public void testAddAll() { + List list = new TreeList(); + list.add(3); + list.add(42); + list.add(16); + list.addAll(0, Arrays.asList(15, 4, -1)); + list.addAll(2, Arrays.asList(6, 14)); + list.addAll(1, Collections.emptyList()); + list.addAll(8, Arrays.asList(null, 7)); + list.addAll(Arrays.asList(-5, 5)); + assertEquals(Arrays.asList(15, 4, 6, 14, -1, 3, 42, 16, null, 7, -5, 5), list); + assertEquals(12, list.size()); + assertFalse(list.isEmpty()); + assertEquals(14, list.get(3).intValue()); + assertEquals(15, list.get(0).intValue()); + assertEquals(5, list.get(11).intValue()); + + list = new TreeList(); + List list2 = new ArrayList(400); + for (int i = 0; i < 200; i++) { + list2.add(i + 100); + } + for (int i = 0; i < 200; i++) { + list2.add(i + 700); + } + list.addAll(list2); + list2.clear(); + for (int i = 0; i < 100; i++) { + list2.add(i); + } + list.addAll(0, list2); + list2.clear(); + for (int i = 0; i < 400; i++) { + list2.add(i + 300); + } + list.addAll(300, list2); + list2.clear(); + for (int i = 0; i < 100; i++) { + list2.add(i + 900); + } + list.addAll(900, list2); + List expected = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + expected.add(i); + } + assertEquals(expected, list); + assertEquals(1000, list.size()); + assertFalse(list.isEmpty()); + assertEquals(123, list.get(123).intValue()); + assertEquals(777, list.get(777).intValue()); + assertEquals(0, list.get(0).intValue()); + assertEquals(999, list.get(999).intValue()); + } + + /** Tests TreeList.subList.clear(). */ + @Test + public void testClearSubList() { + List list = new TreeList(); + list.addAll(Arrays.asList(6, 42, -3, 15, 7, 99, 6, 12)); + list.subList(2, 4).clear(); + list.subList(0, 1).clear(); + list.subList(4, 4).clear(); + list.subList(3, 5).clear(); + assertEquals(Arrays.asList(42, 7, 99), list); + assertEquals(3, list.size()); + assertFalse(list.isEmpty()); + assertEquals(42, list.get(0).intValue()); + assertEquals(7, list.get(1).intValue()); + assertEquals(99, list.get(2).intValue()); + + list = new TreeList(); + for (int i = 0; i < 200; i++) { + list.add(-1); + } + for (int i = 0; i < 500; i++) { + list.add(i); + } + for (int i = 0; i < 400; i++) { + list.add(-1); + } + for (int i = 0; i < 500; i++) { + list.add(i + 500); + } + for (int i = 0; i < 600; i++) { + list.add(-1); + } + list.subList(1600, 2200).clear(); + list.subList(777, 777).clear(); + list.subList(700, 1100).clear(); + list.subList(0, 200).clear(); + List expected = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + expected.add(i); + } + assertEquals(expected, list); + assertEquals(1000, list.size()); + assertFalse(list.isEmpty()); + assertEquals(123, list.get(123).intValue()); + assertEquals(777, list.get(777).intValue()); + assertEquals(0, list.get(0).intValue()); + assertEquals(999, list.get(999).intValue()); + + list = new TreeList(); + list.addAll(Arrays.asList(-3, null, 8, 14, 9, 42)); + list.subList(0, 6).clear(); + assertEquals(Collections.emptyList(), list); + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + } + + /** Tests TreeList(Collection). */ + @Test + public void testConstructor() { + List list = new TreeList(Arrays.asList(1, 5, 42, -6, null, 3)); + assertEquals(Arrays.asList(1, 5, 42, -6, null, 3), list); + assertEquals(6, list.size()); + assertFalse(list.isEmpty()); + assertEquals(42, list.get(2).intValue()); + assertEquals(1, list.get(0).intValue()); + assertEquals(3, list.get(5).intValue()); + + List expected = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + expected.add(i); + } + list = new TreeList(expected); + assertEquals(expected, list); + assertEquals(1000, list.size()); + assertFalse(list.isEmpty()); + assertEquals(123, list.get(123).intValue()); + assertEquals(777, list.get(777).intValue()); + assertEquals(0, list.get(0).intValue()); + assertEquals(999, list.get(999).intValue()); + } + + /** Tests TreeList.clear(). */ + @Test + public void testClear() { + List list = new TreeList(); + list.addAll(Arrays.asList(7, 16, 5, 42)); + list.clear(); + assertEquals(Collections.emptyList(), list); + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + + for (int i = 0; i < 1000; i++) { + list.add(i); + } + list.clear(); + assertEquals(Collections.emptyList(), list); + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + } + + /** Tests TreeList.iterator(). */ + @Test + public void testIterator() { + List list = new TreeList(); + Iterator iterator = list.iterator(); + assertFalse(iterator.hasNext()); + boolean threwException; + try { + iterator.next(); + threwException = false; + } catch (NoSuchElementException exception) { + threwException = true; + } + assertTrue(threwException); + + list = new TreeList(Arrays.asList(42, 16, null, 7, 8, 3, 12)); + iterator = list.iterator(); + try { + iterator.remove(); + threwException = false; + } catch (IllegalStateException exception) { + threwException = true; + } + assertTrue(threwException); + assertTrue(iterator.hasNext()); + assertEquals(42, iterator.next().intValue()); + assertEquals(16, iterator.next().intValue()); + assertEquals(null, iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals(7, iterator.next().intValue()); + iterator.remove(); + try { + iterator.remove(); + threwException = false; + } catch (IllegalStateException exception) { + threwException = true; + } + assertTrue(threwException); + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasNext()); + assertEquals(8, iterator.next().intValue()); + assertTrue(iterator.hasNext()); + assertEquals(3, iterator.next().intValue()); + assertTrue(iterator.hasNext()); + assertEquals(12, iterator.next().intValue()); + assertFalse(iterator.hasNext()); + iterator.remove(); + assertFalse(iterator.hasNext()); + try { + iterator.next(); + threwException = false; + } catch (NoSuchElementException exception) { + threwException = true; + } + assertTrue(threwException); + assertEquals(Arrays.asList(42, 16, null, 8, 3), list); + + list = new TreeList(); + for (int i = 0; i < 1000; i++) { + list.add(i); + } + iterator = list.iterator(); + for (int i = 0; i < 500; i++) { + assertTrue(iterator.hasNext()); + assertEquals(2 * i, iterator.next().intValue()); + assertTrue(iterator.hasNext()); + assertEquals(2 * i + 1, iterator.next().intValue()); + iterator.remove(); + } + assertFalse(iterator.hasNext()); + List expected = new ArrayList(500); + for (int i = 0; i < 500; i++) { + expected.add(2 * i); + } + assertEquals(expected, list); + } + + /** Tests TreeList.listIterator. */ + @Test + public void testListIterator() { + List list = new TreeList(); + list.addAll(Arrays.asList(7, 16, 42, -3, 12, 25, 8, 9)); + boolean threwException; + try { + list.listIterator(-1); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + try { + list.listIterator(18); + threwException = false; + } catch (IndexOutOfBoundsException exception) { + threwException = true; + } + assertTrue(threwException); + ListIterator iterator = list.listIterator(1); + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasPrevious()); + assertEquals(16, iterator.next().intValue()); + iterator.add(6); + try { + iterator.set(-1); + threwException = false; + } catch (IllegalStateException exception) { + threwException = true; + } + assertTrue(threwException); + try { + iterator.add(9); + threwException = false; + } catch (IllegalStateException exception) { + threwException = true; + } + assertTrue(threwException); + try { + iterator.remove(); + threwException = false; + } catch (IllegalStateException exception) { + threwException = true; + } + assertTrue(threwException); + assertEquals(6, iterator.previous().intValue()); + assertEquals(6, iterator.next().intValue()); + assertEquals(2, iterator.previousIndex()); + assertEquals(3, iterator.nextIndex()); + assertEquals(42, iterator.next().intValue()); + assertEquals(-3, iterator.next().intValue()); + assertEquals(12, iterator.next().intValue()); + iterator.remove(); + assertEquals(-3, iterator.previous().intValue()); + iterator.remove(); + assertEquals(25, iterator.next().intValue()); + iterator.set(14); + assertEquals(8, iterator.next().intValue()); + assertEquals(9, iterator.next().intValue()); + assertFalse(iterator.hasNext()); + try { + iterator.next(); + threwException = false; + } catch (NoSuchElementException exception) { + threwException = true; + } + assertTrue(threwException); + assertEquals(9, iterator.previous().intValue()); + iterator.set(10); + assertEquals(Arrays.asList(7, 16, 6, 42, 14, 8, 10), list); + assertEquals(7, list.size()); + assertFalse(list.isEmpty()); + assertEquals(42, list.get(3).intValue()); + assertEquals(7, list.get(0).intValue()); + assertEquals(10, list.get(6).intValue()); + + list = new TreeList(); + for (int i = 0; i < 1000; i++) { + list.add(-1); + } + iterator = list.listIterator(); + for (int i = 0; i < 500; i++) { + iterator.add(i); + iterator.next(); + } + for (int i = 0; i < 500; i++) { + iterator.previous(); + iterator.remove(); + iterator.previous(); + } + iterator = list.listIterator(500); + for (int i = 0; i < 250; i++) { + iterator.next(); + iterator.set(2 * i + 500); + iterator.next(); + } + for (int i = 0; i < 250; i++) { + iterator.previous(); + iterator.set(999 - 2 * i); + iterator.previous(); + } + List expected = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + expected.add(i); + } + assertEquals(expected, list); + assertEquals(1000, list.size()); + assertFalse(list.isEmpty()); + assertEquals(123, list.get(123).intValue()); + assertEquals(777, list.get(777).intValue()); + assertEquals(0, list.get(0).intValue()); + assertEquals(999, list.get(999).intValue()); + } +}