org.jhotdraw8.icollection.ChampAddOnlySet Maven / Gradle / Ivy
/*
* @(#)SimpleImmutableAddOnlySet.java
* Copyright © 2023 The authors and contributors of JHotDraw. MIT License.
*/
package org.jhotdraw8.icollection;
import org.jhotdraw8.icollection.immutable.ImmutableAddOnlySet;
import java.util.Arrays;
import java.util.Objects;
/**
* Implements the {@link ImmutableAddOnlySet} interface using a Compressed
* Hash-Array Mapped Prefix-tree (CHAMP).
*
* Performance characteristics:
*
* - add: O(log₃₂ N)
*
*
* References:
*
* - Michael J. Steindorfer (2017).
* Efficient Immutable Collections.
* - michael.steindorfer.name
*
*
- The Capsule Hash Trie Collections Library.
*
Copyright (c) Michael Steindorfer. BSD-2-Clause License
* - github.com
*
*
* @param the element type
*/
public abstract class ChampAddOnlySet implements ImmutableAddOnlySet {
private static final int ENTRY_LENGTH = 1;
private static final int HASH_CODE_LENGTH = 32;
private static final int BIT_PARTITION_SIZE = 5;
private static final int BIT_PARTITION_MASK = (1 << BIT_PARTITION_SIZE) - 1;
/**
* Constructs a new empty set.
*/
ChampAddOnlySet() {
}
private static int bitpos(int mask) {
return (1 << mask);
}
private static int mask(int keyHash, int shift) {
return (keyHash >>> shift) & BIT_PARTITION_MASK;
}
private static ChampAddOnlySet mergeTwoKeyValPairs(K key0, int keyHash0,
K key1, int keyHash1, int shift) {
assert !(key0.equals(key1));
if (shift >= HASH_CODE_LENGTH) {
HashCollisionNode unchecked = new HashCollisionNode<>(keyHash0, key0, key1);
return unchecked;
}
int mask0 = mask(keyHash0, shift);
int mask1 = mask(keyHash1, shift);
if (mask0 != mask1) {
// both nodes fit on same level
final int dataMap = (bitpos(mask0) | bitpos(mask1));
if (mask0 < mask1) {
return new BitmapIndexedNode<>(0, dataMap, key0, key1);
} else {
return new BitmapIndexedNode<>(0, dataMap, key1, key0);
}
} else {
final ChampAddOnlySet node =
mergeTwoKeyValPairs(key0, keyHash0, key1, keyHash1, shift + BIT_PARTITION_SIZE);
// values fit on next level
final int nodeMap = bitpos(mask0);
return new BitmapIndexedNode<>(nodeMap, 0, node);
}
}
/**
* Returns an empty set.
*
* @param the element type.
* @return an empty set.
*/
@SuppressWarnings("unchecked")
public static ChampAddOnlySet of() {
return (ChampAddOnlySet) BitmapIndexedNode.EMPTY_NODE;
}
/**
* Returns a set that contains the specified elements.
*
* @param elements the specified elements
* @param the element type.
* @return a set of the specified elements.
*/
@SuppressWarnings({"unchecked", "varargs"})
@SafeVarargs
public static ChampAddOnlySet of(E... elements) {
ChampAddOnlySet set = (ChampAddOnlySet) BitmapIndexedNode.EMPTY_NODE;
for (E e : elements) {
set = set.add(e);
}
return set;
}
@Override
public ChampAddOnlySet add(E key) {
return updated(key, key.hashCode(), 0);
}
abstract ChampAddOnlySet updated(E key, int keyHash, int shift);
private static final class BitmapIndexedNode extends ChampAddOnlySet {
private static final ChampAddOnlySet> EMPTY_NODE = new BitmapIndexedNode<>(0, 0);
final Object[] nodes;
private final int nodeMap;
private final int dataMap;
BitmapIndexedNode(int nodeMap,
int dataMap, Object... nodes) {
this.nodeMap = nodeMap;
this.dataMap = dataMap;
this.nodes = nodes;
}
ChampAddOnlySet copyAndInsertValue(int bitpos,
K key) {
final int idx = ENTRY_LENGTH * dataIndex(bitpos);
// copy 'src' and insert 1 element(s) at position 'idx'
final Object[] src = this.nodes;
final Object[] dst = new Object[src.length + 1];
System.arraycopy(src, 0, dst, 0, idx);
System.arraycopy(src, idx, dst, idx + 1, src.length - idx);
dst[idx] = key;
return new BitmapIndexedNode<>(nodeMap, (dataMap | bitpos), dst);
}
ChampAddOnlySet copyAndMigrateFromInlineToNode(int bitpos, ChampAddOnlySet node) {
final int idxOld = ENTRY_LENGTH * dataIndex(bitpos);
final int idxNew = this.nodes.length - ENTRY_LENGTH - nodeIndex(bitpos);
assert idxOld <= idxNew;
// copy 'src' and remove 1 element(s) at position 'idxOld' and
// insert 1 element(s) at position 'idxNew'
final Object[] src = this.nodes;
final Object[] dst = new Object[src.length - 1 + 1];
System.arraycopy(src, 0, dst, 0, idxOld);
System.arraycopy(src, idxOld + 1, dst, idxOld, idxNew - idxOld);
System.arraycopy(src, idxNew + 1, dst, idxNew + 1, src.length - idxNew - 1);
dst[idxNew] = node;
return new BitmapIndexedNode<>((nodeMap | bitpos), (dataMap ^ bitpos), dst);
}
ChampAddOnlySet copyAndSetNode(int bitpos,
ChampAddOnlySet newNode) {
final int nodeIndex = nodeIndex(bitpos);
final int idx = this.nodes.length - 1 - nodeIndex;
// copy 'src' and set 1 element(s) at position 'idx'
final Object[] src = this.nodes;
final Object[] dst = new Object[src.length];
System.arraycopy(src, 0, dst, 0, src.length);
dst[idx] = newNode;
return new BitmapIndexedNode<>(nodeMap, dataMap, dst);
}
int dataIndex(int bitpos) {
return Integer.bitCount(dataMap & (bitpos - 1));
}
@SuppressWarnings("unchecked")
K getKey(int index) {
return (K) nodes[ENTRY_LENGTH * index];
}
@SuppressWarnings("unchecked")
ChampAddOnlySet getNode(int index) {
return (ChampAddOnlySet) nodes[nodes.length - 1 - index];
}
ChampAddOnlySet nodeAt(int bitpos) {
return getNode(nodeIndex(bitpos));
}
int nodeIndex(int bitpos) {
return Integer.bitCount(nodeMap & (bitpos - 1));
}
@Override
ChampAddOnlySet updated(K key, int keyHash,
int shift) {
final int mask = mask(keyHash, shift);
final int bitpos = bitpos(mask);
if ((dataMap & bitpos) != 0) { // in-place value
final int dataIndex = dataIndex(bitpos);
final K currentKey = getKey(dataIndex);
if (Objects.equals(currentKey, key)) {
return this;
} else {
final ChampAddOnlySet subNodeNew = mergeTwoKeyValPairs(currentKey,
currentKey.hashCode(), key, keyHash, shift + BIT_PARTITION_SIZE);
return copyAndMigrateFromInlineToNode(bitpos, subNodeNew);
}
} else if ((nodeMap & bitpos) != 0) { // node (not value)
final ChampAddOnlySet subNode = nodeAt(bitpos);
final ChampAddOnlySet subNodeNew =
subNode.updated(key, keyHash, shift + BIT_PARTITION_SIZE);
if (subNode != subNodeNew) {
return copyAndSetNode(bitpos, subNodeNew);
} else {
return this;
}
} else {
// no value
return copyAndInsertValue(bitpos, key);
}
}
}
private static final class HashCollisionNode extends ChampAddOnlySet {
private final K[] keys;
private final int hash;
@SuppressWarnings("varargs")
@SafeVarargs
HashCollisionNode(int hash, final K... keys) {
this.keys = keys;
this.hash = hash;
}
@Override
ChampAddOnlySet updated(K key,
int keyHash, int shift) {
assert this.hash == keyHash;
for (K k : keys) {
if (Objects.equals(k, key)) {
return this;
}
}
final K[] keysNew = Arrays.copyOf(keys, keys.length + 1);
keysNew[keys.length] = key;
return new HashCollisionNode<>(keyHash, keysNew);
}
}
}