org.javimmutable.collections.list.BranchNode Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of javimmutable-collections Show documentation
Show all versions of javimmutable-collections Show documentation
Library providing immutable/persistent collection classes for
Java. While collections are immutable they provide methods for
adding and removing values by creating new modified copies of
themselves. Each copy shares almost all of its structure with
other copies to minimize memory consumption.
///###////////////////////////////////////////////////////////////////////////
//
// Burton Computer Corporation
// http://www.burton-computer.com
//
// Copyright (c) 2018, Burton Computer Corporation
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in
// the documentation and/or other materials provided with the
// distribution.
//
// Neither the name of the Burton Computer Corporation nor the names
// of its contributors may be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package org.javimmutable.collections.list;
import org.javimmutable.collections.Cursor;
import org.javimmutable.collections.Cursorable;
import org.javimmutable.collections.Indexed;
import org.javimmutable.collections.SplitableIterable;
import org.javimmutable.collections.SplitableIterator;
import org.javimmutable.collections.cursors.LazyMultiCursor;
import org.javimmutable.collections.indexed.IndexedArray;
import org.javimmutable.collections.iterators.LazyMultiIterator;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.util.Iterator;
/**
* Node implementation containing other nodes. Prefix and suffix nodes can contain nodes
* of any depth and size (including empty) while body nodes array contains only full nodes
* of exactly depth - 1. This allows fast access to the body nodes by a computed offset
* based on size but also quick adds and deletes from either end using the prefix and suffix
* nodes. Once a BranchNode reaches its theoretical limit based on depth any insert triggers
* the creation of a new higher depth node containing the branch so that the new parent's prefix
* or suffix can contain the new value.
*/
@Immutable
class BranchNode
implements Node
{
private final int depth;
private final int size;
private final Node prefix; // possibly empty and can be any depth
private final Node[] nodes; // all of these are full and have depth - 1
private final Node suffix; // possibly empty and can be any depth
private BranchNode(int depth,
int size,
Node prefix,
Node[] nodes,
Node suffix)
{
assert nodes.length <= 32;
assert size <= ListHelper.sizeForDepth(depth);
this.depth = depth;
this.size = size;
this.prefix = prefix;
this.nodes = nodes;
this.suffix = suffix;
}
BranchNode(T prefixValue,
Node node)
{
this(node.getDepth() + 1,
node.size() + 1,
new LeafNode<>(prefixValue),
ListHelper.allocateSingleNode(node),
EmptyNode.of());
assert node.isFull();
}
BranchNode(Node node)
{
this(node.getDepth() + 1,
node.size() + 1,
EmptyNode.of(),
ListHelper.allocateSingleNode(node),
EmptyNode.of());
assert node.isFull();
}
BranchNode(Node node,
T suffixValue)
{
this(node.getDepth() + 1,
node.size() + 1,
EmptyNode.of(),
ListHelper.allocateSingleNode(node),
new LeafNode<>(suffixValue));
assert node.isFull();
}
static Node forNodeBuilder(int depth,
int size,
Node prefix,
Indexed> sourceNodes,
int offset,
int limit,
Node suffix)
{
assert limit > offset;
assert ListHelper.allNodesFull(depth, sourceNodes, offset, limit);
final Node[] nodes = ListHelper.allocateNodes(sourceNodes, offset, limit);
return new BranchNode<>(depth, size, prefix, nodes, suffix);
}
static Node of(Indexed extends T> leaves)
{
int nodeCount = leaves.size();
if (nodeCount == 0) {
return EmptyNode.of();
}
if (nodeCount <= 32) {
return LeafNode.fromList(leaves, 0, nodeCount);
}
final Node[] nodes = ListHelper.allocateNodes(1 + (leaves.size() / 32));
int offset = 0;
int index = 0;
while (offset < nodeCount) {
nodes[index++] = LeafNode.fromList(leaves, offset, Math.min(offset + 32, nodeCount));
offset += 32;
}
nodeCount = index;
// loop invariant - all nodes except last one are always full
// last one is possibly full
int depth = 2;
while (nodeCount > 1) {
int dstOffset = 0;
int srcOffset = 0;
// fill all full nodes
while (nodeCount > 32) {
final Node[] newNodes = ListHelper.allocateNodes(32);
System.arraycopy(nodes, srcOffset, newNodes, 0, 32);
nodes[dstOffset++] = new BranchNode<>(depth, ListHelper.sizeForDepth(depth), EmptyNode.of(), newNodes, EmptyNode.of());
srcOffset += 32;
nodeCount -= 32;
}
// collect remaining nodes
if (nodeCount == 1) {
nodes[dstOffset++] = nodes[srcOffset];
} else if (nodeCount > 1) {
final Node lastNode = nodes[srcOffset + nodeCount - 1];
if ((lastNode.getDepth() == (depth - 1)) && lastNode.isFull()) {
// all remaining nodes are full
final Node[] newNodes = ListHelper.allocateNodes(nodeCount);
System.arraycopy(nodes, srcOffset, newNodes, 0, nodeCount);
nodes[dstOffset++] = new BranchNode<>(depth, ListHelper.sizeForDepth(depth - 1) * nodeCount, EmptyNode.of(), newNodes, EmptyNode.of());
} else {
// all but last remaining nodes are full
final int newNodesLength = nodeCount - 1;
final Node[] newNodes = ListHelper.allocateNodes(newNodesLength);
System.arraycopy(nodes, srcOffset, newNodes, 0, newNodesLength);
nodes[dstOffset++] = new BranchNode<>(depth, (ListHelper.sizeForDepth(depth - 1) * newNodesLength) + lastNode.size(), EmptyNode.of(), newNodes, lastNode);
}
}
nodeCount = dstOffset;
depth += 1;
}
assert nodeCount == 1;
return nodes[0];
}
static BranchNode forTesting(Node prefix,
Node[] nodes,
Node suffix)
{
return new BranchNode<>(2,
prefix.size() + (nodes.length * 32) + suffix.size(),
prefix,
nodes.clone(),
suffix);
}
@Override
public boolean isEmpty()
{
return size == 0;
}
@Override
public boolean isFull()
{
return size == ListHelper.sizeForDepth(depth);
}
@Override
public int size()
{
return size;
}
@Override
public int getDepth()
{
return depth;
}
private static Node forDelete(int size,
Node prefix,
Node[] nodes,
Node suffix)
{
if (nodes.length == 0) {
if (prefix.isEmpty()) {
return suffix;
} else if (suffix.isEmpty()) {
return prefix;
} else {
int depth = 1 + Math.max(prefix.getDepth(), suffix.getDepth());
return new BranchNode<>(depth, size, prefix, nodes, suffix);
}
} else if ((nodes.length == 1) && prefix.isEmpty() && suffix.isEmpty()) {
return nodes[0];
} else {
int depth = 1 + nodes[0].getDepth();
return new BranchNode<>(depth, size, prefix, nodes, suffix);
}
}
@Override
public Node deleteFirst()
{
if (!prefix.isEmpty()) {
return forDelete(size - 1, prefix.deleteFirst(), nodes, suffix);
}
if (nodes.length > 0) {
Node newPrefix = nodes[0];
Node[] newNodes = ListHelper.allocateNodes(nodes.length - 1);
System.arraycopy(nodes, 1, newNodes, 0, newNodes.length);
return forDelete(size - 1, newPrefix.deleteFirst(), newNodes, suffix);
}
if (!suffix.isEmpty()) {
return suffix.deleteFirst();
}
throw new IllegalStateException();
}
@Override
public Node deleteLast()
{
if (!suffix.isEmpty()) {
return forDelete(size - 1, prefix, nodes, suffix.deleteLast());
}
if (nodes.length > 0) {
Node newSuffix = nodes[nodes.length - 1];
Node[] newNodes = ListHelper.allocateNodes(nodes.length - 1);
System.arraycopy(nodes, 0, newNodes, 0, newNodes.length);
return forDelete(size - 1, prefix, newNodes, newSuffix.deleteLast());
}
if (!prefix.isEmpty()) {
return prefix.deleteLast();
}
throw new IllegalStateException();
}
@Override
public Node insertFirst(T value)
{
if (isFull()) {
return new BranchNode<>(value, this);
}
if (prefix.getDepth() < (depth - 1)) {
return new BranchNode<>(depth, size + 1, prefix.insertFirst(value), nodes, suffix);
}
assert prefix.getDepth() == (depth - 1);
assert !prefix.isFull();
Node[] newNodes;
Node newPrefix = prefix.insertFirst(value);
if (newPrefix.isFull()) {
newNodes = ListHelper.allocateNodes(nodes.length + 1);
System.arraycopy(nodes, 0, newNodes, 1, nodes.length);
newNodes[0] = newPrefix;
newPrefix = EmptyNode.of();
} else {
newNodes = nodes;
}
return new BranchNode<>(depth, size + 1, newPrefix, newNodes, suffix);
}
@Override
public Node insertLast(T value)
{
if (isFull()) {
return new BranchNode<>(this, value);
}
if (suffix.getDepth() < (depth - 1)) {
return new BranchNode<>(depth, size + 1, prefix, nodes, suffix.insertLast(value));
}
assert suffix.getDepth() == (depth - 1);
assert !suffix.isFull();
Node[] newNodes;
Node newSuffix = suffix.insertLast(value);
if (newSuffix.isFull()) {
newNodes = ListHelper.allocateNodes(nodes.length + 1);
System.arraycopy(nodes, 0, newNodes, 0, nodes.length);
newNodes[nodes.length] = newSuffix;
newSuffix = EmptyNode.of();
} else {
newNodes = nodes;
}
return new BranchNode<>(depth, size + 1, prefix, newNodes, newSuffix);
}
@Override
public boolean containsIndex(int index)
{
return (index >= 0) && (index < size);
}
@Override
public T get(int index)
{
if (prefix.containsIndex(index)) {
return prefix.get(index);
}
index -= prefix.size();
final int fullNodeSize = ListHelper.sizeForDepth(depth - 1);
int arrayIndex = index / fullNodeSize;
if (arrayIndex < nodes.length) {
return nodes[arrayIndex].get(index - (arrayIndex * fullNodeSize));
}
index -= nodes.length * fullNodeSize;
if (suffix.containsIndex(index)) {
return suffix.get(index);
}
throw new IndexOutOfBoundsException();
}
@Override
public Node assign(int index,
T value)
{
if (prefix.containsIndex(index)) {
return new BranchNode<>(depth, size, prefix.assign(index, value), nodes, suffix);
}
index -= prefix.size();
final int fullNodeSize = ListHelper.sizeForDepth(depth - 1);
int arrayIndex = index / fullNodeSize;
if (arrayIndex < nodes.length) {
Node[] newNodes = nodes.clone();
newNodes[arrayIndex] = nodes[arrayIndex].assign(index - (arrayIndex * fullNodeSize), value);
return new BranchNode<>(depth, size, prefix, newNodes, suffix);
}
index -= nodes.length * fullNodeSize;
if (suffix.containsIndex(index)) {
return new BranchNode<>(depth, size, prefix, nodes, suffix.assign(index, value));
}
throw new IndexOutOfBoundsException();
}
/**
* Efficiently add values from an Iterator to those contained in this BranchNode
* to create a new BranchNode of a given maximum size. This is done in two stages.
* First the appropriate (based on insertion order) prefix/suffix is expanded until
* it is full. Then a builder is created from the full nodes of this branch and any
* remaining values from the iterator are added to the builder. Finally the original
* prefix/suffix from the unexpanded side is added to the final result.
*/
@Override
public Node insertAll(int maxSize,
boolean forwardOrder,
@Nonnull Iterator extends T> values)
{
assert maxSize >= size;
assert ListHelper.sizeForDepth(depth) >= size;
if (size >= maxSize || !values.hasNext()) {
return this;
}
final int fullSize = Math.min(ListHelper.sizeForDepth(depth), maxSize);
final int growthAllowed = fullSize - size;
BranchNode newNode;
if (isFull()) {
newNode = this;
} else if (forwardOrder) {
if (suffix.isEmpty()) {
newNode = this;
} else {
final int maxSuffixSize = Math.min(ListHelper.sizeForDepth(depth - 1), suffix.size() + growthAllowed);
final Node newSuffix = suffix.insertAll(maxSuffixSize, true, values);
newNode = withSuffix(newSuffix);
}
if (values.hasNext() && newNode.size() < maxSize && !newNode.isFull()) {
assert newNode.suffix.isEmpty();
// expand on the filled nodes and then add our unused opposite prefix/suffix to the result
newNode = TreeBuilder.expandBranchNode(fullSize - prefix.size(), true, newNode, values).withPrefix(prefix);
}
} else {
if (prefix.isEmpty()) {
newNode = this;
} else {
final int maxPrefixSize = Math.min(ListHelper.sizeForDepth(depth - 1), prefix.size() + growthAllowed);
final Node newPrefix = prefix.insertAll(maxPrefixSize, false, values);
newNode = withPrefix(newPrefix);
}
if (values.hasNext() && newNode.size() < maxSize && !newNode.isFull()) {
assert newNode.prefix.isEmpty();
// expand on the filled nodes and then add our unused opposite prefix/suffix to the result
newNode = TreeBuilder.expandBranchNode(fullSize - suffix.size(), false, newNode, values).withSuffix(suffix);
}
}
assert newNode.isFull() || newNode.size == maxSize || !values.hasNext();
if (newNode.size() < maxSize && values.hasNext()) {
// since we are already full we need to create a parent and expand that
newNode = TreeBuilder.expandBranchNode(maxSize, forwardOrder, new BranchNode<>(newNode), values);
}
assert newNode.size() == maxSize || !values.hasNext();
return newNode;
}
@Nonnull
@Override
public Cursor cursor()
{
return LazyMultiCursor.cursor(indexedForCursor());
}
@Nonnull
@Override
public SplitableIterator iterator()
{
return LazyMultiIterator.iterator(indexedForIterator());
}
private Indexed> indexedForCursor()
{
final int last = nodes.length + 1;
return new Indexed>()
{
@Override
public Cursorable get(int index)
{
return getNode(index, last);
}
@Override
public int size()
{
return last + 1;
}
};
}
private Indexed> indexedForIterator()
{
final int last = nodes.length + 1;
return new Indexed>()
{
@Override
public SplitableIterable get(int index)
{
return getNode(index, last);
}
@Override
public int size()
{
return last + 1;
}
};
}
private Node getNode(int index,
int last)
{
if (index == 0) {
return prefix;
} else if (index == last) {
return suffix;
} else {
return nodes[index - 1];
}
}
@Override
public void checkInvariants()
{
if (nodes.length > 32) {
throw new IllegalStateException();
}
if ((nodes.length == 32) && !(prefix.isEmpty() && suffix.isEmpty())) {
throw new IllegalStateException();
}
for (Node node : nodes) {
if ((node.getDepth() != (depth - 1)) || !node.isFull()) {
throw new IllegalStateException();
}
}
int computedSize = prefix.size() + suffix.size();
for (Node node : nodes) {
computedSize += node.size();
}
if (computedSize != size) {
throw new IllegalStateException();
}
if (prefix.isFull() && (prefix.getDepth() == (depth - 1))) {
throw new IllegalStateException();
}
if (suffix.isFull() && (suffix.getDepth() == (depth - 1))) {
throw new IllegalStateException();
}
prefix.checkInvariants();
for (Node node : nodes) {
node.checkInvariants();
}
suffix.checkInvariants();
}
Node prefix()
{
return prefix;
}
Indexed> filledNodes()
{
return IndexedArray.retained(nodes);
}
Node suffix()
{
return suffix;
}
private BranchNode withPrefix(@Nonnull Node newPrefix)
{
assert newPrefix.getDepth() < depth;
final int baseSize = size - prefix.size();
if (newPrefix.size() == ListHelper.sizeForDepth(depth - 1)) {
final Node[] newNodes = ListHelper.allocateNodes(nodes.length + 1);
System.arraycopy(nodes, 0, newNodes, 1, nodes.length);
newNodes[0] = newPrefix;
return new BranchNode(depth, baseSize + newPrefix.size(), EmptyNode.of(), newNodes, suffix);
} else {
return new BranchNode(depth, baseSize + newPrefix.size(), newPrefix, nodes, suffix);
}
}
private BranchNode withSuffix(@Nonnull Node newSuffix)
{
assert newSuffix.getDepth() < depth;
final int baseSize = size - suffix.size();
if (newSuffix.size() == ListHelper.sizeForDepth(depth - 1)) {
final Node[] newNodes = ListHelper.allocateNodes(nodes.length + 1);
System.arraycopy(nodes, 0, newNodes, 0, nodes.length);
newNodes[nodes.length] = newSuffix;
return new BranchNode(depth, baseSize + newSuffix.size(), prefix, newNodes, EmptyNode.of());
} else {
return new BranchNode(depth, baseSize + newSuffix.size(), prefix, nodes, newSuffix);
}
}
}