diff --git a/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableOpenAddressingBase.java b/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableOpenAddressingBase.java index efd112539..f2241d8b2 100644 --- a/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableOpenAddressingBase.java +++ b/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableOpenAddressingBase.java @@ -1,6 +1,18 @@ /** - * Base class for hashtables with an open addressing collision resolution method such as linear - * probing, quadratic probing and double hashing. + * Base class for hash tables using open addressing collision resolution. + * + * Open addressing stores all entries directly in the bucket array. When a collision occurs, the + * table probes for the next available slot using a probing function (linear, quadratic, double + * hashing, etc.). Subclasses define the probing strategy by implementing setupProbing(), probe(), + * and adjustCapacity(). + * + * Deleted entries are marked with a TOMBSTONE sentinel rather than being nulled out, because + * nulling would break probe chains — a lookup that follows the same probe sequence would + * incorrectly conclude the key doesn't exist when it hits the null gap. + * + * To keep probe chains short, get() and containsKey() perform "lazy relocation": if a tombstone + * is encountered before the target key, the key is moved into the tombstone's slot. This speeds + * up future lookups for the same key. * * @author William Fiset, william.alexandre.fiset@gmail.com */ @@ -14,16 +26,19 @@ public abstract class HashTableOpenAddressingBase implements Iterable { protected double loadFactor; protected int capacity, threshold, modificationCount; - // 'usedBuckets' counts the total number of used buckets inside the - // hash-table (includes cells marked as deleted). While 'keyCount' - // tracks the number of unique keys currently inside the hash-table. + // 'usedBuckets' counts the total number of used buckets inside the hash-table (includes cells + // marked as deleted) and drives resize decisions. While 'keyCount' tracks the number of unique + // keys currently inside the hash-table and is the value returned by size(). protected int usedBuckets, keyCount; // These arrays store the key-value pairs. protected K[] keys; protected V[] values; - // Special marker token used to indicate the deletion of a key-value pair + // Sentinel object marking a deleted slot — distinct from null (empty) and any real key. + // We use a tombstone rather than null because nulling a deleted slot would break probe chains: + // a lookup following the same probe sequence would stop early at the null gap and incorrectly + // conclude the key doesn't exist. protected final K TOMBSTONE = (K) new Object(); private static final int DEFAULT_CAPACITY = 7; @@ -37,10 +52,9 @@ protected HashTableOpenAddressingBase(int capacity) { this(capacity, DEFAULT_LOAD_FACTOR); } - // Designated constructor protected HashTableOpenAddressingBase(int capacity, double loadFactor) { - if (capacity <= 0) throw new IllegalArgumentException("Illegal capacity: " + capacity); - + if (capacity <= 0) + throw new IllegalArgumentException("Illegal capacity: " + capacity); if (loadFactor <= 0 || Double.isNaN(loadFactor) || Double.isInfinite(loadFactor)) throw new IllegalArgumentException("Illegal loadFactor: " + loadFactor); @@ -53,18 +67,23 @@ protected HashTableOpenAddressingBase(int capacity, double loadFactor) { values = (V[]) new Object[this.capacity]; } - // These three methods are used to dictate how the probing is to actually - // occur for whatever open addressing scheme you are implementing. + // These three methods are used to dictate how the probing is to actually occur for whatever + // open addressing scheme you are implementing. setupProbing() is called before probing begins + // for a key — subclasses can cache a secondary hash here (e.g. double hashing). protected abstract void setupProbing(K key); + // Returns the probe offset for the x-th step in the probe sequence. For example, linear + // probing returns x, quadratic probing returns x*x, and double hashing returns x * hash2(key). protected abstract int probe(int x); - // Adjusts the capacity of the hash table after it's been made larger. - // This is important to be able to override because the size of the hashtable - // controls the functionality of the probing function. + // Adjusts the capacity of the hash table after it's been made larger. This is important to be + // able to override because the size of the hash-table controls the functionality of the probing + // function. For example, linear probing requires the capacity to be prime, while quadratic + // probing works best with powers of two. protected abstract void adjustCapacity(); - // Increases the capacity of the hash table. + // Increases the capacity of the hash table. Default is 2n+1; quadratic probing overrides + // to use powers of two. protected void increaseCapacity() { capacity = (2 * capacity) + 1; } @@ -81,7 +100,7 @@ public int size() { return keyCount; } - // Returns the capacity of the hashtable (used mostly for testing) + // Returns the capacity of the hash-table (used mostly for testing) public int getCapacity() { return capacity; } @@ -91,84 +110,32 @@ public boolean isEmpty() { return keyCount == 0; } - public V put(K key, V value) { - return insert(key, value); - } - - public V add(K key, V value) { - return insert(key, value); - } - - // Returns true/false on whether a given key exists within the hash-table. - public boolean containsKey(K key) { - return hasKey(key); - } - - // Returns a list of keys found in the hash table - public List keys() { - List hashtableKeys = new ArrayList<>(size()); - for (int i = 0; i < capacity; i++) { - if (keys[i] != null && keys[i] != TOMBSTONE) { - hashtableKeys.add(keys[i]); - } - } - return hashtableKeys; - } - - // Returns a list of non-unique values found in the hash table - public List values() { - List hashtableValues = new ArrayList<>(size()); - for (int i = 0; i < capacity; i++) { - if (keys[i] != null && keys[i] != TOMBSTONE) { - hashtableValues.add(values[i]); - } - } - return hashtableValues; - } - - // Double the size of the hash-table - protected void resizeTable() { - increaseCapacity(); - adjustCapacity(); - - threshold = (int) (capacity * loadFactor); - - K[] oldKeyTable = keys; - V[] oldValueTable = values; - - keys = (K[]) new Object[capacity]; - values = (V[]) new Object[capacity]; - - // Reset the key count and buckets used since we are about to - // re-insert all the keys into the hash-table. - keyCount = usedBuckets = 0; - - for (int i = 0; i < oldKeyTable.length; i++) { - if (oldKeyTable[i] != null && oldKeyTable[i] != TOMBSTONE) { - insert(oldKeyTable[i], oldValueTable[i]); - } - oldValueTable[i] = null; - oldKeyTable[i] = null; - } - } - - // Converts a hash value to an index. Essentially, this strips the - // negative sign and places the hash value in the domain [0, capacity) + // Converts a hash value to an index. Essentially, this strips the negative sign and places + // the hash value in the domain [0, capacity). protected final int normalizeIndex(int keyHash) { return (keyHash & 0x7FFFFFFF) % capacity; } // Finds the greatest common denominator of a and b. protected static final int gcd(int a, int b) { - if (b == 0) return a; + if (b == 0) + return a; return gcd(b, a % b); } - // Place a key-value pair into the hash-table. If the value already - // exists inside the hash-table then the value is updated. - public V insert(K key, V val) { - if (key == null) throw new IllegalArgumentException("Null key"); - if (usedBuckets >= threshold) resizeTable(); + /** + * Inserts or updates a key-value pair. Returns the previous value, or null if the key is new. + * + *

During probing, if a tombstone is found before an empty slot or matching key, the index is + * saved. If the key is later found, it is relocated to the tombstone's position to shorten + * future probe chains. If an empty slot is reached, the new entry is placed at the tombstone + * (if one was seen) or at the empty slot. + */ + public V put(K key, V val) { + if (key == null) + throw new IllegalArgumentException("Null key"); + if (usedBuckets >= threshold) + resizeTable(); setupProbing(key); final int offset = normalizeIndex(key.hashCode()); @@ -177,17 +144,22 @@ public V insert(K key, V val) { // The current slot was previously deleted if (keys[i] == TOMBSTONE) { - if (j == -1) j = i; + // Remember the first tombstone position for potential relocation + if (j == -1) + j = i; - // The current cell already contains a key + // The current cell already contains a key } else if (keys[i] != null) { // The key we're trying to insert already exists in the hash-table, // so update its value with the most recent value if (keys[i].equals(key)) { V oldValue = values[i]; if (j == -1) { + // No tombstone seen, update in place values[i] = val; } else { + // Previously encountered a tombstone — relocate the key to that earlier + // position so future lookups find it sooner in the probe sequence keys[i] = TOMBSTONE; values[i] = null; keys[j] = key; @@ -197,19 +169,18 @@ public V insert(K key, V val) { return oldValue; } - // Current cell is null so an insertion/update can occur + // Current cell is null so an insertion/update can occur } else { - // No previously encountered deleted buckets if (j == -1) { + // No previously encountered deleted buckets — use this empty slot usedBuckets++; keyCount++; keys[i] = key; values[i] = val; - - // Previously seen deleted bucket. Instead of inserting - // the new element at i where the null element is insert - // it where the deleted token was found. } else { + // Previously seen deleted bucket. Instead of inserting the new element at i + // where the null element is, insert it where the deleted token was found. + // This doesn't increase usedBuckets since we're reusing a tombstone slot. keyCount++; keys[j] = key; values[j] = val; @@ -220,9 +191,15 @@ public V insert(K key, V val) { } } - // Returns true/false on whether a given key exists within the hash-table - public boolean hasKey(K key) { - if (key == null) throw new IllegalArgumentException("Null key"); + /** + * Returns true if the key exists in the table. + * + *

Performs lazy relocation: if a tombstone was encountered before the matching key, the key + * is moved to the tombstone's slot to speed up future lookups. + */ + public boolean containsKey(K key) { + if (key == null) + throw new IllegalArgumentException("Null key"); setupProbing(key); final int offset = normalizeIndex(key.hashCode()); @@ -234,11 +211,11 @@ public boolean hasKey(K key) { // Ignore deleted cells, but record where the first index // of a deleted cell is found to perform lazy relocation later. if (keys[i] == TOMBSTONE) { - if (j == -1) j = i; + if (j == -1) + j = i; - // We hit a non-null key, perhaps it's the one we're looking for. + // We hit a non-null key, perhaps it's the one we're looking for. } else if (keys[i] != null) { - // The key we want is in the hash-table! if (keys[i].equals(key)) { // If j != -1 this means we previously encountered a deleted cell. // We can perform an optimization by swapping the entries in cells @@ -254,16 +231,23 @@ public boolean hasKey(K key) { return true; } - // Key was not found in the hash-table :/ - } else return false; + } else { + // Hit an empty slot — key was not found in the hash-table + return false; + } } } - // Get the value associated with the input key. - // NOTE: returns null if the value is null AND also returns - // null if the key does not exists. + /** + * Returns the value for the given key, or null if not found. + * + *

NOTE: returns null if the value is null AND also returns null if the key does not exist. + * + *

Like containsKey(), performs lazy relocation when a tombstone precedes the target key. + */ public V get(K key) { - if (key == null) throw new IllegalArgumentException("Null key"); + if (key == null) + throw new IllegalArgumentException("Null key"); setupProbing(key); final int offset = normalizeIndex(key.hashCode()); @@ -275,38 +259,42 @@ public V get(K key) { // Ignore deleted cells, but record where the first index // of a deleted cell is found to perform lazy relocation later. if (keys[i] == TOMBSTONE) { - if (j == -1) j = i; + if (j == -1) + j = i; - // We hit a non-null key, perhaps it's the one we're looking for. + // We hit a non-null key, perhaps it's the one we're looking for. } else if (keys[i] != null) { - // The key we want is in the hash-table! if (keys[i].equals(key)) { // If j != -1 this means we previously encountered a deleted cell. // We can perform an optimization by swapping the entries in cells // i and j so that the next time we search for this key it will be // found faster. This is called lazy deletion/relocation. if (j != -1) { - // Swap key-values pairs at indexes i and j. + // Swap key-value pairs at indexes i and j. keys[j] = keys[i]; values[j] = values[i]; keys[i] = TOMBSTONE; values[i] = null; return values[j]; - } else { - return values[i]; } + return values[i]; } - // Element was not found in the hash-table :/ - } else return null; + } else { + // Element was not found in the hash-table + return null; + } } } - // Removes a key from the map and returns the value. - // NOTE: returns null if the value is null AND also returns - // null if the key does not exists. + /** + * Removes a key from the map and returns the value. Marks the slot as TOMBSTONE. + * + *

NOTE: returns null if the value is null AND also returns null if the key does not exist. + */ public V remove(K key) { - if (key == null) throw new IllegalArgumentException("Null key"); + if (key == null) + throw new IllegalArgumentException("Null key"); setupProbing(key); final int offset = normalizeIndex(key.hashCode()); @@ -316,10 +304,12 @@ public V remove(K key) { for (int i = offset, x = 1; ; i = normalizeIndex(offset + probe(x++))) { // Ignore deleted cells - if (keys[i] == TOMBSTONE) continue; + if (keys[i] == TOMBSTONE) + continue; // Key was not found in hash-table. - if (keys[i] == null) return null; + if (keys[i] == null) + return null; // The key we want to remove is in the hash-table! if (keys[i].equals(key)) { @@ -333,52 +323,92 @@ public V remove(K key) { } } + // Returns a list of keys found in the hash table + public List keys() { + List hashtableKeys = new ArrayList<>(keyCount); + for (int i = 0; i < capacity; i++) { + if (keys[i] != null && keys[i] != TOMBSTONE) + hashtableKeys.add(keys[i]); + } + return hashtableKeys; + } + + // Returns a list of non-unique values found in the hash table + public List values() { + List hashtableValues = new ArrayList<>(keyCount); + for (int i = 0; i < capacity; i++) { + if (keys[i] != null && keys[i] != TOMBSTONE) + hashtableValues.add(values[i]); + } + return hashtableValues; + } + + // Doubles capacity, rehashes all live entries, and discards tombstones. + // After resizing, all tombstones are gone because we re-insert only live keys. + protected void resizeTable() { + increaseCapacity(); + adjustCapacity(); + threshold = (int) (capacity * loadFactor); + + K[] oldKeys = keys; + V[] oldValues = values; + + keys = (K[]) new Object[capacity]; + values = (V[]) new Object[capacity]; + + // Reset the key count and buckets used since we are about to + // re-insert all the keys into the hash-table. + keyCount = usedBuckets = 0; + + for (int i = 0; i < oldKeys.length; i++) { + if (oldKeys[i] != null && oldKeys[i] != TOMBSTONE) + put(oldKeys[i], oldValues[i]); + oldKeys[i] = null; + oldValues[i] = null; + } + } + // Return a String view of this hash-table. @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); + StringBuilder sb = new StringBuilder("{"); boolean first = true; for (int i = 0; i < capacity; i++) { if (keys[i] != null && keys[i] != TOMBSTONE) { - if (!first) sb.append(", "); + if (!first) + sb.append(", "); sb.append(keys[i]).append(" => ").append(values[i]); first = false; } } - sb.append("}"); - return sb.toString(); + return sb.append("}").toString(); } @Override public Iterator iterator() { - // Before the iteration begins record the number of modifications - // done to the hash-table. This value should not change as we iterate - // otherwise a concurrent modification has occurred :0 - final int MODIFICATION_COUNT = modificationCount; - + // Before the iteration begins record the number of modifications done to the hash-table. + // This value should not change as we iterate otherwise a concurrent modification has occurred. + final int expectedModCount = modificationCount; return new Iterator() { int index, keysLeft = keyCount; @Override public boolean hasNext() { - // The contents of the table have been altered - if (MODIFICATION_COUNT != modificationCount) throw new ConcurrentModificationException(); + if (expectedModCount != modificationCount) + throw new ConcurrentModificationException(); return keysLeft != 0; } // Find the next element and return it @Override public K next() { - while (keys[index] == null || keys[index] == TOMBSTONE) index++; + if (expectedModCount != modificationCount) + throw new ConcurrentModificationException(); + while (keys[index] == null || keys[index] == TOMBSTONE) + index++; keysLeft--; return keys[index++]; } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } }; } } diff --git a/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChaining.java b/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChaining.java index 3de53deb6..b954f1b75 100644 --- a/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChaining.java +++ b/src/main/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChaining.java @@ -1,5 +1,9 @@ /** - * An implementation of a hash-table using separate chaining with a linked list. + * A hash table implementation using separate chaining with linked lists. + * + * Each bucket holds a linked list of entries. On collision, new entries are appended to the list. + * When the load factor threshold is exceeded, the table doubles in capacity and all entries are + * rehashed. * * @author William Fiset, william.alexandre.fiset@gmail.com */ @@ -7,46 +11,38 @@ import java.util.*; -class Entry { - - int hash; - K key; - V value; +@SuppressWarnings("unchecked") +public class HashTableSeparateChaining implements Iterable { - public Entry(K key, V value) { - this.key = key; - this.value = value; - this.hash = key.hashCode(); - } + private static class Entry { + int hash; + K key; + V value; - @Override - public int hashCode() { - return hash; - } + // Cache the hash code so we don't recompute it on every resize + Entry(K key, V value) { + this.key = key; + this.value = value; + this.hash = key.hashCode(); + } - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - Entry other = (Entry) obj; - if (hash != other.hash) return false; - return key.equals(other.key); - } + boolean keyEquals(K other) { + return key.equals(other); + } - @Override - public String toString() { - return key + " => " + value; + @Override + public String toString() { + return key + " => " + value; + } } -} - -@SuppressWarnings("unchecked") -public class HashTableSeparateChaining implements Iterable { private static final int DEFAULT_CAPACITY = 3; private static final double DEFAULT_LOAD_FACTOR = 0.75; private double maxLoadFactor; - private int capacity, threshold, size = 0; + private int capacity, threshold, size; + // Tracks structural modifications for fail-fast iteration + private int modCount; private LinkedList>[] table; public HashTableSeparateChaining() { @@ -57,9 +53,9 @@ public HashTableSeparateChaining(int capacity) { this(capacity, DEFAULT_LOAD_FACTOR); } - // Designated constructor public HashTableSeparateChaining(int capacity, double maxLoadFactor) { - if (capacity < 0) throw new IllegalArgumentException("Illegal capacity"); + if (capacity < 0) + throw new IllegalArgumentException("Illegal capacity"); if (maxLoadFactor <= 0 || Double.isNaN(maxLoadFactor) || Double.isInfinite(maxLoadFactor)) throw new IllegalArgumentException("Illegal maxLoadFactor"); this.maxLoadFactor = maxLoadFactor; @@ -68,210 +64,197 @@ public HashTableSeparateChaining(int capacity, double maxLoadFactor) { table = new LinkedList[this.capacity]; } - // Returns the number of elements currently inside the hash-table public int size() { return size; } - // Returns true/false depending on whether the hash-table is empty public boolean isEmpty() { return size == 0; } - // Converts a hash value to an index. Essentially, this strips the - // negative sign and places the hash value in the domain [0, capacity) + // Strips the negative sign and maps the hash into [0, capacity) private int normalizeIndex(int keyHash) { return (keyHash & 0x7FFFFFFF) % capacity; } - // Clears all the contents of the hash-table public void clear() { Arrays.fill(table, null); size = 0; + modCount++; } public boolean containsKey(K key) { - return hasKey(key); - } - - // Returns true/false depending on whether a key is in the hash table - public boolean hasKey(K key) { int bucketIndex = normalizeIndex(key.hashCode()); - return bucketSeekEntry(bucketIndex, key) != null; + return seekEntry(bucketIndex, key) != null; } - // Insert, put and add all place a value in the hash-table public V put(K key, V value) { - return insert(key, value); - } - - public V add(K key, V value) { - return insert(key, value); - } - - public V insert(K key, V value) { - if (key == null) throw new IllegalArgumentException("Null key"); + if (key == null) + throw new IllegalArgumentException("Null key"); Entry newEntry = new Entry<>(key, value); int bucketIndex = normalizeIndex(newEntry.hash); - return bucketInsertEntry(bucketIndex, newEntry); + return insertEntry(bucketIndex, newEntry); } - // Gets a key's values from the map and returns the value. - // NOTE: returns null if the value is null AND also returns - // null if the key does not exists, so watch out.. public V get(K key) { - if (key == null) return null; + if (key == null) + return null; int bucketIndex = normalizeIndex(key.hashCode()); - Entry entry = bucketSeekEntry(bucketIndex, key); - if (entry != null) return entry.value; - return null; + Entry entry = seekEntry(bucketIndex, key); + return entry != null ? entry.value : null; } - // Removes a key from the map and returns the value. - // NOTE: returns null if the value is null AND also returns - // null if the key does not exists. public V remove(K key) { - if (key == null) return null; + if (key == null) + return null; int bucketIndex = normalizeIndex(key.hashCode()); - return bucketRemoveEntry(bucketIndex, key); + return removeEntry(bucketIndex, key); } - // Removes an entry from a given bucket if it exists - private V bucketRemoveEntry(int bucketIndex, K key) { - Entry entry = bucketSeekEntry(bucketIndex, key); - if (entry != null) { - LinkedList> links = table[bucketIndex]; - links.remove(entry); - --size; - return entry.value; - } else return null; + // Single-pass find-and-remove using an iterator to avoid scanning the bucket twice + private V removeEntry(int bucketIndex, K key) { + LinkedList> bucket = table[bucketIndex]; + if (bucket == null) + return null; + java.util.Iterator> iter = bucket.iterator(); + while (iter.hasNext()) { + Entry entry = iter.next(); + if (entry.keyEquals(key)) { + iter.remove(); + size--; + modCount++; + return entry.value; + } + } + return null; } - // Inserts an entry in a given bucket only if the entry does not already - // exist in the given bucket, but if it does then update the entry value - private V bucketInsertEntry(int bucketIndex, Entry entry) { + // If the key already exists, update its value and return the old value. + // Otherwise, insert a new entry and return null. + private V insertEntry(int bucketIndex, Entry entry) { LinkedList> bucket = table[bucketIndex]; - if (bucket == null) table[bucketIndex] = bucket = new LinkedList<>(); + if (bucket == null) + table[bucketIndex] = bucket = new LinkedList<>(); - Entry existentEntry = bucketSeekEntry(bucketIndex, entry.key); - if (existentEntry == null) { + Entry existing = seekEntry(bucketIndex, entry.key); + if (existing == null) { bucket.add(entry); - if (++size > threshold) resizeTable(); - return null; // Use null to indicate that there was no previous entry - } else { - V oldVal = existentEntry.value; - existentEntry.value = entry.value; - return oldVal; + if (++size > threshold) + resizeTable(); + modCount++; + return null; } + V oldVal = existing.value; + existing.value = entry.value; + return oldVal; } - // Finds and returns a particular entry in a given bucket if it exists, returns null otherwise - private Entry bucketSeekEntry(int bucketIndex, K key) { - if (key == null) return null; + // Linearly scans the bucket's chain looking for a matching key + private Entry seekEntry(int bucketIndex, K key) { + if (key == null) + return null; LinkedList> bucket = table[bucketIndex]; - if (bucket == null) return null; - for (Entry entry : bucket) if (entry.key.equals(key)) return entry; + if (bucket == null) + return null; + for (Entry entry : bucket) { + if (entry.keyEquals(key)) + return entry; + } return null; } - // Resizes the internal table holding buckets of entries + // Doubles the table capacity and rehashes all entries into new buckets private void resizeTable() { capacity *= 2; threshold = (int) (capacity * maxLoadFactor); - LinkedList>[] newTable = new LinkedList[capacity]; - for (int i = 0; i < table.length; i++) { - if (table[i] != null) { - for (Entry entry : table[i]) { - int bucketIndex = normalizeIndex(entry.hash); - LinkedList> bucket = newTable[bucketIndex]; - if (bucket == null) newTable[bucketIndex] = bucket = new LinkedList<>(); - bucket.add(entry); - } - - // Avoid memory leak. Help the GC - table[i].clear(); - table[i] = null; + for (LinkedList> bucket : table) { + if (bucket == null) + continue; + for (Entry entry : bucket) { + int i = normalizeIndex(entry.hash); + if (newTable[i] == null) + newTable[i] = new LinkedList<>(); + newTable[i].add(entry); } } table = newTable; } - // Returns the list of keys found within the hash table public List keys() { - List keys = new ArrayList<>(size()); - for (LinkedList> bucket : table) - if (bucket != null) for (Entry entry : bucket) keys.add(entry.key); + List keys = new ArrayList<>(size); + for (LinkedList> bucket : table) { + if (bucket != null) + for (Entry entry : bucket) + keys.add(entry.key); + } return keys; } - // Returns the list of values found within the hash table public List values() { - List values = new ArrayList<>(size()); - for (LinkedList> bucket : table) - if (bucket != null) for (Entry entry : bucket) values.add(entry.value); + List values = new ArrayList<>(size); + for (LinkedList> bucket : table) { + if (bucket != null) + for (Entry entry : bucket) + values.add(entry.value); + } return values; } - // Return an iterator to iterate over all the keys in this map @Override - public java.util.Iterator iterator() { - final int MODIFICATION_COUNT = size; // Using size as a proxy for modifications for now, but better to have a dedicated counter - return new java.util.Iterator() { + public Iterator iterator() { + return new Iterator() { + final int expectedModCount = modCount; int bucketIndex = 0; - java.util.Iterator> bucketIter = (table[0] == null) ? null : table[0].iterator(); + Iterator> bucketIter = advanceToNextBucket(); - @Override - public boolean hasNext() { - // An item was added or removed while iterating - if (MODIFICATION_COUNT != size) throw new java.util.ConcurrentModificationException(); - - // No iterator or the current iterator is empty - if (bucketIter == null || !bucketIter.hasNext()) { - // Search next buckets until a valid iterator is found - while (++bucketIndex < capacity) { - if (table[bucketIndex] != null) { - // Make sure this iterator actually has elements -_- - java.util.Iterator> nextIter = table[bucketIndex].iterator(); - if (nextIter.hasNext()) { - bucketIter = nextIter; - break; - } - } - } + private Iterator> advanceToNextBucket() { + while (bucketIndex < capacity) { + if (table[bucketIndex] != null && !table[bucketIndex].isEmpty()) + return table[bucketIndex].iterator(); + bucketIndex++; } - return bucketIndex < capacity; + return null; } @Override - public K next() { - if (!hasNext()) throw new NoSuchElementException(); - return bucketIter.next().key; + public boolean hasNext() { + if (expectedModCount != modCount) + throw new ConcurrentModificationException(); + return bucketIter != null && bucketIter.hasNext(); } @Override - public void remove() { - throw new UnsupportedOperationException(); + public K next() { + if (expectedModCount != modCount) + throw new ConcurrentModificationException(); + if (bucketIter == null || !bucketIter.hasNext()) + throw new NoSuchElementException(); + K key = bucketIter.next().key; + if (!bucketIter.hasNext()) { + bucketIndex++; + bucketIter = advanceToNextBucket(); + } + return key; } }; } - // Returns a string representation of this hash table @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); + StringBuilder sb = new StringBuilder("{"); boolean first = true; - for (int i = 0; i < capacity; i++) { - if (table[i] == null) continue; - for (Entry entry : table[i]) { - if (!first) sb.append(", "); + for (LinkedList> bucket : table) { + if (bucket == null) + continue; + for (Entry entry : bucket) { + if (!first) + sb.append(", "); sb.append(entry); first = false; } } - sb.append("}"); - return sb.toString(); + return sb.append("}").toString(); } } diff --git a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableDoubleHashingTest.java b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableDoubleHashingTest.java index 9db93d8bb..1e3fe3671 100644 --- a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableDoubleHashingTest.java +++ b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableDoubleHashingTest.java @@ -85,13 +85,13 @@ public void testUpdatingValue() { DoubleHashingTestObject o5 = new DoubleHashingTestObject(5); DoubleHashingTestObject on7 = new DoubleHashingTestObject(-7); - map.add(o1, 1); + map.put(o1, 1); assertThat(map.get(o1)).isEqualTo(1); - map.add(o5, 5); + map.put(o5, 5); assertThat(map.get(o5)).isEqualTo(5); - map.add(on7, -7); + map.put(on7, -7); assertThat(map.get(on7)).isEqualTo(-7); } @@ -111,13 +111,13 @@ public void testIterator() { List rand_nums = genRandList(MAX_SIZE); for (DoubleHashingTestObject key : rand_nums) - assertThat(mmap.add(key, key)).isEqualTo(jmap.put(key, key)); + assertThat(mmap.put(key, key)).isEqualTo(jmap.put(key, key)); int count = 0; for (DoubleHashingTestObject key : mmap) { assertThat(mmap.get(key)).isEqualTo(key); assertThat(mmap.get(key)).isEqualTo(jmap.get(key)); - assertThat(mmap.hasKey(key)).isTrue(); + assertThat(mmap.containsKey(key)).isTrue(); assertThat(rand_nums.contains(key)).isTrue(); count++; } @@ -146,10 +146,10 @@ public void testConcurrentModificationException() { DoubleHashingTestObject o2 = new DoubleHashingTestObject(2); DoubleHashingTestObject o3 = new DoubleHashingTestObject(3); DoubleHashingTestObject o4 = new DoubleHashingTestObject(4); - map.add(o1, 1); - map.add(o2, 1); - map.add(o3, 1); - for (DoubleHashingTestObject key : map) map.add(o4, 4); + map.put(o1, 1); + map.put(o2, 1); + map.put(o3, 1); + for (DoubleHashingTestObject key : map) map.put(o4, 4); }); } @@ -161,9 +161,9 @@ public void testConcurrentModificationException2() { DoubleHashingTestObject o1 = new DoubleHashingTestObject(1); DoubleHashingTestObject o2 = new DoubleHashingTestObject(2); DoubleHashingTestObject o3 = new DoubleHashingTestObject(3); - map.add(o1, 1); - map.add(o2, 1); - map.add(o3, 1); + map.put(o1, 1); + map.put(o2, 1); + map.put(o3, 1); for (DoubleHashingTestObject key : map) map.remove(o2); }); } diff --git a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableLinearProbingTest.java b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableLinearProbingTest.java index d22193603..a16e372a6 100644 --- a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableLinearProbingTest.java +++ b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableLinearProbingTest.java @@ -131,8 +131,8 @@ public void testGetNullKey() { } @Test - public void testHasKeyNullKey() { - assertThrows(IllegalArgumentException.class, () -> map.hasKey(null)); + public void testContainsKeyNullKey() { + assertThrows(IllegalArgumentException.class, () -> map.containsKey(null)); } @Test @@ -154,13 +154,13 @@ public void testIterator() { map = new HashTableLinearProbing<>(); List rand_nums = genRandList(MAX_SIZE); - for (Integer key : rand_nums) assertThat(map.add(key, key)).isEqualTo(map2.put(key, key)); + for (Integer key : rand_nums) assertThat(map.put(key, key)).isEqualTo(map2.put(key, key)); int count = 0; for (Integer key : map) { assertThat(map.get(key)).isEqualTo(key); assertThat(map.get(key)).isEqualTo(map2.get(key)); - assertThat(map.hasKey(key)).isTrue(); + assertThat(map.containsKey(key)).isTrue(); assertThat(rand_nums.contains(key)).isTrue(); count++; } @@ -182,10 +182,10 @@ public void testConcurrentModificationException() { assertThrows( ConcurrentModificationException.class, () -> { - map.add(1, 1); - map.add(2, 1); - map.add(3, 1); - for (Integer key : map) map.add(4, 4); + map.put(1, 1); + map.put(2, 1); + map.put(3, 1); + for (Integer key : map) map.put(4, 4); }); } @@ -194,9 +194,9 @@ public void testConcurrentModificationException2() { assertThrows( ConcurrentModificationException.class, () -> { - map.add(1, 1); - map.add(2, 1); - map.add(3, 1); + map.put(1, 1); + map.put(2, 1); + map.put(3, 1); for (Integer key : map) map.remove(2); }); } @@ -264,10 +264,10 @@ public void removeTestComplex1() { HashObject o3 = new HashObject(88, 3); HashObject o4 = new HashObject(88, 4); - map.add(o1, 111); - map.add(o2, 111); - map.add(o3, 111); - map.add(o4, 111); + map.put(o1, 111); + map.put(o2, 111); + map.put(o3, 111); + map.put(o4, 111); map.remove(o2); map.remove(o3); diff --git a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableQuadraticProbingTest.java b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableQuadraticProbingTest.java index 0c88a29fc..dd7d2820f 100644 --- a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableQuadraticProbingTest.java +++ b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableQuadraticProbingTest.java @@ -106,13 +106,13 @@ public void testToString() { @Test public void testUpdatingValue() { - map.add(1, 1); + map.put(1, 1); assertThat(map.get(1)).isEqualTo(1); - map.add(1, 5); + map.put(1, 5); assertThat(map.get(1)).isEqualTo(5); - map.add(1, -7); + map.put(1, -7); assertThat(map.get(1)).isEqualTo(-7); } @@ -132,7 +132,7 @@ public void testTableSize() { ht = new HashTableQuadraticProbing<>(sz); for (int i = 0; i < loops; i++) { assertCapacityIsPowerOfTwo(ht); - ht.add(i, i); + ht.put(i, i); } } } @@ -151,13 +151,13 @@ public void testIterator() { map = new HashTableQuadraticProbing<>(); List rand_nums = genRandList(MAX_SIZE); - for (Integer key : rand_nums) assertThat(map.add(key, key)).isEqualTo(map2.put(key, key)); + for (Integer key : rand_nums) assertThat(map.put(key, key)).isEqualTo(map2.put(key, key)); int count = 0; for (Integer key : map) { assertThat(map.get(key)).isEqualTo(key); assertThat(map.get(key)).isEqualTo(map2.get(key)); - assertThat(map.hasKey(key)).isTrue(); + assertThat(map.containsKey(key)).isTrue(); assertThat(rand_nums.contains(key)).isTrue(); count++; } @@ -179,10 +179,10 @@ public void testConcurrentModificationException() { assertThrows( ConcurrentModificationException.class, () -> { - map.add(1, 1); - map.add(2, 1); - map.add(3, 1); - for (Integer key : map) map.add(4, 4); + map.put(1, 1); + map.put(2, 1); + map.put(3, 1); + for (Integer key : map) map.put(4, 4); }); } @@ -191,9 +191,9 @@ public void testConcurrentModificationException2() { assertThrows( ConcurrentModificationException.class, () -> { - map.add(1, 1); - map.add(2, 1); - map.add(3, 1); + map.put(1, 1); + map.put(2, 1); + map.put(3, 1); for (Integer key : map) map.remove(2); }); } @@ -261,10 +261,10 @@ public void removeTestComplex1() { HashObject o3 = new HashObject(88, 3); HashObject o4 = new HashObject(88, 4); - map.add(o1, 111); - map.add(o2, 111); - map.add(o3, 111); - map.add(o4, 111); + map.put(o1, 111); + map.put(o2, 111); + map.put(o3, 111); + map.put(o4, 111); map.remove(o2); map.remove(o3); diff --git a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChainingTest.java b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChainingTest.java index a943f95e1..07bdd0ea8 100644 --- a/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChainingTest.java +++ b/src/test/java/com/williamfiset/algorithms/datastructures/hashtable/HashTableSeparateChainingTest.java @@ -135,13 +135,13 @@ public void testIterator() { map = new HashTableSeparateChaining<>(); List rand_nums = genRandList(MAX_SIZE); - for (Integer key : rand_nums) assertThat(map.add(key, key)).isEqualTo(map2.put(key, key)); + for (Integer key : rand_nums) assertThat(map.put(key, key)).isEqualTo(map2.put(key, key)); int count = 0; for (Integer key : map) { assertThat(map.get(key)).isEqualTo(key); assertThat(map.get(key)).isEqualTo(map2.get(key)); - assertThat(map.hasKey(key)).isTrue(); + assertThat(map.containsKey(key)).isTrue(); assertThat(rand_nums.contains(key)).isTrue(); count++; } @@ -163,10 +163,10 @@ public void testConcurrentModificationException() { assertThrows( ConcurrentModificationException.class, () -> { - map.add(1, 1); - map.add(2, 1); - map.add(3, 1); - for (Integer key : map) map.add(4, 4); + map.put(1, 1); + map.put(2, 1); + map.put(3, 1); + for (Integer key : map) map.put(4, 4); }); } @@ -175,9 +175,9 @@ public void testConcurrentModificationException2() { assertThrows( ConcurrentModificationException.class, () -> { - map.add(1, 1); - map.add(2, 1); - map.add(3, 1); + map.put(1, 1); + map.put(2, 1); + map.put(3, 1); for (Integer key : map) map.remove(2); }); } @@ -245,10 +245,10 @@ public void removeTestComplex1() { HashObject o3 = new HashObject(88, 3); HashObject o4 = new HashObject(88, 4); - map.add(o1, 111); - map.add(o2, 111); - map.add(o3, 111); - map.add(o4, 111); + map.put(o1, 111); + map.put(o2, 111); + map.put(o3, 111); + map.put(o4, 111); map.remove(o2); map.remove(o3);