shtabnoy.com / challenges / hashmap
✦ Algo & data structures

Building a HashMap
from scratch

No objects. No Map. Just an array of buckets, a hash function, and collision chaining. Add keys below and watch them slot into buckets in real time.

hash tablescollision chainingamortized O(1)genericsdata structures
// try it

Insert, lookup, and watch collisions

Each key gets hashed to a bucket index (0–15). When two keys land in the same bucket, that's a collision — they chain together. Try the "Force collisions" preset to see it clearly.

set(key, value)
get(key) / delete(key)
Entries
0
Collisions
0
Max chain
0
Load factor
0.00
Buckets [0–15] — empty
[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
[11]
[12]
[13]
[14]
[15]
// comparison

HashMap vs Map vs Object

JavaScript gives you three key-value structures. They look similar but behave differently in ways that matter for interviews — and for production code.

HashMap (ours)MapObject
Key typesstring / number / booleanany (objects, functions, etc.)string / Symbol
Insertion order✕ not preserved✓ preserved~mostly preserved
Size.size — O(1) with counter.size — O(1)Object.keys().length — O(n)
Iteration.keys() / .values()for...of / .forEach()for...in / Object.keys()
PerformanceO(1) amortizedO(1) amortizedO(1) amortized
Prototype pollution✕ not possible✕ not possible⚠ possible
SerializationmanualmanualJSON.stringify()
Default keysnonenonetoString, constructor, etc.
💡 When to use what

Use Object for simple string-keyed config or JSON data. Use Map when you need non-string keys, guaranteed insertion order, frequent additions/deletions, or when you need .size without counting. Use Set when you only care about unique keys, not values — it's essentially a HashMap where value is always true.

⚠ Interview trap

"Are object property lookups O(1)?" — Yes, V8 uses hidden classes and inline caching for known shapes, making property access effectively constant time. But deleting properties with delete obj.key is slow because it forces a shape transition. For frequent add/delete patterns, use Map.

// the code

The full implementation

Generic, type-safe, with a prime-multiplier hash function and collision chaining. No built-in objects or Maps used internally — just arrays.

hashMap.ts
class HashMap<K extends string | number | boolean, V> {
  private _buckets: [key: K, value: V][][];
  private _size: number = 0;

  constructor(private bucketCount: number = 16) {
    this._buckets = Array.from({ length: bucketCount }, () => []);
  }

  hash(key: K): number {
    let hash = 0;
    const str = key.toString();
    for (let i = 0; i < str.length; i++) {
      hash = (hash * 31 + str.charCodeAt(i)) % this.bucketCount;
    }
    return hash;                  // key → number → bucket index
  }

  set(key: K, value: V) {
    const bucketIndex = this.hash(key);
    const bucket = this._buckets[bucketIndex];
    const existing = bucket.findIndex((el) => el[0] === key);
    if (existing >= 0) {
      bucket[existing] = [key, value];  // update existing
    } else {
      bucket.push([key, value]);        // insert new
      this._size++;
    }
  }

  get(key: K): V | undefined {
    const bucketIndex = this.hash(key);
    return this._buckets[bucketIndex].find((el) => el[0] === key)?.[1];
  }

  delete(key: K): boolean {
    const bucketIndex = this.hash(key);
    const bucket = this._buckets[bucketIndex];
    const index = bucket.findIndex((el) => el[0] === key);
    if (index === -1) return false;
    bucket.splice(index, 1);
    this._size--;
    return true;
  }

  has(key: K): boolean {
    const bucketIndex = this.hash(key);
    return this._buckets[bucketIndex].some((el) => el[0] === key);
  }

  get size(): number { return this._size; }
  keys(): K[] { return this._buckets.flat().map((el) => el[0]); }
  values(): V[] { return this._buckets.flat().map((el) => el[1]); }
}
// takeaways

What this exercise teaches

🪣
It's just an array of arrays
Under the hood, a hash map is a fixed-size array where each slot holds a chain of [key, value] pairs. The hash function decides which slot. That's the whole data structure.
💥
Collisions are expected, not errors
Two keys landing in the same bucket is normal. Chaining (storing multiple entries per bucket) handles it gracefully. Performance only degrades when many keys collide — which signals a bad hash function.
O(1) is a lie (sort of)
It's amortized O(1) — on average, each operation is constant time. But the hash function is O(k) for key length, and bucket scanning is O(n) in the pathological worst case. Real implementations use good hash functions and resizing to keep it fast.
🔀
No insertion order
Classic hash maps don't preserve insertion order — iteration follows bucket indices, not the order you added entries. JavaScript's native Map maintains order by keeping a separate linked list alongside the hash table.