Approach: implement open-addressing HashSet<E> with linear probing, using an Object[] table and a parallel byte[] state (0=EMPTY,1=OCCUPIED,2=TOMBSTONE). Maintain size and used (occupied + tombstones). Resize (grow) when load factor > 0.6 and rehash to clean tombstones. To avoid clustering degrade, keep load factor low, rehash on many tombstones, and note alternatives (double hashing / robin-hood) if stricter guarantees required.java
import java.util.Arrays;
import java.util.Objects;
public class LinearProbingHashSet<E> {
private Object[] table;
private byte[] state; // 0 EMPTY, 1 OCCUPIED, 2 TOMBSTONE
private int capacity;
private int size; // number of OCCUPIED entries
private int used; // OCCUPIED + TOMBSTONE
private static final double LOAD_FACTOR = 0.6;
public LinearProbingHashSet(int initialCap) {
capacity = Math.max(8, Integer.highestOneBit(initialCap - 1) << 1);
table = new Object[capacity];
state = new byte[capacity];
}
public LinearProbingHashSet() { this(16); }
private int indexFor(Object key) {
int h = Objects.hashCode(key);
h ^= (h >>> 16);
return (h & 0x7fffffff) % capacity;
}
private int findSlot(Object key) {
int idx = indexFor(key);
int firstTombstone = -1;
for (int i = 0; i < capacity; i++) {
int pos = (idx + i) % capacity;
if (state[pos] == 0) { // EMPTY
return firstTombstone != -1 ? firstTombstone : pos;
} else if (state[pos] == 2) { // TOMBSTONE
if (firstTombstone == -1) firstTombstone = pos;
} else {
if (Objects.equals(table[pos], key)) return pos;
}
}
return firstTombstone != -1 ? firstTombstone : -1;
}
public boolean contains(E key) {
int idx = indexFor(key);
for (int i = 0; i < capacity; i++) {
int pos = (idx + i) % capacity;
if (state[pos] == 0) return false;
if (state[pos] == 1 && Objects.equals(table[pos], key)) return true;
}
return false;
}
public boolean add(E key) {
if ((used + 1) > capacity * LOAD_FACTOR) resize(capacity * 2);
int slot = findSlot(key);
if (slot == -1) throw new IllegalStateException("Table full");
if (state[slot] == 1 && Objects.equals(table[slot], key)) return false; // already present
boolean wasTomb = state[slot] == 2;
table[slot] = key;
state[slot] = 1;
size++;
if (!wasTomb) used++;
return true;
}
public boolean remove(E key) {
int idx = indexFor(key);
for (int i = 0; i < capacity; i++) {
int pos = (idx + i) % capacity;
if (state[pos] == 0) return false;
if (state[pos] == 1 && Objects.equals(table[pos], key)) {
table[pos] = null;
state[pos] = 2; // tombstone
size--;
// Optional: if tombstones dominate, rehash to reduce clustering
if (used > capacity * 0.75) resize(capacity); // rehash in place to clean tombstones
return true;
}
}
return false;
}
private void resize(int newCap) {
int target = Math.max(8, Integer.highestOneBit(newCap - 1) << 1);
Object[] oldTable = table;
byte[] oldState = state;
table = new Object[target];
state = new byte[target];
int oldCap = capacity;
capacity = target;
size = 0;
used = 0;
for (int i = 0; i < oldCap; i++) {
if (oldState[i] == 1) add((E) oldTable[i]);
}
}
public int size() { return size; }
}
Key points:- Tombstones mark deleted slots to preserve probe chains; but accumulate tombstones hurt performance so rehash/resize periodically.- Load factor kept ~0.6 to limit probe lengths (reduces primary clustering).- Alternatives to further avoid clustering: double hashing, robin-hood hashing, or hopscotch hashing—these reduce variance in probe lengths.Complexity: average O(1) for add/contains/remove; worst-case O(n) under heavy clustering or poor hash distribution. Edge cases: null keys (current code supports null because Objects.hashCode(null)==0), table full (resize), many tombstones triggering rehash.