diff --git a/src/dorkbox/util/collections/Array.java b/src/dorkbox/util/collections/Array.java new file mode 100644 index 0000000..d5dfa82 --- /dev/null +++ b/src/dorkbox/util/collections/Array.java @@ -0,0 +1,652 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.RandomUtil; + + +/** A resizable, ordered or unordered array of objects. If unordered, this class avoids a memory copy when removing elements (the + * last element is moved to the removed element's position). + * @author Nathan Sweet */ +public class Array implements Iterable { + /** Provides direct access to the underlying array. If the Array's generic type is not Object, this field may only be accessed + * if the {@link Array#Array(boolean, int, Class)} constructor was used. */ + public T[] items; + + public int size; + public boolean ordered; + + private ArrayIterable iterable; + private Predicate.PredicateIterable predicateIterable; + + /** Creates an ordered array with a capacity of 16. */ + public Array () { + this(true, 16); + } + + /** Creates an ordered array with the specified capacity. */ + public Array (int capacity) { + this(true, capacity); + } + + /** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + public Array (boolean ordered, int capacity) { + this.ordered = ordered; + items = (T[])new Object[capacity]; + } + + /** Creates a new array with {@link #items} of the specified type. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + public Array (boolean ordered, int capacity, Class arrayType) { + this.ordered = ordered; + items = (T[])java.lang.reflect.Array.newInstance(arrayType, capacity); + } + + /** Creates an ordered array with {@link #items} of the specified type and a capacity of 16. */ + public Array (Class arrayType) { + this(true, 16, arrayType); + } + + /** Creates a new array containing the elements in the specified array. The new array will have the same type of backing array + * and will be ordered if the specified array is ordered. The capacity is set to the number of elements, so any subsequent + * elements added will cause the backing array to be grown. */ + public Array (Array array) { + this(array.ordered, array.size, array.items.getClass().getComponentType()); + size = array.size; + System.arraycopy(array.items, 0, items, 0, size); + } + + /** Creates a new ordered array containing the elements in the specified array. The new array will have the same type of + * backing array. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array + * to be grown. */ + public Array (T[] array) { + this(true, array, 0, array.length); + } + + /** Creates a new array containing the elements in the specified array. The new array will have the same type of backing array. + * The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be grown. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. */ + public Array (boolean ordered, T[] array, int start, int count) { + this(ordered, count, (Class)array.getClass().getComponentType()); + size = count; + System.arraycopy(array, start, items, 0, size); + } + + public void add (T value) { + T[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size++] = value; + } + + public void add (T value1, T value2) { + T[] items = this.items; + if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + size += 2; + } + + public void add (T value1, T value2, T value3) { + T[] items = this.items; + if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + size += 3; + } + + public void add (T value1, T value2, T value3, T value4) { + T[] items = this.items; + if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5. + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + items[size + 3] = value4; + size += 4; + } + + public void addAll (Array array) { + addAll(array.items, 0, array.size); + } + + public void addAll (Array array, int start, int count) { + if (start + count > array.size) + throw new IllegalArgumentException("start + count must be <= size: " + start + " + " + count + " <= " + array.size); + addAll((T[])array.items, start, count); + } + + public void addAll (T... array) { + addAll(array, 0, array.length); + } + + public void addAll (T[] array, int start, int count) { + T[] items = this.items; + int sizeNeeded = size + count; + if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(array, start, items, size, count); + size += count; + } + + public T get (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + return items[index]; + } + + public void set (int index, T value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] = value; + } + + public void insert (int index, T value) { + if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size); + T[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) + System.arraycopy(items, index, items, index + 1, size - index); + else + items[size] = items[index]; + size++; + items[index] = value; + } + + public void swap (int first, int second) { + if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size); + if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size); + T[] items = this.items; + T firstValue = items[first]; + items[first] = items[second]; + items[second] = firstValue; + } + + /** Returns if this array contains value. + * @param value May be null. + * @param identity If true, == comparison will be used. If false, .equals() comparison will be used. + * @return true if array contains value, false if it doesn't */ + public boolean contains (T value, boolean identity) { + T[] items = this.items; + int i = size - 1; + if (identity || value == null) { + while (i >= 0) + if (items[i--] == value) return true; + } else { + while (i >= 0) + if (value.equals(items[i--])) return true; + } + return false; + } + + /** Returns the index of first occurrence of value in the array, or -1 if no such value exists. + * @param value May be null. + * @param identity If true, == comparison will be used. If false, .equals() comparison will be used. + * @return An index of first occurrence of value in array or -1 if no such value exists */ + public int indexOf (T value, boolean identity) { + T[] items = this.items; + if (identity || value == null) { + for (int i = 0, n = size; i < n; i++) + if (items[i] == value) return i; + } else { + for (int i = 0, n = size; i < n; i++) + if (value.equals(items[i])) return i; + } + return -1; + } + + /** Returns an index of last occurrence of value in array or -1 if no such value exists. Search is started from the end of an + * array. + * @param value May be null. + * @param identity If true, == comparison will be used. If false, .equals() comparison will be used. + * @return An index of last occurrence of value in array or -1 if no such value exists */ + public int lastIndexOf (T value, boolean identity) { + T[] items = this.items; + if (identity || value == null) { + for (int i = size - 1; i >= 0; i--) + if (items[i] == value) return i; + } else { + for (int i = size - 1; i >= 0; i--) + if (value.equals(items[i])) return i; + } + return -1; + } + + /** Removes the first instance of the specified value in the array. + * @param value May be null. + * @param identity If true, == comparison will be used. If false, .equals() comparison will be used. + * @return true if value was found and removed, false otherwise */ + public boolean removeValue (T value, boolean identity) { + T[] items = this.items; + if (identity || value == null) { + for (int i = 0, n = size; i < n; i++) { + if (items[i] == value) { + removeIndex(i); + return true; + } + } + } else { + for (int i = 0, n = size; i < n; i++) { + if (value.equals(items[i])) { + removeIndex(i); + return true; + } + } + } + return false; + } + + /** Removes and returns the item at the specified index. */ + public T removeIndex (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + T[] items = this.items; + T value = (T)items[index]; + size--; + if (ordered) + System.arraycopy(items, index + 1, items, index, size - index); + else + items[index] = items[size]; + items[size] = null; + return value; + } + + /** Removes the items between the specified indices, inclusive. */ + public void removeRange (int start, int end) { + if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size); + if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end); + T[] items = this.items; + int count = end - start + 1; + if (ordered) + System.arraycopy(items, start + count, items, start, size - (start + count)); + else { + int lastIndex = this.size - 1; + for (int i = 0; i < count; i++) + items[start + i] = items[lastIndex - i]; + } + size -= count; + } + + /** Removes from this array all of elements contained in the specified array. + * @param identity True to use ==, false to use .equals(). + * @return true if this array was modified. */ + public boolean removeAll (Array array, boolean identity) { + int size = this.size; + int startSize = size; + T[] items = this.items; + if (identity) { + for (int i = 0, n = array.size; i < n; i++) { + T item = array.get(i); + for (int ii = 0; ii < size; ii++) { + if (item == items[ii]) { + removeIndex(ii); + size--; + break; + } + } + } + } else { + for (int i = 0, n = array.size; i < n; i++) { + T item = array.get(i); + for (int ii = 0; ii < size; ii++) { + if (item.equals(items[ii])) { + removeIndex(ii); + size--; + break; + } + } + } + } + return size != startSize; + } + + /** Removes and returns the last item. */ + public T pop () { + if (size == 0) throw new IllegalStateException("Array is empty."); + --size; + T item = items[size]; + items[size] = null; + return item; + } + + /** Returns the last item. */ + public T peek () { + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[size - 1]; + } + + /** Returns the first item. */ + public T first () { + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[0]; + } + + /** Returns true if the array is empty. */ + public boolean isEmpty () { + return size == 0; + } + + public void clear () { + T[] items = this.items; + for (int i = 0, n = size; i < n; i++) + items[i] = null; + size = 0; + } + + /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items + * have been removed, or if it is known that more items will not be added. + * @return {@link #items} */ + public T[] shrink () { + if (items.length != size) resize(size); + return items; + } + + /** 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. + * @return {@link #items} */ + public T[] ensureCapacity (int additionalCapacity) { + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded)); + return items; + } + + /** Sets the array size, leaving any values beyond the current size null. + * @return {@link #items} */ + public T[] setSize (int newSize) { + truncate(newSize); + if (newSize > items.length) resize(Math.max(8, newSize)); + size = newSize; + return items; + } + + /** Creates a new backing array with the specified size containing the current items. */ + protected T[] resize (int newSize) { + T[] items = this.items; + T[] newItems = (T[])java.lang.reflect.Array.newInstance(items.getClass().getComponentType(), newSize); + System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length)); + this.items = newItems; + return newItems; + } + + /** Sorts this array. The array elements must implement {@link Comparable}. This method is not thread safe (uses + * {@link Sort#instance()}). */ + public void sort () { + Sort.instance().sort(items, 0, size); + } + + /** Sorts the array. This method is not thread safe (uses {@link Sort#instance()}). */ + public void sort (Comparator comparator) { + Sort.instance().sort(items, comparator, 0, size); + } + + /** Selects the nth-lowest element from the Array according to Comparator ranking. This might partially sort the Array. The + * array must have a size greater than 0, or a {@link RuntimeException} will be thrown. + * @see Select + * @param comparator used for comparison + * @param kthLowest rank of desired object according to comparison, n is based on ordinal numbers, not array indices. for min + * value use 1, for max value use size of array, using 0 results in runtime exception. + * @return the value of the Nth lowest ranked object. */ + public T selectRanked (Comparator comparator, int kthLowest) { + if (kthLowest < 1) { + throw new RuntimeException("nth_lowest must be greater than 0, 1 = first, 2 = second..."); + } + return Select.instance().select(items, comparator, kthLowest, size); + } + + /** @see Array#selectRanked(java.util.Comparator, int) + * @param comparator used for comparison + * @param kthLowest rank of desired object according to comparison, n is based on ordinal numbers, not array indices. for min + * value use 1, for max value use size of array, using 0 results in runtime exception. + * @return the index of the Nth lowest ranked object. */ + public int selectRankedIndex (Comparator comparator, int kthLowest) { + if (kthLowest < 1) { + throw new RuntimeException("nth_lowest must be greater than 0, 1 = first, 2 = second..."); + } + return Select.instance().selectIndex(items, comparator, kthLowest, size); + } + + public void reverse () { + T[] items = this.items; + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { + int ii = lastIndex - i; + T temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + public void shuffle () { + T[] items = this.items; + for (int i = size - 1; i >= 0; i--) { + int ii = RandomUtil.int_(i); + T temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + /** Returns an iterator for the items in the array. Remove is supported. Note that the same iterator instance is returned each + * time this method is called. Use the {@link ArrayIterator} constructor for nested or multithreaded iteration. */ + public Iterator iterator () { + if (iterable == null) iterable = new ArrayIterable(this); + return iterable.iterator(); + } + + /** Returns an iterable for the selected items in the array. Remove is supported, but not between hasNext() and next(). Note + * that the same iterable instance is returned each time this method is called. Use the {@link Predicate.PredicateIterable} + * constructor for nested or multithreaded iteration. */ + public Iterable select (Predicate predicate) { + if (predicateIterable == null) + predicateIterable = new Predicate.PredicateIterable(this, predicate); + else + predicateIterable.set(this, predicate); + return predicateIterable; + } + + /** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is + * taken. */ + public void truncate (int newSize) { + if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize); + if (size <= newSize) return; + for (int i = newSize; i < size; i++) + items[i] = null; + size = newSize; + } + + /** Returns a random item from the array, or null if the array is empty. */ + public T random () { + if (size == 0) return null; + return items[RandomUtil.int_(0, size - 1)]; + } + + /** Returns the items as an array. Note the array is typed, so the {@link #Array(Class)} constructor must have been used. + * Otherwise use {@link #toArray(Class)} to specify the array type. */ + public T[] toArray () { + return (T[])toArray(items.getClass().getComponentType()); + } + + public V[] toArray (Class type) { + V[] result = (V[])java.lang.reflect.Array.newInstance(type, size); + System.arraycopy(items, 0, result, 0, size); + return result; + } + + public int hashCode () { + if (!ordered) return super.hashCode(); + Object[] items = this.items; + int h = 1; + for (int i = 0, n = size; i < n; i++) { + h *= 31; + Object item = items[i]; + if (item != null) h += item.hashCode(); + } + return h; + } + + public boolean equals (Object object) { + if (object == this) return true; + if (!ordered) return false; + if (!(object instanceof Array)) return false; + Array array = (Array)object; + if (!array.ordered) return false; + int n = size; + if (n != array.size) return false; + Object[] items1 = this.items; + Object[] items2 = array.items; + for (int i = 0; i < n; i++) { + Object o1 = items1[i]; + Object o2 = items2[i]; + if (!(o1 == null ? o2 == null : o1.equals(o2))) return false; + } + return true; + } + + public String toString () { + if (size == 0) return "[]"; + T[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(items[i]); + } + buffer.append(']'); + return buffer.toString(); + } + + public String toString (String separator) { + if (size == 0) return ""; + T[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(separator); + buffer.append(items[i]); + } + return buffer.toString(); + } + + /** @see #Array(Class) */ + static public Array of (Class arrayType) { + return new Array(arrayType); + } + + /** @see #Array(boolean, int, Class) */ + static public Array of (boolean ordered, int capacity, Class arrayType) { + return new Array(ordered, capacity, arrayType); + } + + /** @see #Array(Object[]) */ + static public Array with (T... array) { + return new Array(array); + } + + static public class ArrayIterator implements Iterator, Iterable { + private final Array array; + private final boolean allowRemove; + int index; + boolean valid = true; + +// ArrayIterable iterable; + + public ArrayIterator (Array array) { + this(array, true); + } + + public ArrayIterator (Array array, boolean allowRemove) { + this.array = array; + this.allowRemove = allowRemove; + } + + public boolean hasNext () { + if (!valid) { +// System.out.println(iterable.lastAcquire); + throw new RuntimeException("#iterator() cannot be used nested."); + } + return index < array.size; + } + + public T next () { + if (index >= array.size) throw new NoSuchElementException(String.valueOf(index)); + if (!valid) { +// System.out.println(iterable.lastAcquire); + throw new RuntimeException("#iterator() cannot be used nested."); + } + return array.items[index++]; + } + + public void remove () { + if (!allowRemove) throw new RuntimeException("Remove not allowed."); + index--; + array.removeIndex(index); + } + + public void reset () { + index = 0; + } + + public Iterator iterator () { + return this; + } + } + + static public class ArrayIterable implements Iterable { + private final Array array; + private final boolean allowRemove; + private ArrayIterator iterator1, iterator2; + +// java.io.StringWriter lastAcquire = new java.io.StringWriter(); + + public ArrayIterable (Array array) { + this(array, true); + } + + public ArrayIterable (Array array, boolean allowRemove) { + this.array = array; + this.allowRemove = allowRemove; + } + + public Iterator iterator () { +// lastAcquire.getBuffer().setLength(0); +// new Throwable().printStackTrace(new java.io.PrintWriter(lastAcquire)); + if (iterator1 == null) { + iterator1 = new ArrayIterator(array, allowRemove); + iterator2 = new ArrayIterator(array, allowRemove); +// iterator1.iterable = this; +// iterator2.iterable = this; + } + if (!iterator1.valid) { + iterator1.index = 0; + iterator1.valid = true; + iterator2.valid = false; + return iterator1; + } + iterator2.index = 0; + iterator2.valid = true; + iterator1.valid = false; + return iterator2; + } + } +} diff --git a/src/dorkbox/util/collections/ArrayMap.java b/src/dorkbox/util/collections/ArrayMap.java new file mode 100644 index 0000000..75bd0cf --- /dev/null +++ b/src/dorkbox/util/collections/ArrayMap.java @@ -0,0 +1,643 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.RandomUtil; +import dorkbox.util.collections.ObjectMap.Entry; + +/** An ordered or unordered map of objects. This implementation uses arrays to store the keys and values, which means + * {@link #getKey(Object, boolean) gets} do a comparison for each key in the map. This is slower than a typical hash map + * implementation, but may be acceptable for small maps and has the benefits that keys and values can be accessed by index, which + * makes iteration fast. Like {@link Array}, if ordered is false, this class avoids a memory copy when removing elements (the last + * element is moved to the removed element's position). + * @author Nathan Sweet */ +public class ArrayMap implements Iterable> { + public K[] keys; + public V[] values; + public int size; + public boolean ordered; + + private Entries entries1, entries2; + private Values valuesIter1, valuesIter2; + private Keys keysIter1, keysIter2; + + /** Creates an ordered map with a capacity of 16. */ + public ArrayMap () { + this(true, 16); + } + + /** Creates an ordered map with the specified capacity. */ + public ArrayMap (int capacity) { + this(true, capacity); + } + + /** @param ordered If false, methods that remove elements may change the order of other elements in the arrays, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing arrays to be grown. */ + public ArrayMap (boolean ordered, int capacity) { + this.ordered = ordered; + keys = (K[])new Object[capacity]; + values = (V[])new Object[capacity]; + } + + /** Creates a new map with {@link #keys} and {@link #values} of the specified type. + * @param ordered If false, methods that remove elements may change the order of other elements in the arrays, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing arrays to be grown. */ + public ArrayMap (boolean ordered, int capacity, Class keyArrayType, Class valueArrayType) { + this.ordered = ordered; + keys = (K[])java.lang.reflect.Array.newInstance(keyArrayType, capacity); + values = (V[])java.lang.reflect.Array.newInstance(valueArrayType, capacity); + } + + /** Creates an ordered map with {@link #keys} and {@link #values} of the specified type and a capacity of 16. */ + public ArrayMap (Class keyArrayType, Class valueArrayType) { + this(false, 16, keyArrayType, valueArrayType); + } + + /** Creates a new map containing the elements in the specified map. The new map will have the same type of backing arrays and + * will be ordered if the specified map is ordered. The capacity is set to the number of elements, so any subsequent elements + * added will cause the backing arrays to be grown. */ + public ArrayMap (ArrayMap array) { + this(array.ordered, array.size, array.keys.getClass().getComponentType(), array.values.getClass().getComponentType()); + size = array.size; + System.arraycopy(array.keys, 0, keys, 0, size); + System.arraycopy(array.values, 0, values, 0, size); + } + + public int put (K key, V value) { + int index = indexOfKey(key); + if (index == -1) { + if (size == keys.length) resize(Math.max(8, (int)(size * 1.75f))); + index = size++; + } + keys[index] = key; + values[index] = value; + return index; + } + + public int put (K key, V value, int index) { + int existingIndex = indexOfKey(key); + if (existingIndex != -1) + removeIndex(existingIndex); + else if (size == keys.length) // + resize(Math.max(8, (int)(size * 1.75f))); + System.arraycopy(keys, index, keys, index + 1, size - index); + System.arraycopy(values, index, values, index + 1, size - index); + keys[index] = key; + values[index] = value; + size++; + return index; + } + + public void putAll (ArrayMap map) { + putAll(map, 0, map.size); + } + + public void putAll (ArrayMap map, int offset, int length) { + if (offset + length > map.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + map.size); + int sizeNeeded = size + length - offset; + if (sizeNeeded >= keys.length) resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(map.keys, offset, keys, size, length); + System.arraycopy(map.values, offset, values, size, length); + size += length; + } + + /** Returns the value for the specified key. Note this does a .equals() comparison of each key in reverse order until the + * specified key is found. */ + public V get (K key) { + Object[] keys = this.keys; + int i = size - 1; + if (key == null) { + for (; i >= 0; i--) + if (keys[i] == key) return values[i]; + } else { + for (; i >= 0; i--) + if (key.equals(keys[i])) return values[i]; + } + return null; + } + + /** Returns the key for the specified value. Note this does a comparison of each value in reverse order until the specified + * value is found. + * @param identity If true, == comparison will be used. If false, .equals() comparison will be used. */ + public K getKey (V value, boolean identity) { + Object[] values = this.values; + int i = size - 1; + if (identity || value == null) { + for (; i >= 0; i--) + if (values[i] == value) return keys[i]; + } else { + for (; i >= 0; i--) + if (value.equals(values[i])) return keys[i]; + } + return null; + } + + public K getKeyAt (int index) { + if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index)); + return keys[index]; + } + + public V getValueAt (int index) { + if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index)); + return values[index]; + } + + public K firstKey () { + if (size == 0) throw new IllegalStateException("Map is empty."); + return keys[0]; + } + + public V firstValue () { + if (size == 0) throw new IllegalStateException("Map is empty."); + return values[0]; + } + + public void setKey (int index, K key) { + if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index)); + keys[index] = key; + } + + public void setValue (int index, V value) { + if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index)); + values[index] = value; + } + + public void insert (int index, K key, V value) { + if (index > size) throw new IndexOutOfBoundsException(String.valueOf(index)); + if (size == keys.length) resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) { + System.arraycopy(keys, index, keys, index + 1, size - index); + System.arraycopy(values, index, values, index + 1, size - index); + } else { + keys[size] = keys[index]; + values[size] = values[index]; + } + size++; + keys[index] = key; + values[index] = value; + } + + public boolean containsKey (K key) { + K[] keys = this.keys; + int i = size - 1; + if (key == null) { + while (i >= 0) + if (keys[i--] == key) return true; + } else { + while (i >= 0) + if (key.equals(keys[i--])) return true; + } + return false; + } + + /** @param identity If true, == comparison will be used. If false, .equals() comparison will be used. */ + public boolean containsValue (V value, boolean identity) { + V[] values = this.values; + int i = size - 1; + if (identity || value == null) { + while (i >= 0) + if (values[i--] == value) return true; + } else { + while (i >= 0) + if (value.equals(values[i--])) return true; + } + return false; + } + + public int indexOfKey (K key) { + Object[] keys = this.keys; + if (key == null) { + for (int i = 0, n = size; i < n; i++) + if (keys[i] == key) return i; + } else { + for (int i = 0, n = size; i < n; i++) + if (key.equals(keys[i])) return i; + } + return -1; + } + + public int indexOfValue (V value, boolean identity) { + Object[] values = this.values; + if (identity || value == null) { + for (int i = 0, n = size; i < n; i++) + if (values[i] == value) return i; + } else { + for (int i = 0, n = size; i < n; i++) + if (value.equals(values[i])) return i; + } + return -1; + } + + public V removeKey (K key) { + Object[] keys = this.keys; + if (key == null) { + for (int i = 0, n = size; i < n; i++) { + if (keys[i] == key) { + V value = values[i]; + removeIndex(i); + return value; + } + } + } else { + for (int i = 0, n = size; i < n; i++) { + if (key.equals(keys[i])) { + V value = values[i]; + removeIndex(i); + return value; + } + } + } + return null; + } + + public boolean removeValue (V value, boolean identity) { + Object[] values = this.values; + if (identity || value == null) { + for (int i = 0, n = size; i < n; i++) { + if (values[i] == value) { + removeIndex(i); + return true; + } + } + } else { + for (int i = 0, n = size; i < n; i++) { + if (value.equals(values[i])) { + removeIndex(i); + return true; + } + } + } + return false; + } + + /** Removes and returns the key/values pair at the specified index. */ + public void removeIndex (int index) { + if (index >= size) throw new IndexOutOfBoundsException(String.valueOf(index)); + Object[] keys = this.keys; + size--; + if (ordered) { + System.arraycopy(keys, index + 1, keys, index, size - index); + System.arraycopy(values, index + 1, values, index, size - index); + } else { + keys[index] = keys[size]; + values[index] = values[size]; + } + keys[size] = null; + values[size] = null; + } + + /** Returns true if the map is empty. */ + public boolean isEmpty () { + return size == 0; + } + + /** Returns the last key. */ + public K peekKey () { + return keys[size - 1]; + } + + /** Returns the last value. */ + public V peekValue () { + return values[size - 1]; + } + + /** 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 (keys.length <= maximumCapacity) { + clear(); + return; + } + size = 0; + resize(maximumCapacity); + } + + public void clear () { + K[] keys = this.keys; + V[] values = this.values; + for (int i = 0, n = size; i < n; i++) { + keys[i] = null; + values[i] = null; + } + size = 0; + } + + /** Reduces the size of the backing arrays to the size of the actual number of entries. This is useful to release memory when + * many items have been removed, or if it is known that more entries will not be added. */ + public void shrink () { + if (keys.length == size) return; + resize(size); + } + + /** Increases the size of the backing arrays to accommodate the specified number of additional entries. Useful before adding + * many entries 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 >= keys.length) resize(Math.max(8, sizeNeeded)); + } + + protected void resize (int newSize) { + K[] newKeys = (K[])java.lang.reflect.Array.newInstance(keys.getClass().getComponentType(), newSize); + System.arraycopy(keys, 0, newKeys, 0, Math.min(size, newKeys.length)); + this.keys = newKeys; + + V[] newValues = (V[])java.lang.reflect.Array.newInstance(values.getClass().getComponentType(), newSize); + System.arraycopy(values, 0, newValues, 0, Math.min(size, newValues.length)); + this.values = newValues; + } + + public void reverse () { + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { + int ii = lastIndex - i; + K tempKey = keys[i]; + keys[i] = keys[ii]; + keys[ii] = tempKey; + + V tempValue = values[i]; + values[i] = values[ii]; + values[ii] = tempValue; + } + } + + public void shuffle () { + for (int i = size - 1; i >= 0; i--) { + int ii = RandomUtil.int_(i); + K tempKey = keys[i]; + keys[i] = keys[ii]; + keys[ii] = tempKey; + + V tempValue = values[i]; + values[i] = values[ii]; + values[ii] = tempValue; + } + } + + /** Reduces the size of the arrays to the specified size. If the arrays are already smaller than the specified size, no action + * is taken. */ + public void truncate (int newSize) { + if (size <= newSize) return; + for (int i = newSize; i < size; i++) { + keys[i] = null; + values[i] = null; + } + size = newSize; + } + + public int hashCode () { + K[] keys = this.keys; + V[] values = this.values; + int h = 0; + for (int i = 0, n = size; i < n; i++) { + K key = keys[i]; + V value = values[i]; + if (key != null) h += key.hashCode() * 31; + if (value != null) h += value.hashCode(); + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof ArrayMap)) return false; + ArrayMap other = (ArrayMap)obj; + if (other.size != size) return false; + K[] keys = this.keys; + V[] values = this.values; + for (int i = 0, n = size; i < n; i++) { + K key = keys[i]; + V value = values[i]; + if (value == null) { + if (!other.containsKey(key) || other.get(key) != null) return false; + } else { + if (!value.equals(other.get(key))) return false; + } + } + return true; + } + + public String toString () { + if (size == 0) return "{}"; + K[] keys = this.keys; + V[] values = this.values; + StringBuilder buffer = new StringBuilder(32); + buffer.append('{'); + buffer.append(keys[0]); + buffer.append('='); + buffer.append(values[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(keys[i]); + buffer.append('='); + buffer.append(values[i]); + } + buffer.append('}'); + return buffer.toString(); + } + + public Iterator> 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.index = 0; + entries1.valid = true; + entries2.valid = false; + return entries1; + } + entries2.index = 0; + 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 (valuesIter1 == null) { + valuesIter1 = new Values(this); + valuesIter2 = new Values(this); + } + if (!valuesIter1.valid) { + valuesIter1.index = 0; + valuesIter1.valid = true; + valuesIter2.valid = false; + return valuesIter1; + } + valuesIter2.index = 0; + valuesIter2.valid = true; + valuesIter1.valid = false; + return valuesIter2; + } + + /** 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 (keysIter1 == null) { + keysIter1 = new Keys(this); + keysIter2 = new Keys(this); + } + if (!keysIter1.valid) { + keysIter1.index = 0; + keysIter1.valid = true; + keysIter2.valid = false; + return keysIter1; + } + keysIter2.index = 0; + keysIter2.valid = true; + keysIter1.valid = false; + return keysIter2; + } + + static public class Entries implements Iterable>, Iterator> { + private final ArrayMap map; + Entry entry = new ObjectMap.Entry(); + int index; + boolean valid = true; + + public Entries (ArrayMap map) { + this.map = map; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return index < map.size; + } + + public Iterator> iterator () { + return this; + } + + /** Note the same entry instance is returned each time this method is called. */ + public Entry next () { + if (index >= map.size) throw new NoSuchElementException(String.valueOf(index)); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + entry.key = map.keys[index]; + entry.value = map.values[index++]; + return entry; + } + + public void remove () { + index--; + map.removeIndex(index); + } + + public void reset () { + index = 0; + } + } + + static public class Values implements Iterable, Iterator { + private final ArrayMap map; + int index; + boolean valid = true; + + public Values (ArrayMap map) { + this.map = map; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return index < map.size; + } + + public Iterator iterator () { + return this; + } + + public V next () { + if (index >= map.size) throw new NoSuchElementException(String.valueOf(index)); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return map.values[index++]; + } + + public void remove () { + index--; + map.removeIndex(index); + } + + public void reset () { + index = 0; + } + + public Array toArray () { + return new Array(true, map.values, index, map.size - index); + } + + public Array toArray (Array array) { + array.addAll(map.values, index, map.size - index); + return array; + } + } + + static public class Keys implements Iterable, Iterator { + private final ArrayMap map; + int index; + boolean valid = true; + + public Keys (ArrayMap map) { + this.map = map; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return index < map.size; + } + + public Iterator iterator () { + return this; + } + + public K next () { + if (index >= map.size) throw new NoSuchElementException(String.valueOf(index)); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return map.keys[index++]; + } + + public void remove () { + index--; + map.removeIndex(index); + } + + public void reset () { + index = 0; + } + + public Array toArray () { + return new Array(true, map.keys, index, map.size - index); + } + + public Array toArray (Array array) { + array.addAll(map.keys, index, map.size - index); + return array; + } + } +} diff --git a/src/dorkbox/util/collections/BooleanArray.java b/src/dorkbox/util/collections/BooleanArray.java new file mode 100644 index 0000000..413f7d9 --- /dev/null +++ b/src/dorkbox/util/collections/BooleanArray.java @@ -0,0 +1,365 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.BitSet; + +import dorkbox.util.RandomUtil; + +/** A resizable, ordered or unordered boolean array. Avoids the boxing that occurs with ArrayList. It is less memory + * efficient than {@link BitSet}, except for very small sizes. It more CPU efficient than {@link BitSet}, except for very large + * sizes or if BitSet functionality such as and, or, xor, etc are needed. If unordered, this class avoids a memory copy when + * removing elements (the last element is moved to the removed element's position). + * @author Nathan Sweet */ +public class BooleanArray { + public boolean[] items; + public int size; + public boolean ordered; + + /** Creates an ordered array with a capacity of 16. */ + public BooleanArray () { + this(true, 16); + } + + /** Creates an ordered array with the specified capacity. */ + public BooleanArray (int capacity) { + this(true, capacity); + } + + /** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + public BooleanArray (boolean ordered, int capacity) { + this.ordered = ordered; + items = new boolean[capacity]; + } + + /** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is + * ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be + * grown. */ + public BooleanArray (BooleanArray array) { + this.ordered = array.ordered; + size = array.size; + items = new boolean[size]; + System.arraycopy(array.items, 0, items, 0, size); + } + + /** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements, + * so any subsequent elements added will cause the backing array to be grown. */ + public BooleanArray (boolean[] array) { + this(true, array, 0, array.length); + } + + /** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any + * subsequent elements added will cause the backing array to be grown. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. */ + public BooleanArray (boolean ordered, boolean[] array, int startIndex, int count) { + this(ordered, count); + size = count; + System.arraycopy(array, startIndex, items, 0, count); + } + + public void add (boolean value) { + boolean[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size++] = value; + } + + public void add (boolean value1, boolean value2) { + boolean[] items = this.items; + if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + size += 2; + } + + public void add (boolean value1, boolean value2, boolean value3) { + boolean[] items = this.items; + if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + size += 3; + } + + public void add (boolean value1, boolean value2, boolean value3, boolean value4) { + boolean[] items = this.items; + if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5. + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + items[size + 3] = value4; + size += 4; + } + + public void addAll (BooleanArray array) { + addAll(array.items, 0, array.size); + } + + public void addAll (BooleanArray array, int offset, int length) { + if (offset + length > array.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); + addAll(array.items, offset, length); + } + + public void addAll (boolean... array) { + addAll(array, 0, array.length); + } + + public void addAll (boolean[] array, int offset, int length) { + boolean[] items = this.items; + int sizeNeeded = size + length; + if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(array, offset, items, size, length); + size += length; + } + + public boolean get (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + return items[index]; + } + + public void set (int index, boolean value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] = value; + } + + public void insert (int index, boolean value) { + if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size); + boolean[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) + System.arraycopy(items, index, items, index + 1, size - index); + else + items[size] = items[index]; + size++; + items[index] = value; + } + + public void swap (int first, int second) { + if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size); + if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size); + boolean[] items = this.items; + boolean firstValue = items[first]; + items[first] = items[second]; + items[second] = firstValue; + } + + /** Removes and returns the item at the specified index. */ + public boolean removeIndex (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + boolean[] items = this.items; + boolean value = items[index]; + size--; + if (ordered) + System.arraycopy(items, index + 1, items, index, size - index); + else + items[index] = items[size]; + return value; + } + + /** Removes the items between the specified indices, inclusive. */ + public void removeRange (int start, int end) { + if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size); + if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end); + boolean[] items = this.items; + int count = end - start + 1; + if (ordered) + System.arraycopy(items, start + count, items, start, size - (start + count)); + else { + int lastIndex = this.size - 1; + for (int i = 0; i < count; i++) + items[start + i] = items[lastIndex - i]; + } + size -= count; + } + + /** Removes from this array all of elements contained in the specified array. + * @return true if this array was modified. */ + public boolean removeAll (BooleanArray array) { + int size = this.size; + int startSize = size; + boolean[] items = this.items; + for (int i = 0, n = array.size; i < n; i++) { + boolean item = array.get(i); + for (int ii = 0; ii < size; ii++) { + if (item == items[ii]) { + removeIndex(ii); + size--; + break; + } + } + } + return size != startSize; + } + + /** Removes and returns the last item. */ + public boolean pop () { + return items[--size]; + } + + /** Returns the last item. */ + public boolean peek () { + return items[size - 1]; + } + + /** Returns the first item. */ + public boolean first () { + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[0]; + } + + /** Returns true if the array is empty. */ + public boolean isEmpty () { + return size == 0; + } + + public void clear () { + size = 0; + } + + /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items + * have been removed, or if it is known that more items will not be added. + * @return {@link #items} */ + public boolean[] shrink () { + if (items.length != size) resize(size); + return items; + } + + /** 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. + * @return {@link #items} */ + public boolean[] ensureCapacity (int additionalCapacity) { + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded)); + return items; + } + + /** Sets the array size, leaving any values beyond the current size undefined. + * @return {@link #items} */ + public boolean[] setSize (int newSize) { + if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize); + if (newSize > items.length) resize(Math.max(8, newSize)); + size = newSize; + return items; + } + + protected boolean[] resize (int newSize) { + boolean[] newItems = new boolean[newSize]; + boolean[] items = this.items; + System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length)); + this.items = newItems; + return newItems; + } + + public void reverse () { + boolean[] items = this.items; + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { + int ii = lastIndex - i; + boolean temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + public void shuffle () { + boolean[] items = this.items; + for (int i = size - 1; i >= 0; i--) { + int ii = RandomUtil.int_(i); + boolean temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + /** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is + * taken. */ + public void truncate (int newSize) { + if (size > newSize) size = newSize; + } + + /** Returns a random item from the array, or false if the array is empty. */ + public boolean random () { + if (size == 0) return false; + return items[RandomUtil.int_(0, size - 1)]; + } + + public boolean[] toArray () { + boolean[] array = new boolean[size]; + System.arraycopy(items, 0, array, 0, size); + return array; + } + + public int hashCode () { + if (!ordered) return super.hashCode(); + boolean[] items = this.items; + int h = 1; + for (int i = 0, n = size; i < n; i++) + h = h * 31 + (items[i] ? 1231 : 1237); + return h; + } + + public boolean equals (Object object) { + if (object == this) return true; + if (!ordered) return false; + if (!(object instanceof BooleanArray)) return false; + BooleanArray array = (BooleanArray)object; + if (!array.ordered) return false; + int n = size; + if (n != array.size) return false; + boolean[] items1 = this.items; + boolean[] items2 = array.items; + for (int i = 0; i < n; i++) + if (items1[i] != items2[i]) return false; + return true; + } + + public String toString () { + if (size == 0) return "[]"; + boolean[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(items[i]); + } + buffer.append(']'); + return buffer.toString(); + } + + public String toString (String separator) { + if (size == 0) return ""; + boolean[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(separator); + buffer.append(items[i]); + } + return buffer.toString(); + } + + /** @see #BooleanArray(boolean[]) */ + static public BooleanArray with (boolean... array) { + return new BooleanArray(array); + } +} diff --git a/src/dorkbox/util/collections/ByteArray.java b/src/dorkbox/util/collections/ByteArray.java new file mode 100644 index 0000000..66f4f31 --- /dev/null +++ b/src/dorkbox/util/collections/ByteArray.java @@ -0,0 +1,410 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Arrays; + +import dorkbox.util.RandomUtil; + +/** A resizable, ordered or unordered byte array. Avoids the boxing that occurs with ArrayList. If unordered, this class + * avoids a memory copy when removing elements (the last element is moved to the removed element's position). + * @author Nathan Sweet */ +public class ByteArray { + public byte[] items; + public int size; + public boolean ordered; + + /** Creates an ordered array with a capacity of 16. */ + public ByteArray () { + this(true, 16); + } + + /** Creates an ordered array with the specified capacity. */ + public ByteArray (int capacity) { + this(true, capacity); + } + + /** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + public ByteArray (boolean ordered, int capacity) { + this.ordered = ordered; + items = new byte[capacity]; + } + + /** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is + * ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be + * grown. */ + public ByteArray (ByteArray array) { + this.ordered = array.ordered; + size = array.size; + items = new byte[size]; + System.arraycopy(array.items, 0, items, 0, size); + } + + /** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements, + * so any subsequent elements added will cause the backing array to be grown. */ + public ByteArray (byte[] array) { + this(true, array, 0, array.length); + } + + /** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any + * subsequent elements added will cause the backing array to be grown. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. */ + public ByteArray (boolean ordered, byte[] array, int startIndex, int count) { + this(ordered, count); + size = count; + System.arraycopy(array, startIndex, items, 0, count); + } + + public void add (byte value) { + byte[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size++] = value; + } + + public void add (byte value1, byte value2) { + byte[] items = this.items; + if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + size += 2; + } + + public void add (byte value1, byte value2, byte value3) { + byte[] items = this.items; + if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + size += 3; + } + + public void add (byte value1, byte value2, byte value3, byte value4) { + byte[] items = this.items; + if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5. + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + items[size + 3] = value4; + size += 4; + } + + public void addAll (ByteArray array) { + addAll(array.items, 0, array.size); + } + + public void addAll (ByteArray array, int offset, int length) { + if (offset + length > array.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); + addAll(array.items, offset, length); + } + + public void addAll (byte... array) { + addAll(array, 0, array.length); + } + + public void addAll (byte[] array, int offset, int length) { + byte[] items = this.items; + int sizeNeeded = size + length; + if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(array, offset, items, size, length); + size += length; + } + + public byte get (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + return items[index]; + } + + public void set (int index, byte value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] = value; + } + + public void incr (int index, byte value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] += value; + } + + public void mul (int index, byte value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] *= value; + } + + public void insert (int index, byte value) { + if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size); + byte[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) + System.arraycopy(items, index, items, index + 1, size - index); + else + items[size] = items[index]; + size++; + items[index] = value; + } + + public void swap (int first, int second) { + if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size); + if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size); + byte[] items = this.items; + byte firstValue = items[first]; + items[first] = items[second]; + items[second] = firstValue; + } + + public boolean contains (byte value) { + int i = size - 1; + byte[] items = this.items; + while (i >= 0) + if (items[i--] == value) return true; + return false; + } + + public int indexOf (byte value) { + byte[] items = this.items; + for (int i = 0, n = size; i < n; i++) + if (items[i] == value) return i; + return -1; + } + + public int lastIndexOf (byte value) { + byte[] items = this.items; + for (int i = size - 1; i >= 0; i--) + if (items[i] == value) return i; + return -1; + } + + public boolean removeValue (byte value) { + byte[] items = this.items; + for (int i = 0, n = size; i < n; i++) { + if (items[i] == value) { + removeIndex(i); + return true; + } + } + return false; + } + + /** Removes and returns the item at the specified index. */ + public int removeIndex (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + byte[] items = this.items; + int value = items[index]; + size--; + if (ordered) + System.arraycopy(items, index + 1, items, index, size - index); + else + items[index] = items[size]; + return value; + } + + /** Removes the items between the specified indices, inclusive. */ + public void removeRange (int start, int end) { + if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size); + if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end); + byte[] items = this.items; + int count = end - start + 1; + if (ordered) + System.arraycopy(items, start + count, items, start, size - (start + count)); + else { + int lastIndex = this.size - 1; + for (int i = 0; i < count; i++) + items[start + i] = items[lastIndex - i]; + } + size -= count; + } + + /** Removes from this array all of elements contained in the specified array. + * @return true if this array was modified. */ + public boolean removeAll (ByteArray array) { + int size = this.size; + int startSize = size; + byte[] items = this.items; + for (int i = 0, n = array.size; i < n; i++) { + int item = array.get(i); + for (int ii = 0; ii < size; ii++) { + if (item == items[ii]) { + removeIndex(ii); + size--; + break; + } + } + } + return size != startSize; + } + + /** Removes and returns the last item. */ + public byte pop () { + return items[--size]; + } + + /** Returns the last item. */ + public byte peek () { + return items[size - 1]; + } + + /** Returns the first item. */ + public byte first () { + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[0]; + } + + /** Returns true if the array is empty. */ + public boolean isEmpty () { + return size == 0; + } + + public void clear () { + size = 0; + } + + /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items + * have been removed, or if it is known that more items will not be added. + * @return {@link #items} */ + public byte[] shrink () { + if (items.length != size) resize(size); + return items; + } + + /** 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. + * @return {@link #items} */ + public byte[] ensureCapacity (int additionalCapacity) { + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded)); + return items; + } + + /** Sets the array size, leaving any values beyond the current size undefined. + * @return {@link #items} */ + public byte[] setSize (int newSize) { + if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize); + if (newSize > items.length) resize(Math.max(8, newSize)); + size = newSize; + return items; + } + + protected byte[] resize (int newSize) { + byte[] newItems = new byte[newSize]; + byte[] items = this.items; + System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length)); + this.items = newItems; + return newItems; + } + + public void sort () { + Arrays.sort(items, 0, size); + } + + public void reverse () { + byte[] items = this.items; + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { + int ii = lastIndex - i; + byte temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + public void shuffle () { + byte[] items = this.items; + for (int i = size - 1; i >= 0; i--) { + int ii = RandomUtil.int_(i); + byte temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + /** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is + * taken. */ + public void truncate (int newSize) { + if (size > newSize) size = newSize; + } + + /** Returns a random item from the array, or zero if the array is empty. */ + public byte random () { + if (size == 0) return 0; + return items[RandomUtil.int_(0, size - 1)]; + } + + public byte[] toArray () { + byte[] array = new byte[size]; + System.arraycopy(items, 0, array, 0, size); + return array; + } + + public int hashCode () { + if (!ordered) return super.hashCode(); + byte[] items = this.items; + int h = 1; + for (int i = 0, n = size; i < n; i++) + h = h * 31 + items[i]; + return h; + } + + public boolean equals (Object object) { + if (object == this) return true; + if (!ordered) return false; + if (!(object instanceof ByteArray)) return false; + ByteArray array = (ByteArray)object; + if (!array.ordered) return false; + int n = size; + if (n != array.size) return false; + byte[] items1 = this.items; + byte[] items2 = array.items; + for (int i = 0; i < n; i++) + if (items1[i] != items2[i]) return false; + return true; + } + + public String toString () { + if (size == 0) return "[]"; + byte[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(items[i]); + } + buffer.append(']'); + return buffer.toString(); + } + + public String toString (String separator) { + if (size == 0) return ""; + byte[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(separator); + buffer.append(items[i]); + } + return buffer.toString(); + } + + /** @see #ByteArray(byte[]) */ + static public ByteArray with (byte... array) { + return new ByteArray(array); + } +} diff --git a/src/dorkbox/util/collections/CharArray.java b/src/dorkbox/util/collections/CharArray.java new file mode 100644 index 0000000..1454389 --- /dev/null +++ b/src/dorkbox/util/collections/CharArray.java @@ -0,0 +1,410 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Arrays; + +import dorkbox.util.RandomUtil; + +/** A resizable, ordered or unordered char array. Avoids the boxing that occurs with ArrayList. If unordered, this + * class avoids a memory copy when removing elements (the last element is moved to the removed element's position). + * @author Nathan Sweet */ +public class CharArray { + public char[] items; + public int size; + public boolean ordered; + + /** Creates an ordered array with a capacity of 16. */ + public CharArray () { + this(true, 16); + } + + /** Creates an ordered array with the specified capacity. */ + public CharArray (int capacity) { + this(true, capacity); + } + + /** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + public CharArray (boolean ordered, int capacity) { + this.ordered = ordered; + items = new char[capacity]; + } + + /** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is + * ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be + * grown. */ + public CharArray (CharArray array) { + this.ordered = array.ordered; + size = array.size; + items = new char[size]; + System.arraycopy(array.items, 0, items, 0, size); + } + + /** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements, + * so any subsequent elements added will cause the backing array to be grown. */ + public CharArray (char[] array) { + this(true, array, 0, array.length); + } + + /** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any + * subsequent elements added will cause the backing array to be grown. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. */ + public CharArray (boolean ordered, char[] array, int startIndex, int count) { + this(ordered, count); + size = count; + System.arraycopy(array, startIndex, items, 0, count); + } + + public void add (char value) { + char[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size++] = value; + } + + public void add (char value1, char value2) { + char[] items = this.items; + if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + size += 2; + } + + public void add (char value1, char value2, char value3) { + char[] items = this.items; + if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + size += 3; + } + + public void add (char value1, char value2, char value3, char value4) { + char[] items = this.items; + if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5. + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + items[size + 3] = value4; + size += 4; + } + + public void addAll (CharArray array) { + addAll(array.items, 0, array.size); + } + + public void addAll (CharArray array, int offset, int length) { + if (offset + length > array.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); + addAll(array.items, offset, length); + } + + public void addAll (char... array) { + addAll(array, 0, array.length); + } + + public void addAll (char[] array, int offset, int length) { + char[] items = this.items; + int sizeNeeded = size + length; + if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(array, offset, items, size, length); + size += length; + } + + public char get (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + return items[index]; + } + + public void set (int index, char value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] = value; + } + + public void incr (int index, char value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] += value; + } + + public void mul (int index, char value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] *= value; + } + + public void insert (int index, char value) { + if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size); + char[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) + System.arraycopy(items, index, items, index + 1, size - index); + else + items[size] = items[index]; + size++; + items[index] = value; + } + + public void swap (int first, int second) { + if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size); + if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size); + char[] items = this.items; + char firstValue = items[first]; + items[first] = items[second]; + items[second] = firstValue; + } + + public boolean contains (char value) { + int i = size - 1; + char[] items = this.items; + while (i >= 0) + if (items[i--] == value) return true; + return false; + } + + public int indexOf (char value) { + char[] items = this.items; + for (int i = 0, n = size; i < n; i++) + if (items[i] == value) return i; + return -1; + } + + public int lastIndexOf (char value) { + char[] items = this.items; + for (int i = size - 1; i >= 0; i--) + if (items[i] == value) return i; + return -1; + } + + public boolean removeValue (char value) { + char[] items = this.items; + for (int i = 0, n = size; i < n; i++) { + if (items[i] == value) { + removeIndex(i); + return true; + } + } + return false; + } + + /** Removes and returns the item at the specified index. */ + public char removeIndex (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + char[] items = this.items; + char value = items[index]; + size--; + if (ordered) + System.arraycopy(items, index + 1, items, index, size - index); + else + items[index] = items[size]; + return value; + } + + /** Removes the items between the specified indices, inclusive. */ + public void removeRange (int start, int end) { + if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size); + if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end); + char[] items = this.items; + int count = end - start + 1; + if (ordered) + System.arraycopy(items, start + count, items, start, size - (start + count)); + else { + int lastIndex = this.size - 1; + for (int i = 0; i < count; i++) + items[start + i] = items[lastIndex - i]; + } + size -= count; + } + + /** Removes from this array all of elements contained in the specified array. + * @return true if this array was modified. */ + public boolean removeAll (CharArray array) { + int size = this.size; + int startSize = size; + char[] items = this.items; + for (int i = 0, n = array.size; i < n; i++) { + char item = array.get(i); + for (int ii = 0; ii < size; ii++) { + if (item == items[ii]) { + removeIndex(ii); + size--; + break; + } + } + } + return size != startSize; + } + + /** Removes and returns the last item. */ + public char pop () { + return items[--size]; + } + + /** Returns the last item. */ + public char peek () { + return items[size - 1]; + } + + /** Returns the first item. */ + public char first () { + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[0]; + } + + /** Returns true if the array is empty. */ + public boolean isEmpty () { + return size == 0; + } + + public void clear () { + size = 0; + } + + /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items + * have been removed, or if it is known that more items will not be added. + * @return {@link #items} */ + public char[] shrink () { + if (items.length != size) resize(size); + return items; + } + + /** 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. + * @return {@link #items} */ + public char[] ensureCapacity (int additionalCapacity) { + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded)); + return items; + } + + /** Sets the array size, leaving any values beyond the current size undefined. + * @return {@link #items} */ + public char[] setSize (int newSize) { + if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize); + if (newSize > items.length) resize(Math.max(8, newSize)); + size = newSize; + return items; + } + + protected char[] resize (int newSize) { + char[] newItems = new char[newSize]; + char[] items = this.items; + System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length)); + this.items = newItems; + return newItems; + } + + public void sort () { + Arrays.sort(items, 0, size); + } + + public void reverse () { + char[] items = this.items; + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { + int ii = lastIndex - i; + char temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + public void shuffle () { + char[] items = this.items; + for (int i = size - 1; i >= 0; i--) { + int ii = RandomUtil.int_(i); + char temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + /** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is + * taken. */ + public void truncate (int newSize) { + if (size > newSize) size = newSize; + } + + /** Returns a random item from the array, or zero if the array is empty. */ + public char random () { + if (size == 0) return 0; + return items[RandomUtil.int_(0, size - 1)]; + } + + public char[] toArray () { + char[] array = new char[size]; + System.arraycopy(items, 0, array, 0, size); + return array; + } + + public int hashCode () { + if (!ordered) return super.hashCode(); + char[] items = this.items; + int h = 1; + for (int i = 0, n = size; i < n; i++) + h = h * 31 + items[i]; + return h; + } + + public boolean equals (Object object) { + if (object == this) return true; + if (!ordered) return false; + if (!(object instanceof CharArray)) return false; + CharArray array = (CharArray)object; + if (!array.ordered) return false; + int n = size; + if (n != array.size) return false; + char[] items1 = this.items; + char[] items2 = array.items; + for (int i = 0; i < n; i++) + if (items1[i] != items2[i]) return false; + return true; + } + + public String toString () { + if (size == 0) return "[]"; + char[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(items[i]); + } + buffer.append(']'); + return buffer.toString(); + } + + public String toString (String separator) { + if (size == 0) return ""; + char[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(separator); + buffer.append(items[i]); + } + return buffer.toString(); + } + + /** @see #CharArray(char[]) */ + static public CharArray with (char... array) { + return new CharArray(array); + } +} diff --git a/src/dorkbox/util/collections/ComparableTimSort.java b/src/dorkbox/util/collections/ComparableTimSort.java new file mode 100644 index 0000000..a8fc5bb --- /dev/null +++ b/src/dorkbox/util/collections/ComparableTimSort.java @@ -0,0 +1,805 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.util.collections; + +/** This is a near duplicate of {@link TimSort}, modified for use with arrays of objects that implement {@link Comparable}, instead + * of using explicit comparators. + * + *

+ * If you are using an optimizing VM, you may find that ComparableTimSort offers no performance benefit over TimSort in + * conjunction with a comparator that simply returns {@code ((Comparable)first).compareTo(Second)}. If this is the case, you are + * better off deleting ComparableTimSort to eliminate the code duplication. (See Arrays.java for details.) */ +class ComparableTimSort { + /** This is the minimum sized sequence that will be merged. Shorter sequences will be lengthened by calling binarySort. If the + * entire array is less than this length, no merges will be performed. + * + * This constant should be a power of two. It was 64 in Tim Peter's C implementation, but 32 was empirically determined to work + * better in this implementation. In the unlikely event that you set this constant to be a number that's not a power of two, + * you'll need to change the {@link #minRunLength} computation. + * + * If you decrease this constant, you must change the stackLen computation in the TimSort constructor, or you risk an + * ArrayOutOfBounds exception. See listsort.txt for a discussion of the minimum stack length required as a function of the + * length of the array being sorted and the minimum merge sequence length. */ + private static final int MIN_MERGE = 32; + + /** The array being sorted. */ + private Object[] a; + + /** When we get into galloping mode, we stay there until both runs win less often than MIN_GALLOP consecutive times. */ + private static final int MIN_GALLOP = 7; + + /** This controls when we get *into* galloping mode. It is initialized to MIN_GALLOP. The mergeLo and mergeHi methods nudge it + * higher for random data, and lower for highly structured data. */ + private int minGallop = MIN_GALLOP; + + /** Maximum initial size of tmp array, which is used for merging. The array can grow to accommodate demand. + * + * Unlike Tim's original C version, we do not allocate this much storage when sorting smaller arrays. This change was required + * for performance. */ + private static final int INITIAL_TMP_STORAGE_LENGTH = 256; + + /** Temp storage for merges. */ + private Object[] tmp; + private int tmpCount; + + /** A stack of pending runs yet to be merged. Run i starts at address base[i] and extends for len[i] elements. It's always true + * (so long as the indices are in bounds) that: + * + * runBase[i] + runLen[i] == runBase[i + 1] + * + * so we could cut the storage for this, but it's a minor amount, and keeping all the info explicit simplifies the code. */ + private int stackSize = 0; // Number of pending runs on stack + private final int[] runBase; + private final int[] runLen; + + /** Asserts have been placed in if-statements for performace. To enable them, set this field to true and enable them in VM with + * a command line flag. If you modify this class, please do test the asserts! */ + private static final boolean DEBUG = false; + + ComparableTimSort () { + tmp = new Object[INITIAL_TMP_STORAGE_LENGTH]; + runBase = new int[40]; + runLen = new int[40]; + } + + public void doSort (Object[] a, int lo, int hi) { + stackSize = 0; + rangeCheck(a.length, lo, hi); + int nRemaining = hi - lo; + if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted + + // If array is small, do a "mini-TimSort" with no merges + if (nRemaining < MIN_MERGE) { + int initRunLen = countRunAndMakeAscending(a, lo, hi); + binarySort(a, lo, hi, lo + initRunLen); + return; + } + + this.a = a; + tmpCount = 0; + + /** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and + * merging runs to maintain stack invariant. */ + int minRun = minRunLength(nRemaining); + do { + // Identify next run + int runLen = countRunAndMakeAscending(a, lo, hi); + + // If run is short, extend to min(minRun, nRemaining) + if (runLen < minRun) { + int force = nRemaining <= minRun ? nRemaining : minRun; + binarySort(a, lo, lo + force, lo + runLen); + runLen = force; + } + + // Push run onto pending-run stack, and maybe merge + pushRun(lo, runLen); + mergeCollapse(); + + // Advance to find next run + lo += runLen; + nRemaining -= runLen; + } while (nRemaining != 0); + + // Merge all remaining runs to complete sort + if (DEBUG) assert lo == hi; + mergeForceCollapse(); + if (DEBUG) assert stackSize == 1; + + this.a = null; + Object[] tmp = this.tmp; + for (int i = 0, n = tmpCount; i < n; i++) + tmp[i] = null; + } + + /** Creates a TimSort instance to maintain the state of an ongoing sort. + * + * @param a the array to be sorted */ + private ComparableTimSort (Object[] a) { + this.a = a; + + // Allocate temp storage (which may be increased later if necessary) + int len = a.length; + Object[] newArray = new Object[len < 2 * INITIAL_TMP_STORAGE_LENGTH ? len >>> 1 : INITIAL_TMP_STORAGE_LENGTH]; + tmp = newArray; + + /* + * Allocate runs-to-be-merged stack (which cannot be expanded). The stack length requirements are described in listsort.txt. + * The C version always uses the same stack length (85), but this was measured to be too expensive when sorting "mid-sized" + * arrays (e.g., 100 elements) in Java. Therefore, we use smaller (but sufficiently large) stack lengths for smaller arrays. + * The "magic numbers" in the computation below must be changed if MIN_MERGE is decreased. See the MIN_MERGE declaration + * above for more information. + */ + int stackLen = (len < 120 ? 5 : len < 1542 ? 10 : len < 119151 ? 19 : 40); + runBase = new int[stackLen]; + runLen = new int[stackLen]; + } + + /* + * The next two methods (which are package private and static) constitute the entire API of this class. Each of these methods + * obeys the contract of the public method with the same signature in java.util.Arrays. + */ + + static void sort (Object[] a) { + sort(a, 0, a.length); + } + + static void sort (Object[] a, int lo, int hi) { + rangeCheck(a.length, lo, hi); + int nRemaining = hi - lo; + if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted + + // If array is small, do a "mini-TimSort" with no merges + if (nRemaining < MIN_MERGE) { + int initRunLen = countRunAndMakeAscending(a, lo, hi); + binarySort(a, lo, hi, lo + initRunLen); + return; + } + + /** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and + * merging runs to maintain stack invariant. */ + ComparableTimSort ts = new ComparableTimSort(a); + int minRun = minRunLength(nRemaining); + do { + // Identify next run + int runLen = countRunAndMakeAscending(a, lo, hi); + + // If run is short, extend to min(minRun, nRemaining) + if (runLen < minRun) { + int force = nRemaining <= minRun ? nRemaining : minRun; + binarySort(a, lo, lo + force, lo + runLen); + runLen = force; + } + + // Push run onto pending-run stack, and maybe merge + ts.pushRun(lo, runLen); + ts.mergeCollapse(); + + // Advance to find next run + lo += runLen; + nRemaining -= runLen; + } while (nRemaining != 0); + + // Merge all remaining runs to complete sort + if (DEBUG) assert lo == hi; + ts.mergeForceCollapse(); + if (DEBUG) assert ts.stackSize == 1; + } + + /** Sorts the specified portion of the specified array using a binary insertion sort. This is the best method for sorting small + * numbers of elements. It requires O(n log n) compares, but O(n^2) data movement (worst case). + * + * If the initial part of the specified range is already sorted, this method can take advantage of it: the method assumes that + * the elements from index {@code lo}, inclusive, to {@code start}, exclusive are already sorted. + * + * @param a the array in which a range is to be sorted + * @param lo the index of the first element in the range to be sorted + * @param hi the index after the last element in the range to be sorted + * @param start the index of the first element in the range that is not already known to be sorted (@code lo <= start <= hi} */ + @SuppressWarnings("fallthrough") + private static void binarySort (Object[] a, int lo, int hi, int start) { + if (DEBUG) assert lo <= start && start <= hi; + if (start == lo) start++; + for (; start < hi; start++) { + @SuppressWarnings("unchecked") + Comparable pivot = (Comparable)a[start]; + + // Set left (and right) to the index where a[start] (pivot) belongs + int left = lo; + int right = start; + if (DEBUG) assert left <= right; + /* + * Invariants: pivot >= all in [lo, left). pivot < all in [right, start). + */ + while (left < right) { + int mid = (left + right) >>> 1; + if (pivot.compareTo(a[mid]) < 0) + right = mid; + else + left = mid + 1; + } + if (DEBUG) assert left == right; + + /* + * The invariants still hold: pivot >= all in [lo, left) and pivot < all in [left, start), so pivot belongs at left. Note + * that if there are elements equal to pivot, left points to the first slot after them -- that's why this sort is stable. + * Slide elements over to make room to make room for pivot. + */ + int n = start - left; // The number of elements to move + // Switch is just an optimization for arraycopy in default case + switch (n) { + case 2: + a[left + 2] = a[left + 1]; + case 1: + a[left + 1] = a[left]; + break; + default: + System.arraycopy(a, left, a, left + 1, n); + } + a[left] = pivot; + } + } + + /** Returns the length of the run beginning at the specified position in the specified array and reverses the run if it is + * descending (ensuring that the run will always be ascending when the method returns). + * + * A run is the longest ascending sequence with: + * + * a[lo] <= a[lo + 1] <= a[lo + 2] <= ... + * + * or the longest descending sequence with: + * + * a[lo] > a[lo + 1] > a[lo + 2] > ... + * + * For its intended use in a stable mergesort, the strictness of the definition of "descending" is needed so that the call can + * safely reverse a descending sequence without violating stability. + * + * @param a the array in which a run is to be counted and possibly reversed + * @param lo index of the first element in the run + * @param hi index after the last element that may be contained in the run. It is required that @code{lo < hi}. + * @return the length of the run beginning at the specified position in the specified array */ + @SuppressWarnings("unchecked") + private static int countRunAndMakeAscending (Object[] a, int lo, int hi) { + if (DEBUG) assert lo < hi; + int runHi = lo + 1; + if (runHi == hi) return 1; + + // Find end of run, and reverse range if descending + if (((Comparable)a[runHi++]).compareTo(a[lo]) < 0) { // Descending + while (runHi < hi && ((Comparable)a[runHi]).compareTo(a[runHi - 1]) < 0) + runHi++; + reverseRange(a, lo, runHi); + } else { // Ascending + while (runHi < hi && ((Comparable)a[runHi]).compareTo(a[runHi - 1]) >= 0) + runHi++; + } + + return runHi - lo; + } + + /** Reverse the specified range of the specified array. + * + * @param a the array in which a range is to be reversed + * @param lo the index of the first element in the range to be reversed + * @param hi the index after the last element in the range to be reversed */ + private static void reverseRange (Object[] a, int lo, int hi) { + hi--; + while (lo < hi) { + Object t = a[lo]; + a[lo++] = a[hi]; + a[hi--] = t; + } + } + + /** Returns the minimum acceptable run length for an array of the specified length. Natural runs shorter than this will be + * extended with {@link #binarySort}. + * + * Roughly speaking, the computation is: + * + * If n < MIN_MERGE, return n (it's too small to bother with fancy stuff). Else if n is an exact power of 2, return + * MIN_MERGE/2. Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k is close to, but strictly less than, an + * exact power of 2. + * + * For the rationale, see listsort.txt. + * + * @param n the length of the array to be sorted + * @return the length of the minimum run to be merged */ + private static int minRunLength (int n) { + if (DEBUG) assert n >= 0; + int r = 0; // Becomes 1 if any 1 bits are shifted off + while (n >= MIN_MERGE) { + r |= (n & 1); + n >>= 1; + } + return n + r; + } + + /** Pushes the specified run onto the pending-run stack. + * + * @param runBase index of the first element in the run + * @param runLen the number of elements in the run */ + private void pushRun (int runBase, int runLen) { + this.runBase[stackSize] = runBase; + this.runLen[stackSize] = runLen; + stackSize++; + } + + /** Examines the stack of runs waiting to be merged and merges adjacent runs until the stack invariants are reestablished: + * + * 1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1] 2. runLen[i - 2] > runLen[i - 1] + * + * This method is called each time a new run is pushed onto the stack, so the invariants are guaranteed to hold for i < + * stackSize upon entry to the method. */ + private void mergeCollapse () { + while (stackSize > 1) { + int n = stackSize - 2; + if (n > 0 && runLen[n - 1] <= runLen[n] + runLen[n + 1]) { + if (runLen[n - 1] < runLen[n + 1]) n--; + mergeAt(n); + } else if (runLen[n] <= runLen[n + 1]) { + mergeAt(n); + } else { + break; // Invariant is established + } + } + } + + /** Merges all runs on the stack until only one remains. This method is called once, to complete the sort. */ + private void mergeForceCollapse () { + while (stackSize > 1) { + int n = stackSize - 2; + if (n > 0 && runLen[n - 1] < runLen[n + 1]) n--; + mergeAt(n); + } + } + + /** Merges the two runs at stack indices i and i+1. Run i must be the penultimate or antepenultimate run on the stack. In other + * words, i must be equal to stackSize-2 or stackSize-3. + * + * @param i stack index of the first of the two runs to merge */ + @SuppressWarnings("unchecked") + private void mergeAt (int i) { + if (DEBUG) assert stackSize >= 2; + if (DEBUG) assert i >= 0; + if (DEBUG) assert i == stackSize - 2 || i == stackSize - 3; + + int base1 = runBase[i]; + int len1 = runLen[i]; + int base2 = runBase[i + 1]; + int len2 = runLen[i + 1]; + if (DEBUG) assert len1 > 0 && len2 > 0; + if (DEBUG) assert base1 + len1 == base2; + + /* + * Record the length of the combined runs; if i is the 3rd-last run now, also slide over the last run (which isn't involved + * in this merge). The current run (i+1) goes away in any case. + */ + runLen[i] = len1 + len2; + if (i == stackSize - 3) { + runBase[i + 1] = runBase[i + 2]; + runLen[i + 1] = runLen[i + 2]; + } + stackSize--; + + /* + * Find where the first element of run2 goes in run1. Prior elements in run1 can be ignored (because they're already in + * place). + */ + int k = gallopRight((Comparable)a[base2], a, base1, len1, 0); + if (DEBUG) assert k >= 0; + base1 += k; + len1 -= k; + if (len1 == 0) return; + + /* + * Find where the last element of run1 goes in run2. Subsequent elements in run2 can be ignored (because they're already in + * place). + */ + len2 = gallopLeft((Comparable)a[base1 + len1 - 1], a, base2, len2, len2 - 1); + if (DEBUG) assert len2 >= 0; + if (len2 == 0) return; + + // Merge remaining runs, using tmp array with min(len1, len2) elements + if (len1 <= len2) + mergeLo(base1, len1, base2, len2); + else + mergeHi(base1, len1, base2, len2); + } + + /** Locates the position at which to insert the specified key into the specified sorted range; if the range contains an element + * equal to key, returns the index of the leftmost equal element. + * + * @param key the key whose insertion point to search for + * @param a the array in which to search + * @param base the index of the first element in the range + * @param len the length of the range; must be > 0 + * @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method + * will run. + * @return the int k, 0 <= k <= n such that a[b + k - 1] < key <= a[b + k], pretending that a[b - 1] is minus infinity and a[b + * + n] is infinity. In other words, key belongs at index b + k; or in other words, the first k elements of a should + * precede key, and the last n - k should follow it. */ + private static int gallopLeft (Comparable key, Object[] a, int base, int len, int hint) { + if (DEBUG) assert len > 0 && hint >= 0 && hint < len; + + int lastOfs = 0; + int ofs = 1; + if (key.compareTo(a[base + hint]) > 0) { + // Gallop right until a[base+hint+lastOfs] < key <= a[base+hint+ofs] + int maxOfs = len - hint; + while (ofs < maxOfs && key.compareTo(a[base + hint + ofs]) > 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to base + lastOfs += hint; + ofs += hint; + } else { // key <= a[base + hint] + // Gallop left until a[base+hint-ofs] < key <= a[base+hint-lastOfs] + final int maxOfs = hint + 1; + while (ofs < maxOfs && key.compareTo(a[base + hint - ofs]) <= 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to base + int tmp = lastOfs; + lastOfs = hint - ofs; + ofs = hint - tmp; + } + if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len; + + /* + * Now a[base+lastOfs] < key <= a[base+ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs. + * Do a binary search, with invariant a[base + lastOfs - 1] < key <= a[base + ofs]. + */ + lastOfs++; + while (lastOfs < ofs) { + int m = lastOfs + ((ofs - lastOfs) >>> 1); + + if (key.compareTo(a[base + m]) > 0) + lastOfs = m + 1; // a[base + m] < key + else + ofs = m; // key <= a[base + m] + } + if (DEBUG) assert lastOfs == ofs; // so a[base + ofs - 1] < key <= a[base + ofs] + return ofs; + } + + /** Like gallopLeft, except that if the range contains an element equal to key, gallopRight returns the index after the + * rightmost equal element. + * + * @param key the key whose insertion point to search for + * @param a the array in which to search + * @param base the index of the first element in the range + * @param len the length of the range; must be > 0 + * @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method + * will run. + * @return the int k, 0 <= k <= n such that a[b + k - 1] <= key < a[b + k] */ + private static int gallopRight (Comparable key, Object[] a, int base, int len, int hint) { + if (DEBUG) assert len > 0 && hint >= 0 && hint < len; + + int ofs = 1; + int lastOfs = 0; + if (key.compareTo(a[base + hint]) < 0) { + // Gallop left until a[b+hint - ofs] <= key < a[b+hint - lastOfs] + int maxOfs = hint + 1; + while (ofs < maxOfs && key.compareTo(a[base + hint - ofs]) < 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to b + int tmp = lastOfs; + lastOfs = hint - ofs; + ofs = hint - tmp; + } else { // a[b + hint] <= key + // Gallop right until a[b+hint + lastOfs] <= key < a[b+hint + ofs] + int maxOfs = len - hint; + while (ofs < maxOfs && key.compareTo(a[base + hint + ofs]) >= 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to b + lastOfs += hint; + ofs += hint; + } + if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len; + + /* + * Now a[b + lastOfs] <= key < a[b + ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs. + * Do a binary search, with invariant a[b + lastOfs - 1] <= key < a[b + ofs]. + */ + lastOfs++; + while (lastOfs < ofs) { + int m = lastOfs + ((ofs - lastOfs) >>> 1); + + if (key.compareTo(a[base + m]) < 0) + ofs = m; // key < a[b + m] + else + lastOfs = m + 1; // a[b + m] <= key + } + if (DEBUG) assert lastOfs == ofs; // so a[b + ofs - 1] <= key < a[b + ofs] + return ofs; + } + + /** Merges two adjacent runs in place, in a stable fashion. The first element of the first run must be greater than the first + * element of the second run (a[base1] > a[base2]), and the last element of the first run (a[base1 + len1-1]) must be greater + * than all elements of the second run. + * + * For performance, this method should be called only when len1 <= len2; its twin, mergeHi should be called if len1 >= len2. + * (Either method may be called if len1 == len2.) + * + * @param base1 index of first element in first run to be merged + * @param len1 length of first run to be merged (must be > 0) + * @param base2 index of first element in second run to be merged (must be aBase + aLen) + * @param len2 length of second run to be merged (must be > 0) */ + @SuppressWarnings("unchecked") + private void mergeLo (int base1, int len1, int base2, int len2) { + if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2; + + // Copy first run into temp array + Object[] a = this.a; // For performance + Object[] tmp = ensureCapacity(len1); + System.arraycopy(a, base1, tmp, 0, len1); + + int cursor1 = 0; // Indexes into tmp array + int cursor2 = base2; // Indexes int a + int dest = base1; // Indexes int a + + // Move first element of second run and deal with degenerate cases + a[dest++] = a[cursor2++]; + if (--len2 == 0) { + System.arraycopy(tmp, cursor1, a, dest, len1); + return; + } + if (len1 == 1) { + System.arraycopy(a, cursor2, a, dest, len2); + a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge + return; + } + + int minGallop = this.minGallop; // Use local variable for performance + outer: + while (true) { + int count1 = 0; // Number of times in a row that first run won + int count2 = 0; // Number of times in a row that second run won + + /* + * Do the straightforward thing until (if ever) one run starts winning consistently. + */ + do { + if (DEBUG) assert len1 > 1 && len2 > 0; + if (((Comparable)a[cursor2]).compareTo(tmp[cursor1]) < 0) { + a[dest++] = a[cursor2++]; + count2++; + count1 = 0; + if (--len2 == 0) break outer; + } else { + a[dest++] = tmp[cursor1++]; + count1++; + count2 = 0; + if (--len1 == 1) break outer; + } + } while ((count1 | count2) < minGallop); + + /* + * One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if + * ever) neither run appears to be winning consistently anymore. + */ + do { + if (DEBUG) assert len1 > 1 && len2 > 0; + count1 = gallopRight((Comparable)a[cursor2], tmp, cursor1, len1, 0); + if (count1 != 0) { + System.arraycopy(tmp, cursor1, a, dest, count1); + dest += count1; + cursor1 += count1; + len1 -= count1; + if (len1 <= 1) // len1 == 1 || len1 == 0 + break outer; + } + a[dest++] = a[cursor2++]; + if (--len2 == 0) break outer; + + count2 = gallopLeft((Comparable)tmp[cursor1], a, cursor2, len2, 0); + if (count2 != 0) { + System.arraycopy(a, cursor2, a, dest, count2); + dest += count2; + cursor2 += count2; + len2 -= count2; + if (len2 == 0) break outer; + } + a[dest++] = tmp[cursor1++]; + if (--len1 == 1) break outer; + minGallop--; + } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP); + if (minGallop < 0) minGallop = 0; + minGallop += 2; // Penalize for leaving gallop mode + } // End of "outer" loop + this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field + + if (len1 == 1) { + if (DEBUG) assert len2 > 0; + System.arraycopy(a, cursor2, a, dest, len2); + a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge + } else if (len1 == 0) { + throw new IllegalArgumentException("Comparison method violates its general contract!"); + } else { + if (DEBUG) assert len2 == 0; + if (DEBUG) assert len1 > 1; + System.arraycopy(tmp, cursor1, a, dest, len1); + } + } + + /** Like mergeLo, except that this method should be called only if len1 >= len2; mergeLo should be called if len1 <= len2. + * (Either method may be called if len1 == len2.) + * + * @param base1 index of first element in first run to be merged + * @param len1 length of first run to be merged (must be > 0) + * @param base2 index of first element in second run to be merged (must be aBase + aLen) + * @param len2 length of second run to be merged (must be > 0) */ + @SuppressWarnings("unchecked") + private void mergeHi (int base1, int len1, int base2, int len2) { + if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2; + + // Copy second run into temp array + Object[] a = this.a; // For performance + Object[] tmp = ensureCapacity(len2); + System.arraycopy(a, base2, tmp, 0, len2); + + int cursor1 = base1 + len1 - 1; // Indexes into a + int cursor2 = len2 - 1; // Indexes into tmp array + int dest = base2 + len2 - 1; // Indexes into a + + // Move last element of first run and deal with degenerate cases + a[dest--] = a[cursor1--]; + if (--len1 == 0) { + System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2); + return; + } + if (len2 == 1) { + dest -= len1; + cursor1 -= len1; + System.arraycopy(a, cursor1 + 1, a, dest + 1, len1); + a[dest] = tmp[cursor2]; + return; + } + + int minGallop = this.minGallop; // Use local variable for performance + outer: + while (true) { + int count1 = 0; // Number of times in a row that first run won + int count2 = 0; // Number of times in a row that second run won + + /* + * Do the straightforward thing until (if ever) one run appears to win consistently. + */ + do { + if (DEBUG) assert len1 > 0 && len2 > 1; + if (((Comparable)tmp[cursor2]).compareTo(a[cursor1]) < 0) { + a[dest--] = a[cursor1--]; + count1++; + count2 = 0; + if (--len1 == 0) break outer; + } else { + a[dest--] = tmp[cursor2--]; + count2++; + count1 = 0; + if (--len2 == 1) break outer; + } + } while ((count1 | count2) < minGallop); + + /* + * One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if + * ever) neither run appears to be winning consistently anymore. + */ + do { + if (DEBUG) assert len1 > 0 && len2 > 1; + count1 = len1 - gallopRight((Comparable)tmp[cursor2], a, base1, len1, len1 - 1); + if (count1 != 0) { + dest -= count1; + cursor1 -= count1; + len1 -= count1; + System.arraycopy(a, cursor1 + 1, a, dest + 1, count1); + if (len1 == 0) break outer; + } + a[dest--] = tmp[cursor2--]; + if (--len2 == 1) break outer; + + count2 = len2 - gallopLeft((Comparable)a[cursor1], tmp, 0, len2, len2 - 1); + if (count2 != 0) { + dest -= count2; + cursor2 -= count2; + len2 -= count2; + System.arraycopy(tmp, cursor2 + 1, a, dest + 1, count2); + if (len2 <= 1) break outer; // len2 == 1 || len2 == 0 + } + a[dest--] = a[cursor1--]; + if (--len1 == 0) break outer; + minGallop--; + } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP); + if (minGallop < 0) minGallop = 0; + minGallop += 2; // Penalize for leaving gallop mode + } // End of "outer" loop + this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field + + if (len2 == 1) { + if (DEBUG) assert len1 > 0; + dest -= len1; + cursor1 -= len1; + System.arraycopy(a, cursor1 + 1, a, dest + 1, len1); + a[dest] = tmp[cursor2]; // Move first elt of run2 to front of merge + } else if (len2 == 0) { + throw new IllegalArgumentException("Comparison method violates its general contract!"); + } else { + if (DEBUG) assert len1 == 0; + if (DEBUG) assert len2 > 0; + System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2); + } + } + + /** Ensures that the external array tmp has at least the specified number of elements, increasing its size if necessary. The + * size increases exponentially to ensure amortized linear time complexity. + * + * @param minCapacity the minimum required capacity of the tmp array + * @return tmp, whether or not it grew */ + private Object[] ensureCapacity (int minCapacity) { + tmpCount = Math.max(tmpCount, minCapacity); + if (tmp.length < minCapacity) { + // Compute smallest power of 2 > minCapacity + int newSize = minCapacity; + newSize |= newSize >> 1; + newSize |= newSize >> 2; + newSize |= newSize >> 4; + newSize |= newSize >> 8; + newSize |= newSize >> 16; + newSize++; + + if (newSize < 0) // Not bloody likely! + newSize = minCapacity; + else + newSize = Math.min(newSize, a.length >>> 1); + + Object[] newArray = new Object[newSize]; + tmp = newArray; + } + return tmp; + } + + /** Checks that fromIndex and toIndex are in range, and throws an appropriate exception if they aren't. + * + * @param arrayLen the length of the array + * @param fromIndex the index of the first element of the range + * @param toIndex the index after the last element of the range + * @throws IllegalArgumentException if fromIndex > toIndex + * @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or toIndex > arrayLen */ + private static void rangeCheck (int arrayLen, int fromIndex, int toIndex) { + if (fromIndex > toIndex) throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); + if (fromIndex < 0) throw new ArrayIndexOutOfBoundsException(fromIndex); + if (toIndex > arrayLen) throw new ArrayIndexOutOfBoundsException(toIndex); + } +} diff --git a/src/dorkbox/util/collections/FloatArray.java b/src/dorkbox/util/collections/FloatArray.java new file mode 100644 index 0000000..4a5c4d5 --- /dev/null +++ b/src/dorkbox/util/collections/FloatArray.java @@ -0,0 +1,425 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Arrays; + +import dorkbox.util.RandomUtil; + +/** A resizable, ordered or unordered float array. Avoids the boxing that occurs with ArrayList. If unordered, this class + * avoids a memory copy when removing elements (the last element is moved to the removed element's position). + * @author Nathan Sweet */ +public class FloatArray { + public float[] items; + public int size; + public boolean ordered; + + /** Creates an ordered array with a capacity of 16. */ + public FloatArray () { + this(true, 16); + } + + /** Creates an ordered array with the specified capacity. */ + public FloatArray (int capacity) { + this(true, capacity); + } + + /** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + public FloatArray (boolean ordered, int capacity) { + this.ordered = ordered; + items = new float[capacity]; + } + + /** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is + * ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be + * grown. */ + public FloatArray (FloatArray array) { + this.ordered = array.ordered; + size = array.size; + items = new float[size]; + System.arraycopy(array.items, 0, items, 0, size); + } + + /** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements, + * so any subsequent elements added will cause the backing array to be grown. */ + public FloatArray (float[] array) { + this(true, array, 0, array.length); + } + + /** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any + * subsequent elements added will cause the backing array to be grown. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. */ + public FloatArray (boolean ordered, float[] array, int startIndex, int count) { + this(ordered, count); + size = count; + System.arraycopy(array, startIndex, items, 0, count); + } + + public void add (float value) { + float[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size++] = value; + } + + public void add (float value1, float value2) { + float[] items = this.items; + if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + size += 2; + } + + public void add (float value1, float value2, float value3) { + float[] items = this.items; + if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + size += 3; + } + + public void add (float value1, float value2, float value3, float value4) { + float[] items = this.items; + if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5. + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + items[size + 3] = value4; + size += 4; + } + + public void addAll (FloatArray array) { + addAll(array.items, 0, array.size); + } + + public void addAll (FloatArray array, int offset, int length) { + if (offset + length > array.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); + addAll(array.items, offset, length); + } + + public void addAll (float... array) { + addAll(array, 0, array.length); + } + + public void addAll (float[] array, int offset, int length) { + float[] items = this.items; + int sizeNeeded = size + length; + if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(array, offset, items, size, length); + size += length; + } + + public float get (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + return items[index]; + } + + public void set (int index, float value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] = value; + } + + public void incr (int index, float value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] += value; + } + + public void mul (int index, float value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] *= value; + } + + public void insert (int index, float value) { + if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size); + float[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) + System.arraycopy(items, index, items, index + 1, size - index); + else + items[size] = items[index]; + size++; + items[index] = value; + } + + public void swap (int first, int second) { + if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size); + if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size); + float[] items = this.items; + float firstValue = items[first]; + items[first] = items[second]; + items[second] = firstValue; + } + + public boolean contains (float value) { + int i = size - 1; + float[] items = this.items; + while (i >= 0) + if (items[i--] == value) return true; + return false; + } + + public int indexOf (float value) { + float[] items = this.items; + for (int i = 0, n = size; i < n; i++) + if (items[i] == value) return i; + return -1; + } + + public int lastIndexOf (char value) { + float[] items = this.items; + for (int i = size - 1; i >= 0; i--) + if (items[i] == value) return i; + return -1; + } + + public boolean removeValue (float value) { + float[] items = this.items; + for (int i = 0, n = size; i < n; i++) { + if (items[i] == value) { + removeIndex(i); + return true; + } + } + return false; + } + + /** Removes and returns the item at the specified index. */ + public float removeIndex (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + float[] items = this.items; + float value = items[index]; + size--; + if (ordered) + System.arraycopy(items, index + 1, items, index, size - index); + else + items[index] = items[size]; + return value; + } + + /** Removes the items between the specified indices, inclusive. */ + public void removeRange (int start, int end) { + if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size); + if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end); + float[] items = this.items; + int count = end - start + 1; + if (ordered) + System.arraycopy(items, start + count, items, start, size - (start + count)); + else { + int lastIndex = this.size - 1; + for (int i = 0; i < count; i++) + items[start + i] = items[lastIndex - i]; + } + size -= count; + } + + /** Removes from this array all of elements contained in the specified array. + * @return true if this array was modified. */ + public boolean removeAll (FloatArray array) { + int size = this.size; + int startSize = size; + float[] items = this.items; + for (int i = 0, n = array.size; i < n; i++) { + float item = array.get(i); + for (int ii = 0; ii < size; ii++) { + if (item == items[ii]) { + removeIndex(ii); + size--; + break; + } + } + } + return size != startSize; + } + + /** Removes and returns the last item. */ + public float pop () { + return items[--size]; + } + + /** Returns the last item. */ + public float peek () { + return items[size - 1]; + } + + /** Returns the first item. */ + public float first () { + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[0]; + } + + /** Returns true if the array is empty. */ + public boolean isEmpty () { + return size == 0; + } + + public void clear () { + size = 0; + } + + /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items + * have been removed, or if it is known that more items will not be added. + * @return {@link #items} */ + public float[] shrink () { + if (items.length != size) resize(size); + return items; + } + + /** 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. + * @return {@link #items} */ + public float[] ensureCapacity (int additionalCapacity) { + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded)); + return items; + } + + /** Sets the array size, leaving any values beyond the current size undefined. + * @return {@link #items} */ + public float[] setSize (int newSize) { + if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize); + if (newSize > items.length) resize(Math.max(8, newSize)); + size = newSize; + return items; + } + + protected float[] resize (int newSize) { + float[] newItems = new float[newSize]; + float[] items = this.items; + System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length)); + this.items = newItems; + return newItems; + } + + public void sort () { + Arrays.sort(items, 0, size); + } + + public void reverse () { + float[] items = this.items; + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { + int ii = lastIndex - i; + float temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + public void shuffle () { + float[] items = this.items; + for (int i = size - 1; i >= 0; i--) { + int ii = RandomUtil.int_(i); + float temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + /** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is + * taken. */ + public void truncate (int newSize) { + if (size > newSize) size = newSize; + } + + /** Returns a random item from the array, or zero if the array is empty. */ + public float random () { + if (size == 0) return 0; + return items[RandomUtil.int_(0, size - 1)]; + } + + public float[] toArray () { + float[] array = new float[size]; + System.arraycopy(items, 0, array, 0, size); + return array; + } + + public int hashCode () { + if (!ordered) return super.hashCode(); + float[] items = this.items; + int h = 1; + for (int i = 0, n = size; i < n; i++) + h = h * 31 + Float.floatToIntBits(items[i]); + return h; + } + + public boolean equals (Object object) { + if (object == this) return true; + if (!ordered) return false; + if (!(object instanceof FloatArray)) return false; + FloatArray array = (FloatArray)object; + if (!array.ordered) return false; + int n = size; + if (n != array.size) return false; + float[] items1 = this.items; + float[] items2 = array.items; + for (int i = 0; i < n; i++) + if (items1[i] != items2[i]) return false; + return true; + } + + public boolean equals (Object object, float epsilon) { + if (object == this) return true; + if (!(object instanceof FloatArray)) return false; + FloatArray array = (FloatArray)object; + int n = size; + if (n != array.size) return false; + if (!ordered) return false; + if (!array.ordered) return false; + float[] items1 = this.items; + float[] items2 = array.items; + for (int i = 0; i < n; i++) + if (Math.abs(items1[i] - items2[i]) > epsilon) return false; + return true; + } + + public String toString () { + if (size == 0) return "[]"; + float[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(items[i]); + } + buffer.append(']'); + return buffer.toString(); + } + + public String toString (String separator) { + if (size == 0) return ""; + float[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(separator); + buffer.append(items[i]); + } + return buffer.toString(); + } + + /** @see #FloatArray(float[]) */ + static public FloatArray with (float... array) { + return new FloatArray(array); + } +} diff --git a/src/dorkbox/util/collections/IdentityMap.java b/src/dorkbox/util/collections/IdentityMap.java new file mode 100644 index 0000000..99ee9c6 --- /dev/null +++ b/src/dorkbox/util/collections/IdentityMap.java @@ -0,0 +1,795 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + +/** An unordered map that uses identity comparison for keys. This implementation is a cuckoo hash map using 3 hashes, random + * walking, and a small stash for problematic keys. Null keys are not allowed. Null values are 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 */ +public class IdentityMap implements Iterable> { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + + public int size; + + K[] keyTable; + V[] 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 IdentityMap () { + 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 IdentityMap (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 IdentityMap (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 = (V[])new Object[keyTable.length]; + } + + /** Creates a new map identical to the specified map. */ + public IdentityMap (IdentityMap 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 V put (K key, V value) { + if (key == null) throw new IllegalArgumentException("key cannot be null."); + K[] keyTable = this.keyTable; + + // Check for existing keys. + int hashCode = System.identityHashCode(key); + int index1 = hashCode & mask; + K key1 = keyTable[index1]; + if (key1 == key) { + V oldValue = valueTable[index1]; + valueTable[index1] = value; + return oldValue; + } + + int index2 = hash2(hashCode); + K key2 = keyTable[index2]; + if (key2 == key) { + V oldValue = valueTable[index2]; + valueTable[index2] = value; + return oldValue; + } + + int index3 = hash3(hashCode); + K key3 = keyTable[index3]; + if (key3 == key) { + V oldValue = valueTable[index3]; + valueTable[index3] = value; + return oldValue; + } + + // Update key in the stash. + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (keyTable[i] == key) { + V oldValue = valueTable[i]; + valueTable[i] = value; + return oldValue; + } + } + + // Check for empty buckets. + if (key1 == null) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + if (key2 == null) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + if (key3 == null) { + keyTable[index3] = key; + valueTable[index3] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + push(key, value, index1, key1, index2, key2, index3, key3); + return null; + } + + /** Skips checks for existing keys. */ + private void putResize (K key, V value) { + // Check for empty buckets. + int hashCode = System.identityHashCode(key); + 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, V insertValue, int index1, K key1, int index2, K key2, int index3, K key3) { + K[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + int mask = this.mask; + + // Push keys until an empty bucket is found. + K evictedKey; + V evictedValue; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(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 = System.identityHashCode(evictedKey); + 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, V 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++; + } + + public V get (K key) { + int hashCode = System.identityHashCode(key); + int index = hashCode & mask; + if (key != keyTable[index]) { + index = hash2(hashCode); + if (key != keyTable[index]) { + index = hash3(hashCode); + if (key != keyTable[index]) return getStash(key, null); + } + } + return valueTable[index]; + } + + public V get (K key, V defaultValue) { + int hashCode = System.identityHashCode(key); + int index = hashCode & mask; + if (key != keyTable[index]) { + index = hash2(hashCode); + if (key != keyTable[index]) { + index = hash3(hashCode); + if (key != keyTable[index]) return getStash(key, defaultValue); + } + } + return valueTable[index]; + } + + private V getStash (K key, V defaultValue) { + K[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (keyTable[i] == key) return valueTable[i]; + return defaultValue; + } + + public V remove (K key) { + int hashCode = System.identityHashCode(key); + int index = hashCode & mask; + if (keyTable[index] == key) { + keyTable[index] = null; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + index = hash2(hashCode); + if (keyTable[index] == key) { + keyTable[index] = null; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + index = hash3(hashCode); + if (keyTable[index] == key) { + keyTable[index] = null; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + return removeStash(key); + } + + V removeStash (K key) { + K[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (keyTable[i] == key) { + V oldValue = valueTable[i]; + removeStashIndex(i); + size--; + return oldValue; + } + } + return null; + } + + 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]; + valueTable[lastIndex] = null; + } else + valueTable[index] = 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 = MathUtil.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; + V[] valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) { + keyTable[i] = null; + valueTable[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. + * @param identity If true, uses == to compare the specified value with values in the map. If false, uses + * {@link #equals(Object)}. */ + public boolean containsValue (Object value, boolean identity) { + V[] valueTable = this.valueTable; + if (value == null) { + K[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != null && valueTable[i] == null) return true; + } else if (identity) { + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return true; + } else { + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) return true; + } + return false; + } + + public boolean containsKey (K key) { + int hashCode = System.identityHashCode(key); + int index = hashCode & mask; + if (key != keyTable[index]) { + index = hash2(hashCode); + if (key != keyTable[index]) { + index = hash3(hashCode); + if (key != 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 (keyTable[i] == key) 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. + * @param identity If true, uses == to compare the specified value with values in the map. If false, uses + * {@link #equals(Object)}. */ + public K findKey (Object value, boolean identity) { + V[] valueTable = this.valueTable; + if (value == null) { + K[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != null && valueTable[i] == null) return keyTable[i]; + } else if (identity) { + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return keyTable[i]; + } else { + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) 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(MathUtil.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; + V[] oldValueTable = valueTable; + + keyTable = (K[])new Object[newSize + stashCapacity]; + valueTable = (V[])new Object[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; + } + + public int hashCode () { + int h = 0; + K[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + K key = keyTable[i]; + if (key != null) { + h += key.hashCode() * 31; + + V value = valueTable[i]; + if (value != null) { + h += value.hashCode(); + } + } + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof IdentityMap)) return false; + IdentityMap other = (IdentityMap) obj; + if (other.size != size) return false; + K[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + K key = keyTable[i]; + if (key != null) { + V value = valueTable[i]; + if (value == null) { + if (!other.containsKey(key) || other.get(key) != null) { + return false; + } + } else { + if (!value.equals(other.get(key))) { + return false; + } + } + } + } + return true; + } + + public String toString () { + if (size == 0) return "[]"; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + K[] keyTable = this.keyTable; + V[] 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(); + } + + public Iterator> 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 V value; + + public String toString () { + return key + "=" + value; + } + } + + static private abstract class MapIterator implements Iterable, Iterator { + public boolean hasNext; + + final IdentityMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (IdentityMap 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; + map.valueTable[currentIndex] = null; + } + currentIndex = -1; + map.size--; + } + } + + static public class Entries extends MapIterator> { + private Entry entry = new Entry(); + + public Entries (IdentityMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + 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; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Iterator> iterator () { + return this; + } + } + + static public class Values extends MapIterator { + public Values (IdentityMap map) { + super((IdentityMap)map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public V next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + V value = map.valueTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return value; + } + + public Iterator iterator () { + return this; + } + + /** Returns a new array containing the remaining values. */ + public Array toArray () { + Array array = new Array(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + + /** Adds the remaining values to the specified array. */ + public void toArray (Array array) { + while (hasNext) + array.add(next()); + } + } + + static public class Keys extends MapIterator { + public Keys (IdentityMap map) { + super((IdentityMap)map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + 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; + } + + public Iterator 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; + } + } +} diff --git a/src/dorkbox/util/collections/IntArray.java b/src/dorkbox/util/collections/IntArray.java index 6ca4155..e1311e4 100644 --- a/src/dorkbox/util/collections/IntArray.java +++ b/src/dorkbox/util/collections/IntArray.java @@ -1,5 +1,7 @@ -/* - * Copyright 2010 Mario Zechner (contact@badlogicgames.com), Nathan Sweet (admin@esotericsoftware.com) +/******************************************************************************* + * 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. @@ -12,13 +14,10 @@ * 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. - */ - -// from libGDX + ******************************************************************************/ package dorkbox.util.collections; - import java.util.Arrays; import dorkbox.util.RandomUtil; @@ -42,110 +41,129 @@ public class IntArray { } /** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a - * memory copy. - * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ public IntArray (boolean ordered, int capacity) { this.ordered = ordered; - this.items = new int[capacity]; + items = new int[capacity]; } /** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is - * ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be - * grown. */ + * ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be + * grown. */ public IntArray (IntArray array) { this.ordered = array.ordered; - this.size = array.size; - this.items = new int[this.size]; - System.arraycopy(array.items, 0, this.items, 0, this.size); + size = array.size; + items = new int[size]; + System.arraycopy(array.items, 0, items, 0, size); } /** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements, - * so any subsequent elements added will cause the backing array to be grown. */ + * so any subsequent elements added will cause the backing array to be grown. */ public IntArray (int[] array) { this(true, array, 0, array.length); } /** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any - * subsequent elements added will cause the backing array to be grown. - * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a - * memory copy. */ + * subsequent elements added will cause the backing array to be grown. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. */ public IntArray (boolean ordered, int[] array, int startIndex, int count) { - this(ordered, array.length); - this.size = count; - System.arraycopy(array, startIndex, this.items, 0, count); + this(ordered, count); + size = count; + System.arraycopy(array, startIndex, items, 0, count); } public void add (int value) { int[] items = this.items; - if (this.size == items.length) { - items = resize(Math.max(8, (int)(this.size * 1.75f))); - } - items[this.size++] = value; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size++] = value; + } + + public void add (int value1, int value2) { + int[] items = this.items; + if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + size += 2; + } + + public void add (int value1, int value2, int value3) { + int[] items = this.items; + if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + size += 3; + } + + public void add (int value1, int value2, int value3, int value4) { + int[] items = this.items; + if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5. + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + items[size + 3] = value4; + size += 4; } public void addAll (IntArray array) { - addAll(array, 0, array.size); + addAll(array.items, 0, array.size); } public void addAll (IntArray array, int offset, int length) { - if (offset + length > array.size) { + if (offset + length > array.size) throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); - } addAll(array.items, offset, length); } - public void addAll (int[] array) { + public void addAll (int... array) { addAll(array, 0, array.length); } public void addAll (int[] array, int offset, int length) { int[] items = this.items; - int sizeNeeded = this.size + length; - if (sizeNeeded >= items.length) { - items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); - } - System.arraycopy(array, offset, items, this.size, length); - this.size += length; + int sizeNeeded = size + length; + if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(array, offset, items, size, length); + size += length; } public int get (int index) { - if (index >= this.size) { - throw new IndexOutOfBoundsException(String.valueOf(index)); - } - return this.items[index]; + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + return items[index]; } public void set (int index, int value) { - if (index >= this.size) { - throw new IndexOutOfBoundsException(String.valueOf(index)); - } - this.items[index] = value; + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] = value; + } + + public void incr (int index, int value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] += value; + } + + public void mul (int index, int value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] *= value; } public void insert (int index, int value) { - if (index > this.size) { - throw new IndexOutOfBoundsException(String.valueOf(index)); - } + if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size); int[] items = this.items; - if (this.size == items.length) { - items = resize(Math.max(8, (int)(this.size * 1.75f))); - } - if (this.ordered) { - System.arraycopy(items, index, items, index + 1, this.size - index); - } else { - items[this.size] = items[index]; - } - this.size++; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) + System.arraycopy(items, index, items, index + 1, size - index); + else + items[size] = items[index]; + size++; items[index] = value; } public void swap (int first, int second) { - if (first >= this.size) { - throw new IndexOutOfBoundsException(String.valueOf(first)); - } - if (second >= this.size) { - throw new IndexOutOfBoundsException(String.valueOf(second)); - } + if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size); + if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size); int[] items = this.items; int firstValue = items[first]; items[first] = items[second]; @@ -153,39 +171,30 @@ public class IntArray { } public boolean contains (int value) { - int i = this.size - 1; + int i = size - 1; int[] items = this.items; - while (i >= 0) { - if (items[i--] == value) { - return true; - } - } + while (i >= 0) + if (items[i--] == value) return true; return false; } public int indexOf (int value) { int[] items = this.items; - for (int i = 0, n = this.size; i < n; i++) { - if (items[i] == value) { - return i; - } - } + for (int i = 0, n = size; i < n; i++) + if (items[i] == value) return i; return -1; } public int lastIndexOf (int value) { int[] items = this.items; - for (int i = this.size - 1; i >= 0; i--) { - if (items[i] == value) { - return i; - } - } + for (int i = size - 1; i >= 0; i--) + if (items[i] == value) return i; return -1; } public boolean removeValue (int value) { int[] items = this.items; - for (int i = 0, n = this.size; i < n; i++) { + for (int i = 0, n = size; i < n; i++) { if (items[i] == value) { removeIndex(i); return true; @@ -196,22 +205,35 @@ public class IntArray { /** Removes and returns the item at the specified index. */ public int removeIndex (int index) { - if (index >= this.size) { - throw new IndexOutOfBoundsException(String.valueOf(index)); - } + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); int[] items = this.items; int value = items[index]; - this.size--; - if (this.ordered) { - System.arraycopy(items, index + 1, items, index, this.size - index); - } else { - items[index] = items[this.size]; - } + size--; + if (ordered) + System.arraycopy(items, index + 1, items, index, size - index); + else + items[index] = items[size]; return value; } + /** Removes the items between the specified indices, inclusive. */ + public void removeRange (int start, int end) { + if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size); + if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end); + int[] items = this.items; + int count = end - start + 1; + if (ordered) + System.arraycopy(items, start + count, items, start, size - (start + count)); + else { + int lastIndex = this.size - 1; + for (int i = 0; i < count; i++) + items[start + i] = items[lastIndex - i]; + } + size -= count; + } + /** Removes from this array all of elements contained in the specified array. - * @return true if this array was modified. */ + * @return true if this array was modified. */ public boolean removeAll (IntArray array) { int size = this.size; int startSize = size; @@ -231,136 +253,137 @@ public class IntArray { /** Removes and returns the last item. */ public int pop () { - return this.items[--this.size]; + return items[--size]; } /** Returns the last item. */ public int peek () { - return this.items[this.size - 1]; + return items[size - 1]; } /** Returns the first item. */ public int first () { - if (this.size == 0) { - throw new IllegalStateException("Array is empty."); - } - return this.items[0]; + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[0]; + } + + /** Returns true if the array is empty. */ + public boolean isEmpty () { + return size == 0; } public void clear () { - this.size = 0; + size = 0; } - /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items have - * been removed, or if it is known that more items will not be added. */ - public void shrink () { - resize(this.size); + /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items + * have been removed, or if it is known that more items will not be added. + * @return {@link #items} */ + public int[] shrink () { + if (items.length != size) resize(size); + return items; } - /** Increases the size of the backing array to acommodate the specified number of additional items. Useful before adding many - * items to avoid multiple backing array resizes. - * @return {@link #items} */ + /** 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. + * @return {@link #items} */ public int[] ensureCapacity (int additionalCapacity) { - int sizeNeeded = this.size + additionalCapacity; - if (sizeNeeded >= this.items.length) { - resize(Math.max(8, sizeNeeded)); - } - return this.items; + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded)); + return items; + } + + /** Sets the array size, leaving any values beyond the current size undefined. + * @return {@link #items} */ + public int[] setSize (int newSize) { + if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize); + if (newSize > items.length) resize(Math.max(8, newSize)); + size = newSize; + return items; } protected int[] resize (int newSize) { int[] newItems = new int[newSize]; int[] items = this.items; - System.arraycopy(items, 0, newItems, 0, Math.min(this.size, newItems.length)); + System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length)); this.items = newItems; return newItems; } public void sort () { - Arrays.sort(this.items, 0, this.size); + Arrays.sort(items, 0, size); } public void reverse () { - for (int i = 0, lastIndex = this.size - 1, n = this.size / 2; i < n; i++) { + int[] items = this.items; + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { int ii = lastIndex - i; - int temp = this.items[i]; - this.items[i] = this.items[ii]; - this.items[ii] = temp; + int temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; } } public void shuffle () { - for (int i = this.size - 1; i >= 0; i--) { + int[] items = this.items; + for (int i = size - 1; i >= 0; i--) { int ii = RandomUtil.int_(i); - int temp = this.items[i]; - this.items[i] = this.items[ii]; - this.items[ii] = temp; + int temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; } } /** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is - * taken. */ + * taken. */ public void truncate (int newSize) { - if (this.size > newSize) { - this.size = newSize; - } + if (size > newSize) size = newSize; } /** Returns a random item from the array, or zero if the array is empty. */ public int random () { - if (this.size == 0) { - return 0; - } - return this.items[RandomUtil.int_(0, this.size - 1)]; + if (size == 0) return 0; + return items[RandomUtil.int_(0, size - 1)]; } public int[] toArray () { - int[] array = new int[this.size]; - System.arraycopy(this.items, 0, array, 0, this.size); + int[] array = new int[size]; + System.arraycopy(items, 0, array, 0, size); return array; } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(this.items); - result = prime * result + (this.ordered ? 1231 : 1237); - result = prime * result + this.size; - return result; + public int hashCode () { + if (!ordered) return super.hashCode(); + int[] items = this.items; + int h = 1; + for (int i = 0, n = size; i < n; i++) + h = h * 31 + items[i]; + return h; } - @Override public boolean equals (Object object) { - if (object == this) { - return true; - } - if (!(object instanceof IntArray)) { - return false; - } + if (object == this) return true; + if (!ordered) return false; + if (!(object instanceof IntArray)) return false; IntArray array = (IntArray)object; - int n = this.size; - if (n != array.size) { - return false; - } - for (int i = 0; i < n; i++) { - if (this.items[i] != array.items[i]) { - return false; - } - } + if (!array.ordered) return false; + int n = size; + if (n != array.size) return false; + int[] items1 = this.items; + int[] items2 = array.items; + for (int i = 0; i < n; i++) + if (items[i] != array.items[i]) return false; return true; } - @Override public String toString () { - if (this.size == 0) { - return "[]"; - } + if (size == 0) return "[]"; int[] items = this.items; StringBuilder buffer = new StringBuilder(32); buffer.append('['); buffer.append(items[0]); - for (int i = 1; i < this.size; i++) { + for (int i = 1; i < size; i++) { buffer.append(", "); buffer.append(items[i]); } @@ -369,16 +392,19 @@ public class IntArray { } public String toString (String separator) { - if (this.size == 0) { - return ""; - } + if (size == 0) return ""; int[] items = this.items; StringBuilder buffer = new StringBuilder(32); buffer.append(items[0]); - for (int i = 1; i < this.size; i++) { + for (int i = 1; i < size; i++) { buffer.append(separator); buffer.append(items[i]); } return buffer.toString(); } + + /** @see #IntArray(int[]) */ + static public IntArray with (int... array) { + return new IntArray(array); + } } diff --git a/src/dorkbox/util/collections/IntFloatMap.java b/src/dorkbox/util/collections/IntFloatMap.java new file mode 100644 index 0000000..2d39113 --- /dev/null +++ b/src/dorkbox/util/collections/IntFloatMap.java @@ -0,0 +1,845 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + +/** An unordered map where the keys are ints and values are floats. 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 */ +public class IntFloatMap implements Iterable { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + private static final int EMPTY = 0; + + public int size; + + int[] keyTable; + float[] valueTable; + int capacity, stashSize; + float zeroValue; + boolean hasZeroValue; + + 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 IntFloatMap () { + 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 IntFloatMap (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 IntFloatMap (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 = new int[capacity + stashCapacity]; + valueTable = new float[keyTable.length]; + } + + /** Creates a new map identical to the specified map. */ + public IntFloatMap (IntFloatMap 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; + zeroValue = map.zeroValue; + hasZeroValue = map.hasZeroValue; + } + + public void put (int key, float value) { + if (key == 0) { + zeroValue = value; + if (!hasZeroValue) { + hasZeroValue = true; + size++; + } + return; + } + + int[] keyTable = this.keyTable; + + // Check for existing keys. + int index1 = key & mask; + int key1 = keyTable[index1]; + if (key == key1) { + valueTable[index1] = value; + return; + } + + int index2 = hash2(key); + int key2 = keyTable[index2]; + if (key == key2) { + valueTable[index2] = value; + return; + } + + int index3 = hash3(key); + int key3 = keyTable[index3]; + if (key == key3) { + valueTable[index3] = value; + return; + } + + // Update key in the stash. + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key == keyTable[i]) { + valueTable[i] = value; + return; + } + } + + // Check for empty buckets. + if (key1 == EMPTY) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + if (key2 == EMPTY) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + if (key3 == EMPTY) { + 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 (IntFloatMap map) { + for (Entry entry : map.entries()) + put(entry.key, entry.value); + } + + /** Skips checks for existing keys. */ + private void putResize (int key, float value) { + if (key == 0) { + zeroValue = value; + hasZeroValue = true; + return; + } + + // Check for empty buckets. + int index1 = key & mask; + int key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index2 = hash2(key); + int key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index3 = hash3(key); + int key3 = keyTable[index3]; + if (key3 == EMPTY) { + 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 (int insertKey, float insertValue, int index1, int key1, int index2, int key2, int index3, int key3) { + int[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + int mask = this.mask; + + // Push keys until an empty bucket is found. + int evictedKey; + float evictedValue; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(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. + index1 = evictedKey & mask; + key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = evictedKey; + valueTable[index1] = evictedValue; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index2 = hash2(evictedKey); + key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = evictedKey; + valueTable[index2] = evictedValue; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index3 = hash3(evictedKey); + key3 = keyTable[index3]; + if (key3 == EMPTY) { + 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 (int key, float 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 float get (int key, float defaultValue) { + if (key == 0) { + if (!hasZeroValue) return defaultValue; + return zeroValue; + } + int index = key & mask; + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return getStash(key, defaultValue); + } + } + return valueTable[index]; + } + + private float getStash (int key, float defaultValue) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key == 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 float getAndIncrement (int key, float defaultValue, float increment) { + if (key == 0) { + if (hasZeroValue) { + float value = zeroValue; + zeroValue += increment; + return value; + } else { + hasZeroValue = true; + zeroValue = defaultValue + increment; + ++size; + return defaultValue; + } + } + int index = key & mask; + if (key != keyTable[index]) { + index = hash2(key); + if (key != keyTable[index]) { + index = hash3(key); + if (key != keyTable[index]) return getAndIncrementStash(key, defaultValue, increment); + } + } + float value = valueTable[index]; + valueTable[index] = value + increment; + return value; + } + + private float getAndIncrementStash (int key, float defaultValue, float increment) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key == keyTable[i]) { + float value = valueTable[i]; + valueTable[i] = value + increment; + return value; + } + put(key, defaultValue + increment); + return defaultValue; + } + + public float remove (int key, float defaultValue) { + if (key == 0) { + if (!hasZeroValue) return defaultValue; + hasZeroValue = false; + size--; + return zeroValue; + } + + int index = key & mask; + if (key == keyTable[index]) { + keyTable[index] = EMPTY; + float oldValue = valueTable[index]; + size--; + return oldValue; + } + + index = hash2(key); + if (key == keyTable[index]) { + keyTable[index] = EMPTY; + float oldValue = valueTable[index]; + size--; + return oldValue; + } + + index = hash3(key); + if (key == keyTable[index]) { + keyTable[index] = EMPTY; + float oldValue = valueTable[index]; + size--; + return oldValue; + } + + return removeStash(key, defaultValue); + } + + float removeStash (int key, float defaultValue) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key == keyTable[i]) { + float 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]; + } + } + + /** 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 = MathUtil.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; + } + hasZeroValue = false; + size = 0; + resize(maximumCapacity); + } + + public void clear () { + if (size == 0) return; + int[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + keyTable[i] = EMPTY; + hasZeroValue = false; + 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 (float value) { + if (hasZeroValue && zeroValue == value) return true; + int[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != 0 && valueTable[i] == value) return true; + return false; + } + + /** 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 (float value, float epsilon) { + if (hasZeroValue && Math.abs(zeroValue - value) <= epsilon) return true; + float[] valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) + if (Math.abs(valueTable[i] - value) <= epsilon) return true; + return false; + } + + public boolean containsKey (int key) { + if (key == 0) return hasZeroValue; + int index = key & mask; + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return containsKeyStash(key); + } + } + return true; + } + + private boolean containsKeyStash (int key) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key == 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 int findKey (float value, int notFound) { + if (hasZeroValue && zeroValue == value) return 0; + int[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != 0 && valueTable[i] == value) return keyTable[i]; + return notFound; + } + + /** 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(MathUtil.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); + + int[] oldKeyTable = keyTable; + float[] oldValueTable = valueTable; + + keyTable = new int[newSize + stashCapacity]; + valueTable = new float[newSize + stashCapacity]; + + int oldSize = size; + size = hasZeroValue ? 1 : 0; + stashSize = 0; + if (oldSize > 0) { + for (int i = 0; i < oldEndIndex; i++) { + int key = oldKeyTable[i]; + if (key != EMPTY) 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; + } + + public int hashCode () { + int h = 0; + if (hasZeroValue) { + h += Float.floatToIntBits(zeroValue); + } + int[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + int key = keyTable[i]; + if (key != EMPTY) { + h += key * 31; + + float value = valueTable[i]; + h += Float.floatToIntBits(value); + } + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof IntFloatMap)) return false; + IntFloatMap other = (IntFloatMap)obj; + if (other.size != size) return false; + if (other.hasZeroValue != hasZeroValue) return false; + if (hasZeroValue && other.zeroValue != zeroValue) { + return false; + } + int[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + int key = keyTable[i]; + if (key != EMPTY) { + float otherValue = other.get(key, 0f); + if (otherValue == 0f && !other.containsKey(key)) return false; + float value = valueTable[i]; + if (otherValue != value) return false; + } + } + return true; + } + + public String toString () { + if (size == 0) return "{}"; + StringBuilder buffer = new StringBuilder(32); + buffer.append('{'); + int[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + int i = keyTable.length; + if (hasZeroValue) { + buffer.append("0="); + buffer.append(zeroValue); + } else { + while (i-- > 0) { + int key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(key); + buffer.append('='); + buffer.append(valueTable[i]); + break; + } + } + while (i-- > 0) { + int key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(", "); + buffer.append(key); + buffer.append('='); + buffer.append(valueTable[i]); + } + buffer.append('}'); + return buffer.toString(); + } + + public Iterator 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 int key; + public float value; + + public String toString () { + return key + "=" + value; + } + } + + static private class MapIterator { + static final int INDEX_ILLEGAL = -2; + static final int INDEX_ZERO = -1; + + public boolean hasNext; + + final IntFloatMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (IntFloatMap map) { + this.map = map; + reset(); + } + + public void reset () { + currentIndex = INDEX_ILLEGAL; + nextIndex = INDEX_ZERO; + if (map.hasZeroValue) + hasNext = true; + else + findNextIndex(); + } + + void findNextIndex () { + hasNext = false; + int[] keyTable = map.keyTable; + for (int n = map.capacity + map.stashSize; ++nextIndex < n;) { + if (keyTable[nextIndex] != EMPTY) { + hasNext = true; + break; + } + } + } + + public void remove () { + if (currentIndex == INDEX_ZERO && map.hasZeroValue) { + map.hasZeroValue = false; + } else if (currentIndex < 0) { + throw new IllegalStateException("next must be called before remove."); + } else if (currentIndex >= map.capacity) { + map.removeStashIndex(currentIndex); + nextIndex = currentIndex - 1; + findNextIndex(); + } else { + map.keyTable[currentIndex] = EMPTY; + } + currentIndex = INDEX_ILLEGAL; + map.size--; + } + } + + static public class Entries extends MapIterator implements Iterable, Iterator { + private Entry entry = new Entry(); + + public Entries (IntFloatMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + public Entry next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + int[] keyTable = map.keyTable; + if (nextIndex == INDEX_ZERO) { + entry.key = 0; + entry.value = map.zeroValue; + } else { + entry.key = keyTable[nextIndex]; + entry.value = map.valueTable[nextIndex]; + } + currentIndex = nextIndex; + findNextIndex(); + return entry; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Iterator iterator () { + return this; + } + + public void remove () { + super.remove(); + } + } + + static public class Values extends MapIterator { + public Values (IntFloatMap map) { + super(map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public float next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + float value; + if (nextIndex == INDEX_ZERO) + value = map.zeroValue; + else + value = map.valueTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return value; + } + + /** Returns a new array containing the remaining values. */ + public FloatArray toArray () { + FloatArray array = new FloatArray(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + } + + static public class Keys extends MapIterator { + public Keys (IntFloatMap map) { + super(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 key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return key; + } + + /** Returns a new array containing the remaining keys. */ + public IntArray toArray () { + IntArray array = new IntArray(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + } +} diff --git a/src/dorkbox/util/collections/IntIntMap.java b/src/dorkbox/util/collections/IntIntMap.java new file mode 100644 index 0000000..746e059 --- /dev/null +++ b/src/dorkbox/util/collections/IntIntMap.java @@ -0,0 +1,831 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + +/** An unordered map where the keys and values are ints. This implementation is a cuckoo hash map using 3 hashes, random walking, + * and a small stash for problematic keys. 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 */ +public class IntIntMap implements Iterable { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + private static final int EMPTY = 0; + + public int size; + + int[] keyTable, valueTable; + int capacity, stashSize; + int zeroValue; + boolean hasZeroValue; + + 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 IntIntMap () { + 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 IntIntMap (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 IntIntMap (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 = new int[capacity + stashCapacity]; + valueTable = new int[keyTable.length]; + } + + /** Creates a new map identical to the specified map. */ + public IntIntMap (IntIntMap 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; + zeroValue = map.zeroValue; + hasZeroValue = map.hasZeroValue; + } + + public void put (int key, int value) { + if (key == 0) { + zeroValue = value; + if (!hasZeroValue) { + hasZeroValue = true; + size++; + } + return; + } + + int[] keyTable = this.keyTable; + + // Check for existing keys. + int index1 = key & mask; + int key1 = keyTable[index1]; + if (key == key1) { + valueTable[index1] = value; + return; + } + + int index2 = hash2(key); + int key2 = keyTable[index2]; + if (key == key2) { + valueTable[index2] = value; + return; + } + + int index3 = hash3(key); + int key3 = keyTable[index3]; + if (key == key3) { + valueTable[index3] = value; + return; + } + + // Update key in the stash. + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key == keyTable[i]) { + valueTable[i] = value; + return; + } + } + + // Check for empty buckets. + if (key1 == EMPTY) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + if (key2 == EMPTY) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + if (key3 == EMPTY) { + 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 (IntIntMap map) { + for (Entry entry : map.entries()) + put(entry.key, entry.value); + } + + /** Skips checks for existing keys. */ + private void putResize (int key, int value) { + if (key == 0) { + zeroValue = value; + hasZeroValue = true; + return; + } + + // Check for empty buckets. + int index1 = key & mask; + int key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index2 = hash2(key); + int key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index3 = hash3(key); + int key3 = keyTable[index3]; + if (key3 == EMPTY) { + 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 (int insertKey, int insertValue, int index1, int key1, int index2, int key2, int index3, int key3) { + int[] keyTable = this.keyTable; + int[] valueTable = this.valueTable; + int mask = this.mask; + + // Push keys until an empty bucket is found. + int evictedKey; + int evictedValue; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(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. + index1 = evictedKey & mask; + key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = evictedKey; + valueTable[index1] = evictedValue; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index2 = hash2(evictedKey); + key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = evictedKey; + valueTable[index2] = evictedValue; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index3 = hash3(evictedKey); + key3 = keyTable[index3]; + if (key3 == EMPTY) { + 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 (int 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 (int key, int defaultValue) { + if (key == 0) { + if (!hasZeroValue) return defaultValue; + return zeroValue; + } + int index = key & mask; + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return getStash(key, defaultValue); + } + } + return valueTable[index]; + } + + private int getStash (int key, int defaultValue) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key == 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 (int key, int defaultValue, int increment) { + if (key == 0) { + if (hasZeroValue) { + int value = zeroValue; + zeroValue += increment; + return value; + } else { + hasZeroValue = true; + zeroValue = defaultValue + increment; + ++size; + return defaultValue; + } + } + int index = key & mask; + if (key != keyTable[index]) { + index = hash2(key); + if (key != keyTable[index]) { + index = hash3(key); + if (key != keyTable[index]) return getAndIncrementStash(key, defaultValue, increment); + } + } + int value = valueTable[index]; + valueTable[index] = value + increment; + return value; + } + + private int getAndIncrementStash (int key, int defaultValue, int increment) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key == keyTable[i]) { + int value = valueTable[i]; + valueTable[i] = value + increment; + return value; + } + put(key, defaultValue + increment); + return defaultValue; + } + + public int remove (int key, int defaultValue) { + if (key == 0) { + if (!hasZeroValue) return defaultValue; + hasZeroValue = false; + size--; + return zeroValue; + } + + int index = key & mask; + if (key == keyTable[index]) { + keyTable[index] = EMPTY; + int oldValue = valueTable[index]; + size--; + return oldValue; + } + + index = hash2(key); + if (key == keyTable[index]) { + keyTable[index] = EMPTY; + int oldValue = valueTable[index]; + size--; + return oldValue; + } + + index = hash3(key); + if (key == keyTable[index]) { + keyTable[index] = EMPTY; + int oldValue = valueTable[index]; + size--; + return oldValue; + } + + return removeStash(key, defaultValue); + } + + int removeStash (int key, int defaultValue) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key == 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]; + } + } + + /** 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 = MathUtil.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; + } + hasZeroValue = false; + size = 0; + resize(maximumCapacity); + } + + public void clear () { + if (size == 0) return; + int[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + keyTable[i] = EMPTY; + size = 0; + stashSize = 0; + hasZeroValue = false; + } + + /** 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) { + if (hasZeroValue && zeroValue == value) return true; + int[] keyTable = this.keyTable, valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != 0 && valueTable[i] == value) return true; + return false; + } + + public boolean containsKey (int key) { + if (key == 0) return hasZeroValue; + int index = key & mask; + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return containsKeyStash(key); + } + } + return true; + } + + private boolean containsKeyStash (int key) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key == 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 int findKey (int value, int notFound) { + if (hasZeroValue && zeroValue == value) return 0; + int[] keyTable = this.keyTable, valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != 0 && valueTable[i] == value) return keyTable[i]; + return notFound; + } + + /** 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(MathUtil.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); + + int[] oldKeyTable = keyTable; + int[] oldValueTable = valueTable; + + keyTable = new int[newSize + stashCapacity]; + valueTable = new int[newSize + stashCapacity]; + + int oldSize = size; + size = hasZeroValue ? 1 : 0; + stashSize = 0; + if (oldSize > 0) { + for (int i = 0; i < oldEndIndex; i++) { + int key = oldKeyTable[i]; + if (key != EMPTY) 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; + } + + public int hashCode () { + int h = 0; + if (hasZeroValue) { + h += Float.floatToIntBits(zeroValue); + } + int[] keyTable = this.keyTable; + int[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + int key = keyTable[i]; + if (key != EMPTY) { + h += key * 31; + + int value = valueTable[i]; + h += value; + } + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof IntIntMap)) return false; + IntIntMap other = (IntIntMap)obj; + if (other.size != size) return false; + if (other.hasZeroValue != hasZeroValue) return false; + if (hasZeroValue && other.zeroValue != zeroValue) { + return false; + } + int[] keyTable = this.keyTable; + int[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + int key = keyTable[i]; + if (key != EMPTY) { + 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; + } + + public String toString () { + if (size == 0) return "{}"; + StringBuilder buffer = new StringBuilder(32); + buffer.append('{'); + int[] keyTable = this.keyTable; + int[] valueTable = this.valueTable; + int i = keyTable.length; + if (hasZeroValue) { + buffer.append("0="); + buffer.append(zeroValue); + } else { + while (i-- > 0) { + int key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(key); + buffer.append('='); + buffer.append(valueTable[i]); + break; + } + } + while (i-- > 0) { + int key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(", "); + buffer.append(key); + buffer.append('='); + buffer.append(valueTable[i]); + } + buffer.append('}'); + return buffer.toString(); + } + + public Iterator 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 int key; + public int value; + + public String toString () { + return key + "=" + value; + } + } + + static private class MapIterator { + static final int INDEX_ILLEGAL = -2; + static final int INDEX_ZERO = -1; + + public boolean hasNext; + + final IntIntMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (IntIntMap map) { + this.map = map; + reset(); + } + + public void reset () { + currentIndex = INDEX_ILLEGAL; + nextIndex = INDEX_ZERO; + if (map.hasZeroValue) + hasNext = true; + else + findNextIndex(); + } + + void findNextIndex () { + hasNext = false; + int[] keyTable = map.keyTable; + for (int n = map.capacity + map.stashSize; ++nextIndex < n;) { + if (keyTable[nextIndex] != EMPTY) { + hasNext = true; + break; + } + } + } + + public void remove () { + if (currentIndex == INDEX_ZERO && map.hasZeroValue) { + map.hasZeroValue = false; + } else if (currentIndex < 0) { + throw new IllegalStateException("next must be called before remove."); + } else if (currentIndex >= map.capacity) { + map.removeStashIndex(currentIndex); + nextIndex = currentIndex - 1; + findNextIndex(); + } else { + map.keyTable[currentIndex] = EMPTY; + } + currentIndex = INDEX_ILLEGAL; + map.size--; + } + } + + static public class Entries extends MapIterator implements Iterable, Iterator { + private Entry entry = new Entry(); + + public Entries (IntIntMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + public Entry next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + int[] keyTable = map.keyTable; + if (nextIndex == INDEX_ZERO) { + entry.key = 0; + entry.value = map.zeroValue; + } else { + entry.key = keyTable[nextIndex]; + entry.value = map.valueTable[nextIndex]; + } + currentIndex = nextIndex; + findNextIndex(); + return entry; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Iterator iterator () { + return this; + } + + public void remove () { + super.remove(); + } + } + + static public class Values extends MapIterator { + public Values (IntIntMap map) { + super(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; + if (nextIndex == INDEX_ZERO) + value = map.zeroValue; + else + 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 { + public Keys (IntIntMap map) { + super(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 key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return key; + } + + /** Returns a new array containing the remaining keys. */ + public IntArray toArray () { + IntArray array = new IntArray(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + } +} diff --git a/src/dorkbox/util/collections/IntMap.java b/src/dorkbox/util/collections/IntMap.java index 3262ebe..f1e7ff7 100644 --- a/src/dorkbox/util/collections/IntMap.java +++ b/src/dorkbox/util/collections/IntMap.java @@ -1,5 +1,7 @@ -/* - * Copyright 2010 Mario Zechner (contact@badlogicgames.com), Nathan Sweet (admin@esotericsoftware.com) +/******************************************************************************* + * 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. @@ -12,30 +14,24 @@ * 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. - */ - -// slightly tweaked from libGDX, by dorkbox, llc + ******************************************************************************/ package dorkbox.util.collections; - import java.util.Iterator; import java.util.NoSuchElementException; import dorkbox.util.MathUtil; import dorkbox.util.RandomUtil; -/** An unordered map that uses int keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small stash - * for problematic keys. Null values are allowed. No allocation is done except when growing the table size.
+/** An unordered map that uses int keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small + * stash for problematic keys. Null values are 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({"rawtypes", "unchecked"}) -public class IntMap { - @SuppressWarnings("unused") +public class IntMap implements Iterable> { private static final int PRIME1 = 0xbe1f14b1; private static final int PRIME2 = 0xb4b82e39; private static final int PRIME3 = 0xced1c241; @@ -58,51 +54,57 @@ public class IntMap { private Values values1, values2; private Keys keys1, keys2; - /** Creates a new map with an initial capacity of 32 and a load factor of 0.8. This map will hold 25 items before growing the - * backing table. */ + /** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */ public IntMap () { - this(32, 0.8f); + this(51, 0.8f); } - /** Creates a new map with a load factor of 0.8. This map will hold initialCapacity * 0.8 items before growing the backing - * table. */ + /** 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 IntMap (int initialCapacity) { this(initialCapacity, 0.8f); } - /** Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity * loadFactor items - * before growing the backing table. */ + /** 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 IntMap (int initialCapacity, float loadFactor) { - if (initialCapacity < 0) { - throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); - } - if (this.capacity > 1 << 30) { - throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity); - } - this.capacity = MathUtil.nextPowerOfTwo(initialCapacity); + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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); - } + if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor); this.loadFactor = loadFactor; - this.threshold = (int)(this.capacity * loadFactor); - this.mask = this.capacity - 1; - this.hashShift = 31 - Integer.numberOfTrailingZeros(this.capacity); - this.stashCapacity = Math.max(3, (int)Math.ceil(Math.log(this.capacity)) * 2); - this.pushIterations = Math.max(Math.min(this.capacity, 8), (int)Math.sqrt(this.capacity) / 8); + 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); - this.keyTable = new int[this.capacity + this.stashCapacity]; - this.valueTable = (V[])new Object[this.keyTable.length]; + keyTable = new int[capacity + stashCapacity]; + valueTable = (V[])new Object[keyTable.length]; + } + + /** Creates a new map identical to the specified map. */ + public IntMap (IntMap 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; + zeroValue = map.zeroValue; + hasZeroValue = map.hasZeroValue; } public V put (int key, V value) { if (key == 0) { - V oldValue = this.zeroValue; - this.zeroValue = value; - if (!this.hasZeroValue) { - this.hasZeroValue = true; - this.size++; + V oldValue = zeroValue; + zeroValue = value; + if (!hasZeroValue) { + hasZeroValue = true; + size++; } return oldValue; } @@ -110,35 +112,35 @@ public class IntMap { int[] keyTable = this.keyTable; // Check for existing keys. - int index1 = key & this.mask; + int index1 = key & mask; int key1 = keyTable[index1]; if (key1 == key) { - V oldValue = this.valueTable[index1]; - this.valueTable[index1] = value; + V oldValue = valueTable[index1]; + valueTable[index1] = value; return oldValue; } int index2 = hash2(key); int key2 = keyTable[index2]; if (key2 == key) { - V oldValue = this.valueTable[index2]; - this.valueTable[index2] = value; + V oldValue = valueTable[index2]; + valueTable[index2] = value; return oldValue; } int index3 = hash3(key); int key3 = keyTable[index3]; if (key3 == key) { - V oldValue = this.valueTable[index3]; - this.valueTable[index3] = value; + V oldValue = valueTable[index3]; + valueTable[index3] = value; return oldValue; } // Update key in the stash. - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { + for (int i = capacity, n = i + stashSize; i < n; i++) { if (keyTable[i] == key) { - V oldValue = this.valueTable[i]; - this.valueTable[i] = value; + V oldValue = valueTable[i]; + valueTable[i] = value; return oldValue; } } @@ -146,28 +148,22 @@ public class IntMap { // Check for empty buckets. if (key1 == EMPTY) { keyTable[index1] = key; - this.valueTable[index1] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); return null; } if (key2 == EMPTY) { keyTable[index2] = key; - this.valueTable[index2] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); return null; } if (key3 == EMPTY) { keyTable[index3] = key; - this.valueTable[index3] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + valueTable[index3] = value; + if (size++ >= threshold) resize(capacity << 1); return null; } @@ -175,51 +171,44 @@ public class IntMap { return null; } - public void putAll (IntMap map) { - for (Entry entry : map.entries()) { + public void putAll (IntMap map) { + for (Entry entry : map.entries()) put(entry.key, entry.value); - } } /** Skips checks for existing keys. */ private void putResize (int key, V value) { if (key == 0) { - this.zeroValue = value; - this.hasZeroValue = true; + zeroValue = value; + hasZeroValue = true; return; } // Check for empty buckets. - int index1 = key & this.mask; - int key1 = this.keyTable[index1]; + int index1 = key & mask; + int key1 = keyTable[index1]; if (key1 == EMPTY) { - this.keyTable[index1] = key; - this.valueTable[index1] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); return; } int index2 = hash2(key); - int key2 = this.keyTable[index2]; + int key2 = keyTable[index2]; if (key2 == EMPTY) { - this.keyTable[index2] = key; - this.valueTable[index2] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); return; } int index3 = hash3(key); - int key3 = this.keyTable[index3]; + int key3 = keyTable[index3]; if (key3 == EMPTY) { - this.keyTable[index3] = key; - this.valueTable[index3] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + keyTable[index3] = key; + valueTable[index3] = value; + if (size++ >= threshold) resize(capacity << 1); return; } @@ -239,24 +228,24 @@ public class IntMap { do { // Replace the key and value for one of the hashes. switch (RandomUtil.int_(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; + 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. @@ -265,9 +254,7 @@ public class IntMap { if (key1 == EMPTY) { keyTable[index1] = evictedKey; valueTable[index1] = evictedValue; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + if (size++ >= threshold) resize(capacity << 1); return; } @@ -276,9 +263,7 @@ public class IntMap { if (key2 == EMPTY) { keyTable[index2] = evictedKey; valueTable[index2] = evictedValue; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + if (size++ >= threshold) resize(capacity << 1); return; } @@ -287,15 +272,11 @@ public class IntMap { if (key3 == EMPTY) { keyTable[index3] = evictedKey; valueTable[index3] = evictedValue; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + if (size++ >= threshold) resize(capacity << 1); return; } - if (++i == pushIterations) { - break; - } + if (++i == pushIterations) break; insertKey = evictedKey; insertValue = evictedValue; @@ -305,106 +286,93 @@ public class IntMap { } private void putStash (int key, V value) { - if (this.stashSize == this.stashCapacity) { + if (stashSize == stashCapacity) { // Too many pushes occurred and the stash is full, increase the table size. - resize(this.capacity << 1); - put(key, value); + resize(capacity << 1); + putResize(key, value); return; } // Store key in the stash. - int index = this.capacity + this.stashSize; - this.keyTable[index] = key; - this.valueTable[index] = value; - this.stashSize++; - this.size++; + int index = capacity + stashSize; + keyTable[index] = key; + valueTable[index] = value; + stashSize++; + size++; } public V get (int key) { if (key == 0) { - if (!this.hasZeroValue) { - return null; - } - return this.zeroValue; + if (!hasZeroValue) return null; + return zeroValue; } - int index = key & this.mask; - if (this.keyTable[index] != key) { + int index = key & mask; + if (keyTable[index] != key) { index = hash2(key); - if (this.keyTable[index] != key) { + if (keyTable[index] != key) { index = hash3(key); - if (this.keyTable[index] != key) { - return getStash(key, null); - } + if (keyTable[index] != key) return getStash(key, null); } } - return this.valueTable[index]; + return valueTable[index]; } public V get (int key, V defaultValue) { if (key == 0) { - if (!this.hasZeroValue) { - return defaultValue; - } - return this.zeroValue; + if (!hasZeroValue) return defaultValue; + return zeroValue; } - int index = key & this.mask; - if (this.keyTable[index] != key) { + int index = key & mask; + if (keyTable[index] != key) { index = hash2(key); - if (this.keyTable[index] != key) { + if (keyTable[index] != key) { index = hash3(key); - if (this.keyTable[index] != key) { - return getStash(key, defaultValue); - } + if (keyTable[index] != key) return getStash(key, defaultValue); } } - return this.valueTable[index]; + return valueTable[index]; } private V getStash (int key, V defaultValue) { int[] keyTable = this.keyTable; - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { - if (keyTable[i] == key) { - return this.valueTable[i]; - } - } + for (int i = capacity, n = i + stashSize; i < n; i++) + if (keyTable[i] == key) return valueTable[i]; return defaultValue; } public V remove (int key) { if (key == 0) { - if (!this.hasZeroValue) { - return null; - } - V oldValue = this.zeroValue; - this.zeroValue = null; - this.hasZeroValue = false; - this.size--; + if (!hasZeroValue) return null; + V oldValue = zeroValue; + zeroValue = null; + hasZeroValue = false; + size--; return oldValue; } - int index = key & this.mask; - if (this.keyTable[index] == key) { - this.keyTable[index] = EMPTY; - V oldValue = this.valueTable[index]; - this.valueTable[index] = null; - this.size--; + int index = key & mask; + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; return oldValue; } index = hash2(key); - if (this.keyTable[index] == key) { - this.keyTable[index] = EMPTY; - V oldValue = this.valueTable[index]; - this.valueTable[index] = null; - this.size--; + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; return oldValue; } index = hash3(key); - if (this.keyTable[index] == key) { - this.keyTable[index] = EMPTY; - V oldValue = this.valueTable[index]; - this.valueTable[index] = null; - this.size--; + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; return oldValue; } @@ -413,11 +381,11 @@ public class IntMap { V removeStash (int key) { int[] keyTable = this.keyTable; - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { + for (int i = capacity, n = i + stashSize; i < n; i++) { if (keyTable[i] == key) { - V oldValue = this.valueTable[i]; + V oldValue = valueTable[i]; removeStashIndex(i); - this.size--; + size--; return oldValue; } } @@ -426,80 +394,88 @@ public class IntMap { void removeStashIndex (int index) { // If the removed location was not last, move the last tuple to the removed location. - this.stashSize--; - int lastIndex = this.capacity + this.stashSize; + stashSize--; + int lastIndex = capacity + stashSize; if (index < lastIndex) { - this.keyTable[index] = this.keyTable[lastIndex]; - this.valueTable[index] = this.valueTable[lastIndex]; - this.valueTable[lastIndex] = null; - } else { - this.valueTable[index] = null; + keyTable[index] = keyTable[lastIndex]; + valueTable[index] = valueTable[lastIndex]; + valueTable[lastIndex] = null; + } else + valueTable[index] = 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 = MathUtil.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; } + zeroValue = null; + hasZeroValue = false; + size = 0; + resize(maximumCapacity); } public void clear () { + if (size == 0) return; int[] keyTable = this.keyTable; V[] valueTable = this.valueTable; - for (int i = this.capacity + this.stashSize; i-- > 0;) { + for (int i = capacity + stashSize; i-- > 0;) { keyTable[i] = EMPTY; valueTable[i] = null; } - this.size = 0; - this.stashSize = 0; - this.zeroValue = null; - this.hasZeroValue = false; + size = 0; + stashSize = 0; + zeroValue = null; + hasZeroValue = false; } - /** 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. - * @param identity If true, uses == to compare the specified value with values in the map. If false, uses - * {@link #equals(Object)}. */ + /** 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. + * @param identity If true, uses == to compare the specified value with values in the map. If false, uses + * {@link #equals(Object)}. */ public boolean containsValue (Object value, boolean identity) { V[] valueTable = this.valueTable; if (value == null) { - if (this.hasZeroValue && this.zeroValue == null) { - return true; - } + if (hasZeroValue && zeroValue == null) return true; int[] keyTable = this.keyTable; - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (keyTable[i] != EMPTY && valueTable[i] == null) { - return true; - } - } + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != EMPTY && valueTable[i] == null) return true; } else if (identity) { - if (value == this.zeroValue) { - return true; - } - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (valueTable[i] == value) { - return true; - } - } + if (value == zeroValue) return true; + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return true; } else { - if (this.hasZeroValue && value.equals(this.zeroValue)) { - return true; - } - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (value.equals(valueTable[i])) { - return true; - } - } + if (hasZeroValue && value.equals(zeroValue)) return true; + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) return true; } return false; } public boolean containsKey (int key) { - if (key == 0) { - return this.hasZeroValue; - } - int index = key & this.mask; - if (this.keyTable[index] != key) { + if (key == 0) return hasZeroValue; + int index = key & mask; + if (keyTable[index] != key) { index = hash2(key); - if (this.keyTable[index] != key) { + if (keyTable[index] != key) { index = hash3(key); - if (this.keyTable[index] != key) { - return containsKeyStash(key); - } + if (keyTable[index] != key) return containsKeyStash(key); } } return true; @@ -507,116 +483,143 @@ public class IntMap { private boolean containsKeyStash (int key) { int[] keyTable = this.keyTable; - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { - if (keyTable[i] == key) { - return true; - } - } + for (int i = capacity, n = i + stashSize; i < n; i++) + if (keyTable[i] == key) return true; return false; } /** Returns the key for the specified value, or notFound 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 - * {@link #equals(Object)}. */ + * 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 + * {@link #equals(Object)}. */ public int findKey (Object value, boolean identity, int notFound) { V[] valueTable = this.valueTable; if (value == null) { - if (this.hasZeroValue && this.zeroValue == null) { - return 0; - } + if (hasZeroValue && zeroValue == null) return 0; int[] keyTable = this.keyTable; - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (keyTable[i] != EMPTY && valueTable[i] == null) { - return keyTable[i]; - } - } + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != EMPTY && valueTable[i] == null) return keyTable[i]; } else if (identity) { - if (value == this.zeroValue) { - return 0; - } - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (valueTable[i] == value) { - return this.keyTable[i]; - } - } + if (value == zeroValue) return 0; + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return keyTable[i]; } else { - if (this.hasZeroValue && value.equals(this.zeroValue)) { - return 0; - } - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (value.equals(valueTable[i])) { - return this.keyTable[i]; - } - } + if (hasZeroValue && value.equals(zeroValue)) return 0; + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) return keyTable[i]; } return notFound; } - /** Increases the size of the backing array to acommodate the specified number of additional items. Useful before adding many - * items to avoid multiple backing array resizes. */ + /** 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) { - int sizeNeeded = this.size + additionalCapacity; - if (sizeNeeded >= this.threshold) { - resize(MathUtil.nextPowerOfTwo((int) (sizeNeeded / this.loadFactor))); - } + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor))); } private void resize (int newSize) { - int oldEndIndex = this.capacity + this.stashSize; + int oldEndIndex = capacity + stashSize; - this.capacity = newSize; - this.threshold = (int)(newSize * this.loadFactor); - this.mask = newSize - 1; - this.hashShift = 31 - Integer.numberOfTrailingZeros(newSize); - this.stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2); - this.pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8); + 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); - int[] oldKeyTable = this.keyTable; - V[] oldValueTable = this.valueTable; + int[] oldKeyTable = keyTable; + V[] oldValueTable = valueTable; - this.keyTable = new int[newSize + this.stashCapacity]; - this.valueTable = (V[])new Object[newSize + this.stashCapacity]; + keyTable = new int[newSize + stashCapacity]; + valueTable = (V[])new Object[newSize + stashCapacity]; - this.size = this.hasZeroValue ? 1 : 0; - this.stashSize = 0; - for (int i = 0; i < oldEndIndex; i++) { - int key = oldKeyTable[i]; - if (key != EMPTY) { - putResize(key, oldValueTable[i]); + int oldSize = size; + size = hasZeroValue ? 1 : 0; + stashSize = 0; + if (oldSize > 0) { + for (int i = 0; i < oldEndIndex; i++) { + int key = oldKeyTable[i]; + if (key != EMPTY) putResize(key, oldValueTable[i]); } } } private int hash2 (int h) { h *= PRIME2; - return (h ^ h >>> this.hashShift) & this.mask; + return (h ^ h >>> hashShift) & mask; } private int hash3 (int h) { h *= PRIME3; - return (h ^ h >>> this.hashShift) & this.mask; + return (h ^ h >>> hashShift) & mask; } - @Override - public String toString () { - if (this.size == 0) { - return "[]"; + public int hashCode () { + int h = 0; + if (hasZeroValue && zeroValue != null) { + h += zeroValue.hashCode(); } + int[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + int key = keyTable[i]; + if (key != EMPTY) { + h += key * 31; + + V value = valueTable[i]; + if (value != null) { + h += value.hashCode(); + } + } + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof IntMap)) return false; + IntMap other = (IntMap)obj; + if (other.size != size) return false; + if (other.hasZeroValue != hasZeroValue) return false; + if (hasZeroValue) { + if (other.zeroValue == null) { + if (zeroValue != null) return false; + } else { + if (!other.zeroValue.equals(zeroValue)) return false; + } + } + int[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + int key = keyTable[i]; + if (key != EMPTY) { + V value = valueTable[i]; + if (value == null) { + if (!other.containsKey(key) || other.get(key) != null) return false; + } else { + if (!value.equals(other.get(key))) return false; + } + } + } + return true; + } + + public String toString () { + if (size == 0) return "[]"; StringBuilder buffer = new StringBuilder(32); buffer.append('['); int[] keyTable = this.keyTable; V[] valueTable = this.valueTable; int i = keyTable.length; - if (this.hasZeroValue) { + if (hasZeroValue) { buffer.append("0="); - buffer.append(this.zeroValue); + buffer.append(zeroValue); } else { while (i-- > 0) { int key = keyTable[i]; - if (key == EMPTY) { - continue; - } + if (key == EMPTY) continue; buffer.append(key); buffer.append('='); buffer.append(valueTable[i]); @@ -625,9 +628,7 @@ public class IntMap { } while (i-- > 0) { int key = keyTable[i]; - if (key == EMPTY) { - continue; - } + if (key == EMPTY) continue; buffer.append(", "); buffer.append(key); buffer.append('='); @@ -637,70 +638,73 @@ public class IntMap { return buffer.toString(); } + public Iterator> 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. */ + * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */ public Entries entries () { - if (this.entries1 == null) { - this.entries1 = new Entries(this); - this.entries2 = new Entries(this); + if (entries1 == null) { + entries1 = new Entries(this); + entries2 = new Entries(this); } - if (!this.entries1.valid) { - this.entries1.reset(); - this.entries1.valid = true; - this.entries2.valid = false; - return this.entries1; + if (!entries1.valid) { + entries1.reset(); + entries1.valid = true; + entries2.valid = false; + return entries1; } - this.entries2.reset(); - this.entries2.valid = true; - this.entries1.valid = false; - return this.entries2; + 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. */ + * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. */ public Values values () { - if (this.values1 == null) { - this.values1 = new Values(this); - this.values2 = new Values(this); + if (values1 == null) { + values1 = new Values(this); + values2 = new Values(this); } - if (!this.values1.valid) { - this.values1.reset(); - this.values1.valid = true; - this.values2.valid = false; - return this.values1; + if (!values1.valid) { + values1.reset(); + values1.valid = true; + values2.valid = false; + return values1; } - this.values2.reset(); - this.values2.valid = true; - this.values1.valid = false; - return this.values2; + 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. */ + /** 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 (this.keys1 == null) { - this.keys1 = new Keys(this); - this.keys2 = new Keys(this); + if (keys1 == null) { + keys1 = new Keys(this); + keys2 = new Keys(this); } - if (!this.keys1.valid) { - this.keys1.reset(); - this.keys1.valid = true; - this.keys2.valid = false; - return this.keys1; + if (!keys1.valid) { + keys1.reset(); + keys1.valid = true; + keys2.valid = false; + return keys1; } - this.keys2.reset(); - this.keys2.valid = true; - this.keys1.valid = false; - return this.keys2; + keys2.reset(); + keys2.valid = true; + keys1.valid = false; + return keys2; } static public class Entry { public int key; public V value; - @Override public String toString () { - return this.key + "=" + this.value; + return key + "=" + value; } } @@ -720,40 +724,41 @@ public class IntMap { } public void reset () { - this.currentIndex = INDEX_ILLEGAL; - this.nextIndex = INDEX_ZERO; - if (this.map.hasZeroValue) { - this.hasNext = true; - } else { + currentIndex = INDEX_ILLEGAL; + nextIndex = INDEX_ZERO; + if (map.hasZeroValue) + hasNext = true; + else findNextIndex(); - } } void findNextIndex () { - this.hasNext = false; - int[] keyTable = this.map.keyTable; - for (int n = this.map.capacity + this.map.stashSize; ++this.nextIndex < n;) { - if (keyTable[this.nextIndex] != EMPTY) { - this.hasNext = true; + hasNext = false; + int[] keyTable = map.keyTable; + for (int n = map.capacity + map.stashSize; ++nextIndex < n;) { + if (keyTable[nextIndex] != EMPTY) { + hasNext = true; break; } } } public void remove () { - if (this.currentIndex == INDEX_ZERO && this.map.hasZeroValue) { - this.map.zeroValue = null; - this.map.hasZeroValue = false; - } else if (this.currentIndex < 0) { + if (currentIndex == INDEX_ZERO && map.hasZeroValue) { + map.zeroValue = null; + map.hasZeroValue = false; + } else if (currentIndex < 0) { throw new IllegalStateException("next must be called before remove."); - } else if (this.currentIndex >= this.map.capacity) { - this.map.removeStashIndex(this.currentIndex); + } else if (currentIndex >= map.capacity) { + map.removeStashIndex(currentIndex); + nextIndex = currentIndex - 1; + findNextIndex(); } else { - this.map.keyTable[this.currentIndex] = EMPTY; - this.map.valueTable[this.currentIndex] = null; + map.keyTable[currentIndex] = EMPTY; + map.valueTable[currentIndex] = null; } - this.currentIndex = INDEX_ILLEGAL; - this.map.size--; + currentIndex = INDEX_ILLEGAL; + map.size--; } } @@ -765,36 +770,34 @@ public class IntMap { } /** Note the same entry instance is returned each time this method is called. */ - @Override public Entry next () { - if (!this.hasNext) { - throw new NoSuchElementException(); - } - if (!this.valid) { - throw new RuntimeException("#iterator() cannot be used nested."); - } - int[] keyTable = this.map.keyTable; - if (this.nextIndex == INDEX_ZERO) { - this.entry.key = 0; - this.entry.value = this.map.zeroValue; + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + int[] keyTable = map.keyTable; + if (nextIndex == INDEX_ZERO) { + entry.key = 0; + entry.value = map.zeroValue; } else { - this.entry.key = keyTable[this.nextIndex]; - this.entry.value = this.map.valueTable[this.nextIndex]; + entry.key = keyTable[nextIndex]; + entry.value = map.valueTable[nextIndex]; } - this.currentIndex = this.nextIndex; + currentIndex = nextIndex; findNextIndex(); - return this.entry; + return entry; } - @Override public boolean hasNext () { - return this.hasNext; + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; } - @Override public Iterator> iterator () { return this; } + + public void remove () { + super.remove(); + } } static public class Values extends MapIterator implements Iterable, Iterator { @@ -802,43 +805,39 @@ public class IntMap { super(map); } - @Override public boolean hasNext () { - return this.hasNext; + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; } - @Override public V next () { - if (!this.hasNext) { - throw new NoSuchElementException(); - } - if (!this.valid) { - throw new RuntimeException("#iterator() cannot be used nested."); - } + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); V value; - if (this.nextIndex == INDEX_ZERO) { - value = this.map.zeroValue; - } else { - value = this.map.valueTable[this.nextIndex]; - } - this.currentIndex = this.nextIndex; + if (nextIndex == INDEX_ZERO) + value = map.zeroValue; + else + value = map.valueTable[nextIndex]; + currentIndex = nextIndex; findNextIndex(); return value; } - @Override public Iterator iterator () { return this; } /** Returns a new array containing the remaining values. */ -// public Array toArray () { -// Array array = new Array(true, map.size); -// while (hasNext) { -// array.add(next()); -// } -// return array; -// } + public Array toArray () { + Array array = new Array(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + + public void remove () { + super.remove(); + } } static public class Keys extends MapIterator { @@ -847,24 +846,19 @@ public class IntMap { } public int next () { - if (!this.hasNext) { - throw new NoSuchElementException(); - } - if (!this.valid) { - throw new RuntimeException("#iterator() cannot be used nested."); - } - int key = this.nextIndex == INDEX_ZERO ? 0 : this.map.keyTable[this.nextIndex]; - this.currentIndex = this.nextIndex; + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + int key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex]; + currentIndex = nextIndex; findNextIndex(); return key; } /** Returns a new array containing the remaining keys. */ public IntArray toArray () { - IntArray array = new IntArray(true, this.map.size); - while (this.hasNext) { + IntArray array = new IntArray(true, map.size); + while (hasNext) array.add(next()); - } return array; } } diff --git a/src/dorkbox/util/collections/IntSet.java b/src/dorkbox/util/collections/IntSet.java new file mode 100644 index 0000000..36f96af --- /dev/null +++ b/src/dorkbox/util/collections/IntSet.java @@ -0,0 +1,573 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + +/** An unordered set that uses int keys. This implementation uses cuckoo hashing using 3 hashes, random walking, and a small stash + * for problematic keys. No allocation is done except when growing the table size.
+ *
+ * This set performs very fast contains and remove (typically O(1), worst case O(log(n))). Add may be a bit slower, depending on + * hash collisions. Load factors greater than 0.91 greatly increase the chances the set will have to rehash to the next higher POT + * size. + * @author Nathan Sweet */ +public class IntSet { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + private static final int EMPTY = 0; + + public int size; + + int[] keyTable; + int capacity, stashSize; + boolean hasZeroValue; + + private float loadFactor; + private int hashShift, mask, threshold; + private int stashCapacity; + private int pushIterations; + + private IntSetIterator iterator1, iterator2; + + /** Creates a new set with an initial capacity of 51 and a load factor of 0.8. */ + public IntSet () { + this(51, 0.8f); + } + + /** Creates a new set 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 IntSet (int initialCapacity) { + this(initialCapacity, 0.8f); + } + + /** Creates a new set with the specified initial capacity and load factor. This set 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 IntSet (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 = new int[capacity + stashCapacity]; + } + + /** Creates a new set identical to the specified set. */ + public IntSet (IntSet set) { + this((int)Math.floor(set.capacity * set.loadFactor), set.loadFactor); + stashSize = set.stashSize; + System.arraycopy(set.keyTable, 0, keyTable, 0, set.keyTable.length); + size = set.size; + hasZeroValue = set.hasZeroValue; + } + + /** Returns true if the key was not already in the set. */ + public boolean add (int key) { + if (key == 0) { + if (hasZeroValue) return false; + hasZeroValue = true; + size++; + return true; + } + + int[] keyTable = this.keyTable; + + // Check for existing keys. + int index1 = key & mask; + int key1 = keyTable[index1]; + if (key1 == key) return false; + + int index2 = hash2(key); + int key2 = keyTable[index2]; + if (key2 == key) return false; + + int index3 = hash3(key); + int key3 = keyTable[index3]; + if (key3 == key) return false; + + // Find key in the stash. + for (int i = capacity, n = i + stashSize; i < n; i++) + if (keyTable[i] == key) return false; + + // Check for empty buckets. + if (key1 == EMPTY) { + keyTable[index1] = key; + if (size++ >= threshold) resize(capacity << 1); + return true; + } + + if (key2 == EMPTY) { + keyTable[index2] = key; + if (size++ >= threshold) resize(capacity << 1); + return true; + } + + if (key3 == EMPTY) { + keyTable[index3] = key; + if (size++ >= threshold) resize(capacity << 1); + return true; + } + + push(key, index1, key1, index2, key2, index3, key3); + return true; + } + + public void addAll (IntArray array) { + addAll(array.items, 0, array.size); + } + + public void addAll (IntArray array, int offset, int length) { + if (offset + length > array.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); + addAll(array.items, offset, length); + } + + public void addAll (int... array) { + addAll(array, 0, array.length); + } + + public void addAll (int[] array, int offset, int length) { + ensureCapacity(length); + for (int i = offset, n = i + length; i < n; i++) + add(array[i]); + } + + public void addAll (IntSet set) { + ensureCapacity(set.size); + IntSetIterator iterator = set.iterator(); + while (iterator.hasNext) + add(iterator.next()); + } + + /** Skips checks for existing keys. */ + private void addResize (int key) { + if (key == 0) { + hasZeroValue = true; + return; + } + + // Check for empty buckets. + int index1 = key & mask; + int key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = key; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index2 = hash2(key); + int key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = key; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index3 = hash3(key); + int key3 = keyTable[index3]; + if (key3 == EMPTY) { + keyTable[index3] = key; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + push(key, index1, key1, index2, key2, index3, key3); + } + + private void push (int insertKey, int index1, int key1, int index2, int key2, int index3, int key3) { + int[] keyTable = this.keyTable; + + int mask = this.mask; + + // Push keys until an empty bucket is found. + int evictedKey; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(2)) { + case 0: + evictedKey = key1; + keyTable[index1] = insertKey; + break; + case 1: + evictedKey = key2; + keyTable[index2] = insertKey; + break; + default: + evictedKey = key3; + keyTable[index3] = insertKey; + break; + } + + // If the evicted key hashes to an empty bucket, put it there and stop. + index1 = evictedKey & mask; + key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = evictedKey; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index2 = hash2(evictedKey); + key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = evictedKey; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index3 = hash3(evictedKey); + key3 = keyTable[index3]; + if (key3 == EMPTY) { + keyTable[index3] = evictedKey; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + if (++i == pushIterations) break; + + insertKey = evictedKey; + } while (true); + + addStash(evictedKey); + } + + private void addStash (int key) { + if (stashSize == stashCapacity) { + // Too many pushes occurred and the stash is full, increase the table size. + resize(capacity << 1); + addResize(key); + return; + } + // Store key in the stash. + int index = capacity + stashSize; + keyTable[index] = key; + stashSize++; + size++; + } + + /** Returns true if the key was removed. */ + public boolean remove (int key) { + if (key == 0) { + if (!hasZeroValue) return false; + hasZeroValue = false; + size--; + return true; + } + + int index = key & mask; + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + size--; + return true; + } + + index = hash2(key); + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + size--; + return true; + } + + index = hash3(key); + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + size--; + return true; + } + + return removeStash(key); + } + + boolean removeStash (int key) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (keyTable[i] == key) { + removeStashIndex(i); + size--; + return true; + } + } + return false; + } + + 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]; + } + + /** Returns true if the set 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 set 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 = MathUtil.nextPowerOfTwo(maximumCapacity); + resize(maximumCapacity); + } + + /** Clears the set 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; + } + hasZeroValue = false; + size = 0; + resize(maximumCapacity); + } + + public void clear () { + if (size == 0) return; + int[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + keyTable[i] = EMPTY; + size = 0; + stashSize = 0; + hasZeroValue = false; + } + + public boolean contains (int key) { + if (key == 0) return hasZeroValue; + int index = key & mask; + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return containsKeyStash(key); + } + } + return true; + } + + private boolean containsKeyStash (int key) { + int[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (keyTable[i] == key) return true; + return false; + } + + public int first () { + if (hasZeroValue) return 0; + int[] keyTable = this.keyTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) + if (keyTable[i] != EMPTY) return keyTable[i]; + throw new IllegalStateException("IntSet is empty."); + } + + /** 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(MathUtil.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); + + int[] oldKeyTable = keyTable; + + keyTable = new int[newSize + stashCapacity]; + + int oldSize = size; + size = hasZeroValue ? 1 : 0; + stashSize = 0; + if (oldSize > 0) { + for (int i = 0; i < oldEndIndex; i++) { + int key = oldKeyTable[i]; + if (key != EMPTY) addResize(key); + } + } + } + + private int hash2 (int h) { + h *= PRIME2; + return (h ^ h >>> hashShift) & mask; + } + + private int hash3 (int h) { + h *= PRIME3; + return (h ^ h >>> hashShift) & mask; + } + + public int hashCode () { + int h = 0; + for (int i = 0, n = capacity + stashSize; i < n; i++) + if (keyTable[i] != EMPTY) h += keyTable[i]; + return h; + } + + public boolean equals (Object obj) { + if (!(obj instanceof IntSet)) return false; + IntSet other = (IntSet)obj; + if (other.size != size) return false; + if (other.hasZeroValue != hasZeroValue) return false; + for (int i = 0, n = capacity + stashSize; i < n; i++) + if (keyTable[i] != EMPTY && !other.contains(keyTable[i])) return false; + return true; + } + + public String toString () { + if (size == 0) return "[]"; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + int[] keyTable = this.keyTable; + int i = keyTable.length; + if (hasZeroValue) + buffer.append("0"); + else { + while (i-- > 0) { + int key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(key); + break; + } + } + while (i-- > 0) { + int key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(", "); + buffer.append(key); + } + buffer.append(']'); + return buffer.toString(); + } + + /** Returns an iterator for the keys in the set. Remove is supported. Note that the same iterator instance is returned each time + * this method is called. Use the {@link IntSetIterator} constructor for nested or multithreaded iteration. */ + public IntSetIterator iterator () { + if (iterator1 == null) { + iterator1 = new IntSetIterator(this); + iterator2 = new IntSetIterator(this); + } + if (!iterator1.valid) { + iterator1.reset(); + iterator1.valid = true; + iterator2.valid = false; + return iterator1; + } + iterator2.reset(); + iterator2.valid = true; + iterator1.valid = false; + return iterator2; + } + + static public IntSet with (int... array) { + IntSet set = new IntSet(); + set.addAll(array); + return set; + } + + static public class IntSetIterator { + static final int INDEX_ILLEGAL = -2; + static final int INDEX_ZERO = -1; + + public boolean hasNext; + + final IntSet set; + int nextIndex, currentIndex; + boolean valid = true; + + public IntSetIterator (IntSet set) { + this.set = set; + reset(); + } + + public void reset () { + currentIndex = INDEX_ILLEGAL; + nextIndex = INDEX_ZERO; + if (set.hasZeroValue) + hasNext = true; + else + findNextIndex(); + } + + void findNextIndex () { + hasNext = false; + int[] keyTable = set.keyTable; + for (int n = set.capacity + set.stashSize; ++nextIndex < n;) { + if (keyTable[nextIndex] != EMPTY) { + hasNext = true; + break; + } + } + } + + public void remove () { + if (currentIndex == INDEX_ZERO && set.hasZeroValue) { + set.hasZeroValue = false; + } else if (currentIndex < 0) { + throw new IllegalStateException("next must be called before remove."); + } else if (currentIndex >= set.capacity) { + set.removeStashIndex(currentIndex); + nextIndex = currentIndex - 1; + findNextIndex(); + } else { + set.keyTable[currentIndex] = EMPTY; + } + currentIndex = INDEX_ILLEGAL; + set.size--; + } + + public int next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + int key = nextIndex == INDEX_ZERO ? 0 : set.keyTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return key; + } + + /** Returns a new array containing the remaining keys. */ + public IntArray toArray () { + IntArray array = new IntArray(true, set.size); + while (hasNext) + array.add(next()); + return array; + } + } +} diff --git a/src/dorkbox/util/collections/LongArray.java b/src/dorkbox/util/collections/LongArray.java new file mode 100644 index 0000000..9cb2f0a --- /dev/null +++ b/src/dorkbox/util/collections/LongArray.java @@ -0,0 +1,410 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Arrays; + +import dorkbox.util.RandomUtil; + +/** A resizable, ordered or unordered long array. Avoids the boxing that occurs with ArrayList. If unordered, this class + * avoids a memory copy when removing elements (the last element is moved to the removed element's position). + * @author Nathan Sweet */ +public class LongArray { + public long[] items; + public int size; + public boolean ordered; + + /** Creates an ordered array with a capacity of 16. */ + public LongArray () { + this(true, 16); + } + + /** Creates an ordered array with the specified capacity. */ + public LongArray (int capacity) { + this(true, capacity); + } + + /** @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. + * @param capacity Any elements added beyond this will cause the backing array to be grown. */ + public LongArray (boolean ordered, int capacity) { + this.ordered = ordered; + items = new long[capacity]; + } + + /** Creates a new array containing the elements in the specific array. The new array will be ordered if the specific array is + * ordered. The capacity is set to the number of elements, so any subsequent elements added will cause the backing array to be + * grown. */ + public LongArray (LongArray array) { + this.ordered = array.ordered; + size = array.size; + items = new long[size]; + System.arraycopy(array.items, 0, items, 0, size); + } + + /** Creates a new ordered array containing the elements in the specified array. The capacity is set to the number of elements, + * so any subsequent elements added will cause the backing array to be grown. */ + public LongArray (long[] array) { + this(true, array, 0, array.length); + } + + /** Creates a new array containing the elements in the specified array. The capacity is set to the number of elements, so any + * subsequent elements added will cause the backing array to be grown. + * @param ordered If false, methods that remove elements may change the order of other elements in the array, which avoids a + * memory copy. */ + public LongArray (boolean ordered, long[] array, int startIndex, int count) { + this(ordered, count); + size = count; + System.arraycopy(array, startIndex, items, 0, count); + } + + public void add (long value) { + long[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size++] = value; + } + + public void add (long value1, long value2) { + long[] items = this.items; + if (size + 1 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + size += 2; + } + + public void add (long value1, long value2, long value3) { + long[] items = this.items; + if (size + 2 >= items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + size += 3; + } + + public void add (long value1, long value2, long value3, long value4) { + long[] items = this.items; + if (size + 3 >= items.length) items = resize(Math.max(8, (int)(size * 1.8f))); // 1.75 isn't enough when size=5. + items[size] = value1; + items[size + 1] = value2; + items[size + 2] = value3; + items[size + 3] = value4; + size += 4; + } + + public void addAll (LongArray array) { + addAll(array.items, 0, array.size); + } + + public void addAll (LongArray array, int offset, int length) { + if (offset + length > array.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); + addAll(array.items, offset, length); + } + + public void addAll (long... array) { + addAll(array, 0, array.length); + } + + public void addAll (long[] array, int offset, int length) { + long[] items = this.items; + int sizeNeeded = size + length; + if (sizeNeeded > items.length) items = resize(Math.max(8, (int)(sizeNeeded * 1.75f))); + System.arraycopy(array, offset, items, size, length); + size += length; + } + + public long get (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + return items[index]; + } + + public void set (int index, long value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] = value; + } + + public void incr (int index, long value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] += value; + } + + public void mul (int index, long value) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + items[index] *= value; + } + + public void insert (int index, long value) { + if (index > size) throw new IndexOutOfBoundsException("index can't be > size: " + index + " > " + size); + long[] items = this.items; + if (size == items.length) items = resize(Math.max(8, (int)(size * 1.75f))); + if (ordered) + System.arraycopy(items, index, items, index + 1, size - index); + else + items[size] = items[index]; + size++; + items[index] = value; + } + + public void swap (int first, int second) { + if (first >= size) throw new IndexOutOfBoundsException("first can't be >= size: " + first + " >= " + size); + if (second >= size) throw new IndexOutOfBoundsException("second can't be >= size: " + second + " >= " + size); + long[] items = this.items; + long firstValue = items[first]; + items[first] = items[second]; + items[second] = firstValue; + } + + public boolean contains (long value) { + int i = size - 1; + long[] items = this.items; + while (i >= 0) + if (items[i--] == value) return true; + return false; + } + + public int indexOf (long value) { + long[] items = this.items; + for (int i = 0, n = size; i < n; i++) + if (items[i] == value) return i; + return -1; + } + + public int lastIndexOf (char value) { + long[] items = this.items; + for (int i = size - 1; i >= 0; i--) + if (items[i] == value) return i; + return -1; + } + + public boolean removeValue (long value) { + long[] items = this.items; + for (int i = 0, n = size; i < n; i++) { + if (items[i] == value) { + removeIndex(i); + return true; + } + } + return false; + } + + /** Removes and returns the item at the specified index. */ + public long removeIndex (int index) { + if (index >= size) throw new IndexOutOfBoundsException("index can't be >= size: " + index + " >= " + size); + long[] items = this.items; + long value = items[index]; + size--; + if (ordered) + System.arraycopy(items, index + 1, items, index, size - index); + else + items[index] = items[size]; + return value; + } + + /** Removes the items between the specified indices, inclusive. */ + public void removeRange (int start, int end) { + if (end >= size) throw new IndexOutOfBoundsException("end can't be >= size: " + end + " >= " + size); + if (start > end) throw new IndexOutOfBoundsException("start can't be > end: " + start + " > " + end); + long[] items = this.items; + int count = end - start + 1; + if (ordered) + System.arraycopy(items, start + count, items, start, size - (start + count)); + else { + int lastIndex = this.size - 1; + for (int i = 0; i < count; i++) + items[start + i] = items[lastIndex - i]; + } + size -= count; + } + + /** Removes from this array all of elements contained in the specified array. + * @return true if this array was modified. */ + public boolean removeAll (LongArray array) { + int size = this.size; + int startSize = size; + long[] items = this.items; + for (int i = 0, n = array.size; i < n; i++) { + long item = array.get(i); + for (int ii = 0; ii < size; ii++) { + if (item == items[ii]) { + removeIndex(ii); + size--; + break; + } + } + } + return size != startSize; + } + + /** Removes and returns the last item. */ + public long pop () { + return items[--size]; + } + + /** Returns the last item. */ + public long peek () { + return items[size - 1]; + } + + /** Returns the first item. */ + public long first () { + if (size == 0) throw new IllegalStateException("Array is empty."); + return items[0]; + } + + /** Returns true if the array is empty. */ + public boolean isEmpty () { + return size == 0; + } + + public void clear () { + size = 0; + } + + /** Reduces the size of the backing array to the size of the actual items. This is useful to release memory when many items + * have been removed, or if it is known that more items will not be added. + * @return {@link #items} */ + public long[] shrink () { + if (items.length != size) resize(size); + return items; + } + + /** 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. + * @return {@link #items} */ + public long[] ensureCapacity (int additionalCapacity) { + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded > items.length) resize(Math.max(8, sizeNeeded)); + return items; + } + + /** Sets the array size, leaving any values beyond the current size undefined. + * @return {@link #items} */ + public long[] setSize (int newSize) { + if (newSize < 0) throw new IllegalArgumentException("newSize must be >= 0: " + newSize); + if (newSize > items.length) resize(Math.max(8, newSize)); + size = newSize; + return items; + } + + protected long[] resize (int newSize) { + long[] newItems = new long[newSize]; + long[] items = this.items; + System.arraycopy(items, 0, newItems, 0, Math.min(size, newItems.length)); + this.items = newItems; + return newItems; + } + + public void sort () { + Arrays.sort(items, 0, size); + } + + public void reverse () { + long[] items = this.items; + for (int i = 0, lastIndex = size - 1, n = size / 2; i < n; i++) { + int ii = lastIndex - i; + long temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + public void shuffle () { + long[] items = this.items; + for (int i = size - 1; i >= 0; i--) { + int ii = RandomUtil.int_(i); + long temp = items[i]; + items[i] = items[ii]; + items[ii] = temp; + } + } + + /** Reduces the size of the array to the specified size. If the array is already smaller than the specified size, no action is + * taken. */ + public void truncate (int newSize) { + if (size > newSize) size = newSize; + } + + /** Returns a random item from the array, or zero if the array is empty. */ + public long random () { + if (size == 0) return 0; + return items[RandomUtil.int_(0, size - 1)]; + } + + public long[] toArray () { + long[] array = new long[size]; + System.arraycopy(items, 0, array, 0, size); + return array; + } + + public int hashCode () { + if (!ordered) return super.hashCode(); + long[] items = this.items; + int h = 1; + for (int i = 0, n = size; i < n; i++) + h = h * 31 + (int)(items[i] ^ (items[i] >>> 32)); + return h; + } + + public boolean equals (Object object) { + if (object == this) return true; + if (!ordered) return false; + if (!(object instanceof LongArray)) return false; + LongArray array = (LongArray)object; + if (!array.ordered) return false; + int n = size; + if (n != array.size) return false; + long[] items1 = this.items; + long[] items2 = array.items; + for (int i = 0; i < n; i++) + if (items[i] != array.items[i]) return false; + return true; + } + + public String toString () { + if (size == 0) return "[]"; + long[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(items[i]); + } + buffer.append(']'); + return buffer.toString(); + } + + public String toString (String separator) { + if (size == 0) return ""; + long[] items = this.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(separator); + buffer.append(items[i]); + } + return buffer.toString(); + } + + /** @see #LongArray(long[]) */ + static public LongArray with (long... array) { + return new LongArray(array); + } +} diff --git a/src/dorkbox/util/collections/LongMap.java b/src/dorkbox/util/collections/LongMap.java new file mode 100644 index 0000000..c538eee --- /dev/null +++ b/src/dorkbox/util/collections/LongMap.java @@ -0,0 +1,857 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + +/** An unordered map that uses long keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small + * stash for problematic keys. Null values are 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 */ +public class LongMap implements Iterable> { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + private static final int EMPTY = 0; + + public int size; + + long[] keyTable; + V[] valueTable; + int capacity, stashSize; + V zeroValue; + boolean hasZeroValue; + + 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 LongMap () { + 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 LongMap (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 LongMap (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 = 63 - Long.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 = new long[capacity + stashCapacity]; + valueTable = (V[])new Object[keyTable.length]; + } + + /** Creates a new map identical to the specified map. */ + public LongMap (LongMap 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; + zeroValue = map.zeroValue; + hasZeroValue = map.hasZeroValue; + } + + public V put (long key, V value) { + if (key == 0) { + V oldValue = zeroValue; + zeroValue = value; + if (!hasZeroValue) { + hasZeroValue = true; + size++; + } + return oldValue; + } + + long[] keyTable = this.keyTable; + + // Check for existing keys. + int index1 = (int)(key & mask); + long key1 = keyTable[index1]; + if (key1 == key) { + V oldValue = valueTable[index1]; + valueTable[index1] = value; + return oldValue; + } + + int index2 = hash2(key); + long key2 = keyTable[index2]; + if (key2 == key) { + V oldValue = valueTable[index2]; + valueTable[index2] = value; + return oldValue; + } + + int index3 = hash3(key); + long key3 = keyTable[index3]; + if (key3 == key) { + V oldValue = valueTable[index3]; + valueTable[index3] = value; + return oldValue; + } + + // Update key in the stash. + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (keyTable[i] == key) { + V oldValue = valueTable[i]; + valueTable[i] = value; + return oldValue; + } + } + + // Check for empty buckets. + if (key1 == EMPTY) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + if (key2 == EMPTY) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + if (key3 == EMPTY) { + keyTable[index3] = key; + valueTable[index3] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + push(key, value, index1, key1, index2, key2, index3, key3); + return null; + } + + public void putAll (LongMap map) { + for (Entry entry : map.entries()) + put(entry.key, entry.value); + } + + /** Skips checks for existing keys. */ + private void putResize (long key, V value) { + if (key == 0) { + zeroValue = value; + hasZeroValue = true; + return; + } + + // Check for empty buckets. + int index1 = (int)(key & mask); + long key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index2 = hash2(key); + long key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index3 = hash3(key); + long key3 = keyTable[index3]; + if (key3 == EMPTY) { + 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 (long insertKey, V insertValue, int index1, long key1, int index2, long key2, int index3, long key3) { + long[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + int mask = this.mask; + + // Push keys until an empty bucket is found. + long evictedKey; + V evictedValue; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(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. + index1 = (int)(evictedKey & mask); + key1 = keyTable[index1]; + if (key1 == EMPTY) { + keyTable[index1] = evictedKey; + valueTable[index1] = evictedValue; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index2 = hash2(evictedKey); + key2 = keyTable[index2]; + if (key2 == EMPTY) { + keyTable[index2] = evictedKey; + valueTable[index2] = evictedValue; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index3 = hash3(evictedKey); + key3 = keyTable[index3]; + if (key3 == EMPTY) { + 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 (long key, V 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++; + } + + public V get (long key) { + if (key == 0) { + if (!hasZeroValue) return null; + return zeroValue; + } + int index = (int)(key & mask); + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return getStash(key, null); + } + } + return valueTable[index]; + } + + public V get (long key, V defaultValue) { + if (key == 0) { + if (!hasZeroValue) return defaultValue; + return zeroValue; + } + int index = (int)(key & mask); + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return getStash(key, defaultValue); + } + } + return valueTable[index]; + } + + private V getStash (long key, V defaultValue) { + long[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (keyTable[i] == key) return valueTable[i]; + return defaultValue; + } + + public V remove (long key) { + if (key == 0) { + if (!hasZeroValue) return null; + V oldValue = zeroValue; + zeroValue = null; + hasZeroValue = false; + size--; + return oldValue; + } + + int index = (int)(key & mask); + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + index = hash2(key); + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + index = hash3(key); + if (keyTable[index] == key) { + keyTable[index] = EMPTY; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + return removeStash(key); + } + + V removeStash (long key) { + long[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (keyTable[i] == key) { + V oldValue = valueTable[i]; + removeStashIndex(i); + size--; + return oldValue; + } + } + return null; + } + + 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]; + valueTable[lastIndex] = null; + } else + valueTable[index] = 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 = MathUtil.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; + } + zeroValue = null; + hasZeroValue = false; + size = 0; + resize(maximumCapacity); + } + + public void clear () { + if (size == 0) return; + long[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) { + keyTable[i] = EMPTY; + valueTable[i] = null; + } + size = 0; + stashSize = 0; + zeroValue = null; + hasZeroValue = false; + } + + /** 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 (Object value, boolean identity) { + V[] valueTable = this.valueTable; + if (value == null) { + if (hasZeroValue && zeroValue == null) return true; + long[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != EMPTY && valueTable[i] == null) return true; + } else if (identity) { + if (value == zeroValue) return true; + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return true; + } else { + if (hasZeroValue && value.equals(zeroValue)) return true; + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) return true; + } + return false; + } + + public boolean containsKey (long key) { + if (key == 0) return hasZeroValue; + int index = (int)(key & mask); + if (keyTable[index] != key) { + index = hash2(key); + if (keyTable[index] != key) { + index = hash3(key); + if (keyTable[index] != key) return containsKeyStash(key); + } + } + return true; + } + + private boolean containsKeyStash (long key) { + long[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (keyTable[i] == key) return true; + return false; + } + + /** Returns the key for the specified value, or notFound 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 + * {@link #equals(Object)}. */ + public long findKey (Object value, boolean identity, long notFound) { + V[] valueTable = this.valueTable; + if (value == null) { + if (hasZeroValue && zeroValue == null) return 0; + long[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != EMPTY && valueTable[i] == null) return keyTable[i]; + } else if (identity) { + if (value == zeroValue) return 0; + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return keyTable[i]; + } else { + if (hasZeroValue && value.equals(zeroValue)) return 0; + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) return keyTable[i]; + } + return notFound; + } + + /** 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(MathUtil.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 = 63 - Long.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); + + long[] oldKeyTable = keyTable; + V[] oldValueTable = valueTable; + + keyTable = new long[newSize + stashCapacity]; + valueTable = (V[])new Object[newSize + stashCapacity]; + + int oldSize = size; + size = hasZeroValue ? 1 : 0; + stashSize = 0; + if (oldSize > 0) { + for (int i = 0; i < oldEndIndex; i++) { + long key = oldKeyTable[i]; + if (key != EMPTY) putResize(key, oldValueTable[i]); + } + } + } + + private int hash2 (long h) { + h *= PRIME2; + return (int)((h ^ h >>> hashShift) & mask); + } + + private int hash3 (long h) { + h *= PRIME3; + return (int)((h ^ h >>> hashShift) & mask); + } + + public int hashCode () { + int h = 0; + if (hasZeroValue && zeroValue != null) { + h += zeroValue.hashCode(); + } + long[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + long key = keyTable[i]; + if (key != EMPTY) { + h += (int)(key ^ (key >>> 32)) * 31; + + V value = valueTable[i]; + if (value != null) { + h += value.hashCode(); + } + } + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof LongMap)) return false; + LongMap other = (LongMap)obj; + if (other.size != size) return false; + if (other.hasZeroValue != hasZeroValue) return false; + if (hasZeroValue) { + if (other.zeroValue == null) { + if (zeroValue != null) return false; + } else { + if (!other.zeroValue.equals(zeroValue)) return false; + } + } + long[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + long key = keyTable[i]; + if (key != EMPTY) { + V value = valueTable[i]; + if (value == null) { + if (!other.containsKey(key) || other.get(key) != null) return false; + } else { + if (!value.equals(other.get(key))) return false; + } + } + } + return true; + } + + public String toString () { + if (size == 0) return "[]"; + StringBuilder buffer = new StringBuilder(32); + buffer.append('['); + long[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + int i = keyTable.length; + while (i-- > 0) { + long key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(key); + buffer.append('='); + buffer.append(valueTable[i]); + break; + } + while (i-- > 0) { + long key = keyTable[i]; + if (key == EMPTY) continue; + buffer.append(", "); + buffer.append(key); + buffer.append('='); + buffer.append(valueTable[i]); + } + buffer.append(']'); + return buffer.toString(); + } + + public Iterator> 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 long key; + public V value; + + public String toString () { + return key + "=" + value; + } + } + + static private class MapIterator { + static final int INDEX_ILLEGAL = -2; + static final int INDEX_ZERO = -1; + + public boolean hasNext; + + final LongMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (LongMap map) { + this.map = map; + reset(); + } + + public void reset () { + currentIndex = INDEX_ILLEGAL; + nextIndex = INDEX_ZERO; + if (map.hasZeroValue) + hasNext = true; + else + findNextIndex(); + } + + void findNextIndex () { + hasNext = false; + long[] keyTable = map.keyTable; + for (int n = map.capacity + map.stashSize; ++nextIndex < n;) { + if (keyTable[nextIndex] != EMPTY) { + hasNext = true; + break; + } + } + } + + public void remove () { + if (currentIndex == INDEX_ZERO && map.hasZeroValue) { + map.zeroValue = null; + map.hasZeroValue = false; + } else if (currentIndex < 0) { + throw new IllegalStateException("next must be called before remove."); + } else if (currentIndex >= map.capacity) { + map.removeStashIndex(currentIndex); + nextIndex = currentIndex - 1; + findNextIndex(); + } else { + map.keyTable[currentIndex] = EMPTY; + map.valueTable[currentIndex] = null; + } + currentIndex = INDEX_ILLEGAL; + map.size--; + } + } + + static public class Entries extends MapIterator implements Iterable>, Iterator> { + private Entry entry = new Entry(); + + public Entries (LongMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + public Entry next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + long[] keyTable = map.keyTable; + if (nextIndex == INDEX_ZERO) { + entry.key = 0; + entry.value = map.zeroValue; + } else { + entry.key = keyTable[nextIndex]; + entry.value = map.valueTable[nextIndex]; + } + currentIndex = nextIndex; + findNextIndex(); + return entry; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Iterator> iterator () { + return this; + } + + public void remove () { + super.remove(); + } + } + + static public class Values extends MapIterator implements Iterable, Iterator { + public Values (LongMap map) { + super(map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public V next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + V value; + if (nextIndex == INDEX_ZERO) + value = map.zeroValue; + else + value = map.valueTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return value; + } + + public Iterator iterator () { + return this; + } + + /** Returns a new array containing the remaining values. */ + public Array toArray () { + Array array = new Array(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + + public void remove () { + super.remove(); + } + } + + static public class Keys extends MapIterator { + public Keys (LongMap map) { + super(map); + } + + public long next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + long key = nextIndex == INDEX_ZERO ? 0 : map.keyTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return key; + } + + /** Returns a new array containing the remaining values. */ + public LongArray toArray () { + LongArray array = new LongArray(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + } +} diff --git a/src/dorkbox/util/collections/ObjectFloatMap.java b/src/dorkbox/util/collections/ObjectFloatMap.java new file mode 100644 index 0000000..f1275c1 --- /dev/null +++ b/src/dorkbox/util/collections/ObjectFloatMap.java @@ -0,0 +1,781 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + + +/** An unordered map where the values are floats. 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 */ +public class ObjectFloatMap implements Iterable> { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + + public int size; + + K[] keyTable; + float[] 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 ObjectFloatMap () { + 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 ObjectFloatMap (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 ObjectFloatMap (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 float[keyTable.length]; + } + + /** Creates a new map identical to the specified map. */ + public ObjectFloatMap (ObjectFloatMap 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, float 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 (ObjectFloatMap map) { + for (Entry entry : map.entries()) + put(entry.key, entry.value); + } + + /** Skips checks for existing keys. */ + private void putResize (K key, float 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, float insertValue, int index1, K key1, int index2, K key2, int index3, K key3) { + K[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + int mask = this.mask; + + // Push keys until an empty bucket is found. + K evictedKey; + float evictedValue; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(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, float 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 float get (K key, float 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 float getStash (K key, float 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 float getAndIncrement (K key, float defaultValue, float 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); + } + } + float value = valueTable[index]; + valueTable[index] = value + increment; + return value; + } + + private float getAndIncrementStash (K key, float defaultValue, float increment) { + K[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key.equals(keyTable[i])) { + float value = valueTable[i]; + valueTable[i] = value + increment; + return value; + } + put(key, defaultValue + increment); + return defaultValue; + } + + public float remove (K key, float defaultValue) { + int hashCode = key.hashCode(); + int index = hashCode & mask; + if (key.equals(keyTable[index])) { + keyTable[index] = null; + float oldValue = valueTable[index]; + size--; + return oldValue; + } + + index = hash2(hashCode); + if (key.equals(keyTable[index])) { + keyTable[index] = null; + float oldValue = valueTable[index]; + size--; + return oldValue; + } + + index = hash3(hashCode); + if (key.equals(keyTable[index])) { + keyTable[index] = null; + float oldValue = valueTable[index]; + size--; + return oldValue; + } + + return removeStash(key, defaultValue); + } + + float removeStash (K key, float defaultValue) { + K[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key.equals(keyTable[i])) { + float 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 = MathUtil.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 (float value) { + K[] keyTable = this.keyTable; + float[] 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 (float value) { + K[] keyTable = this.keyTable; + float[] 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(MathUtil.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; + float[] oldValueTable = valueTable; + + keyTable = (K[])new Object[newSize + stashCapacity]; + valueTable = new float[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; + } + + public int hashCode () { + int h = 0; + K[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + K key = keyTable[i]; + if (key != null) { + h += key.hashCode() * 31; + + float value = valueTable[i]; + h += Float.floatToIntBits(value); + } + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof ObjectFloatMap)) return false; + ObjectFloatMap other = (ObjectFloatMap) obj; + if (other.size != size) return false; + K[] keyTable = this.keyTable; + float[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + K key = keyTable[i]; + if (key != null) { + float otherValue = other.get(key, 0f); + if (otherValue == 0f && !other.containsKey(key)) return false; + float value = valueTable[i]; + if (otherValue != value) return false; + } + } + return true; + } + + public String toString () { + if (size == 0) return "{}"; + StringBuilder buffer = new StringBuilder(32); + buffer.append('{'); + K[] keyTable = this.keyTable; + float[] 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(); + } + + 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 float value; + + public String toString () { + return key + "=" + value; + } + } + + static private class MapIterator { + public boolean hasNext; + + final ObjectFloatMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (ObjectFloatMap 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 (ObjectFloatMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + 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; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Entries iterator () { + return this; + } + + public void remove () { + super.remove(); + } + } + + static public class Values extends MapIterator { + public Values (ObjectFloatMap map) { + super((ObjectFloatMap)map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public float next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + float value = map.valueTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return value; + } + + /** Returns a new array containing the remaining values. */ + public FloatArray toArray () { + FloatArray array = new FloatArray(true, map.size); + while (hasNext) + array.add(next()); + return array; + } + } + + static public class Keys extends MapIterator implements Iterable, Iterator { + public Keys (ObjectFloatMap map) { + super((ObjectFloatMap)map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + 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; + } + + 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; + } + + public void remove () { + super.remove(); + } + } +} diff --git a/src/dorkbox/util/collections/ObjectIntMap.java b/src/dorkbox/util/collections/ObjectIntMap.java index 3addcf8..2352a09 100644 --- a/src/dorkbox/util/collections/ObjectIntMap.java +++ b/src/dorkbox/util/collections/ObjectIntMap.java @@ -1,5 +1,7 @@ -/* - * Copyright 2010 Mario Zechner (contact@badlogicgames.com), Nathan Sweet (admin@esotericsoftware.com) +/******************************************************************************* + * 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. @@ -12,13 +14,17 @@ * 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.util.collections; -import com.esotericsoftware.kryo.util.ObjectMap; +import java.util.Iterator; +import java.util.NoSuchElementException; +import dorkbox.util.MathUtil; import dorkbox.util.RandomUtil; + /** 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.
*
@@ -26,8 +32,7 @@ import dorkbox.util.RandomUtil; * 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 */ -public class ObjectIntMap { - @SuppressWarnings("unused") +public class ObjectIntMap implements Iterable> { private static final int PRIME1 = 0xbe1f14b1; private static final int PRIME2 = 0xb4b82e39; private static final int PRIME3 = 0xced1c241; @@ -43,101 +48,83 @@ public class ObjectIntMap { private int stashCapacity; private int pushIterations; - /** Creates a new map with an initial capacity of 32 and a load factor of 0.8. This map will hold 25 items before growing the - * backing table. */ + 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(32, 0.8f); + this(51, 0.8f); } - /** Creates a new map with a load factor of 0.8. This map will hold initialCapacity * 0.8 items before growing the backing - * table. */ + /** 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 * loadFactor items - * before growing the backing table. */ - @SuppressWarnings("unchecked") + /** 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); - } - if (initialCapacity > 1 << 30) { - throw new IllegalArgumentException("initialCapacity is too large: " + initialCapacity); - } - this.capacity = ObjectMap.nextPowerOfTwo(initialCapacity); + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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); - } + if (loadFactor <= 0) throw new IllegalArgumentException("loadFactor must be > 0: " + loadFactor); this.loadFactor = loadFactor; - this.threshold = (int)(this.capacity * loadFactor); - this.mask = this.capacity - 1; - this.hashShift = 31 - Integer.numberOfTrailingZeros(this.capacity); - this.stashCapacity = Math.max(3, (int)Math.ceil(Math.log(this.capacity)) * 2); - this.pushIterations = Math.max(Math.min(this.capacity, 8), (int)Math.sqrt(this.capacity) / 8); + 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); - this.keyTable = (K[])new Object[this.capacity + this.stashCapacity]; - this.valueTable = new int[this.keyTable.length]; + 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(map.capacity, map.loadFactor); - this.stashSize = map.stashSize; - System.arraycopy(map.keyTable, 0, this.keyTable, 0, map.keyTable.length); - System.arraycopy(map.valueTable, 0, this.valueTable, 0, map.valueTable.length); - this.size = map.size; - } - - public - void putAll(final ObjectIntMap map) { - K[] keyTable = map.keyTable; - int[] valueTable = map.valueTable; - - for (int i = 0, length = map.capacity + map.stashSize; i < length; i++) { - K k = keyTable[i]; - int v = valueTable[i]; - if (k != null && v != 0) { - put(k, v); - } - } + 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."); - } + 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 & this.mask; + int index1 = hashCode & mask; K key1 = keyTable[index1]; if (key.equals(key1)) { - this.valueTable[index1] = value; + valueTable[index1] = value; return; } int index2 = hash2(hashCode); K key2 = keyTable[index2]; if (key.equals(key2)) { - this.valueTable[index2] = value; + valueTable[index2] = value; return; } int index3 = hash3(hashCode); K key3 = keyTable[index3]; if (key.equals(key3)) { - this.valueTable[index3] = value; + valueTable[index3] = value; return; } // Update key in the stash. - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { + for (int i = capacity, n = i + stashSize; i < n; i++) { if (key.equals(keyTable[i])) { - this.valueTable[i] = value; + valueTable[i] = value; return; } } @@ -145,68 +132,61 @@ public class ObjectIntMap { // Check for empty buckets. if (key1 == null) { keyTable[index1] = key; - this.valueTable[index1] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); return; } if (key2 == null) { keyTable[index2] = key; - this.valueTable[index2] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); return; } if (key3 == null) { keyTable[index3] = key; - this.valueTable[index3] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + 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 & this.mask; - K key1 = this.keyTable[index1]; + int index1 = hashCode & mask; + K key1 = keyTable[index1]; if (key1 == null) { - this.keyTable[index1] = key; - this.valueTable[index1] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); return; } int index2 = hash2(hashCode); - K key2 = this.keyTable[index2]; + K key2 = keyTable[index2]; if (key2 == null) { - this.keyTable[index2] = key; - this.valueTable[index2] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); return; } int index3 = hash3(hashCode); - K key3 = this.keyTable[index3]; + K key3 = keyTable[index3]; if (key3 == null) { - this.keyTable[index3] = key; - this.valueTable[index3] = value; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + keyTable[index3] = key; + valueTable[index3] = value; + if (size++ >= threshold) resize(capacity << 1); return; } @@ -225,24 +205,24 @@ public class ObjectIntMap { do { // Replace the key and value for one of the hashes. switch (RandomUtil.int_(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; + 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. @@ -252,9 +232,7 @@ public class ObjectIntMap { if (key1 == null) { keyTable[index1] = evictedKey; valueTable[index1] = evictedValue; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + if (size++ >= threshold) resize(capacity << 1); return; } @@ -263,9 +241,7 @@ public class ObjectIntMap { if (key2 == null) { keyTable[index2] = evictedKey; valueTable[index2] = evictedValue; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + if (size++ >= threshold) resize(capacity << 1); return; } @@ -274,15 +250,11 @@ public class ObjectIntMap { if (key3 == null) { keyTable[index3] = evictedKey; valueTable[index3] = evictedValue; - if (this.size++ >= this.threshold) { - resize(this.capacity << 1); - } + if (size++ >= threshold) resize(capacity << 1); return; } - if (++i == pushIterations) { - break; - } + if (++i == pushIterations) break; insertKey = evictedKey; insertValue = evictedValue; @@ -292,101 +264,93 @@ public class ObjectIntMap { } private void putStash (K key, int value) { - if (this.stashSize == this.stashCapacity) { + if (stashSize == stashCapacity) { // Too many pushes occurred and the stash is full, increase the table size. - resize(this.capacity << 1); - put(key, value); + resize(capacity << 1); + putResize(key, value); return; } // Store key in the stash. - int index = this.capacity + this.stashSize; - this.keyTable[index] = key; - this.valueTable[index] = value; - this.stashSize++; - this.size++; + 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 & this.mask; - if (!key.equals(this.keyTable[index])) { + int index = hashCode & mask; + if (!key.equals(keyTable[index])) { index = hash2(hashCode); - if (!key.equals(this.keyTable[index])) { + if (!key.equals(keyTable[index])) { index = hash3(hashCode); - if (!key.equals(this.keyTable[index])) { - return getStash(key, defaultValue); - } + if (!key.equals(keyTable[index])) return getStash(key, defaultValue); } } - return this.valueTable[index]; + return valueTable[index]; } private int getStash (K key, int defaultValue) { K[] keyTable = this.keyTable; - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { - if (key.equals(keyTable[i])) { - return this.valueTable[i]; - } - } + 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. */ + * put into the map. */ public int getAndIncrement (K key, int defaultValue, int increment) { int hashCode = key.hashCode(); - int index = hashCode & this.mask; - if (!key.equals(this.keyTable[index])) { + int index = hashCode & mask; + if (!key.equals(keyTable[index])) { index = hash2(hashCode); - if (!key.equals(this.keyTable[index])) { + if (!key.equals(keyTable[index])) { index = hash3(hashCode); - if (!key.equals(this.keyTable[index])) { - return getAndIncrementStash(key, defaultValue, increment); - } + if (!key.equals(keyTable[index])) return getAndIncrementStash(key, defaultValue, increment); } } - int value = this.valueTable[index]; - this.valueTable[index] = value + 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 = this.capacity, n = i + this.stashSize; i < n; i++) { + for (int i = capacity, n = i + stashSize; i < n; i++) if (key.equals(keyTable[i])) { - int value = this.valueTable[i]; - this.valueTable[i] = value + increment; + 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 & this.mask; - if (key.equals(this.keyTable[index])) { - this.keyTable[index] = null; - int oldValue = this.valueTable[index]; - this.size--; + 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(this.keyTable[index])) { - this.keyTable[index] = null; - int oldValue = this.valueTable[index]; - this.size--; + if (key.equals(keyTable[index])) { + keyTable[index] = null; + int oldValue = valueTable[index]; + size--; return oldValue; } index = hash3(hashCode); - if (key.equals(this.keyTable[index])) { - this.keyTable[index] = null; - int oldValue = this.valueTable[index]; - this.size--; + if (key.equals(keyTable[index])) { + keyTable[index] = null; + int oldValue = valueTable[index]; + size--; return oldValue; } @@ -395,11 +359,11 @@ public class ObjectIntMap { int removeStash (K key, int defaultValue) { K[] keyTable = this.keyTable; - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { + for (int i = capacity, n = i + stashSize; i < n; i++) { if (key.equals(keyTable[i])) { - int oldValue = this.valueTable[i]; + int oldValue = valueTable[i]; removeStashIndex(i); - this.size--; + size--; return oldValue; } } @@ -408,71 +372,68 @@ public class ObjectIntMap { void removeStashIndex (int index) { // If the removed location was not last, move the last tuple to the removed location. - this.stashSize--; - int lastIndex = this.capacity + this.stashSize; + stashSize--; + int lastIndex = capacity + stashSize; if (index < lastIndex) { - this.keyTable[index] = this.keyTable[lastIndex]; - this.valueTable[index] = this.valueTable[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. */ + * 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 (this.size > maximumCapacity) { - maximumCapacity = this.size; - } - if (this.capacity <= maximumCapacity) { - return; - } - maximumCapacity = ObjectMap.nextPowerOfTwo(maximumCapacity); + if (maximumCapacity < 0) throw new IllegalArgumentException("maximumCapacity must be >= 0: " + maximumCapacity); + if (size > maximumCapacity) maximumCapacity = size; + if (capacity <= maximumCapacity) return; + maximumCapacity = MathUtil.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 (this.capacity <= maximumCapacity) { + if (capacity <= maximumCapacity) { clear(); return; } - this.size = 0; + size = 0; resize(maximumCapacity); } public void clear () { + if (size == 0) return; K[] keyTable = this.keyTable; - for (int i = this.capacity + this.stashSize; i-- > 0;) { + for (int i = capacity + stashSize; i-- > 0;) keyTable[i] = null; - } - this.size = 0; - this.stashSize = 0; + 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. */ + * an expensive operation. */ public boolean containsValue (int value) { + K[] keyTable = this.keyTable; int[] valueTable = this.valueTable; - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (keyTable[i] != null && valueTable[i] == value) { - return true; - } - } + 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 & this.mask; - if (!key.equals(this.keyTable[index])) { + int index = hashCode & mask; + if (!key.equals(keyTable[index])) { index = hash2(hashCode); - if (!key.equals(this.keyTable[index])) { + if (!key.equals(keyTable[index])) { index = hash3(hashCode); - if (!key.equals(this.keyTable[index])) { - return containsKeyStash(key); - } + if (!key.equals(keyTable[index])) return containsKeyStash(key); } } return true; @@ -480,80 +441,103 @@ public class ObjectIntMap { private boolean containsKeyStash (K key) { K[] keyTable = this.keyTable; - for (int i = this.capacity, n = i + this.stashSize; i < n; i++) { - if (key.equals(keyTable[i])) { - return true; - } - } + 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. */ + * every value, which may be an expensive operation. */ public K findKey (int value) { + K[] keyTable = this.keyTable; int[] valueTable = this.valueTable; - for (int i = this.capacity + this.stashSize; i-- > 0;) { - if (keyTable[i] != null && valueTable[i] == value) { - return this.keyTable[i]; - } - } + 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 acommodate the specified number of additional items. Useful before adding many - * items to avoid multiple backing array resizes. */ + /** 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) { - int sizeNeeded = this.size + additionalCapacity; - if (sizeNeeded >= this.threshold) { - resize(ObjectMap.nextPowerOfTwo((int)(sizeNeeded / this.loadFactor))); - } + if (additionalCapacity < 0) throw new IllegalArgumentException("additionalCapacity must be >= 0: " + additionalCapacity); + int sizeNeeded = size + additionalCapacity; + if (sizeNeeded >= threshold) resize(MathUtil.nextPowerOfTwo((int)Math.ceil(sizeNeeded / loadFactor))); } - @SuppressWarnings("unchecked") private void resize (int newSize) { - int oldEndIndex = this.capacity + this.stashSize; + int oldEndIndex = capacity + stashSize; - this.capacity = newSize; - this.threshold = (int)(newSize * this.loadFactor); - this.mask = newSize - 1; - this.hashShift = 31 - Integer.numberOfTrailingZeros(newSize); - this.stashCapacity = Math.max(3, (int)Math.ceil(Math.log(newSize)) * 2); - this.pushIterations = Math.max(Math.min(newSize, 8), (int)Math.sqrt(newSize) / 8); + 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 = this.keyTable; - int[] oldValueTable = this.valueTable; + K[] oldKeyTable = keyTable; + int[] oldValueTable = valueTable; - this.keyTable = (K[])new Object[newSize + this.stashCapacity]; - this.valueTable = new int[newSize + this.stashCapacity]; + keyTable = (K[])new Object[newSize + stashCapacity]; + valueTable = new int[newSize + stashCapacity]; - int oldSize = this.size; - this.size = 0; - this.stashSize = 0; + 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]); - } + if (key != null) putResize(key, oldValueTable[i]); } } } private int hash2 (int h) { h *= PRIME2; - return (h ^ h >>> this.hashShift) & this.mask; + return (h ^ h >>> hashShift) & mask; } private int hash3 (int h) { h *= PRIME3; - return (h ^ h >>> this.hashShift) & this.mask; + return (h ^ h >>> hashShift) & mask; } - @Override - public String toString () { - if (this.size == 0) { - return "{}"; + 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; + } + + 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; + } + + public String toString () { + if (size == 0) return "{}"; StringBuilder buffer = new StringBuilder(32); buffer.append('{'); K[] keyTable = this.keyTable; @@ -561,9 +545,7 @@ public class ObjectIntMap { int i = keyTable.length; while (i-- > 0) { K key = keyTable[i]; - if (key == null) { - continue; - } + if (key == null) continue; buffer.append(key); buffer.append('='); buffer.append(valueTable[i]); @@ -571,9 +553,7 @@ public class ObjectIntMap { } while (i-- > 0) { K key = keyTable[i]; - if (key == null) { - continue; - } + if (key == null) continue; buffer.append(", "); buffer.append(key); buffer.append('='); @@ -582,4 +562,221 @@ public class ObjectIntMap { buffer.append('}'); return buffer.toString(); } + + 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; + + 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. */ + 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; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Entries iterator () { + return this; + } + + 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); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + 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; + } + + 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; + } + + public void remove () { + super.remove(); + } + } } diff --git a/src/dorkbox/util/collections/ObjectMap.java b/src/dorkbox/util/collections/ObjectMap.java new file mode 100644 index 0000000..4b2b4d9 --- /dev/null +++ b/src/dorkbox/util/collections/ObjectMap.java @@ -0,0 +1,819 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + +/** An unordered map. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small stash for problematic + * keys. Null keys are not allowed. Null values are 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.
+ *
+ * Iteration can be very slow for a map with a large capacity. {@link #clear(int)} and {@link #shrink(int)} can be used to reduce + * the capacity. {@link OrderedMap} provides much faster iteration. + * @author Nathan Sweet */ +public class ObjectMap implements Iterable> { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + + public int size; + + K[] keyTable; + V[] 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 ObjectMap () { + 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 ObjectMap (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 ObjectMap (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 = (V[])new Object[keyTable.length]; + } + + /** Creates a new map identical to the specified map. */ + public ObjectMap (ObjectMap 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; + } + + /** Returns the old value associated with the specified key, or null. */ + public V put (K key, V 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)) { + V oldValue = valueTable[index1]; + valueTable[index1] = value; + return oldValue; + } + + int index2 = hash2(hashCode); + K key2 = keyTable[index2]; + if (key.equals(key2)) { + V oldValue = valueTable[index2]; + valueTable[index2] = value; + return oldValue; + } + + int index3 = hash3(hashCode); + K key3 = keyTable[index3]; + if (key.equals(key3)) { + V oldValue = valueTable[index3]; + valueTable[index3] = value; + return oldValue; + } + + // Update key in the stash. + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key.equals(keyTable[i])) { + V oldValue = valueTable[i]; + valueTable[i] = value; + return oldValue; + } + } + + // Check for empty buckets. + if (key1 == null) { + keyTable[index1] = key; + valueTable[index1] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + if (key2 == null) { + keyTable[index2] = key; + valueTable[index2] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + if (key3 == null) { + keyTable[index3] = key; + valueTable[index3] = value; + if (size++ >= threshold) resize(capacity << 1); + return null; + } + + push(key, value, index1, key1, index2, key2, index3, key3); + return null; + } + + public void putAll (ObjectMap map) { + ensureCapacity(map.size); + for (Entry entry : map) + put(entry.key, entry.value); + } + + /** Skips checks for existing keys. */ + private void putResize (K key, V 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, V insertValue, int index1, K key1, int index2, K key2, int index3, K key3) { + K[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + int mask = this.mask; + + // Push keys until an empty bucket is found. + K evictedKey; + V evictedValue; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(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, V 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++; + } + + /** Returns the value for the specified key, or null if the key is not in the map. */ + public V get (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 getStash(key, null); + } + } + return valueTable[index]; + } + + /** Returns the value for the specified key, or the default value if the key is not in the map. */ + public V get (K key, V 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 V getStash (K key, V 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 value associated with the key, or null. */ + public V remove (K key) { + int hashCode = key.hashCode(); + int index = hashCode & mask; + if (key.equals(keyTable[index])) { + keyTable[index] = null; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + index = hash2(hashCode); + if (key.equals(keyTable[index])) { + keyTable[index] = null; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + index = hash3(hashCode); + if (key.equals(keyTable[index])) { + keyTable[index] = null; + V oldValue = valueTable[index]; + valueTable[index] = null; + size--; + return oldValue; + } + + return removeStash(key); + } + + V removeStash (K key) { + K[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key.equals(keyTable[i])) { + V oldValue = valueTable[i]; + removeStashIndex(i); + size--; + return oldValue; + } + } + return null; + } + + 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; + valueTable[lastIndex] = null; + } else { + keyTable[index] = null; + valueTable[index] = 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 = MathUtil.nextPowerOfTwo(maximumCapacity); + resize(maximumCapacity); + } + + /** Clears the map and reduces the size of the backing arrays to be the specified capacity, if they are larger. The reduction + * is done by allocating new arrays, though for large arrays this can be faster than clearing the existing array. */ + public void clear (int maximumCapacity) { + if (capacity <= maximumCapacity) { + clear(); + return; + } + size = 0; + resize(maximumCapacity); + } + + /** Clears the map, leaving the backing arrays at the current capacity. When the capacity is high and the population is low, + * iteration can be unnecessarily slow. {@link #clear(int)} can be used to reduce the capacity. */ + public void clear () { + if (size == 0) return; + K[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = capacity + stashSize; i-- > 0;) { + keyTable[i] = null; + valueTable[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. + * @param identity If true, uses == to compare the specified value with values in the map. If false, uses + * {@link #equals(Object)}. */ + public boolean containsValue (Object value, boolean identity) { + V[] valueTable = this.valueTable; + if (value == null) { + K[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != null && valueTable[i] == null) return true; + } else if (identity) { + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return true; + } else { + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) 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. + * @param identity If true, uses == to compare the specified value with values in the map. If false, uses + * {@link #equals(Object)}. */ + public K findKey (Object value, boolean identity) { + V[] valueTable = this.valueTable; + if (value == null) { + K[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + if (keyTable[i] != null && valueTable[i] == null) return keyTable[i]; + } else if (identity) { + for (int i = capacity + stashSize; i-- > 0;) + if (valueTable[i] == value) return keyTable[i]; + } else { + for (int i = capacity + stashSize; i-- > 0;) + if (value.equals(valueTable[i])) 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(MathUtil.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; + V[] oldValueTable = valueTable; + + keyTable = (K[])new Object[newSize + stashCapacity]; + valueTable = (V[])new Object[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; + } + + public int hashCode () { + int h = 0; + K[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + K key = keyTable[i]; + if (key != null) { + h += key.hashCode() * 31; + + V value = valueTable[i]; + if (value != null) { + h += value.hashCode(); + } + } + } + return h; + } + + public boolean equals (Object obj) { + if (obj == this) return true; + if (!(obj instanceof ObjectMap)) return false; + ObjectMap other = (ObjectMap)obj; + if (other.size != size) return false; + K[] keyTable = this.keyTable; + V[] valueTable = this.valueTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) { + K key = keyTable[i]; + if (key != null) { + V value = valueTable[i]; + if (value == null) { + if (!other.containsKey(key) || other.get(key) != null) return false; + } else { + if (!value.equals(other.get(key))) return false; + } + } + } + return true; + } + + public String toString (String separator) { + return toString(separator, false); + } + + public String toString () { + return toString(", ", true); + } + + private String toString (String separator, boolean braces) { + if (size == 0) return braces ? "{}" : ""; + StringBuilder buffer = new StringBuilder(32); + if (braces) buffer.append('{'); + K[] keyTable = this.keyTable; + V[] 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(separator); + buffer.append(key); + buffer.append('='); + buffer.append(valueTable[i]); + } + if (braces) buffer.append('}'); + return buffer.toString(); + } + + 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 Values} 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 Keys} 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 V value; + + public String toString () { + return key + "=" + value; + } + } + + static private abstract class MapIterator implements Iterable, Iterator { + public boolean hasNext; + + final ObjectMap map; + int nextIndex, currentIndex; + boolean valid = true; + + public MapIterator (ObjectMap 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; + map.valueTable[currentIndex] = null; + } + currentIndex = -1; + map.size--; + } + } + + static public class Entries extends MapIterator> { + Entry entry = new Entry(); + + public Entries (ObjectMap map) { + super(map); + } + + /** Note the same entry instance is returned each time this method is called. */ + 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; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public Entries iterator () { + return this; + } + } + + static public class Values extends MapIterator { + public Values (ObjectMap map) { + super((ObjectMap)map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public V next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + V value = map.valueTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return value; + } + + public Values iterator () { + return this; + } + + /** Returns a new array containing the remaining values. */ + public Array toArray () { + return toArray(new Array(true, map.size)); + } + + /** Adds the remaining values to the specified array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + } + + static public class Keys extends MapIterator { + public Keys (ObjectMap map) { + super((ObjectMap)map); + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + 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; + } + + public Keys iterator () { + return this; + } + + /** Returns a new array containing the remaining keys. */ + public Array toArray () { + return toArray(new Array(true, map.size)); + } + + /** Adds the remaining keys to the array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + } +} diff --git a/src/dorkbox/util/collections/ObjectSet.java b/src/dorkbox/util/collections/ObjectSet.java new file mode 100644 index 0000000..89e22c4 --- /dev/null +++ b/src/dorkbox/util/collections/ObjectSet.java @@ -0,0 +1,581 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import dorkbox.util.MathUtil; +import dorkbox.util.RandomUtil; + +/** An unordered set where the keys are objects. This implementation uses cuckoo hashing 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 set performs very fast contains and remove (typically O(1), worst case O(log(n))). Add may be a bit slower, depending on + * hash collisions. Load factors greater than 0.91 greatly increase the chances the set will have to rehash to the next higher POT + * size.
+ *
+ * Iteration can be very slow for a set with a large capacity. {@link #clear(int)} and {@link #shrink(int)} can be used to reduce + * the capacity. {@link OrderedSet} provides much faster iteration. + * @author Nathan Sweet */ +public class ObjectSet implements Iterable { + private static final int PRIME1 = 0xbe1f14b1; + private static final int PRIME2 = 0xb4b82e39; + private static final int PRIME3 = 0xced1c241; + + public int size; + + T[] keyTable; + int capacity, stashSize; + + private float loadFactor; + private int hashShift, mask, threshold; + private int stashCapacity; + private int pushIterations; + + private ObjectSetIterator iterator1, iterator2; + + /** Creates a new set with an initial capacity of 51 and a load factor of 0.8. */ + public ObjectSet () { + this(51, 0.8f); + } + + /** Creates a new set 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 ObjectSet (int initialCapacity) { + this(initialCapacity, 0.8f); + } + + /** Creates a new set with the specified initial capacity and load factor. This set 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 ObjectSet (int initialCapacity, float loadFactor) { + if (initialCapacity < 0) throw new IllegalArgumentException("initialCapacity must be >= 0: " + initialCapacity); + initialCapacity = MathUtil.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 = (T[])new Object[capacity + stashCapacity]; + } + + /** Creates a new set identical to the specified set. */ + public ObjectSet (ObjectSet set) { + this((int)Math.floor(set.capacity * set.loadFactor), set.loadFactor); + stashSize = set.stashSize; + System.arraycopy(set.keyTable, 0, keyTable, 0, set.keyTable.length); + size = set.size; + } + + /** Returns true if the key was not already in the set. If this set already contains the key, the call leaves the set unchanged + * and returns false. */ + public boolean add (T key) { + if (key == null) throw new IllegalArgumentException("key cannot be null."); + T[] keyTable = this.keyTable; + + // Check for existing keys. + int hashCode = key.hashCode(); + int index1 = hashCode & mask; + T key1 = keyTable[index1]; + if (key.equals(key1)) return false; + + int index2 = hash2(hashCode); + T key2 = keyTable[index2]; + if (key.equals(key2)) return false; + + int index3 = hash3(hashCode); + T key3 = keyTable[index3]; + if (key.equals(key3)) return false; + + // Find key in the stash. + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key.equals(keyTable[i])) return false; + + // Check for empty buckets. + if (key1 == null) { + keyTable[index1] = key; + if (size++ >= threshold) resize(capacity << 1); + return true; + } + + if (key2 == null) { + keyTable[index2] = key; + if (size++ >= threshold) resize(capacity << 1); + return true; + } + + if (key3 == null) { + keyTable[index3] = key; + if (size++ >= threshold) resize(capacity << 1); + return true; + } + + push(key, index1, key1, index2, key2, index3, key3); + return true; + } + + public void addAll (Array array) { + addAll(array.items, 0, array.size); + } + + public void addAll (Array array, int offset, int length) { + if (offset + length > array.size) + throw new IllegalArgumentException("offset + length must be <= size: " + offset + " + " + length + " <= " + array.size); + addAll((T[])array.items, offset, length); + } + + public void addAll (T... array) { + addAll(array, 0, array.length); + } + + public void addAll (T[] array, int offset, int length) { + ensureCapacity(length); + for (int i = offset, n = i + length; i < n; i++) + add(array[i]); + } + + public void addAll (ObjectSet set) { + ensureCapacity(set.size); + for (T key : set) + add(key); + } + + /** Skips checks for existing keys. */ + private void addResize (T key) { + // Check for empty buckets. + int hashCode = key.hashCode(); + int index1 = hashCode & mask; + T key1 = keyTable[index1]; + if (key1 == null) { + keyTable[index1] = key; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index2 = hash2(hashCode); + T key2 = keyTable[index2]; + if (key2 == null) { + keyTable[index2] = key; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + int index3 = hash3(hashCode); + T key3 = keyTable[index3]; + if (key3 == null) { + keyTable[index3] = key; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + push(key, index1, key1, index2, key2, index3, key3); + } + + private void push (T insertKey, int index1, T key1, int index2, T key2, int index3, T key3) { + T[] keyTable = this.keyTable; + int mask = this.mask; + + // Push keys until an empty bucket is found. + T evictedKey; + int i = 0, pushIterations = this.pushIterations; + do { + // Replace the key and value for one of the hashes. + switch (RandomUtil.int_(2)) { + case 0: + evictedKey = key1; + keyTable[index1] = insertKey; + break; + case 1: + evictedKey = key2; + keyTable[index2] = insertKey; + break; + default: + evictedKey = key3; + keyTable[index3] = insertKey; + 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; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index2 = hash2(hashCode); + key2 = keyTable[index2]; + if (key2 == null) { + keyTable[index2] = evictedKey; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + index3 = hash3(hashCode); + key3 = keyTable[index3]; + if (key3 == null) { + keyTable[index3] = evictedKey; + if (size++ >= threshold) resize(capacity << 1); + return; + } + + if (++i == pushIterations) break; + + insertKey = evictedKey; + } while (true); + + addStash(evictedKey); + } + + private void addStash (T key) { + if (stashSize == stashCapacity) { + // Too many pushes occurred and the stash is full, increase the table size. + resize(capacity << 1); + addResize(key); + return; + } + // Store key in the stash. + int index = capacity + stashSize; + keyTable[index] = key; + stashSize++; + size++; + } + + /** Returns true if the key was removed. */ + public boolean remove (T key) { + int hashCode = key.hashCode(); + int index = hashCode & mask; + if (key.equals(keyTable[index])) { + keyTable[index] = null; + size--; + return true; + } + + index = hash2(hashCode); + if (key.equals(keyTable[index])) { + keyTable[index] = null; + size--; + return true; + } + + index = hash3(hashCode); + if (key.equals(keyTable[index])) { + keyTable[index] = null; + size--; + return true; + } + + return removeStash(key); + } + + boolean removeStash (T key) { + T[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) { + if (key.equals(keyTable[i])) { + removeStashIndex(i); + size--; + return true; + } + } + return false; + } + + 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]; + keyTable[lastIndex] = null; + } + } + + /** Returns true if the set 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 set 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 = MathUtil.nextPowerOfTwo(maximumCapacity); + resize(maximumCapacity); + } + + /** Clears the set and reduces the size of the backing arrays to be the specified capacity, if they are larger. The reduction + * is done by allocating new arrays, though for large arrays this can be faster than clearing the existing array. */ + public void clear (int maximumCapacity) { + if (capacity <= maximumCapacity) { + clear(); + return; + } + size = 0; + resize(maximumCapacity); + } + + /** Clears the set, leaving the backing arrays at the current capacity. When the capacity is high and the population is low, + * iteration can be unnecessarily slow. {@link #clear(int)} can be used to reduce the capacity. */ + public void clear () { + if (size == 0) return; + T[] keyTable = this.keyTable; + for (int i = capacity + stashSize; i-- > 0;) + keyTable[i] = null; + size = 0; + stashSize = 0; + } + + public boolean contains (T 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 getKeyStash(key) != null; + } + } + return true; + } + + /** @return May be null. */ + public T get (T key) { + int hashCode = key.hashCode(); + int index = hashCode & mask; + T found = keyTable[index]; + if (!key.equals(found)) { + index = hash2(hashCode); + found = keyTable[index]; + if (!key.equals(found)) { + index = hash3(hashCode); + found = keyTable[index]; + if (!key.equals(found)) return getKeyStash(key); + } + } + return found; + } + + private T getKeyStash (T key) { + T[] keyTable = this.keyTable; + for (int i = capacity, n = i + stashSize; i < n; i++) + if (key.equals(keyTable[i])) return keyTable[i]; + return null; + } + + public T first () { + T[] keyTable = this.keyTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) + if (keyTable[i] != null) return keyTable[i]; + throw new IllegalStateException("ObjectSet is empty."); + } + + /** 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(MathUtil.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); + + T[] oldKeyTable = keyTable; + + keyTable = (T[])new Object[newSize + stashCapacity]; + + int oldSize = size; + size = 0; + stashSize = 0; + if (oldSize > 0) { + for (int i = 0; i < oldEndIndex; i++) { + T key = oldKeyTable[i]; + if (key != null) addResize(key); + } + } + } + + private int hash2 (int h) { + h *= PRIME2; + return (h ^ h >>> hashShift) & mask; + } + + private int hash3 (int h) { + h *= PRIME3; + return (h ^ h >>> hashShift) & mask; + } + + public int hashCode () { + int h = 0; + for (int i = 0, n = capacity + stashSize; i < n; i++) + if (keyTable[i] != null) h += keyTable[i].hashCode(); + return h; + } + + public boolean equals (Object obj) { + if (!(obj instanceof ObjectSet)) return false; + ObjectSet other = (ObjectSet)obj; + if (other.size != size) return false; + T[] keyTable = this.keyTable; + for (int i = 0, n = capacity + stashSize; i < n; i++) + if (keyTable[i] != null && !other.contains(keyTable[i])) return false; + return true; + } + + public String toString () { + return '{' + toString(", ") + '}'; + } + + public String toString (String separator) { + if (size == 0) return ""; + StringBuilder buffer = new StringBuilder(32); + T[] keyTable = this.keyTable; + int i = keyTable.length; + while (i-- > 0) { + T key = keyTable[i]; + if (key == null) continue; + buffer.append(key); + break; + } + while (i-- > 0) { + T key = keyTable[i]; + if (key == null) continue; + buffer.append(separator); + buffer.append(key); + } + return buffer.toString(); + } + + /** Returns an iterator for the keys in the set. Remove is supported. Note that the same iterator instance is returned each + * time this method is called. Use the {@link ObjectSetIterator} constructor for nested or multithreaded iteration. */ + public ObjectSetIterator iterator () { + if (iterator1 == null) { + iterator1 = new ObjectSetIterator(this); + iterator2 = new ObjectSetIterator(this); + } + if (!iterator1.valid) { + iterator1.reset(); + iterator1.valid = true; + iterator2.valid = false; + return iterator1; + } + iterator2.reset(); + iterator2.valid = true; + iterator1.valid = false; + return iterator2; + } + + static public ObjectSet with (T... array) { + ObjectSet set = new ObjectSet(); + set.addAll(array); + return set; + } + + static public class ObjectSetIterator implements Iterable, Iterator { + public boolean hasNext; + + final ObjectSet set; + int nextIndex, currentIndex; + boolean valid = true; + + public ObjectSetIterator (ObjectSet set) { + this.set = set; + reset(); + } + + public void reset () { + currentIndex = -1; + nextIndex = -1; + findNextIndex(); + } + + private void findNextIndex () { + hasNext = false; + K[] keyTable = set.keyTable; + for (int n = set.capacity + set.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 >= set.capacity) { + set.removeStashIndex(currentIndex); + nextIndex = currentIndex - 1; + findNextIndex(); + } else { + set.keyTable[currentIndex] = null; + } + currentIndex = -1; + set.size--; + } + + public boolean hasNext () { + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + return hasNext; + } + + public K next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + K key = set.keyTable[nextIndex]; + currentIndex = nextIndex; + findNextIndex(); + return key; + } + + public ObjectSetIterator iterator () { + return this; + } + + /** Adds the remaining values to the array. */ + public Array toArray (Array array) { + while (hasNext) + array.add(next()); + return array; + } + + /** Returns a new array containing the remaining values. */ + public Array toArray () { + return toArray(new Array(true, set.size)); + } + } +} diff --git a/src/dorkbox/util/collections/OrderedMap.java b/src/dorkbox/util/collections/OrderedMap.java new file mode 100644 index 0000000..518dba5 --- /dev/null +++ b/src/dorkbox/util/collections/OrderedMap.java @@ -0,0 +1,251 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.NoSuchElementException; + +/** An {@link ObjectMap} that also stores keys in an {@link Array} using the insertion order. Iteration over the + * {@link #entries()}, {@link #keys()}, and {@link #values()} is ordered and faster than an unordered map. Keys can also be + * accessed and the order changed using {@link #orderedKeys()}. There is some additional overhead for put and remove. When used + * for faster iteration versus ObjectMap and the order does not actually matter, copying during remove can be greatly reduced by + * setting {@link Array#ordered} to false for {@link OrderedMap#orderedKeys()}. + * @author Nathan Sweet */ +public class OrderedMap extends ObjectMap { + final Array keys; + + private Entries entries1, entries2; + private Values values1, values2; + private Keys keys1, keys2; + + public OrderedMap () { + keys = new Array(); + } + + public OrderedMap (int initialCapacity) { + super(initialCapacity); + keys = new Array(capacity); + } + + public OrderedMap (int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + keys = new Array(capacity); + } + + public OrderedMap (OrderedMap map) { + super(map); + keys = new Array(map.keys); + } + + public V put (K key, V value) { + if (!containsKey(key)) keys.add(key); + return super.put(key, value); + } + + public V remove (K key) { + keys.removeValue(key, false); + return super.remove(key); + } + + public V removeIndex (int index) { + return super.remove(keys.removeIndex(index)); + } + + public void clear (int maximumCapacity) { + keys.clear(); + super.clear(maximumCapacity); + } + + public void clear () { + keys.clear(); + super.clear(); + } + + public Array orderedKeys () { + return keys; + } + + 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 OrderedMapEntries} constructor for nested or multithreaded iteration. */ + public Entries entries () { + if (entries1 == null) { + entries1 = new OrderedMapEntries(this); + entries2 = new OrderedMapEntries(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 OrderedMapValues} constructor for nested or multithreaded iteration. */ + public Values values () { + if (values1 == null) { + values1 = new OrderedMapValues(this); + values2 = new OrderedMapValues(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 OrderedMapKeys} constructor for nested or multithreaded iteration. */ + public Keys keys () { + if (keys1 == null) { + keys1 = new OrderedMapKeys(this); + keys2 = new OrderedMapKeys(this); + } + if (!keys1.valid) { + keys1.reset(); + keys1.valid = true; + keys2.valid = false; + return keys1; + } + keys2.reset(); + keys2.valid = true; + keys1.valid = false; + return keys2; + } + + public String toString () { + if (size == 0) return "{}"; + StringBuilder buffer = new StringBuilder(32); + buffer.append('{'); + Array keys = this.keys; + for (int i = 0, n = keys.size; i < n; i++) { + K key = keys.get(i); + if (i > 0) buffer.append(", "); + buffer.append(key); + buffer.append('='); + buffer.append(get(key)); + } + buffer.append('}'); + return buffer.toString(); + } + + static public class OrderedMapEntries extends Entries { + private Array keys; + + public OrderedMapEntries (OrderedMap map) { + super(map); + keys = map.keys; + } + + public void reset () { + nextIndex = 0; + hasNext = map.size > 0; + } + + public Entry next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + entry.key = keys.get(nextIndex); + entry.value = map.get(entry.key); + nextIndex++; + hasNext = nextIndex < map.size; + return entry; + } + + public void remove () { + if (currentIndex < 0) throw new IllegalStateException("next must be called before remove."); + map.remove(entry.key); + nextIndex--; + } + } + + static public class OrderedMapKeys extends Keys { + private Array keys; + + public OrderedMapKeys (OrderedMap map) { + super(map); + keys = map.keys; + } + + public void reset () { + nextIndex = 0; + hasNext = map.size > 0; + } + + public K next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + K key = keys.get(nextIndex); + currentIndex = nextIndex; + nextIndex++; + hasNext = nextIndex < map.size; + return key; + } + + public void remove () { + if (currentIndex < 0) throw new IllegalStateException("next must be called before remove."); + ((OrderedMap)map).removeIndex(nextIndex - 1); + nextIndex = currentIndex; + currentIndex = -1; + } + } + + static public class OrderedMapValues extends Values { + private Array keys; + + public OrderedMapValues (OrderedMap map) { + super(map); + keys = map.keys; + } + + public void reset () { + nextIndex = 0; + hasNext = map.size > 0; + } + + public V next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + V value = (V)map.get(keys.get(nextIndex)); + currentIndex = nextIndex; + nextIndex++; + hasNext = nextIndex < map.size; + return value; + } + + public void remove () { + if (currentIndex < 0) throw new IllegalStateException("next must be called before remove."); + ((OrderedMap)map).removeIndex(currentIndex); + nextIndex = currentIndex; + currentIndex = -1; + } + } +} diff --git a/src/dorkbox/util/collections/OrderedSet.java b/src/dorkbox/util/collections/OrderedSet.java new file mode 100644 index 0000000..5629b37 --- /dev/null +++ b/src/dorkbox/util/collections/OrderedSet.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.NoSuchElementException; + +/** An {@link ObjectSet} that also stores keys in an {@link Array} using the insertion order. {@link #iterator() Iteration} is + * ordered and faster than an unordered set. Keys can also be accessed and the order changed using {@link #orderedItems()}. There + * is some additional overhead for put and remove. When used for faster iteration versus ObjectSet and the order does not actually + * matter, copying during remove can be greatly reduced by setting {@link Array#ordered} to false for + * {@link OrderedSet#orderedItems()}. + * @author Nathan Sweet */ +public class OrderedSet extends ObjectSet { + final Array items; + OrderedSetIterator iterator1, iterator2; + + public OrderedSet () { + items = new Array(); + } + + public OrderedSet (int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + items = new Array(capacity); + } + + public OrderedSet (int initialCapacity) { + super(initialCapacity); + items = new Array(capacity); + } + + public OrderedSet (OrderedSet set) { + super(set); + items = new Array(capacity); + items.addAll(set.items); + } + + public boolean add (T key) { + if (!super.add(key)) return false; + items.add(key); + return true; + } + + public boolean add (T key, int index) { + if (!super.add(key)) { + items.removeValue(key, true); + items.insert(index, key); + return false; + } + items.insert(index, key); + return true; + } + + public boolean remove (T key) { + if (!super.remove(key)) return false; + items.removeValue(key, false); + return true; + } + + public T removeIndex (int index) { + T key = items.removeIndex(index); + super.remove(key); + return key; + } + + public void clear (int maximumCapacity) { + items.clear(); + super.clear(maximumCapacity); + } + + public void clear () { + items.clear(); + super.clear(); + } + + public Array orderedItems () { + return items; + } + + public OrderedSetIterator iterator () { + if (iterator1 == null) { + iterator1 = new OrderedSetIterator(this); + iterator2 = new OrderedSetIterator(this); + } + if (!iterator1.valid) { + iterator1.reset(); + iterator1.valid = true; + iterator2.valid = false; + return iterator1; + } + iterator2.reset(); + iterator2.valid = true; + iterator1.valid = false; + return iterator2; + } + + public String toString () { + if (size == 0) return "{}"; + T[] items = this.items.items; + StringBuilder buffer = new StringBuilder(32); + buffer.append('{'); + buffer.append(items[0]); + for (int i = 1; i < size; i++) { + buffer.append(", "); + buffer.append(items[i]); + } + buffer.append('}'); + return buffer.toString(); + } + + public String toString (String separator) { + return items.toString(separator); + } + + static public class OrderedSetIterator extends ObjectSetIterator { + private Array items; + + public OrderedSetIterator (OrderedSet set) { + super(set); + items = set.items; + } + + public void reset () { + nextIndex = 0; + hasNext = set.size > 0; + } + + public T next () { + if (!hasNext) throw new NoSuchElementException(); + if (!valid) throw new RuntimeException("#iterator() cannot be used nested."); + T key = items.get(nextIndex); + nextIndex++; + hasNext = nextIndex < set.size; + return key; + } + + public void remove () { + if (nextIndex < 0) throw new IllegalStateException("next must be called before remove."); + nextIndex--; + ((OrderedSet)set).removeIndex(nextIndex); + } + } +} diff --git a/src/dorkbox/util/collections/Predicate.java b/src/dorkbox/util/collections/Predicate.java new file mode 100644 index 0000000..4c58b25 --- /dev/null +++ b/src/dorkbox/util/collections/Predicate.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Iterator; + +/** Interface used to select items within an iterator against a predicate. + * @author Xoppa */ +public interface Predicate { + + /** @return true if the item matches the criteria and should be included in the iterator's items */ + boolean evaluate (T arg0); + + public class PredicateIterator implements Iterator { + public Iterator iterator; + public Predicate predicate; + public boolean end = false; + public boolean peeked = false; + public T next = null; + + public PredicateIterator (final Iterable iterable, final Predicate predicate) { + this(iterable.iterator(), predicate); + } + + public PredicateIterator (final Iterator iterator, final Predicate predicate) { + set(iterator, predicate); + } + + public void set (final Iterable iterable, final Predicate predicate) { + set(iterable.iterator(), predicate); + } + + public void set (final Iterator iterator, final Predicate predicate) { + this.iterator = iterator; + this.predicate = predicate; + end = peeked = false; + next = null; + } + + @Override + public boolean hasNext () { + if (end) return false; + if (next != null) return true; + peeked = true; + while (iterator.hasNext()) { + final T n = iterator.next(); + if (predicate.evaluate(n)) { + next = n; + return true; + } + } + end = true; + return false; + } + + @Override + public T next () { + if (next == null && !hasNext()) return null; + final T result = next; + next = null; + peeked = false; + return result; + } + + @Override + public void remove () { + if (peeked) throw new RuntimeException("Cannot remove between a call to hasNext() and next()."); + iterator.remove(); + } + } + + public static class PredicateIterable implements Iterable { + public Iterable iterable; + public Predicate predicate; + public PredicateIterator iterator = null; + + public PredicateIterable (Iterable iterable, Predicate predicate) { + set(iterable, predicate); + } + + public void set (Iterable iterable, Predicate predicate) { + this.iterable = iterable; + this.predicate = predicate; + } + + /** Returns an iterator. Note that the same iterator instance is returned each time this method is called. Use the + * {@link Predicate.PredicateIterator} constructor for nested or multithreaded iteration. */ + @Override + public Iterator iterator () { + if (iterator == null) + iterator = new PredicateIterator(iterable.iterator(), predicate); + else + iterator.set(iterable.iterator(), predicate); + return iterator; + } + } +} diff --git a/src/dorkbox/util/collections/QuickSelect.java b/src/dorkbox/util/collections/QuickSelect.java new file mode 100644 index 0000000..9dfdbd8 --- /dev/null +++ b/src/dorkbox/util/collections/QuickSelect.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Comparator; + +/** Implementation of Tony Hoare's quickselect algorithm. Running time is generally O(n), but worst case is O(n^2) Pivot choice is + * median of three method, providing better performance than a random pivot for partially sorted data. + * http://en.wikipedia.org/wiki/Quickselect + * @author Jon Renner */ +public class QuickSelect { + private T[] array; + private Comparator comp; + + public int select (T[] items, Comparator comp, int n, int size) { + this.array = items; + this.comp = comp; + return recursiveSelect(0, size - 1, n); + } + + private int partition (int left, int right, int pivot) { + T pivotValue = array[pivot]; + swap(right, pivot); + int storage = left; + for (int i = left; i < right; i++) { + if (comp.compare(array[i], pivotValue) < 0) { + swap(storage, i); + storage++; + } + } + swap(right, storage); + return storage; + } + + private int recursiveSelect (int left, int right, int k) { + if (left == right) return left; + int pivotIndex = medianOfThreePivot(left, right); + int pivotNewIndex = partition(left, right, pivotIndex); + int pivotDist = (pivotNewIndex - left) + 1; + int result; + if (pivotDist == k) { + result = pivotNewIndex; + } else if (k < pivotDist) { + result = recursiveSelect(left, pivotNewIndex - 1, k); + } else { + result = recursiveSelect(pivotNewIndex + 1, right, k - pivotDist); + } + return result; + } + + /** Median of Three has the potential to outperform a random pivot, especially for partially sorted arrays */ + private int medianOfThreePivot (int leftIdx, int rightIdx) { + T left = array[leftIdx]; + int midIdx = (leftIdx + rightIdx) / 2; + T mid = array[midIdx]; + T right = array[rightIdx]; + + // spaghetti median of three algorithm + // does at most 3 comparisons + if (comp.compare(left, mid) > 0) { + if (comp.compare(mid, right) > 0) { + return midIdx; + } else if (comp.compare(left, right) > 0) { + return rightIdx; + } else { + return leftIdx; + } + } else { + if (comp.compare(left, right) > 0) { + return leftIdx; + } else if (comp.compare(mid, right) > 0) { + return rightIdx; + } else { + return midIdx; + } + } + } + + private void swap (int left, int right) { + T tmp = array[left]; + array[left] = array[right]; + array[right] = tmp; + } +} diff --git a/src/dorkbox/util/collections/Select.java b/src/dorkbox/util/collections/Select.java new file mode 100644 index 0000000..6b1076c --- /dev/null +++ b/src/dorkbox/util/collections/Select.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * 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.util.collections; + +import java.util.Comparator; + +/** This class is for selecting a ranked element (kth ordered statistic) from an unordered list in faster time than sorting the + * whole array. Typical applications include finding the nearest enemy unit(s), and other operations which are likely to run as + * often as every x frames. Certain values of k will result in a partial sorting of the Array. + *

+ * The lowest ranking element starts at 1, not 0. 1 = first, 2 = second, 3 = third, etc. calling with a value of zero will result + * in a {@link RuntimeException} + *

+ *

+ * This class uses very minimal extra memory, as it makes no copies of the array. The underlying algorithms used are a naive + * single-pass for k=min and k=max, and Hoare's quickselect for values in between. + *

+ * @author Jon Renner */ +public class Select { + private static Select instance; + private QuickSelect quickSelect; + + /** Provided for convenience */ + public static Select instance () { + if (instance == null) instance = new Select(); + return instance; + } + + public T select (T[] items, Comparator comp, int kthLowest, int size) { + int idx = selectIndex(items, comp, kthLowest, size); + return items[idx]; + } + + public int selectIndex (T[] items, Comparator comp, int kthLowest, int size) { + if (size < 1) { + throw new RuntimeException("cannot select from empty array (size < 1)"); + } else if (kthLowest > size) { + throw new RuntimeException("Kth rank is larger than size. k: " + kthLowest + ", size: " + size); + } + int idx; + // naive partial selection sort almost certain to outperform quickselect where n is min or max + if (kthLowest == 1) { + // find min + idx = fastMin(items, comp, size); + } else if (kthLowest == size) { + // find max + idx = fastMax(items, comp, size); + } else { + // quickselect a better choice for cases of k between min and max + if (quickSelect == null) quickSelect = new QuickSelect(); + idx = quickSelect.select(items, comp, kthLowest, size); + } + return idx; + } + + /** Faster than quickselect for n = min */ + private int fastMin (T[] items, Comparator comp, int size) { + int lowestIdx = 0; + for (int i = 1; i < size; i++) { + int comparison = comp.compare(items[i], items[lowestIdx]); + if (comparison < 0) { + lowestIdx = i; + } + } + return lowestIdx; + } + + /** Faster than quickselect for n = max */ + private int fastMax (T[] items, Comparator comp, int size) { + int highestIdx = 0; + for (int i = 1; i < size; i++) { + int comparison = comp.compare(items[i], items[highestIdx]); + if (comparison > 0) { + highestIdx = i; + } + } + return highestIdx; + } +} diff --git a/src/dorkbox/util/collections/Sort.java b/src/dorkbox/util/collections/Sort.java new file mode 100644 index 0000000..dc47a02 --- /dev/null +++ b/src/dorkbox/util/collections/Sort.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.util.collections; + +import java.util.Comparator; + +/** Provides methods to sort arrays of objects. Sorting requires working memory and this class allows that memory to be reused to + * avoid allocation. The sorting is otherwise identical to the Arrays.sort methods (uses timsort).
+ *
+ * Note that sorting primitive arrays with the Arrays.sort methods does not allocate memory (unless sorting large arrays of char, + * short, or byte). + * @author Nathan Sweet */ +public class Sort { + static private Sort instance; + + private TimSort timSort; + private ComparableTimSort comparableTimSort; + + public void sort (Array a) { + if (comparableTimSort == null) comparableTimSort = new ComparableTimSort(); + comparableTimSort.doSort((Object[])a.items, 0, a.size); + } + + public void sort (T[] a) { + if (comparableTimSort == null) comparableTimSort = new ComparableTimSort(); + comparableTimSort.doSort(a, 0, a.length); + } + + public void sort (T[] a, int fromIndex, int toIndex) { + if (comparableTimSort == null) comparableTimSort = new ComparableTimSort(); + comparableTimSort.doSort(a, fromIndex, toIndex); + } + + public void sort (Array a, Comparator c) { + if (timSort == null) timSort = new TimSort(); + timSort.doSort((Object[])a.items, (Comparator)c, 0, a.size); + } + + public void sort (T[] a, Comparator c) { + if (timSort == null) timSort = new TimSort(); + timSort.doSort(a, c, 0, a.length); + } + + public void sort (T[] a, Comparator c, int fromIndex, int toIndex) { + if (timSort == null) timSort = new TimSort(); + timSort.doSort(a, c, fromIndex, toIndex); + } + + /** Returns a Sort instance for convenience. Multiple threads must not use this instance at the same time. */ + static public Sort instance () { + if (instance == null) instance = new Sort(); + return instance; + } +} diff --git a/src/dorkbox/util/collections/TimSort.java b/src/dorkbox/util/collections/TimSort.java new file mode 100644 index 0000000..6813bad --- /dev/null +++ b/src/dorkbox/util/collections/TimSort.java @@ -0,0 +1,839 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.util.collections; + +import java.util.Arrays; +import java.util.Comparator; + +/** A stable, adaptive, iterative mergesort that requires far fewer than n lg(n) comparisons when running on partially sorted + * arrays, while offering performance comparable to a traditional mergesort when run on random arrays. Like all proper mergesorts, + * this sort is stable and runs O(n log n) time (worst case). In the worst case, this sort requires temporary storage space for + * n/2 object references; in the best case, it requires only a small constant amount of space. + * + * This implementation was adapted from Tim Peters's list sort for Python, which is described in detail here: + * + * http://svn.python.org/projects/python/trunk/Objects/listsort.txt + * + * Tim's C code may be found here: + * + * http://svn.python.org/projects/python/trunk/Objects/listobject.c + * + * The underlying techniques are described in this paper (and may have even earlier origins): + * + * "Optimistic Sorting and Information Theoretic Complexity" Peter McIlroy SODA (Fourth Annual ACM-SIAM Symposium on Discrete + * Algorithms), pp 467-474, Austin, Texas, 25-27 January 1993. + * + * While the API to this class consists solely of static methods, it is (privately) instantiable; a TimSort instance holds the + * state of an ongoing sort, assuming the input array is large enough to warrant the full-blown TimSort. Small arrays are sorted + * in place, using a binary insertion sort. */ +class TimSort { + /** This is the minimum sized sequence that will be merged. Shorter sequences will be lengthened by calling binarySort. If the + * entire array is less than this length, no merges will be performed. + * + * This constant should be a power of two. It was 64 in Tim Peter's C implementation, but 32 was empirically determined to work + * better in this implementation. In the unlikely event that you set this constant to be a number that's not a power of two, + * you'll need to change the {@link #minRunLength} computation. + * + * If you decrease this constant, you must change the stackLen computation in the TimSort constructor, or you risk an + * ArrayOutOfBounds exception. See listsort.txt for a discussion of the minimum stack length required as a function of the + * length of the array being sorted and the minimum merge sequence length. */ + private static final int MIN_MERGE = 32; + + /** The array being sorted. */ + private T[] a; + + /** The comparator for this sort. */ + private Comparator c; + + /** When we get into galloping mode, we stay there until both runs win less often than MIN_GALLOP consecutive times. */ + private static final int MIN_GALLOP = 7; + + /** This controls when we get *into* galloping mode. It is initialized to MIN_GALLOP. The mergeLo and mergeHi methods nudge it + * higher for random data, and lower for highly structured data. */ + private int minGallop = MIN_GALLOP; + + /** Maximum initial size of tmp array, which is used for merging. The array can grow to accommodate demand. + * + * Unlike Tim's original C version, we do not allocate this much storage when sorting smaller arrays. This change was required + * for performance. */ + private static final int INITIAL_TMP_STORAGE_LENGTH = 256; + + /** Temp storage for merges. */ + private T[] tmp; // Actual runtime type will be Object[], regardless of T + private int tmpCount; + + /** A stack of pending runs yet to be merged. Run i starts at address base[i] and extends for len[i] elements. It's always true + * (so long as the indices are in bounds) that: + * + * runBase[i] + runLen[i] == runBase[i + 1] + * + * so we could cut the storage for this, but it's a minor amount, and keeping all the info explicit simplifies the code. */ + private int stackSize = 0; // Number of pending runs on stack + private final int[] runBase; + private final int[] runLen; + + /** Asserts have been placed in if-statements for performance. To enable them, set this field to true and enable them in VM with + * a command line flag. If you modify this class, please do test the asserts! */ + private static final boolean DEBUG = false; + + TimSort () { + tmp = (T[])new Object[INITIAL_TMP_STORAGE_LENGTH]; + runBase = new int[40]; + runLen = new int[40]; + } + + public void doSort (T[] a, Comparator c, int lo, int hi) { + stackSize = 0; + rangeCheck(a.length, lo, hi); + int nRemaining = hi - lo; + if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted + + // If array is small, do a "mini-TimSort" with no merges + if (nRemaining < MIN_MERGE) { + int initRunLen = countRunAndMakeAscending(a, lo, hi, c); + binarySort(a, lo, hi, lo + initRunLen, c); + return; + } + + this.a = a; + this.c = c; + tmpCount = 0; + + /** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and + * merging runs to maintain stack invariant. */ + int minRun = minRunLength(nRemaining); + do { + // Identify next run + int runLen = countRunAndMakeAscending(a, lo, hi, c); + + // If run is short, extend to min(minRun, nRemaining) + if (runLen < minRun) { + int force = nRemaining <= minRun ? nRemaining : minRun; + binarySort(a, lo, lo + force, lo + runLen, c); + runLen = force; + } + + // Push run onto pending-run stack, and maybe merge + pushRun(lo, runLen); + mergeCollapse(); + + // Advance to find next run + lo += runLen; + nRemaining -= runLen; + } while (nRemaining != 0); + + // Merge all remaining runs to complete sort + if (DEBUG) assert lo == hi; + mergeForceCollapse(); + if (DEBUG) assert stackSize == 1; + + this.a = null; + this.c = null; + T[] tmp = this.tmp; + for (int i = 0, n = tmpCount; i < n; i++) + tmp[i] = null; + } + + /** Creates a TimSort instance to maintain the state of an ongoing sort. + * + * @param a the array to be sorted + * @param c the comparator to determine the order of the sort */ + private TimSort (T[] a, Comparator c) { + this.a = a; + this.c = c; + + // Allocate temp storage (which may be increased later if necessary) + int len = a.length; + T[] newArray = (T[])new Object[len < 2 * INITIAL_TMP_STORAGE_LENGTH ? len >>> 1 : INITIAL_TMP_STORAGE_LENGTH]; + tmp = newArray; + + /* + * Allocate runs-to-be-merged stack (which cannot be expanded). The stack length requirements are described in listsort.txt. + * The C version always uses the same stack length (85), but this was measured to be too expensive when sorting "mid-sized" + * arrays (e.g., 100 elements) in Java. Therefore, we use smaller (but sufficiently large) stack lengths for smaller arrays. + * The "magic numbers" in the computation below must be changed if MIN_MERGE is decreased. See the MIN_MERGE declaration + * above for more information. + */ + int stackLen = (len < 120 ? 5 : len < 1542 ? 10 : len < 119151 ? 19 : 40); + runBase = new int[stackLen]; + runLen = new int[stackLen]; + } + + /* + * The next two methods (which are package private and static) constitute the entire API of this class. Each of these methods + * obeys the contract of the public method with the same signature in java.util.Arrays. + */ + + static void sort (T[] a, Comparator c) { + sort(a, 0, a.length, c); + } + + static void sort (T[] a, int lo, int hi, Comparator c) { + if (c == null) { + Arrays.sort(a, lo, hi); + return; + } + + rangeCheck(a.length, lo, hi); + int nRemaining = hi - lo; + if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted + + // If array is small, do a "mini-TimSort" with no merges + if (nRemaining < MIN_MERGE) { + int initRunLen = countRunAndMakeAscending(a, lo, hi, c); + binarySort(a, lo, hi, lo + initRunLen, c); + return; + } + + /** March over the array once, left to right, finding natural runs, extending short natural runs to minRun elements, and + * merging runs to maintain stack invariant. */ + TimSort ts = new TimSort(a, c); + int minRun = minRunLength(nRemaining); + do { + // Identify next run + int runLen = countRunAndMakeAscending(a, lo, hi, c); + + // If run is short, extend to min(minRun, nRemaining) + if (runLen < minRun) { + int force = nRemaining <= minRun ? nRemaining : minRun; + binarySort(a, lo, lo + force, lo + runLen, c); + runLen = force; + } + + // Push run onto pending-run stack, and maybe merge + ts.pushRun(lo, runLen); + ts.mergeCollapse(); + + // Advance to find next run + lo += runLen; + nRemaining -= runLen; + } while (nRemaining != 0); + + // Merge all remaining runs to complete sort + if (DEBUG) assert lo == hi; + ts.mergeForceCollapse(); + if (DEBUG) assert ts.stackSize == 1; + } + + /** Sorts the specified portion of the specified array using a binary insertion sort. This is the best method for sorting small + * numbers of elements. It requires O(n log n) compares, but O(n^2) data movement (worst case). + * + * If the initial part of the specified range is already sorted, this method can take advantage of it: the method assumes that + * the elements from index {@code lo}, inclusive, to {@code start}, exclusive are already sorted. + * + * @param a the array in which a range is to be sorted + * @param lo the index of the first element in the range to be sorted + * @param hi the index after the last element in the range to be sorted + * @param start the index of the first element in the range that is not already known to be sorted (@code lo <= start <= hi} + * @param c comparator to used for the sort */ + @SuppressWarnings("fallthrough") + private static void binarySort (T[] a, int lo, int hi, int start, Comparator c) { + if (DEBUG) assert lo <= start && start <= hi; + if (start == lo) start++; + for (; start < hi; start++) { + T pivot = a[start]; + + // Set left (and right) to the index where a[start] (pivot) belongs + int left = lo; + int right = start; + if (DEBUG) assert left <= right; + /* + * Invariants: pivot >= all in [lo, left). pivot < all in [right, start). + */ + while (left < right) { + int mid = (left + right) >>> 1; + if (c.compare(pivot, a[mid]) < 0) + right = mid; + else + left = mid + 1; + } + if (DEBUG) assert left == right; + + /* + * The invariants still hold: pivot >= all in [lo, left) and pivot < all in [left, start), so pivot belongs at left. Note + * that if there are elements equal to pivot, left points to the first slot after them -- that's why this sort is stable. + * Slide elements over to make room for pivot. + */ + int n = start - left; // The number of elements to move + // Switch is just an optimization for arraycopy in default case + switch (n) { + case 2: + a[left + 2] = a[left + 1]; + case 1: + a[left + 1] = a[left]; + break; + default: + System.arraycopy(a, left, a, left + 1, n); + } + a[left] = pivot; + } + } + + /** Returns the length of the run beginning at the specified position in the specified array and reverses the run if it is + * descending (ensuring that the run will always be ascending when the method returns). + * + * A run is the longest ascending sequence with: + * + * a[lo] <= a[lo + 1] <= a[lo + 2] <= ... + * + * or the longest descending sequence with: + * + * a[lo] > a[lo + 1] > a[lo + 2] > ... + * + * For its intended use in a stable mergesort, the strictness of the definition of "descending" is needed so that the call can + * safely reverse a descending sequence without violating stability. + * + * @param a the array in which a run is to be counted and possibly reversed + * @param lo index of the first element in the run + * @param hi index after the last element that may be contained in the run. It is required that @code{lo < hi}. + * @param c the comparator to used for the sort + * @return the length of the run beginning at the specified position in the specified array */ + private static int countRunAndMakeAscending (T[] a, int lo, int hi, Comparator c) { + if (DEBUG) assert lo < hi; + int runHi = lo + 1; + if (runHi == hi) return 1; + + // Find end of run, and reverse range if descending + if (c.compare(a[runHi++], a[lo]) < 0) { // Descending + while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0) + runHi++; + reverseRange(a, lo, runHi); + } else { // Ascending + while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0) + runHi++; + } + + return runHi - lo; + } + + /** Reverse the specified range of the specified array. + * + * @param a the array in which a range is to be reversed + * @param lo the index of the first element in the range to be reversed + * @param hi the index after the last element in the range to be reversed */ + private static void reverseRange (Object[] a, int lo, int hi) { + hi--; + while (lo < hi) { + Object t = a[lo]; + a[lo++] = a[hi]; + a[hi--] = t; + } + } + + /** Returns the minimum acceptable run length for an array of the specified length. Natural runs shorter than this will be + * extended with {@link #binarySort}. + * + * Roughly speaking, the computation is: + * + * If n < MIN_MERGE, return n (it's too small to bother with fancy stuff). Else if n is an exact power of 2, return + * MIN_MERGE/2. Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k is close to, but strictly less than, an + * exact power of 2. + * + * For the rationale, see listsort.txt. + * + * @param n the length of the array to be sorted + * @return the length of the minimum run to be merged */ + private static int minRunLength (int n) { + if (DEBUG) assert n >= 0; + int r = 0; // Becomes 1 if any 1 bits are shifted off + while (n >= MIN_MERGE) { + r |= (n & 1); + n >>= 1; + } + return n + r; + } + + /** Pushes the specified run onto the pending-run stack. + * + * @param runBase index of the first element in the run + * @param runLen the number of elements in the run */ + private void pushRun (int runBase, int runLen) { + this.runBase[stackSize] = runBase; + this.runLen[stackSize] = runLen; + stackSize++; + } + + /** Examines the stack of runs waiting to be merged and merges adjacent runs until the stack invariants are reestablished: + * + * 1. runLen[n - 2] > runLen[n - 1] + runLen[n] 2. runLen[n - 1] > runLen[n] + * + * where n is the index of the last run in runLen. + * + * This method has been formally verified to be correct after checking the last 4 runs. + * Checking for 3 runs results in an exception for large arrays. + * (Source: http://envisage-project.eu/proving-android-java-and-python-sorting-algorithm-is-broken-and-how-to-fix-it/) + * + * This method is called each time a new run is pushed onto the stack, so the invariants are guaranteed to hold for i < + * stackSize upon entry to the method. */ + private void mergeCollapse () { + while (stackSize > 1) { + int n = stackSize - 2; + if ((n >= 1 && runLen[n - 1] <= runLen[n] + runLen[n + 1]) || (n >= 2 && runLen[n - 2] <= runLen[n] + runLen[n - 1])) { + if (runLen[n - 1] < runLen[n + 1]) n--; + } else if (runLen[n] > runLen[n + 1]) { + break; // Invariant is established + } + mergeAt(n); + } + } + + /** Merges all runs on the stack until only one remains. This method is called once, to complete the sort. */ + private void mergeForceCollapse () { + while (stackSize > 1) { + int n = stackSize - 2; + if (n > 0 && runLen[n - 1] < runLen[n + 1]) n--; + mergeAt(n); + } + } + + /** Merges the two runs at stack indices i and i+1. Run i must be the penultimate or antepenultimate run on the stack. In other + * words, i must be equal to stackSize-2 or stackSize-3. + * + * @param i stack index of the first of the two runs to merge */ + private void mergeAt (int i) { + if (DEBUG) assert stackSize >= 2; + if (DEBUG) assert i >= 0; + if (DEBUG) assert i == stackSize - 2 || i == stackSize - 3; + + int base1 = runBase[i]; + int len1 = runLen[i]; + int base2 = runBase[i + 1]; + int len2 = runLen[i + 1]; + if (DEBUG) assert len1 > 0 && len2 > 0; + if (DEBUG) assert base1 + len1 == base2; + + /* + * Record the length of the combined runs; if i is the 3rd-last run now, also slide over the last run (which isn't involved + * in this merge). The current run (i+1) goes away in any case. + */ + runLen[i] = len1 + len2; + if (i == stackSize - 3) { + runBase[i + 1] = runBase[i + 2]; + runLen[i + 1] = runLen[i + 2]; + } + stackSize--; + + /* + * Find where the first element of run2 goes in run1. Prior elements in run1 can be ignored (because they're already in + * place). + */ + int k = gallopRight(a[base2], a, base1, len1, 0, c); + if (DEBUG) assert k >= 0; + base1 += k; + len1 -= k; + if (len1 == 0) return; + + /* + * Find where the last element of run1 goes in run2. Subsequent elements in run2 can be ignored (because they're already in + * place). + */ + len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c); + if (DEBUG) assert len2 >= 0; + if (len2 == 0) return; + + // Merge remaining runs, using tmp array with min(len1, len2) elements + if (len1 <= len2) + mergeLo(base1, len1, base2, len2); + else + mergeHi(base1, len1, base2, len2); + } + + /** Locates the position at which to insert the specified key into the specified sorted range; if the range contains an element + * equal to key, returns the index of the leftmost equal element. + * + * @param key the key whose insertion point to search for + * @param a the array in which to search + * @param base the index of the first element in the range + * @param len the length of the range; must be > 0 + * @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method + * will run. + * @param c the comparator used to order the range, and to search + * @return the int k, 0 <= k <= n such that a[b + k - 1] < key <= a[b + k], pretending that a[b - 1] is minus infinity and a[b + * + n] is infinity. In other words, key belongs at index b + k; or in other words, the first k elements of a should + * precede key, and the last n - k should follow it. */ + private static int gallopLeft (T key, T[] a, int base, int len, int hint, Comparator c) { + if (DEBUG) assert len > 0 && hint >= 0 && hint < len; + int lastOfs = 0; + int ofs = 1; + if (c.compare(key, a[base + hint]) > 0) { + // Gallop right until a[base+hint+lastOfs] < key <= a[base+hint+ofs] + int maxOfs = len - hint; + while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) > 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to base + lastOfs += hint; + ofs += hint; + } else { // key <= a[base + hint] + // Gallop left until a[base+hint-ofs] < key <= a[base+hint-lastOfs] + final int maxOfs = hint + 1; + while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) <= 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to base + int tmp = lastOfs; + lastOfs = hint - ofs; + ofs = hint - tmp; + } + if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len; + + /* + * Now a[base+lastOfs] < key <= a[base+ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs. + * Do a binary search, with invariant a[base + lastOfs - 1] < key <= a[base + ofs]. + */ + lastOfs++; + while (lastOfs < ofs) { + int m = lastOfs + ((ofs - lastOfs) >>> 1); + + if (c.compare(key, a[base + m]) > 0) + lastOfs = m + 1; // a[base + m] < key + else + ofs = m; // key <= a[base + m] + } + if (DEBUG) assert lastOfs == ofs; // so a[base + ofs - 1] < key <= a[base + ofs] + return ofs; + } + + /** Like gallopLeft, except that if the range contains an element equal to key, gallopRight returns the index after the + * rightmost equal element. + * + * @param key the key whose insertion point to search for + * @param a the array in which to search + * @param base the index of the first element in the range + * @param len the length of the range; must be > 0 + * @param hint the index at which to begin the search, 0 <= hint < n. The closer hint is to the result, the faster this method + * will run. + * @param c the comparator used to order the range, and to search + * @return the int k, 0 <= k <= n such that a[b + k - 1] <= key < a[b + k] */ + private static int gallopRight (T key, T[] a, int base, int len, int hint, Comparator c) { + if (DEBUG) assert len > 0 && hint >= 0 && hint < len; + + int ofs = 1; + int lastOfs = 0; + if (c.compare(key, a[base + hint]) < 0) { + // Gallop left until a[b+hint - ofs] <= key < a[b+hint - lastOfs] + int maxOfs = hint + 1; + while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) < 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to b + int tmp = lastOfs; + lastOfs = hint - ofs; + ofs = hint - tmp; + } else { // a[b + hint] <= key + // Gallop right until a[b+hint + lastOfs] <= key < a[b+hint + ofs] + int maxOfs = len - hint; + while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) >= 0) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + if (ofs <= 0) // int overflow + ofs = maxOfs; + } + if (ofs > maxOfs) ofs = maxOfs; + + // Make offsets relative to b + lastOfs += hint; + ofs += hint; + } + if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len; + + /* + * Now a[b + lastOfs] <= key < a[b + ofs], so key belongs somewhere to the right of lastOfs but no farther right than ofs. + * Do a binary search, with invariant a[b + lastOfs - 1] <= key < a[b + ofs]. + */ + lastOfs++; + while (lastOfs < ofs) { + int m = lastOfs + ((ofs - lastOfs) >>> 1); + + if (c.compare(key, a[base + m]) < 0) + ofs = m; // key < a[b + m] + else + lastOfs = m + 1; // a[b + m] <= key + } + if (DEBUG) assert lastOfs == ofs; // so a[b + ofs - 1] <= key < a[b + ofs] + return ofs; + } + + /** Merges two adjacent runs in place, in a stable fashion. The first element of the first run must be greater than the first + * element of the second run (a[base1] > a[base2]), and the last element of the first run (a[base1 + len1-1]) must be greater + * than all elements of the second run. + * + * For performance, this method should be called only when len1 <= len2; its twin, mergeHi should be called if len1 >= len2. + * (Either method may be called if len1 == len2.) + * + * @param base1 index of first element in first run to be merged + * @param len1 length of first run to be merged (must be > 0) + * @param base2 index of first element in second run to be merged (must be aBase + aLen) + * @param len2 length of second run to be merged (must be > 0) */ + private void mergeLo (int base1, int len1, int base2, int len2) { + if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2; + + // Copy first run into temp array + T[] a = this.a; // For performance + T[] tmp = ensureCapacity(len1); + System.arraycopy(a, base1, tmp, 0, len1); + + int cursor1 = 0; // Indexes into tmp array + int cursor2 = base2; // Indexes int a + int dest = base1; // Indexes int a + + // Move first element of second run and deal with degenerate cases + a[dest++] = a[cursor2++]; + if (--len2 == 0) { + System.arraycopy(tmp, cursor1, a, dest, len1); + return; + } + if (len1 == 1) { + System.arraycopy(a, cursor2, a, dest, len2); + a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge + return; + } + + Comparator c = this.c; // Use local variable for performance + int minGallop = this.minGallop; // " " " " " + outer: + while (true) { + int count1 = 0; // Number of times in a row that first run won + int count2 = 0; // Number of times in a row that second run won + + /* + * Do the straightforward thing until (if ever) one run starts winning consistently. + */ + do { + if (DEBUG) assert len1 > 1 && len2 > 0; + if (c.compare(a[cursor2], tmp[cursor1]) < 0) { + a[dest++] = a[cursor2++]; + count2++; + count1 = 0; + if (--len2 == 0) break outer; + } else { + a[dest++] = tmp[cursor1++]; + count1++; + count2 = 0; + if (--len1 == 1) break outer; + } + } while ((count1 | count2) < minGallop); + + /* + * One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if + * ever) neither run appears to be winning consistently anymore. + */ + do { + if (DEBUG) assert len1 > 1 && len2 > 0; + count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c); + if (count1 != 0) { + System.arraycopy(tmp, cursor1, a, dest, count1); + dest += count1; + cursor1 += count1; + len1 -= count1; + if (len1 <= 1) // len1 == 1 || len1 == 0 + break outer; + } + a[dest++] = a[cursor2++]; + if (--len2 == 0) break outer; + + count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c); + if (count2 != 0) { + System.arraycopy(a, cursor2, a, dest, count2); + dest += count2; + cursor2 += count2; + len2 -= count2; + if (len2 == 0) break outer; + } + a[dest++] = tmp[cursor1++]; + if (--len1 == 1) break outer; + minGallop--; + } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP); + if (minGallop < 0) minGallop = 0; + minGallop += 2; // Penalize for leaving gallop mode + } // End of "outer" loop + this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field + + if (len1 == 1) { + if (DEBUG) assert len2 > 0; + System.arraycopy(a, cursor2, a, dest, len2); + a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge + } else if (len1 == 0) { + throw new IllegalArgumentException("Comparison method violates its general contract!"); + } else { + if (DEBUG) assert len2 == 0; + if (DEBUG) assert len1 > 1; + System.arraycopy(tmp, cursor1, a, dest, len1); + } + } + + /** Like mergeLo, except that this method should be called only if len1 >= len2; mergeLo should be called if len1 <= len2. + * (Either method may be called if len1 == len2.) + * + * @param base1 index of first element in first run to be merged + * @param len1 length of first run to be merged (must be > 0) + * @param base2 index of first element in second run to be merged (must be aBase + aLen) + * @param len2 length of second run to be merged (must be > 0) */ + private void mergeHi (int base1, int len1, int base2, int len2) { + if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2; + + // Copy second run into temp array + T[] a = this.a; // For performance + T[] tmp = ensureCapacity(len2); + System.arraycopy(a, base2, tmp, 0, len2); + + int cursor1 = base1 + len1 - 1; // Indexes into a + int cursor2 = len2 - 1; // Indexes into tmp array + int dest = base2 + len2 - 1; // Indexes into a + + // Move last element of first run and deal with degenerate cases + a[dest--] = a[cursor1--]; + if (--len1 == 0) { + System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2); + return; + } + if (len2 == 1) { + dest -= len1; + cursor1 -= len1; + System.arraycopy(a, cursor1 + 1, a, dest + 1, len1); + a[dest] = tmp[cursor2]; + return; + } + + Comparator c = this.c; // Use local variable for performance + int minGallop = this.minGallop; // " " " " " + outer: + while (true) { + int count1 = 0; // Number of times in a row that first run won + int count2 = 0; // Number of times in a row that second run won + + /* + * Do the straightforward thing until (if ever) one run appears to win consistently. + */ + do { + if (DEBUG) assert len1 > 0 && len2 > 1; + if (c.compare(tmp[cursor2], a[cursor1]) < 0) { + a[dest--] = a[cursor1--]; + count1++; + count2 = 0; + if (--len1 == 0) break outer; + } else { + a[dest--] = tmp[cursor2--]; + count2++; + count1 = 0; + if (--len2 == 1) break outer; + } + } while ((count1 | count2) < minGallop); + + /* + * One run is winning so consistently that galloping may be a huge win. So try that, and continue galloping until (if + * ever) neither run appears to be winning consistently anymore. + */ + do { + if (DEBUG) assert len1 > 0 && len2 > 1; + count1 = len1 - gallopRight(tmp[cursor2], a, base1, len1, len1 - 1, c); + if (count1 != 0) { + dest -= count1; + cursor1 -= count1; + len1 -= count1; + System.arraycopy(a, cursor1 + 1, a, dest + 1, count1); + if (len1 == 0) break outer; + } + a[dest--] = tmp[cursor2--]; + if (--len2 == 1) break outer; + + count2 = len2 - gallopLeft(a[cursor1], tmp, 0, len2, len2 - 1, c); + if (count2 != 0) { + dest -= count2; + cursor2 -= count2; + len2 -= count2; + System.arraycopy(tmp, cursor2 + 1, a, dest + 1, count2); + if (len2 <= 1) // len2 == 1 || len2 == 0 + break outer; + } + a[dest--] = a[cursor1--]; + if (--len1 == 0) break outer; + minGallop--; + } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP); + if (minGallop < 0) minGallop = 0; + minGallop += 2; // Penalize for leaving gallop mode + } // End of "outer" loop + this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field + + if (len2 == 1) { + if (DEBUG) assert len1 > 0; + dest -= len1; + cursor1 -= len1; + System.arraycopy(a, cursor1 + 1, a, dest + 1, len1); + a[dest] = tmp[cursor2]; // Move first elt of run2 to front of merge + } else if (len2 == 0) { + throw new IllegalArgumentException("Comparison method violates its general contract!"); + } else { + if (DEBUG) assert len1 == 0; + if (DEBUG) assert len2 > 0; + System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2); + } + } + + /** Ensures that the external array tmp has at least the specified number of elements, increasing its size if necessary. The + * size increases exponentially to ensure amortized linear time complexity. + * + * @param minCapacity the minimum required capacity of the tmp array + * @return tmp, whether or not it grew */ + private T[] ensureCapacity (int minCapacity) { + tmpCount = Math.max(tmpCount, minCapacity); + if (tmp.length < minCapacity) { + // Compute smallest power of 2 > minCapacity + int newSize = minCapacity; + newSize |= newSize >> 1; + newSize |= newSize >> 2; + newSize |= newSize >> 4; + newSize |= newSize >> 8; + newSize |= newSize >> 16; + newSize++; + + if (newSize < 0) // Not bloody likely! + newSize = minCapacity; + else + newSize = Math.min(newSize, a.length >>> 1); + + T[] newArray = (T[])new Object[newSize]; + tmp = newArray; + } + return tmp; + } + + /** Checks that fromIndex and toIndex are in range, and throws an appropriate exception if they aren't. + * + * @param arrayLen the length of the array + * @param fromIndex the index of the first element of the range + * @param toIndex the index after the last element of the range + * @throws IllegalArgumentException if fromIndex > toIndex + * @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or toIndex > arrayLen */ + private static void rangeCheck (int arrayLen, int fromIndex, int toIndex) { + if (fromIndex > toIndex) throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); + if (fromIndex < 0) throw new ArrayIndexOutOfBoundsException(fromIndex); + if (toIndex > arrayLen) throw new ArrayIndexOutOfBoundsException(toIndex); + } +}