From aa332ac56b8c0baa624f9b30c23809a0016ce697 Mon Sep 17 00:00:00 2001 From: Robinson Date: Tue, 1 Aug 2023 21:22:43 -0600 Subject: [PATCH] ported ObjectIntMap --- src/dorkbox/collections/ObjectIntMap.java | 795 ------------------ src/dorkbox/collections/ObjectIntMap.kt | 952 ++++++++++++++++++++++ 2 files changed, 952 insertions(+), 795 deletions(-) delete mode 100644 src/dorkbox/collections/ObjectIntMap.java create mode 100644 src/dorkbox/collections/ObjectIntMap.kt diff --git a/src/dorkbox/collections/ObjectIntMap.java b/src/dorkbox/collections/ObjectIntMap.java deleted file mode 100644 index a215279..0000000 --- a/src/dorkbox/collections/ObjectIntMap.java +++ /dev/null @@ -1,795 +0,0 @@ -/******************************************************************************* - * Copyright 2011 LibGDX. - * Mario Zechner - * Nathan Sweet - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ******************************************************************************/ - -package dorkbox.collections; - -import java.util.Iterator; -import java.util.NoSuchElementException; - - -/** An unordered map where the values are ints. This implementation is a cuckoo hash map using 3 hashes, random walking, and a - * small stash for problematic keys. Null keys are not allowed. No allocation is done except when growing the table size.
- *
- * This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower, - * depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the - * next higher POT size. - * @author Nathan Sweet */ -@SuppressWarnings({"unchecked", "NullableProblems"}) -public class ObjectIntMap implements Iterable> { - public static final String version = Collections.version; - - private static final int PRIME1 = 0xbe1f14b1; - private static final int PRIME2 = 0xb4b82e39; - private static final int PRIME3 = 0xced1c241; - - public int size; - - K[] keyTable; - int[] valueTable; - int capacity, stashSize; - - private float loadFactor; - private int hashShift, mask, threshold; - private int stashCapacity; - private int pushIterations; - - private Entries entries1, entries2; - private Values values1, values2; - private Keys keys1, keys2; - - /** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */ - public ObjectIntMap () { - this(51, 0.8f); - } - - /** Creates a new map with a load factor of 0.8. - * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */ - public ObjectIntMap (int initialCapacity) { - this(initialCapacity, 0.8f); - } - - /** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before - * growing the backing table. - * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */ - public ObjectIntMap (int initialCapacity, float loadFactor) { - if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); - initialCapacity = Collections.nextPowerOfTwo((int)Math.ceil(initialCapacity / loadFactor)); - if (initialCapacity > 1 << 30) throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity); - capacity = initialCapacity; - - if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor); - this.loadFactor = loadFactor; - - threshold = (int)(capacity * loadFactor); - mask = capacity - 1; - hashShift = 31 - Integer.numberOfTrailingZeros(capacity); - stashCapacity = Math.max(3, (int)Math.ceil(Math.log(capacity)) * 2); - pushIterations = Math.max(Math.min(capacity, 8), (int)Math.sqrt(capacity) / 8); - - keyTable = (K[])new Object[capacity + stashCapacity]; - valueTable = new int[keyTable.length]; - } - - /** Creates a new map identical to the specified map. */ - public ObjectIntMap (ObjectIntMap map) { - this((int)Math.floor(map.capacity * map.loadFactor), map.loadFactor); - stashSize = map.stashSize; - System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.length); - System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.length); - size = map.size; - } - - public void put (K key, int value) { - if (key == null) throw new IllegalArgumentException("key cannot be null."); - K[] keyTable = this.keyTable; - - // Check for existing keys. - int hashCode = key.hashCode(); - int index1 = hashCode & mask; - K key1 = keyTable[index1]; - if (key.equals(key1)) { - valueTable[index1] = value; - return; - } - - int index2 = hash2(hashCode); - K key2 = keyTable[index2]; - if (key.equals(key2)) { - valueTable[index2] = value; - return; - } - - int index3 = hash3(hashCode); - K key3 = keyTable[index3]; - if (key.equals(key3)) { - valueTable[index3] = value; - return; - } - - // Update key in the stash. - for (int i = capacity, n = i + stashSize; i < n; i++) { - if (key.equals(keyTable[i])) { - valueTable[i] = value; - return; - } - } - - // Check for empty buckets. - if (key1 == null) { - keyTable[index1] = key; - valueTable[index1] = value; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - if (key2 == null) { - keyTable[index2] = key; - valueTable[index2] = value; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - if (key3 == null) { - keyTable[index3] = key; - valueTable[index3] = value; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - push(key, value, index1, key1, index2, key2, index3, key3); - } - - public void putAll (ObjectIntMap map) { - for (Entry entry : map.entries()) - put(entry.key, entry.value); - } - - /** Skips checks for existing keys. */ - private void putResize (K key, int value) { - // Check for empty buckets. - int hashCode = key.hashCode(); - int index1 = hashCode & mask; - K key1 = keyTable[index1]; - if (key1 == null) { - keyTable[index1] = key; - valueTable[index1] = value; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - int index2 = hash2(hashCode); - K key2 = keyTable[index2]; - if (key2 == null) { - keyTable[index2] = key; - valueTable[index2] = value; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - int index3 = hash3(hashCode); - K key3 = keyTable[index3]; - if (key3 == null) { - keyTable[index3] = key; - valueTable[index3] = value; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - push(key, value, index1, key1, index2, key2, index3, key3); - } - - private void push (K insertKey, int insertValue, int index1, K key1, int index2, K key2, int index3, K key3) { - K[] keyTable = this.keyTable; - int[] valueTable = this.valueTable; - int mask = this.mask; - - // Push keys until an empty bucket is found. - K evictedKey; - int evictedValue; - int i = 0, pushIterations = this.pushIterations; - do { - // Replace the key and value for one of the hashes. - switch (Collections.INSTANCE.random(2)) { - case 0: - evictedKey = key1; - evictedValue = valueTable[index1]; - keyTable[index1] = insertKey; - valueTable[index1] = insertValue; - break; - case 1: - evictedKey = key2; - evictedValue = valueTable[index2]; - keyTable[index2] = insertKey; - valueTable[index2] = insertValue; - break; - default: - evictedKey = key3; - evictedValue = valueTable[index3]; - keyTable[index3] = insertKey; - valueTable[index3] = insertValue; - break; - } - - // If the evicted key hashes to an empty bucket, put it there and stop. - int hashCode = evictedKey.hashCode(); - index1 = hashCode & mask; - key1 = keyTable[index1]; - if (key1 == null) { - keyTable[index1] = evictedKey; - valueTable[index1] = evictedValue; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - index2 = hash2(hashCode); - key2 = keyTable[index2]; - if (key2 == null) { - keyTable[index2] = evictedKey; - valueTable[index2] = evictedValue; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - index3 = hash3(hashCode); - key3 = keyTable[index3]; - if (key3 == null) { - keyTable[index3] = evictedKey; - valueTable[index3] = evictedValue; - if (size++ >= threshold) resize(capacity << 1); - return; - } - - if (++i == pushIterations) break; - - insertKey = evictedKey; - insertValue = evictedValue; - } while (true); - - putStash(evictedKey, evictedValue); - } - - private void putStash (K key, int value) { - if (stashSize == stashCapacity) { - // Too many pushes occurred and the stash is full, increase the table size. - resize(capacity << 1); - putResize(key, value); - return; - } - // Store key in the stash. - int index = capacity + stashSize; - keyTable[index] = key; - valueTable[index] = value; - stashSize++; - size++; - } - - /** @param defaultValue Returned if the key was not associated with a value. */ - public int get (K key, int defaultValue) { - int hashCode = key.hashCode(); - int index = hashCode & mask; - if (!key.equals(keyTable[index])) { - index = hash2(hashCode); - if (!key.equals(keyTable[index])) { - index = hash3(hashCode); - if (!key.equals(keyTable[index])) return getStash(key, defaultValue); - } - } - return valueTable[index]; - } - - private int getStash (K key, int defaultValue) { - K[] keyTable = this.keyTable; - for (int i = capacity, n = i + stashSize; i < n; i++) - if (key.equals(keyTable[i])) return valueTable[i]; - return defaultValue; - } - - /** Returns the key's current value and increments the stored value. If the key is not in the map, defaultValue + increment is - * put into the map. */ - public int getAndIncrement (K key, int defaultValue, int increment) { - int hashCode = key.hashCode(); - int index = hashCode & mask; - if (!key.equals(keyTable[index])) { - index = hash2(hashCode); - if (!key.equals(keyTable[index])) { - index = hash3(hashCode); - if (!key.equals(keyTable[index])) return getAndIncrementStash(key, defaultValue, increment); - } - } - int value = valueTable[index]; - valueTable[index] = value + increment; - return value; - } - - private int getAndIncrementStash (K key, int defaultValue, int increment) { - K[] keyTable = this.keyTable; - for (int i = capacity, n = i + stashSize; i < n; i++) - if (key.equals(keyTable[i])) { - int value = valueTable[i]; - valueTable[i] = value + increment; - return value; - } - put(key, defaultValue + increment); - return defaultValue; - } - - public int remove (K key, int defaultValue) { - int hashCode = key.hashCode(); - int index = hashCode & mask; - if (key.equals(keyTable[index])) { - keyTable[index] = null; - int oldValue = valueTable[index]; - size--; - return oldValue; - } - - index = hash2(hashCode); - if (key.equals(keyTable[index])) { - keyTable[index] = null; - int oldValue = valueTable[index]; - size--; - return oldValue; - } - - index = hash3(hashCode); - if (key.equals(keyTable[index])) { - keyTable[index] = null; - int oldValue = valueTable[index]; - size--; - return oldValue; - } - - return removeStash(key, defaultValue); - } - - int removeStash (K key, int defaultValue) { - K[] keyTable = this.keyTable; - for (int i = capacity, n = i + stashSize; i < n; i++) { - if (key.equals(keyTable[i])) { - int oldValue = valueTable[i]; - removeStashIndex(i); - size--; - return oldValue; - } - } - return defaultValue; - } - - void removeStashIndex (int index) { - // If the removed location was not last, move the last tuple to the removed location. - stashSize--; - int lastIndex = capacity + stashSize; - if (index < lastIndex) { - keyTable[index] = keyTable[lastIndex]; - valueTable[index] = valueTable[lastIndex]; - keyTable[lastIndex] = null; - } - } - - /** Returns true if the map is empty. */ - public boolean isEmpty () { - return size == 0; - } - - /** Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is - * done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead. */ - public void shrink (int maximumCapacity) { - if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity); - if (size > maximumCapacity) maximumCapacity = size; - if (capacity <= maximumCapacity) return; - maximumCapacity = Collections.nextPowerOfTwo(maximumCapacity); - resize(maximumCapacity); - } - - /** Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger. */ - public void clear (int maximumCapacity) { - if (capacity <= maximumCapacity) { - clear(); - return; - } - size = 0; - resize(maximumCapacity); - } - - public void clear () { - if (size == 0) return; - K[] keyTable = this.keyTable; - for (int i = capacity + stashSize; i-- > 0;) - keyTable[i] = null; - size = 0; - stashSize = 0; - } - - /** Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be - * an expensive operation. */ - public boolean containsValue (int value) { - K[] keyTable = this.keyTable; - int[] valueTable = this.valueTable; - for (int i = capacity + stashSize; i-- > 0;) - if (keyTable[i] != null && valueTable[i] == value) return true; - return false; - - } - - public boolean containsKey (K key) { - int hashCode = key.hashCode(); - int index = hashCode & mask; - if (!key.equals(keyTable[index])) { - index = hash2(hashCode); - if (!key.equals(keyTable[index])) { - index = hash3(hashCode); - if (!key.equals(keyTable[index])) return containsKeyStash(key); - } - } - return true; - } - - private boolean containsKeyStash (K key) { - K[] keyTable = this.keyTable; - for (int i = capacity, n = i + stashSize; i < n; i++) - if (key.equals(keyTable[i])) return true; - return false; - } - - /** Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares - * every value, which may be an expensive operation. */ - public K findKey (int value) { - K[] keyTable = this.keyTable; - int[] valueTable = this.valueTable; - for (int i = capacity + stashSize; i-- > 0;) - if (keyTable[i] != null && valueTable[i] == value) return keyTable[i]; - return null; - } - - /** Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many - * items to avoid multiple backing array resizes. */ - public void ensureCapacity (int additionalCapacity) { - if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); - int sizeNeeded = size + additionalCapacity; - if (sizeNeeded >= threshold) resize(Collections.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor))); - } - - private void resize (int newSize) { - int oldEndIndex = capacity + stashSize; - - capacity = newSize; - threshold = (int)(newSize * loadFactor); - mask = newSize - 1; - hashShift = 31 - Integer.numberOfTrailingZeros(newSize); - stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2); - pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8); - - K[] oldKeyTable = keyTable; - int[] oldValueTable = valueTable; - - keyTable = (K[])new Object[newSize + stashCapacity]; - valueTable = new int[newSize + stashCapacity]; - - int oldSize = size; - size = 0; - stashSize = 0; - if (oldSize > 0) { - for (int i = 0; i < oldEndIndex; i++) { - K key = oldKeyTable[i]; - if (key != null) putResize(key, oldValueTable[i]); - } - } - } - - private int hash2 (int h) { - h *= PRIME2; - return (h ^ h >>> hashShift) & mask; - } - - private int hash3 (int h) { - h *= PRIME3; - return (h ^ h >>> hashShift) & mask; - } - - @Override - public int hashCode () { - int h = 0; - K[] keyTable = this.keyTable; - int[] valueTable = this.valueTable; - for (int i = 0, n = capacity + stashSize; i < n; i++) { - K key = keyTable[i]; - if (key != null) { - h += key.hashCode() * 31; - - int value = valueTable[i]; - h += value; - } - } - return h; - } - - @Override - public boolean equals (Object obj) { - if (obj == this) return true; - if (!(obj instanceof ObjectIntMap)) return false; - ObjectIntMap other = (ObjectIntMap)obj; - if (other.size != size) return false; - K[] keyTable = this.keyTable; - int[] valueTable = this.valueTable; - for (int i = 0, n = capacity + stashSize; i < n; i++) { - K key = keyTable[i]; - if (key != null) { - int otherValue = other.get(key, 0); - if (otherValue == 0 && !other.containsKey(key)) return false; - int value = valueTable[i]; - if (otherValue != value) return false; - } - } - return true; - } - - @Override - public String toString () { - if (size == 0) return "{}"; - StringBuilder buffer = new StringBuilder(32); - buffer.append('{'); - K[] keyTable = this.keyTable; - int[] valueTable = this.valueTable; - int i = keyTable.length; - while (i-- > 0) { - K key = keyTable[i]; - if (key == null) continue; - buffer.append(key); - buffer.append('='); - buffer.append(valueTable[i]); - break; - } - while (i-- > 0) { - K key = keyTable[i]; - if (key == null) continue; - buffer.append(", "); - buffer.append(key); - buffer.append('='); - buffer.append(valueTable[i]); - } - buffer.append('}'); - return buffer.toString(); - } - - @Override - public Entries iterator () { - return entries(); - } - - /** Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each - * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */ - public Entries entries () { - if (entries1 == null) { - entries1 = new Entries(this); - entries2 = new Entries(this); - } - if (!entries1.valid) { - entries1.reset(); - entries1.valid = true; - entries2.valid = false; - return entries1; - } - entries2.reset(); - entries2.valid = true; - entries1.valid = false; - return entries2; - } - - /** Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each - * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */ - public Values values () { - if (values1 == null) { - values1 = new Values(this); - values2 = new Values(this); - } - if (!values1.valid) { - values1.reset(); - values1.valid = true; - values2.valid = false; - return values1; - } - values2.reset(); - values2.valid = true; - values1.valid = false; - return values2; - } - - /** Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each time - * this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */ - public Keys keys () { - if (keys1 == null) { - keys1 = new Keys(this); - keys2 = new Keys(this); - } - if (!keys1.valid) { - keys1.reset(); - keys1.valid = true; - keys2.valid = false; - return keys1; - } - keys2.reset(); - keys2.valid = true; - keys1.valid = false; - return keys2; - } - - static public class Entry { - public K key; - public int value; - - @Override - public String toString () { - return key + "=" + value; - } - } - - static private class MapIterator { - public boolean hasNext; - - final ObjectIntMap map; - int nextIndex, currentIndex; - boolean valid = true; - - public MapIterator (ObjectIntMap map) { - this.map = map; - reset(); - } - - public void reset () { - currentIndex = -1; - nextIndex = -1; - findNextIndex(); - } - - void findNextIndex () { - hasNext = false; - K[] keyTable = map.keyTable; - for (int n = map.capacity + map.stashSize; ++nextIndex < n;) { - if (keyTable[nextIndex] != null) { - hasNext = true; - break; - } - } - } - - public void remove () { - if (currentIndex < 0) throw new IllegalStateException("next must be called before remove."); - if (currentIndex >= map.capacity) { - map.removeStashIndex(currentIndex); - nextIndex = currentIndex - 1; - findNextIndex(); - } else { - map.keyTable[currentIndex] = null; - } - currentIndex = -1; - map.size--; - } - } - - static public class Entries extends MapIterator implements Iterable>, Iterator> { - private Entry entry = new Entry(); - - public Entries (ObjectIntMap map) { - super(map); - } - - /** Note the same entry instance is returned each time this method is called. */ - @Override - public Entry next () { - if (!hasNext) throw new NoSuchElementException(); - if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); - K[] keyTable = map.keyTable; - entry.key = keyTable[nextIndex]; - entry.value = map.valueTable[nextIndex]; - currentIndex = nextIndex; - findNextIndex(); - return entry; - } - - @Override - public boolean hasNext () { - if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); - return hasNext; - } - - @Override - public Entries iterator () { - return this; - } - - @Override - public void remove () { - super.remove(); - } - } - - static public class Values extends MapIterator { - public Values (ObjectIntMap map) { - super((ObjectIntMap)map); - } - - public boolean hasNext () { - if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); - return hasNext; - } - - public int next () { - if (!hasNext) throw new NoSuchElementException(); - if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); - int value = map.valueTable[nextIndex]; - currentIndex = nextIndex; - findNextIndex(); - return value; - } - - /** Returns a new array containing the remaining values. */ - public IntArray toArray () { - IntArray array = new IntArray(true, map.size); - while (hasNext) - array.add(next()); - return array; - } - } - - static public class Keys extends MapIterator implements Iterable, Iterator { - public Keys (ObjectIntMap map) { - super((ObjectIntMap)map); - } - - @Override - public boolean hasNext () { - if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); - return hasNext; - } - - @Override - public K next () { - if (!hasNext) throw new NoSuchElementException(); - if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); - K key = map.keyTable[nextIndex]; - currentIndex = nextIndex; - findNextIndex(); - return key; - } - - @Override - public Keys iterator () { - return this; - } - - /** Returns a new array containing the remaining keys. */ - public Array toArray () { - Array array = new Array(true, map.size); - while (hasNext) - array.add(next()); - return array; - } - - /** Adds the remaining keys to the array. */ - public Array toArray (Array array) { - while (hasNext) - array.add(next()); - return array; - } - - @Override - public void remove () { - super.remove(); - } - } -} diff --git a/src/dorkbox/collections/ObjectIntMap.kt b/src/dorkbox/collections/ObjectIntMap.kt new file mode 100644 index 0000000..86099bf --- /dev/null +++ b/src/dorkbox/collections/ObjectIntMap.kt @@ -0,0 +1,952 @@ +/* + * Copyright 2023 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/******************************************************************************* + * Copyright 2011 LibGDX. + * Mario Zechner @gmail.com> + * Nathan Sweet @gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("UNCHECKED_CAST") + +package dorkbox.collections + +import dorkbox.collections.Collections.allocateIterators +import dorkbox.collections.ObjectSet.Companion.tableSize +import java.lang.IllegalStateException +import java.util.* + +/** + * An unordered map where the keys are objects and values are ints. Null keys are not allowed. No allocation is done except when growing + * the table size. + * + * This class performs fast contains and remove (typically O(1), worst case O(n) but that is rare in practice). Add may be + * slightly slower, depending on hash collisions. Hashcodes are rehashed to reduce collisions and the need to resize. Load factors + * greater than 0.91 greatly increase the chances to resize to the next higher POT size. + * + * Unordered sets and maps are not designed to provide especially fast iteration. Iteration is faster with OrderedSet and + * OrderedMap. + * + * This implementation uses linear probing with the backward shift algorithm for removal. Hashcodes are rehashed using Fibonacci + * hashing, instead of the more common power-of-two mask, to better distribute poor hashCodes (see [Malte Skarupke's blog post](https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/)). + * + * Linear probing continues to work even when all hashCodes collide, just more slowly. + * + * @author dorkbox, llc + * @author Nathan Sweet + * @author Tommy Ettinger + */ +open class ObjectIntMap : MutableMap, MutableIterable> { + + companion object { + const val version = Collections.version + + // This is used to tell the difference between a legit NULL value in a map, and a non-existent value + internal val dummy = Any() + } + + protected var mapSize = 0 + + var keyTable: Array + var valueTable: IntArray + var loadFactor: Float + var threshold: Int + + /** + * Used by [.place] to bit shift the upper bits of a `long` into a usable range (>= 0 and <= + * [.mask]). The shift can be negative, which is convenient to match the number of bits in mask: if mask is a 7-bit + * number, a shift of -7 shifts the upper 7 bits into the lowest 7 positions. This class sets the shift > 32 and < 64, + * which if used with an int will still move the upper bits of an int to the lower bits due to Java's implicit modulus on + * shifts. + * + * [.mask] can also be used to mask the low bits of a number, which may be faster for some hashcodes, if + * [.place] is overridden. + */ + protected var shift: Int + + /** + * A bitmask used to confine hashcodes to the size of the table. Must be all 1 bits in its low positions, ie a power of two + * minus 1. + * If [.place] is overridden, this can be used instead of [.shift] to isolate usable bits of a + * hash. + */ + protected var mask: Int + + @Transient + var entries1: Entries? = null + + @Transient + var entries2: Entries? = null + + @Transient + var values1: Values? = null + + @Transient + var values2: Values? = null + + @Transient + var keys1: Keys? = null + + @Transient + var keys2: Keys? = null + + /** + * Creates a new map with the default capacity of 51 and loadfactor of 0.8 + */ + constructor() : this(51, 0.8f) + + /** + * Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before + * growing the backing table. + * + * @param initialCapacity The backing array size is initialCapacity / loadFactor, increased to the next power of two. + * @param loadFactor The loadfactor used to determine backing array growth + */ + constructor(initialCapacity: Int = 51, loadFactor: Float = 0.8f) { + require(!(loadFactor <= 0f || loadFactor >= 1f)) { "loadFactor must be > 0 and < 1: $loadFactor" } + + this.loadFactor = loadFactor + val tableSize = tableSize(initialCapacity, loadFactor) + + threshold = (tableSize * loadFactor).toInt() + mask = tableSize - 1 + shift = java.lang.Long.numberOfLeadingZeros(mask.toLong()) + keyTable = arrayOfNulls(tableSize) as Array + valueTable = IntArray(tableSize) + } + + /** + * Creates a new map identical to the specified map. + */ + constructor(map: ObjectIntMap) : this((map.keyTable.size * map.loadFactor).toInt(), map.loadFactor) { + System.arraycopy(map.keyTable, 0, keyTable, 0, map.keyTable.size) + System.arraycopy(map.valueTable, 0, valueTable, 0, map.valueTable.size) + mapSize = map.mapSize + } + + override val size: Int + get() { + return mapSize + } + + /** + * Returns an index >= 0 and <= [.mask] for the specified `item`. + * + * The default implementation uses Fibonacci hashing on the item's [Object.hashCode]: the hashcode is multiplied by a + * long constant (2 to the 64th, divided by the golden ratio) then the uppermost bits are shifted into the lowest positions to + * obtain an index in the desired range. Multiplication by a long may be slower than int (eg on GWT) but greatly improves + * rehashing, allowing even very poor hashcodes, such as those that only differ in their upper bits, to be used without high + * collision rates. Fibonacci hashing has increased collision rates when all or most hashcodes are multiples of larger + * Fibonacci numbers (see [Malte Skarupke's blog post](https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/)). + * + * + * This method can be overriden to customizing hashing. This may be useful eg in the unlikely event that most hashcodes are + * Fibonacci numbers, if keys provide poor or incorrect hashcodes, or to simplify hashing if keys provide high quality + * hashcodes and don't need Fibonacci hashing: `return item.hashCode() & mask;` + */ + protected open fun place(item: Any): Int { + return (item.hashCode() * -0x61c8864680b583ebL ushr shift).toInt() + } + + /** + * Returns the index of the key if already present, else -(index + 1) for the next empty index. This can be overridden in this + * package to compare for equality differently than [Object.equals]. + */ + open fun locateKey(key: Any): Int { + val keyTable = keyTable + var i = place(key) + while (true) { + val other: K = keyTable[i] ?: return -(i + 1) + // Empty space is available. + if (other == key) return i // Same key was found. + i = (i + 1) and mask + } + } + + /** + * Returns the old value associated with the specified key, or null. + */ + override fun put(key: K, value: Int): Int? { + var i = locateKey(key) + if (i >= 0) { // Existing key was found. + val oldValue = valueTable[i] + valueTable[i] = value + return oldValue + } + i = -(i + 1) // Empty space was found. + keyTable[i] = key + valueTable[i] = value + if (++mapSize >= threshold) resize(keyTable.size shl 1) + return null + } + + open fun putAll(from: ObjectIntMap) { + ensureCapacity(from.mapSize) + + val keyTable = from.keyTable + val valueTable = from.valueTable + var key: K? + var i = 0 + val n = keyTable.size + while (i < n) { + key = keyTable[i] + if (key != null) { + put(key, valueTable[i]) + } + i++ + } + } + + override fun putAll(from: Map) { + ensureCapacity(from.size) + + from.forEach { (k, v) -> + put(k, v) + } + } + + /** + * Skips checks for existing keys, doesn't increment size. + */ + private fun putResize(key: K, value: Int) { + val keyTable = keyTable + var i = place(key) + while (true) { + if (keyTable[i] == null) { + keyTable[i] = key + valueTable[i] = value + return + } + i = (i + 1) and mask + } + } + + /** + * Returns the value for the specified key, or null if the key is not in the map. + */ + override operator fun get(key: K): Int? { + val i = locateKey(key) + return if (i < 0) null else valueTable[i] + } + + /** + * Returns the value for the specified key, or the default value if the key is not in the map. + */ + operator fun get(key: K, defaultValue: Int?): Int? { + val i = locateKey(key) + return if (i < 0) { + defaultValue + } else { + valueTable[i] + } + } + + /** + * Returns the value for the removed key, or null if the key is not in the map. + */ + override fun remove(key: K): Int? { + var i = locateKey(key) + if (i < 0) return null + + val keyTable = keyTable + val valueTable = valueTable + + val oldValue = valueTable[i] + val mask = mask + var next = (i + 1) and mask + + var k: K? + while (keyTable[next].also { k = it } != null) { + val placement = place(k!!) + if ((next - placement and mask) > (i - placement and mask)) { + keyTable[i] = k + valueTable[i] = valueTable[next] + i = next + } + next = (next + 1) and mask + } + + keyTable[i] = null + valueTable[i] = 0 + mapSize-- + return oldValue + } + + /** + * Returns true if the map has one or more items. + * */ + fun notEmpty(): Boolean { + return mapSize > 0 + } + + /** Returns true if the map is empty. */ + override fun isEmpty(): Boolean { + return mapSize == 0 + } + + /** + * Reduces the size of the backing arrays to be the specified capacity / loadFactor, or less. If the capacity is already less, + * nothing is done. If the map contains more items than the specified capacity, the next highest power of two capacity is used + * instead. + */ + open fun shrink(maximumCapacity: Int) { + require(maximumCapacity >= 0) { "maximumCapacity must be >= 0: $maximumCapacity" } + val tableSize = tableSize(maximumCapacity, loadFactor) + if (keyTable.size > tableSize) resize(tableSize) + } + + /** + * Clears the map and reduces the size of the backing arrays to be the specified capacity / loadFactor, if they are larger. + */ + open fun clear(maximumCapacity: Int) { + val tableSize = tableSize(maximumCapacity, loadFactor) + if (keyTable.size <= tableSize) { + clear() + return + } + mapSize = 0 + resize(tableSize) + } + + override fun clear() { + if (mapSize == 0) return + mapSize = 0 + Arrays.fill(keyTable, null) + Arrays.fill(valueTable, 0) + } + + /** + * Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may + * be an expensive operation. + */ + override fun containsValue(value: Int): Boolean { + val valueTable = valueTable + if (value == 0) { + val keyTable = keyTable + for (i in valueTable.indices.reversed()) if (keyTable[i] != null && valueTable[i] == 0) return true + } + else { + for (i in valueTable.indices.reversed()) if (valueTable[i] == value) return true + } + + return false + } + + override fun containsKey(key: K): Boolean { + return locateKey(key) >= 0 + } + + /** + * Returns the key for the specified value, or null if it is not in the map. Note this traverses the entire map and compares + * every value, which may be an expensive operation. + * + * @param identity If true, uses == to compare the specified value with values in the map. If false, uses + * [.equals]. + */ + fun findKey(value: Any?, identity: Boolean): K? { + val valueTable = valueTable + if (value == null) { + val keyTable = keyTable + for (i in valueTable.indices.reversed()) if (keyTable[i] != null && valueTable[i] == 0) return keyTable[i] + } + else if (identity) { + for (i in valueTable.indices.reversed()) if (valueTable[i] == value) return keyTable[i] + } + else { + for (i in valueTable.indices.reversed()) if (value == valueTable[i]) return keyTable[i] + } + return null + } + + /** + * Increases the size of the backing array to accommodate the specified number of additional items / loadFactor. Useful before + * adding many items to avoid multiple backing array resizes. + */ + fun ensureCapacity(additionalCapacity: Int) { + val tableSize = tableSize(mapSize + additionalCapacity, loadFactor) + if (keyTable.size < tableSize) resize(tableSize) + } + + fun resize(newSize: Int) { + val oldCapacity = keyTable.size + threshold = (newSize * loadFactor).toInt() + mask = newSize - 1 + shift = java.lang.Long.numberOfLeadingZeros(mask.toLong()) + + val oldKeyTable = keyTable + val oldValueTable = valueTable + keyTable = arrayOfNulls(newSize) as Array + valueTable = IntArray(newSize) + + if (mapSize > 0) { + for (i in 0 until oldCapacity) { + val key = oldKeyTable[i] + if (key != null) putResize(key, oldValueTable[i]) + } + } + } + + override fun hashCode(): Int { + var h = mapSize + val keyTable = keyTable + val valueTable = valueTable + var i = 0 + val n = keyTable.size + while (i < n) { + val key: K? = keyTable[i] + if (key != null) { + h += key.hashCode() + val value = valueTable[i] + if (value != 0) h += value.hashCode() + } + i++ + } + return h + } + + override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other !is ObjectIntMap<*>) return false + other as ObjectIntMap + + if (other.size != mapSize) return false + val keyTable = keyTable + val valueTable = valueTable + + var i = 0 + val n = keyTable.size + while (i < n) { + val key = keyTable[i] + if (key != null) { + val otherValue = other.get(key, 0) + if (otherValue == 0 && !other.containsKey(key)) return false + if (otherValue != valueTable[i]) return false + } + i++ + } + return true + } + + + fun toString(separator: String): String { + return toString(separator, false) + } + + override fun toString(): String { + return toString(", ", true) + } + + protected open fun toString(separator: String, braces: Boolean): String { + if (mapSize == 0) return if (braces) "{}" else "" + val buffer = StringBuilder(32) + if (braces) buffer.append('{') + + val keyTable = keyTable + val valueTable = valueTable + + var i = keyTable.size + while (i-- > 0) { + val key: K = keyTable[i] ?: continue + buffer.append(key) + buffer.append('=') + buffer.append(valueTable[i]) + break + } + + while (i-- > 0) { + val key: K = keyTable[i] ?: continue + buffer.append(separator) + buffer.append(key) + buffer.append('=') + buffer.append(valueTable[i]) + } + + if (braces) buffer.append('}') + return buffer.toString() + } + + override fun iterator(): MutableIterator> { + return entries() + } + + override val entries: MutableSet> + get() = entries() as MutableSet> + + + /** + * Returns an iterator for the entries in the map. Remove is supported. + * + * + * If [Collections.allocateIterators] is false, the same iterator instance is returned each time this method is called. + * + * Use the [Entries] constructor for nested or multithreaded iteration. + */ + open fun entries(): Entries { + if (allocateIterators) return Entries(this) + if (entries1 == null) { + entries1 = Entries(this) + entries2 = Entries(this) + } + if (!entries1!!.valid) { + entries1!!.reset() + entries1!!.valid = true + entries2!!.valid = false + return entries1 as Entries + } + entries2!!.reset() + entries2!!.valid = true + entries1!!.valid = false + return entries2 as Entries + } + + override val values: MutableCollection + get() = values() + + /** + * Returns an iterator for the values in the map. Remove is supported. + * + * If [Collections.allocateIterators] is false, the same iterator instance is returned each time this method is called. + * + * Use the [Values] constructor for nested or multithreaded iteration. + */ + open fun values(): Values { + if (allocateIterators) return Values(this) + if (values1 == null) { + values1 = Values(this) + values2 = Values(this) + } + if (!values1!!.valid) { + values1!!.reset() + values1!!.valid = true + values2!!.valid = false + return values1 as Values + } + values2!!.reset() + values2!!.valid = true + values1!!.valid = false + return values2 as Values + } + + override val keys: MutableSet + get() = keys() + + /** + * Returns an iterator for the keys in the map. Remove is supported. + * + * If [Collections.allocateIterators] is false, the same iterator instance is returned each time this method is called. + * + * Use the [Keys] constructor for nested or multithreaded iteration. + */ + open fun keys(): Keys { + if (allocateIterators) return Keys(this) + if (keys1 == null) { + keys1 = Keys(this) + keys2 = Keys(this) + } + if (!keys1!!.valid) { + keys1!!.reset() + keys1!!.valid = true + keys2!!.valid = false + return keys1 as Keys + } + keys2!!.reset() + keys2!!.valid = true + keys1!!.valid = false + return keys2 as Keys + } + + class Entry: MutableMap.MutableEntry { + override lateinit var key: K + override var value: Int = 0 + + override fun setValue(newValue: Int): Int { + val oldValue = value + value = newValue + return oldValue + } + + override fun toString(): String { + return "$key=$value" + } + } + + abstract class MapIterator(val map: ObjectIntMap) : Iterable, MutableIterator { + var hasNext = false + var nextIndex = 0 + var currentIndex = 0 + var valid = true + + init { + @Suppress("LeakingThis") + reset() + } + + open fun reset() { + currentIndex = -1 + nextIndex = -1 + findNextIndex() + } + + fun findNextIndex() { + val keyTable = map.keyTable + val n = keyTable.size + while (++nextIndex < n) { + if (keyTable[nextIndex] != null) { + hasNext = true + return + } + } + hasNext = false + } + + override fun remove() { + var i = currentIndex + check(i >= 0) { "next must be called before remove." } + + val keyTable = map.keyTable + val valueTable = map.valueTable + + val mask = map.mask + var next = (i + 1) and mask + + var key: K? + while (keyTable[next].also { key = it } != null) { + val placement = map.place(key!!) + if ((next - placement and mask) > (i - placement and mask)) { + keyTable[i] = key + valueTable[i] = valueTable[next] + i = next + } + next = (next + 1) and mask + } + keyTable[i] = null + valueTable[i] = 0 + map.mapSize-- + if (i != currentIndex) --nextIndex + currentIndex = -1 + } + } + + open class Entries(map: ObjectIntMap) : MutableSet>, MapIterator>(map) { + var entry = Entry() + + /** Note the same entry instance is returned each time this method is called. */ + override fun next(): Entry { + if (!hasNext) throw NoSuchElementException() + if (!valid) throw RuntimeException("#iterator() cannot be used nested.") + val keyTable = map.keyTable + entry.key = keyTable[nextIndex]!! + entry.value = map.valueTable[nextIndex] + currentIndex = nextIndex + findNextIndex() + return entry + } + + override fun hasNext(): Boolean { + if (!valid) throw RuntimeException("#iterator() cannot be used nested.") + return hasNext + } + + override fun add(element: Entry): Boolean { + map.put(element.key, element.value) + return true + } + + override fun addAll(elements: Collection>): Boolean { + var added = false + elements.forEach { + map.put(it.key, it.value) + added = true + } + + return added + } + + override val size: Int + get() = map.mapSize + + override fun clear() { + map.clear() + reset() + } + + override fun isEmpty(): Boolean { + return map.isEmpty() + } + + override fun containsAll(elements: Collection>): Boolean { + elements.forEach {(k,v) -> + if (map.get(k) != v) { + return false + } + } + return true + } + + override fun contains(element: Entry): Boolean { + return (map.get(element.key) == element.value) + } + + override fun iterator(): Entries { + return this + } + + override fun retainAll(elements: Collection>): Boolean { + var removed = false + map.keyTable.forEach { key -> + if (key != null) { + val hasElement = elements.firstOrNull { it.key == key } != null + if (!hasElement) { + removed = map.remove(key) != null || removed + } + } + } + reset() + return removed + } + + override fun removeAll(elements: Collection>): Boolean { + var removed = false + elements.forEach { (k,_) -> + removed = map.remove(k) != null || removed + } + reset() + return removed + } + + override fun remove(element: Entry): Boolean { + val removed = map.remove(entry.key) != null + reset() + return removed + } + } + + open class Values(map: ObjectIntMap<*>) : MutableCollection, MapIterator(map as ObjectIntMap) { + override fun hasNext(): Boolean { + if (!valid) throw RuntimeException("#iterator() cannot be used nested.") + return hasNext + } + + override fun next(): Int { + if (!hasNext) throw NoSuchElementException() + if (!valid) throw RuntimeException("#iterator() cannot be used nested.") + val value = map.valueTable[nextIndex] + currentIndex = nextIndex + findNextIndex() + return value + } + + override val size: Int + get() = map.mapSize + + override fun clear() { + map.clear() + reset() + } + + override fun addAll(elements: Collection): Boolean { + throw IllegalStateException("Cannot add values to a map without keys") + } + + override fun add(element: Int): Boolean { + throw IllegalStateException("Cannot add values to a map without keys") + } + + override fun isEmpty(): Boolean { + return map.isEmpty() + } + + override fun containsAll(elements: Collection): Boolean { + elements.forEach { + if (!map.containsValue(it)) { + return false + } + } + return true + } + + override fun contains(element: Int): Boolean { + return map.containsValue(element) + } + + override fun iterator(): Values { + return this + } + + override fun retainAll(elements: Collection): Boolean { + var removed = false + map.keyTable.forEach { key -> + if (key != null) { + val value = map[key] + if (!elements.contains(value)) { + map.remove(key) + removed = true + } + } + } + reset() + return removed + } + + override fun removeAll(elements: Collection): Boolean { + var removed = false + elements.forEach { + val key = map.findKey(it, false) + if (key != null) { + removed = map.remove(key) != null || removed + } + } + reset() + return removed + } + + override fun remove(element: Int): Boolean { + var removed = false + val key = map.findKey(element, false) + if (key != null) { + removed = map.remove(key) != null + } + reset() + return removed + } + + /** Returns a new array containing the remaining values. */ + open fun toArray(): IntArray { + val array = IntArray(map.size) + var index = 0 + while (hasNext()) { + array[index++] = next() + } + return array + } + + /** Adds the remaining values to the specified array. */ + fun toArray(array: IntArray): IntArray { + var index = 0 + while (hasNext) { + array[index++] = next() + } + return array + } + } + + open class Keys(map: ObjectIntMap) : MutableSet, MapIterator(map) { + override fun hasNext(): Boolean { + if (!valid) throw RuntimeException("#iterator() cannot be used nested.") + return hasNext + } + + override fun next(): K { + if (!hasNext) throw NoSuchElementException() + if (!valid) throw RuntimeException("#iterator() cannot be used nested.") + val key = map.keyTable[nextIndex] + currentIndex = nextIndex + findNextIndex() + return key!! + } + + override fun add(element: K): Boolean { + throw IllegalStateException("Cannot add keys to a map without values") + } + + override fun addAll(elements: Collection): Boolean { + throw IllegalStateException("Cannot add keys to a map without values") + } + + override val size: Int + get() = map.mapSize + + override fun clear() { + map.clear() + reset() + } + + override fun isEmpty(): Boolean { + return map.isEmpty() + } + + override fun containsAll(elements: Collection): Boolean { + elements.forEach { + if (!map.containsKey(it)) { + return false + } + } + return true + } + + override fun contains(element: K): Boolean { + return map.containsKey(element) + } + + override fun iterator(): Keys { + return this + } + + override fun retainAll(elements: Collection): Boolean { + var removed = false + map.keyTable.forEach { + if (it != null && !elements.contains(it)) { + map.remove(it) + removed = true + } + } + reset() + return removed + } + + override fun removeAll(elements: Collection): Boolean { + var removed = false + elements.forEach { + if (map.remove(it) == null) { + removed = true + } + } + reset() + return removed + } + + override fun remove(element: K): Boolean { + val removed = map.remove(element) == null + reset() + return removed + } + + /** Returns a new array containing the remaining keys. */ + @Suppress("USELESS_CAST") + open fun toArray(): Array { + return Array(map.mapSize) { next() as Any } as Array + } + + /** Adds the remaining keys to the array. */ + fun toArray(array: Array): Array { + var index = 0 + while (hasNext) { + array[index++] = next() as T + } + return array + } + } +}