add Sparse2DLongArray
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package org.lucares.collections;
|
||||
|
||||
public interface BiLongObjectFunction<O> {
|
||||
O apply(long key, O value);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.lucares.collections;
|
||||
|
||||
public interface LongObjConsumer<O> {
|
||||
void accept(long key, O value);
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package org.lucares.collections;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A hash map where the key is a long and the value is a generic object.
|
||||
*
|
||||
* @see LongLongHashMap
|
||||
*/
|
||||
public class LongObjHashMap<V> {
|
||||
|
||||
// There is no equivalent to null for primitive values. Therefore we have to add
|
||||
// special handling for one long value. Otherwise we couldn't tell if a key is
|
||||
// in the map or not. We chose 0L, because LongList is initially all 0L.
|
||||
private static final long NULL_KEY = 0L;
|
||||
|
||||
private static final long EMPTY_SLOT = 0L;
|
||||
|
||||
/**
|
||||
* The maximum size of an array.
|
||||
*/
|
||||
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
|
||||
|
||||
private final double fillFactor;
|
||||
|
||||
private long[] keys;
|
||||
private V[] values;
|
||||
private int size = 0;
|
||||
|
||||
private V zeroValue = null;
|
||||
|
||||
/**
|
||||
* Create a new {@link LongLongHashMap} with the given initial capacity and load
|
||||
* factor.
|
||||
*
|
||||
* @param initialCapacity the initial capacity
|
||||
* @param loadFactor the load factor
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public LongObjHashMap(final int initialCapacity, final double loadFactor) {
|
||||
|
||||
if (initialCapacity < 0) {
|
||||
throw new IllegalArgumentException("initial capacity must be non-negative");
|
||||
}
|
||||
if (initialCapacity > MAX_ARRAY_SIZE) {
|
||||
throw new IllegalArgumentException("initial capacity must be smaller or equal to " + MAX_ARRAY_SIZE);
|
||||
}
|
||||
if (loadFactor <= 0 || Double.isNaN(loadFactor))
|
||||
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
|
||||
|
||||
this.fillFactor = loadFactor;
|
||||
keys = new long[initialCapacity];
|
||||
values = (V[]) new Object[initialCapacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LongLongHashMap} with initial capacity 8 and load factor
|
||||
* 0.75.
|
||||
*/
|
||||
public LongObjHashMap() {
|
||||
this(8, 0.75);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of entries in this map.
|
||||
*
|
||||
* @return the size
|
||||
*/
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* The capacity of this map.
|
||||
*
|
||||
* @return the capacity
|
||||
*/
|
||||
int getCapacity() {
|
||||
return keys.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given key and value to the map.
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
public void put(final long key, final V value) {
|
||||
|
||||
if (key == NULL_KEY) {
|
||||
size += zeroValue == null ? 1 : 0;
|
||||
zeroValue = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((keys.length * fillFactor) < size) {
|
||||
growAndRehash();
|
||||
}
|
||||
|
||||
final boolean added = putInternal(key, value);
|
||||
if (added) {
|
||||
size++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean putInternal(final long key, final V value) {
|
||||
final int searchStart = spread(key);
|
||||
int currentPosition = searchStart;
|
||||
|
||||
do {
|
||||
// found a free place, insert the value
|
||||
if (keys[currentPosition] == EMPTY_SLOT) {
|
||||
keys[currentPosition] = key;
|
||||
values[currentPosition] = value;
|
||||
return true;
|
||||
}
|
||||
// value exists, update it
|
||||
if (keys[currentPosition] == key) {
|
||||
keys[currentPosition] = key;
|
||||
values[currentPosition] = value;
|
||||
return false;
|
||||
}
|
||||
currentPosition = (currentPosition + 1) % keys.length;
|
||||
} while (currentPosition != searchStart);
|
||||
|
||||
throw new IllegalStateException("map is full");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the given key if it exists. This method throws a
|
||||
* {@link NoSuchElementException} if the key does not exist. Use
|
||||
* {@link #containsKey(long)} to check before calling {@link #get(long)}.
|
||||
*
|
||||
* @param key the key
|
||||
* @return the value if it exists, or {@code null} if the value does not exist
|
||||
*/
|
||||
public V get(final long key) {
|
||||
|
||||
if (key == NULL_KEY) {
|
||||
if (zeroValue != null) {
|
||||
return zeroValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final int searchStart = spread(key);
|
||||
int currentPosition = searchStart;
|
||||
do {
|
||||
if (keys[currentPosition] == key) {
|
||||
return values[currentPosition];
|
||||
}
|
||||
currentPosition = (currentPosition + 1) % keys.length;
|
||||
} while (currentPosition != searchStart);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the map contains the given key.
|
||||
*
|
||||
* @param key the key
|
||||
* @return true iff the map contains the key
|
||||
*/
|
||||
public boolean containsKey(final long key) {
|
||||
|
||||
if (key == NULL_KEY) {
|
||||
return zeroValue != null;
|
||||
}
|
||||
|
||||
final int searchStart = spread(key);
|
||||
int currentPosition = searchStart;
|
||||
do {
|
||||
if (keys[currentPosition] == key) {
|
||||
return true;
|
||||
}
|
||||
currentPosition = (currentPosition + 1) % keys.length;
|
||||
} while (currentPosition != searchStart);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given key and its value from the map.
|
||||
*
|
||||
* @param key the key
|
||||
*/
|
||||
public void remove(final long key) {
|
||||
|
||||
if (key == NULL_KEY) {
|
||||
size -= zeroValue != null ? 1 : 0;
|
||||
zeroValue = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final int searchStart = spread(key);
|
||||
int currentPosition = searchStart;
|
||||
do {
|
||||
if (keys[currentPosition] == key) {
|
||||
keys[currentPosition] = EMPTY_SLOT;
|
||||
size--;
|
||||
return;
|
||||
}
|
||||
currentPosition = (currentPosition + 1) % keys.length;
|
||||
} while (currentPosition != searchStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a mapping for the given key and its current value.
|
||||
* <p>
|
||||
* The mapping for given key is updated by calling {@code function} with the old
|
||||
* value. The return value will be set as new value. If the map does not contain
|
||||
* a mapping for the key, then {@code function} is called with
|
||||
* {@code initialValueIfAbsent}.
|
||||
*
|
||||
* @param key the key
|
||||
* @param initialValueIfAbsent a {@link Supplier} returning the value used if there is no current mapping for the
|
||||
* key
|
||||
* @param function called to update an existing value
|
||||
*/
|
||||
public void compute(final long key, final Supplier<V> initialValueIfAbsent, final BiLongObjectFunction<V> function) {
|
||||
if (key == NULL_KEY) {
|
||||
if (zeroValue != null) {
|
||||
zeroValue = function.apply(NULL_KEY, zeroValue);
|
||||
return;
|
||||
}
|
||||
zeroValue = function.apply(NULL_KEY, initialValueIfAbsent.get());
|
||||
return;
|
||||
}
|
||||
|
||||
final int searchStart = spread(key);
|
||||
int currentPosition = searchStart;
|
||||
do {
|
||||
if (keys[currentPosition] == key) {
|
||||
final V updatedValue = function.apply(key, values[currentPosition]);
|
||||
values[currentPosition] = updatedValue;
|
||||
return;
|
||||
}
|
||||
currentPosition = (currentPosition + 1) % keys.length;
|
||||
} while (currentPosition != searchStart);
|
||||
|
||||
// key not found -> add it
|
||||
final V newZeroValue = function.apply(key, initialValueIfAbsent.get());
|
||||
put(key, newZeroValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the {@link LongObjConsumer#accept(long, Object)} method for all entries
|
||||
* in this map. The order is based on the hash value and is therefore not
|
||||
* deterministic. Don't rely on the order!
|
||||
*
|
||||
* @param consumer the consumer
|
||||
*/
|
||||
public void forEach(final LongObjConsumer<V> consumer) {
|
||||
|
||||
if (zeroValue != null) {
|
||||
consumer.accept(0, zeroValue);
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
if (keys[i] != EMPTY_SLOT) {
|
||||
consumer.accept(keys[i], values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the {@link LongObjConsumer#accept(long, Object)} method for all entries
|
||||
* in this map. This method iterates over the keys in ascending order.
|
||||
* <p>
|
||||
* Note: this method is slower than {@link #forEach(LongLongConsumer)}.
|
||||
*
|
||||
* @param consumer the consumer
|
||||
*/
|
||||
public void forEachOrdered(final LongObjConsumer<V> consumer) {
|
||||
|
||||
if (zeroValue != null) {
|
||||
consumer.accept(0, zeroValue);
|
||||
}
|
||||
|
||||
final long[] sortedKeys = Arrays.copyOf(keys, keys.length);
|
||||
Arrays.parallelSort(sortedKeys);
|
||||
|
||||
for (int i = 0; i < sortedKeys.length; i++) {
|
||||
final long key = sortedKeys[i];
|
||||
if (key != EMPTY_SLOT) {
|
||||
consumer.accept(key, get(key));
|
||||
} else if (key == EMPTY_SLOT) {
|
||||
final int posFirstKey = findPosOfFirstPositiveKey(sortedKeys);
|
||||
if (posFirstKey < 0) {
|
||||
return;
|
||||
}
|
||||
i = posFirstKey - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int findPosOfFirstPositiveKey(final long[] sortedKeys) {
|
||||
|
||||
if (sortedKeys.length == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (sortedKeys.length == 1) {
|
||||
return sortedKeys[0] > EMPTY_SLOT ? 0 : -1;
|
||||
}
|
||||
|
||||
int low = 0;
|
||||
int high = sortedKeys.length - 1;
|
||||
int pos = -1;
|
||||
|
||||
while (low <= high) {
|
||||
pos = (low + high) / 2;
|
||||
if (sortedKeys[pos] <= EMPTY_SLOT) {
|
||||
low = pos + 1;
|
||||
} else {
|
||||
high = pos - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (low < sortedKeys.length && sortedKeys[low] <= EMPTY_SLOT) {
|
||||
low++;
|
||||
}
|
||||
|
||||
return low < sortedKeys.length && sortedKeys[low] > EMPTY_SLOT ? low : -1;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void growAndRehash() {
|
||||
final long[] oldKeys = keys;
|
||||
final V[] oldValues = values;
|
||||
|
||||
final int newSize = Math.min(keys.length * 2, MAX_ARRAY_SIZE);
|
||||
|
||||
keys = new long[newSize];
|
||||
values = (V[]) new Object[newSize];
|
||||
|
||||
for (int i = 0; i < oldKeys.length; i++) {
|
||||
final long key = oldKeys[i];
|
||||
if (key != EMPTY_SLOT) {
|
||||
final V value = oldValues[i];
|
||||
putInternal(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visible for test
|
||||
int spread(final long key) {
|
||||
return hash(key) % keys.length;
|
||||
}
|
||||
|
||||
private int hash(final long l) {
|
||||
return Math.abs(Long.hashCode(l));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package org.lucares.collections;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A sparse 2-dimensional array of primitive longs.
|
||||
*/
|
||||
public class Sparse2DLongArray {
|
||||
|
||||
public static final int MAX_SIZE = -1;
|
||||
|
||||
private static final class MinMaxValue implements TripleLongConsumer {
|
||||
|
||||
long minIndex1 = Long.MAX_VALUE;
|
||||
long maxIndex1 = Long.MIN_VALUE;
|
||||
|
||||
long minIndex2 = Long.MAX_VALUE;
|
||||
long maxIndex2 = Long.MIN_VALUE;
|
||||
|
||||
long minValue = Long.MAX_VALUE;
|
||||
long maxValue = Long.MIN_VALUE;
|
||||
|
||||
@Override
|
||||
public void apply(long index1, long index2, long value) {
|
||||
|
||||
minIndex1 = Math.min(minIndex1, index1);
|
||||
maxIndex1 = Math.max(maxIndex1, index1);
|
||||
|
||||
minIndex2 = Math.min(minIndex2, index2);
|
||||
maxIndex2 = Math.max(maxIndex2, index2);
|
||||
|
||||
minValue = Math.min(minValue, value);
|
||||
maxValue = Math.max(maxValue, value);
|
||||
}
|
||||
|
||||
public long getMaxIndex1() {
|
||||
return maxIndex1;
|
||||
}
|
||||
|
||||
public long getMaxIndex2() {
|
||||
return maxIndex2;
|
||||
}
|
||||
|
||||
public long getMaxValue() {
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
public long getMinValue() {
|
||||
return minValue;
|
||||
}
|
||||
}
|
||||
|
||||
private final LongObjHashMap<LongLongHashMap> matrix;
|
||||
private final Supplier<LongLongHashMap> initialValueSupplier;
|
||||
private long sizeX;
|
||||
private long sizeY;
|
||||
|
||||
/**
|
||||
* Create a new {@link Sparse2DLongArray} with arbitrary size. You can use all
|
||||
* values in the interval 0 (inclusive) to {@link Long#MAX_VALUE} (inclusive) as
|
||||
* index for the x/y axis.
|
||||
*/
|
||||
public Sparse2DLongArray() {
|
||||
this(MAX_SIZE, MAX_SIZE, 8, 0.75);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Sparse2DLongArray} with specified size.
|
||||
*
|
||||
* @param sizeX size of the x-axis, use {@link Sparse2DLongArray#MAX_SIZE} if
|
||||
* you want to be able to use {@link Long#MAX_VALUE} as index for
|
||||
* the x-axis
|
||||
* @param sizeY size of the y-axis, use {@link Sparse2DLongArray#MAX_SIZE} if
|
||||
* you want to be able to use {@link Long#MAX_VALUE} as index for
|
||||
* the y-axis
|
||||
*/
|
||||
public Sparse2DLongArray(long sizeX, long sizeY) {
|
||||
this(sizeX, sizeY, 8, 0.75);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Sparse2DLongArray} with the given initial capacity and
|
||||
* load factor.
|
||||
*
|
||||
* @param sizeX size of the x-axis, use
|
||||
* {@link Sparse2DLongArray#MAX_SIZE} if you want to be
|
||||
* able to use {@link Long#MAX_VALUE} as index for the
|
||||
* x-axis
|
||||
* @param sizeY size of the y-axis, use
|
||||
* {@link Sparse2DLongArray#MAX_SIZE} if you want to be
|
||||
* able to use {@link Long#MAX_VALUE} as index for the
|
||||
* y-axis
|
||||
* @param initialCapacity the initial capacity
|
||||
* @param loadFactor the load factor
|
||||
*/
|
||||
public Sparse2DLongArray(long sizeX, long sizeY, int initialCapacity, double loadFactor) {
|
||||
if (sizeX <= 0 && sizeX != MAX_SIZE || sizeY <= 0 & sizeY != MAX_SIZE) {
|
||||
throw new IllegalArgumentException("size in x and y axis must be positive");
|
||||
}
|
||||
|
||||
this.sizeX = sizeX;
|
||||
this.sizeY = sizeY;
|
||||
this.matrix = new LongObjHashMap<LongLongHashMap>(initialCapacity, loadFactor);
|
||||
initialValueSupplier = () -> new LongLongHashMap(initialCapacity, loadFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or overwrite a value at position {@code x}×{@code y} in the array.
|
||||
*
|
||||
* @param x index of the first dimension, must not be negative
|
||||
* @param y index of the second dimension, must not be negative
|
||||
* @param value the value
|
||||
*/
|
||||
public void put(long x, long y, long value) {
|
||||
|
||||
if (x < 0 || y < 0) {
|
||||
throw new IllegalArgumentException("indexes must be non-negative");
|
||||
}
|
||||
if (sizeX != MAX_SIZE && x >= sizeX) {
|
||||
throw new IndexOutOfBoundsException("x-axis out of range: " + x);
|
||||
}
|
||||
if (sizeY != MAX_SIZE && y >= sizeY) {
|
||||
throw new IndexOutOfBoundsException("y-axis out of range: " + y);
|
||||
}
|
||||
matrix.compute(x, initialValueSupplier, (key, oldValue) -> {
|
||||
oldValue.put(y, value);
|
||||
return oldValue;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from position {@code x}×{@code y} of the array
|
||||
*
|
||||
* @param x index of the first dimension, must not be negative
|
||||
* @param y index of the second dimension, must not be negative
|
||||
* @return the value, or 0 if no value exists
|
||||
*/
|
||||
public long get(long x, long y) {
|
||||
if (x < 0 || y < 0) {
|
||||
throw new IllegalArgumentException("indexes must be non-negative");
|
||||
}
|
||||
|
||||
LongLongHashMap longLongHashMap = matrix.get(x);
|
||||
if (longLongHashMap != null) {
|
||||
return longLongHashMap.get(y, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the {@link TripleLongConsumer#apply(long, long, long)} method for all
|
||||
* entries in this map. The order is not deterministic. Don't rely on the order!
|
||||
*
|
||||
* @param consumer the consumer
|
||||
*/
|
||||
public void forEach(TripleLongConsumer consumer) {
|
||||
matrix.forEach((x, yDimension) -> {
|
||||
yDimension.forEach((y, value) -> consumer.apply(x, y, value));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(120);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints (parts) of the matrix as 2-dimensional table with
|
||||
* {@code maxWidthInCharacters}
|
||||
*
|
||||
* @param maxWidthInCharacters maximum number of characters (horizontally)
|
||||
* @return the matrix
|
||||
*/
|
||||
public String toString(long maxWidthInCharacters) {
|
||||
|
||||
final MinMaxValue minMaxValue = new MinMaxValue();
|
||||
forEach(minMaxValue);
|
||||
|
||||
final long maxIndex1 = minMaxValue.getMaxIndex1();
|
||||
|
||||
final long maxIndex2 = minMaxValue.getMaxIndex2();
|
||||
|
||||
final long lengthOfMaxValue = Math.max(Long.toString(minMaxValue.getMaxValue()).length(),
|
||||
Long.toString(minMaxValue.getMinValue()).length());
|
||||
|
||||
final StringBuilder s = new StringBuilder();
|
||||
if (maxIndex2 < maxWidthInCharacters / (lengthOfMaxValue + 1)) {
|
||||
String format = "%" + (lengthOfMaxValue + 1) + "d";
|
||||
|
||||
for (int i = 0; i <= maxIndex1; i++) {
|
||||
for (int j = 0; j <= maxIndex2; j++) {
|
||||
s.append(String.format(format, get(i, j)));
|
||||
}
|
||||
s.append("\n");
|
||||
}
|
||||
return s.toString();
|
||||
} else {
|
||||
forEach((index1, index2, value) -> s.append(String.format("(%d, %d) = %d\n", index1, index2, value)));
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.lucares.collections;
|
||||
|
||||
/**
|
||||
* Consumer for iterating over {@link Sparse2DLongArray}s.
|
||||
*/
|
||||
public interface TripleLongConsumer {
|
||||
|
||||
/**
|
||||
* Performs this operation on the given arguments
|
||||
* @param x the index in the x-dimension
|
||||
* @param y the index in the y-dimension
|
||||
* @param value the value at {@code x}×{@code y}
|
||||
*/
|
||||
public void apply(long x, long y, long value);
|
||||
}
|
||||
Reference in New Issue
Block a user