# So Much for O(1)

> I'd known the theory for years but couldn't have built one from scratch. So I did, with interactive demos you can poke at.

March 25, 2026 · 34 min read · https://yasint.dev/hash-maps-under-the-hood/
Tags: data-structures, interactive

---

`HashMap`, `dict`, `Map`, `object`, whatever your language calls it, you probably use one every day. I definitely do.

I'd known the theory for years. Hashing, buckets, O(1), sure. If you'd asked me to walk through what actually happens when you call `map.get("email")`, I would've hand-waved about hashing and buckets and moved on. I knew it was "O(1)" because that's what you say in interviews and everyone nods. Could I have built one? No.

So I did. And honestly? It's not that complicated. It's just _really_ clever.

This article walks through the whole thing with interactive demos you can poke at. By the end you'll understand hash maps better than most people who use them daily.

---

## It all starts with arrays

OK so here's the thing. Arrays have this one superpower that makes everything else possible: **instant access by index**. When you do `arr[3]`, the CPU doesn't search for anything. It just calculates `base_address + 3 * element_size` and jumps straight there. O(1). Done. No questions asked.

That's the magic trick we want for our hash map.

Our keys aren't numbers though. They're strings like `"email"` and `"name"` and `"age"`. You can't just do `arr["email"]` (well, you can in JavaScript, but that's a whole different rabbit hole). Arrays need _numbers_.

So we need something that takes any key and turns it into a number. A number we can use as an array index.

That something is called a **hash function**.

## Turning gibberish into numbers

A hash function takes any input (a string, a number, whatever) and spits out an integer. Same input, same output, every time. Different inputs _usually_ give different outputs.

That "usually" is going to bite us later. Keep it in the back of your mind.

Here's a real one. It's called **djb2**, first reported by [Dan Bernstein](https://en.wikipedia.org/wiki/Daniel_J._Bernstein) in `comp.lang.c` many years ago. Not the best hash function by modern standards, but fast, simple, and perfect for understanding the concept:

```c
unsigned long hash(unsigned char *str) {
    unsigned long hash = 5381;
    int c;
    while (c = *str++)
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
    return hash;
}
```

Each iteration multiplies hash by 33 (that's what `(hash << 5) + hash` does) and adds the next character. Dead simple. The interactive demos below use a JavaScript port of the same algorithm.

Why 33? According to [oz's classic hash function page](https://web.archive.org/web/2024/http://www.cse.yorku.ca/~oz/hash.html), "the magic of number 33 — why it works better than many other constants, prime or not — has never been adequately explained." Why 5381? [Nobody really knows](https://stackoverflow.com/questions/10696223/reason-for-5381-number-in-djb-hash-function). Bernstein picked these constants and they just work. Sometimes engineering is just vibes and benchmarks.

The raw hash is a massive number, way bigger than our array. So we squeeze it into range with modulo: `hash % capacity`. If we have 8 slots, `djb2("email") % 8` gives us a number between 0 and 7. That's our index. (Technically the hash function itself is O(key length), not O(1), but keys are usually short so we treat it as constant.)

Go ahead, type anything in here and watch it get hashed:

<script is:inline>{`
window.__hm = {
  djb2: function(str) {
    var hash = 5381;
    for (var i = 0; i < str.length; i++) {
      hash = ((hash << 5) + hash) + str.charCodeAt(i);
      hash = hash | 0;
    }
    return hash >>> 0;
  },
  c: {
    bg:'#0d0c0b', surface:'#1a1816', border:'#2a2622',
    accent:'#d4976a', accentHover:'#e8b08a', text:'#b8b0a3',
    heading:'#e8e2d9', muted:'#5c564e', warm300:'#9c9488',
    warm400:'#7a7268', warm700:'#2a2622'
  },
  mono: "'Space Mono', monospace",
  reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
  el: function(tag, styles, attrs) {
    var e = document.createElement(tag);
    if (styles) Object.assign(e.style, styles);
    if (attrs) Object.keys(attrs).forEach(function(k) { e.setAttribute(k, attrs[k]); });
    return e;
  },
  btn: function(label, onClick, small, ariaLabel) {
    var b = document.createElement('button');
    b.textContent = label;
    if (ariaLabel) b.setAttribute('aria-label', ariaLabel);
    Object.assign(b.style, {
      fontFamily: "'Space Mono', monospace", fontSize: small ? '13px' : '14px',
      padding: small ? '6px 12px' : '8px 18px',
      background: 'transparent', color: '#d4976a', border: '1px solid #2a2622',
      borderRadius: '6px', cursor: 'pointer', transition: 'all 0.15s'
    });
    b.addEventListener('mouseenter', function() { b.style.borderColor = '#d4976a'; b.style.background = '#1a1816'; });
    b.addEventListener('mouseleave', function() { b.style.borderColor = '#2a2622'; b.style.background = 'transparent'; });
    b.addEventListener('click', onClick);
    return b;
  },
  text: function(str, color) {
    var s = document.createElement('span');
    s.textContent = str;
    if (color) s.style.color = color;
    return s;
  },
  clearChildren: function(el) {
    while (el.firstChild) el.removeChild(el.firstChild);
  }
};
`}</script>

<Figure n="01" label="Hash explorer">
  <div id="w-hash" class="not-prose" role="region" aria-label="Interactive hash explorer">
    <noscript><p style="color:#9c9488;font-family:'Space Mono',monospace;font-size:13px;">This interactive widget requires JavaScript.</p></noscript>
  </div>
</Figure>

<script is:inline>{`
(function() {
  var root = document.getElementById('w-hash');
  if (!root) return;
  var H = window.__hm, djb2 = H.djb2, c = H.c, el = H.el;
  var CAP = 8;

  var wrap = el('div', { background: c.surface, border: '1px solid ' + c.border, borderRadius: '8px', padding: '24px', fontFamily: H.mono, fontSize: '14px' });

  var label = el('label', { color: c.warm300, display: 'block', marginBottom: '8px', fontSize: '13px' });
  label.textContent = 'Type a key';
  label.setAttribute('for', 'hash-input');

  var input = el('input', {
    width: '100%', boxSizing: 'border-box', padding: '10px 12px', fontFamily: H.mono,
    fontSize: '15px', background: c.bg, color: c.heading, border: '1px solid ' + c.border,
    borderRadius: '6px', outline: 'none'
  }, { type: 'text', id: 'hash-input', placeholder: 'email', autocomplete: 'off', spellcheck: 'false' });
  input.addEventListener('focus', function() { input.style.borderColor = c.accent; });
  input.addEventListener('blur', function() { input.style.borderColor = c.border; });

  var output = el('div', { marginTop: '16px', display: 'none' });
  var hashLine = el('div', { color: c.warm300, marginBottom: '4px', wordBreak: 'break-all' });
  var idxLine = el('div', { color: c.accent, fontWeight: '700', fontSize: '16px' });
  output.appendChild(hashLine);
  output.appendChild(idxLine);

  var bucketRow = el('div', { display: 'flex', gap: '4px', marginTop: '16px' });
  var cells = [];
  for (var i = 0; i < CAP; i++) {
    var cell = el('div', {
      flex: '1', textAlign: 'center', padding: '8px 0', borderRadius: '4px',
      border: '1px solid ' + c.border, color: c.warm300, fontSize: '13px',
      transition: 'all 0.2s', minWidth: '0'
    });
    cell.textContent = i;
    cells.push(cell);
    bucketRow.appendChild(cell);
  }

  var live = el('div', { position: 'absolute', width: '1px', height: '1px', overflow: 'hidden', clip: 'rect(0,0,0,0)' }, { 'aria-live': 'polite', role: 'status' });

  wrap.appendChild(label);
  wrap.appendChild(input);
  wrap.appendChild(output);
  wrap.appendChild(bucketRow);
  wrap.appendChild(live);
  root.appendChild(wrap);

  var prevIdx = -1;
  input.addEventListener('input', function() {
    var key = input.value;
    if (!key) {
      output.style.display = 'none';
      cells.forEach(function(cc) { cc.style.background = 'transparent'; cc.style.borderColor = c.border; cc.style.color = c.warm400; });
      prevIdx = -1;
      return;
    }
    var hash = djb2(key);
    var idx = hash % CAP;
    output.style.display = 'block';
    hashLine.textContent = 'djb2("' + key + '") = ' + hash;
    idxLine.textContent = hash + ' % ' + CAP + ' = bucket ' + idx;
    cells.forEach(function(cc, j) {
      if (j === idx) {
        cc.style.background = c.accent; cc.style.borderColor = c.accent; cc.style.color = c.bg;
      } else {
        cc.style.background = 'transparent'; cc.style.borderColor = c.border; cc.style.color = c.warm400;
      }
    });
    if (idx !== prevIdx) live.textContent = 'Key "' + key + '" maps to bucket ' + idx;
    prevIdx = idx;
  });
})();
`}</script>

## Where stuff actually goes

Cool, so now we have a function that turns any string into a number between 0 and 7. Now what?

We make an array (usually called a **bucket array**) and use that number to decide _which slot_ each key-value pair lands in. You want to store `"name": "Alice"`? Hash `"name"`, get a bucket index, drop the entry there. If the key already exists, just update its value. That's it.

Let me walk you through it. Hit "Next" to step through each insertion one by one:

<Figure n="02" label="Insertion, step by step">
  <div id="w-insert" class="not-prose" role="region" aria-label="Step-by-step insertion demo">
    <noscript><p style="color:#9c9488;font-family:'Space Mono',monospace;font-size:13px;">This interactive widget requires JavaScript.</p></noscript>
  </div>
</Figure>

<script is:inline>{`
(function() {
  var root = document.getElementById('w-insert');
  if (!root) return;
  var H = window.__hm, djb2 = H.djb2, c = H.c, el = H.el;
  var CAP = 8;
  var kvs = [
    { key: 'name', val: 'Alice' },
    { key: 'email', val: 'a@b.com' },
    { key: 'age', val: '30' }
  ];
  var steps = [];
  steps.push({ msg: 'We start with ' + CAP + ' empty buckets, indexed 0 through ' + (CAP - 1) + '.', highlight: -1, entries: [] });
  var placed = [];
  kvs.forEach(function(e) {
    var hash = djb2(e.key);
    var idx = hash % CAP;
    steps.push({ msg: 'Hashing "' + e.key + '"... djb2("' + e.key + '") % ' + CAP + ' = bucket ' + idx + '.', highlight: idx, entries: placed.slice() });
    placed.push({ key: e.key, val: e.val, bucket: idx });
    steps.push({ msg: '"' + e.key + '": "' + e.val + '" placed in bucket ' + idx + '.', highlight: idx, entries: placed.slice() });
  });
  steps.push({ msg: 'Three keys, three different buckets. No collisions — each key got its own slot.', highlight: -1, entries: placed.slice() });

  var step = 0;
  var wrap = el('div', { background: c.surface, border: '1px solid ' + c.border, borderRadius: '8px', padding: '24px', fontFamily: H.mono, fontSize: '14px' });
  var narration = el('div', { color: c.text, lineHeight: '1.6', minHeight: '44px', marginBottom: '16px', fontFamily: "'Space Mono', monospace", fontSize: '16px' }, { 'aria-live': 'polite' });
  var bucketArea = el('div', { display: 'flex', flexDirection: 'column', gap: '3px' });
  var rows = [];
  for (var i = 0; i < CAP; i++) {
    var row = el('div', { display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', borderRadius: '4px', border: '1px solid ' + c.border, transition: 'all 0.2s', minHeight: '32px' });
    var idxSpan = el('span', { color: c.warm300, width: '20px', textAlign: 'right', fontSize: '13px', flexShrink: '0' });
    idxSpan.textContent = i;
    var content = el('span', { color: c.heading, fontSize: '14px', flex: '1' });
    row.appendChild(idxSpan);
    row.appendChild(content);
    rows.push({ row: row, content: content });
    bucketArea.appendChild(row);
  }
  var controls = el('div', { display: 'flex', gap: '8px', marginTop: '16px', alignItems: 'center' });
  var stepLabel = el('span', { color: c.warm300, fontSize: '13px', marginLeft: 'auto' });
  var prevBtn = H.btn('\u2190', function() { if (step > 0) { step--; render(); } }, true, 'Previous step');
  var nextBtn = H.btn('\u2192', function() { if (step < steps.length - 1) { step++; render(); } }, true, 'Next step');
  var playBtn = H.btn('\u25B6', function() { togglePlay(); }, true, 'Play');
  var resetBtn = H.btn('Reset', function() { step = 0; playing = false; playBtn.textContent = '\u25B6'; playBtn.setAttribute('aria-label', 'Play'); render(); }, true);
  controls.appendChild(prevBtn);
  controls.appendChild(playBtn);
  controls.appendChild(nextBtn);
  controls.appendChild(resetBtn);
  controls.appendChild(stepLabel);
  wrap.appendChild(narration);
  wrap.appendChild(bucketArea);
  wrap.appendChild(controls);
  root.appendChild(wrap);

  var playing = false, playTimer = null;
  function togglePlay() {
    if (playing) { playing = false; clearTimeout(playTimer); playBtn.textContent = '\u25B6'; playBtn.setAttribute('aria-label', 'Play'); }
    else { playing = true; playBtn.textContent = '\u23F8'; playBtn.setAttribute('aria-label', 'Pause'); advancePlay(); }
  }
  function advancePlay() {
    if (!playing || step >= steps.length - 1) { playing = false; playBtn.textContent = '\u25B6'; return; }
    step++;
    render();
    playTimer = setTimeout(advancePlay, 1200);
  }

  function render() {
    var s = steps[step];
    narration.textContent = s.msg;
    stepLabel.textContent = (step + 1) + ' / ' + steps.length;
    rows.forEach(function(r) { r.content.textContent = ''; r.row.style.background = 'transparent'; r.row.style.borderColor = c.border; });
    s.entries.forEach(function(e) {
      var r = rows[e.bucket];
      var t = r.content.textContent;
      r.content.textContent = t ? t + ' \u2192 ' + e.key + ':' + e.val : e.key + ':' + e.val;
    });
    if (s.highlight >= 0) { rows[s.highlight].row.style.background = '#1f1b17'; rows[s.highlight].row.style.borderColor = c.accent; }
    nextBtn.style.opacity = step >= steps.length - 1 ? '0.3' : '1';
    nextBtn.disabled = step >= steps.length - 1;
    prevBtn.style.opacity = step <= 0 ? '0.3' : '1';
    prevBtn.disabled = step <= 0;
  }
  render();
})();
`}</script>

Three insertions, three different buckets. Each lookup is just: hash the key, jump to the bucket, grab the value. O(1). This is the happy path.

Remember that "usually" from earlier? Yeah. Time to deal with that.

## When things go wrong (kind of)

Here's a fact that might make you uncomfortable: we have 8 buckets and an _infinite_ number of possible strings. There is literally no way to guarantee every key gets its own slot. This is the [pigeonhole principle](https://en.wikipedia.org/wiki/Pigeonhole_principle): more pigeons than holes means at least two pigeons are sharing. Math doesn't care about your feelings.

So what happens when `"name"` and `"city"` both hash to bucket 6? Does it just... break?

Nope. The simplest solution is called **chaining**. Instead of each bucket holding one entry, it holds a _list_ (a chain) of entries. On lookup, you hash to the bucket, then walk the chain comparing keys until you find yours. It's a tiny linear search, but only within that one bucket.

This is way easier to see than to explain. Step through it:

<Figure n="03" label="Collision chaining">
  <div id="w-collision" class="not-prose" role="region" aria-label="Collision demonstration">
    <noscript><p style="color:#9c9488;font-family:'Space Mono',monospace;font-size:13px;">This interactive widget requires JavaScript.</p></noscript>
  </div>
</Figure>

<script is:inline>{`
(function() {
  var root = document.getElementById('w-collision');
  if (!root) return;
  var H = window.__hm, djb2 = H.djb2, c = H.c, el = H.el, txt = H.text, clr = H.clearChildren;
  var CAP = 8;
  var initial = [
    { key: 'name', val: 'Alice', bucket: djb2('name') % CAP },
    { key: 'email', val: 'a@b.com', bucket: djb2('email') % CAP },
    { key: 'age', val: '30', bucket: djb2('age') % CAP }
  ];
  var cityIdx = djb2('city') % CAP;
  var phoneIdx = djb2('phone') % CAP;
  var allEntries = initial.slice();

  var steps = [];
  steps.push({ msg: 'Our table from before: 3 entries, no collisions.', hl: -1, entries: allEntries.slice(), phase: 'insert' });
  steps.push({ msg: 'Insert "city": "Oslo". djb2("city") % 8 = ' + cityIdx + '. Bucket ' + cityIdx + ' already has "name"!', hl: cityIdx, entries: allEntries.slice(), phase: 'insert' });
  allEntries = allEntries.concat([{ key: 'city', val: 'Oslo', bucket: cityIdx }]);
  steps.push({ msg: 'Collision. "city" chains onto bucket ' + cityIdx + ': name \u2192 city. Both live in the same slot.', hl: cityIdx, entries: allEntries.slice(), phase: 'insert' });
  allEntries = allEntries.concat([{ key: 'phone', val: '555-1234', bucket: phoneIdx }]);
  steps.push({ msg: '"phone" hashes to bucket ' + phoneIdx + '. No collision — placed directly.', hl: phoneIdx, entries: allEntries.slice(), phase: 'insert' });
  steps.push({ msg: 'Now try looking up a key. Type one of: name, email, age, city, phone.', hl: -1, entries: allEntries.slice(), phase: 'lookup' });

  var step = 0;
  var wrap = el('div', { background: c.surface, border: '1px solid ' + c.border, borderRadius: '8px', padding: '24px', fontFamily: H.mono, fontSize: '14px' });
  var narration = el('div', { color: c.text, lineHeight: '1.6', minHeight: '44px', marginBottom: '16px', fontFamily: "'Space Mono', monospace", fontSize: '16px' }, { 'aria-live': 'polite' });
  var bucketArea = el('div', { display: 'flex', flexDirection: 'column', gap: '3px' });
  var rows = [];
  for (var i = 0; i < CAP; i++) {
    var row = el('div', { display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', borderRadius: '4px', border: '1px solid ' + c.border, transition: 'all 0.2s', minHeight: '32px' });
    var idxSpan = el('span', { color: c.warm300, width: '20px', textAlign: 'right', fontSize: '13px', flexShrink: '0' });
    idxSpan.textContent = i;
    var content = el('span', { color: c.heading, fontSize: '14px', flex: '1', wordBreak: 'break-all' });
    row.appendChild(idxSpan);
    row.appendChild(content);
    rows.push({ row: row, content: content });
    bucketArea.appendChild(row);
  }
  var controls = el('div', { display: 'flex', gap: '8px', marginTop: '16px', alignItems: 'center', flexWrap: 'wrap' });
  var stepLabel = el('span', { color: c.warm300, fontSize: '13px', marginLeft: 'auto' });
  var prevBtn2 = H.btn('\u2190', function() { if (step > 0) { step--; render(); } }, true, 'Previous step');
  var playBtn2 = H.btn('\u25B6', function() { togglePlay2(); }, true, 'Play');
  var nextBtn = H.btn('\u2192', function() { if (step < steps.length - 1) { step++; render(); } }, true, 'Next step');
  var resetBtn = H.btn('Reset', function() { step = 0; playing2 = false; playBtn2.textContent = '\u25B6'; playBtn2.setAttribute('aria-label', 'Play'); render(); }, true);
  controls.appendChild(prevBtn2);
  controls.appendChild(playBtn2);
  controls.appendChild(nextBtn);
  controls.appendChild(resetBtn);
  controls.appendChild(stepLabel);

  var playing2 = false, playTimer2 = null;
  function togglePlay2() {
    if (playing2) { playing2 = false; clearTimeout(playTimer2); playBtn2.textContent = '\u25B6'; playBtn2.setAttribute('aria-label', 'Play'); }
    else { playing2 = true; playBtn2.textContent = '\u23F8'; playBtn2.setAttribute('aria-label', 'Pause'); advancePlay2(); }
  }
  function advancePlay2() {
    if (!playing2 || step >= steps.length - 1) { playing2 = false; playBtn2.textContent = '\u25B6'; return; }
    step++;
    render();
    playTimer2 = setTimeout(advancePlay2, 1200);
  }

  var lookupWrap = el('div', { display: 'none', marginTop: '12px' });
  var lookupLabel = el('label', { color: c.warm300, fontSize: '13px', display: 'block', marginBottom: '6px' });
  lookupLabel.textContent = 'Look up a key';
  lookupLabel.setAttribute('for', 'collision-lookup');
  var lookupInput = el('input', {
    width: '100%', boxSizing: 'border-box', padding: '10px 12px', fontFamily: H.mono,
    fontSize: '15px', background: c.bg, color: c.heading, border: '1px solid ' + c.border,
    borderRadius: '6px', outline: 'none'
  }, { type: 'text', id: 'collision-lookup', placeholder: 'city', autocomplete: 'off', spellcheck: 'false' });
  lookupInput.addEventListener('focus', function() { lookupInput.style.borderColor = c.accent; });
  lookupInput.addEventListener('blur', function() { lookupInput.style.borderColor = c.border; });
  var lookupResult = el('div', { color: c.text, marginTop: '8px', minHeight: '20px', fontFamily: "'Space Mono', monospace", fontSize: '16px' }, { 'aria-live': 'polite' });
  lookupWrap.appendChild(lookupLabel);
  lookupWrap.appendChild(lookupInput);
  lookupWrap.appendChild(lookupResult);

  wrap.appendChild(narration);
  wrap.appendChild(bucketArea);
  wrap.appendChild(controls);
  wrap.appendChild(lookupWrap);
  root.appendChild(wrap);

  function renderBuckets(entries, hl) {
    rows.forEach(function(r) { r.content.textContent = ''; r.row.style.background = 'transparent'; r.row.style.borderColor = c.border; });
    var bMap = {};
    entries.forEach(function(e) { if (!bMap[e.bucket]) bMap[e.bucket] = []; bMap[e.bucket].push(e); });
    Object.keys(bMap).forEach(function(b) {
      rows[parseInt(b)].content.textContent = bMap[b].map(function(e) { return e.key + ':' + e.val; }).join(' \u2192 ');
    });
    if (hl >= 0) { rows[hl].row.style.background = '#1f1b17'; rows[hl].row.style.borderColor = c.accent; }
  }

  function render() {
    var s = steps[step];
    narration.textContent = s.msg;
    stepLabel.textContent = (step + 1) + ' / ' + steps.length;
    renderBuckets(s.entries, s.hl);
    lookupWrap.style.display = s.phase === 'lookup' ? 'block' : 'none';
    nextBtn.style.opacity = step >= steps.length - 1 ? '0.3' : '1';
    nextBtn.disabled = step >= steps.length - 1;
    prevBtn2.style.opacity = step <= 0 ? '0.3' : '1';
    prevBtn2.disabled = step <= 0;
    if (s.phase === 'lookup') lookupInput.focus();
  }

  lookupInput.addEventListener('input', function() {
    var key = lookupInput.value.trim();
    clr(lookupResult);
    if (!key) { renderBuckets(allEntries, -1); return; }
    var idx = djb2(key) % CAP;
    var bucket = allEntries.filter(function(e) { return e.bucket === idx; });
    renderBuckets(allEntries, idx);
    if (!bucket.length) {
      lookupResult.appendChild(txt('Bucket ' + idx + ' is empty. Key not found.', c.warm300));
      return;
    }
    var found = null;
    var trail = [];
    bucket.some(function(e) { trail.push(e.key); if (e.key === key) { found = e; return true; } });
    lookupResult.appendChild(txt('Bucket ' + idx + ' \u2192 checked ' + trail.join(', then ') + ' \u2192 ', c.warm300));
    if (found) {
      lookupResult.appendChild(txt('found "' + found.val + '"', c.accent));
    } else {
      lookupResult.appendChild(txt('not found.', c.warm300));
    }
  });

  render();
})();
`}</script>

So a collision doesn't break anything. It just adds one extra comparison per chained entry. As long as the chains stay short (one or two entries) you barely notice.

Wait. What _stops_ the chains from getting long? If I keep cramming entries in, won't everything pile up into a few long chains and basically become a linked list? At that point our "O(1)" hash map is just an O(n) list with extra steps. So much for constant time.

Turns out the people who designed this thought of that.

## When the table says "I'm full"

The **load factor** is dead simple: `entries / capacity`. It tells you the average number of entries per bucket. With a good hash function, keeping the load factor bounded keeps chain lengths bounded too. That's the whole trick.

[Java](https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html) draws the line at **0.75**. Other languages choose differently: [Python](https://github.com/python/cpython/blob/main/Objects/dictobject.c) uses ~0.67, [Rust](https://github.com/rust-lang/hashbrown/blob/master/src/raw.rs#L182) ~0.875, [C++](https://en.cppreference.com/w/cpp/container/unordered_map/max_load_factor) defaults to 1.0. Lower means more wasted space, higher means more collisions before a resize.

Cross the threshold, and the hash map _doubles_ its size and **rehashes every single entry** into the new, larger array.

This blew my mind when I first got it. The old bucket indices are completely invalid after a resize because `hash % 4` gives a totally different result than `hash % 8`. So _every_ entry has to be re-placed. It's expensive, but it happens rarely enough that the math works out.

Keep pressing "Insert" here and watch the load factor bar creep up. When it crosses the line, watch closely:

<Figure n="04" label="Load factor &amp; resize">
  <div id="w-resize" class="not-prose" role="region" aria-label="Resize demonstration">
    <noscript><p style="color:#9c9488;font-family:'Space Mono',monospace;font-size:13px;">This interactive widget requires JavaScript.</p></noscript>
  </div>
</Figure>

<script is:inline>{`
(function() {
  var root = document.getElementById('w-resize');
  if (!root) return;
  var H = window.__hm, djb2 = H.djb2, c = H.c, el = H.el, txt = H.text, clr = H.clearChildren;
  var OLD_CAP = 4, NEW_CAP = 8;

  var keysToInsert = [
    { key: 'team', val: 'eng' },
    { key: 'email', val: 'y@b.com' },
    { key: 'name', val: 'Alice' },
    { key: 'phone', val: '555' },
    { key: 'city', val: 'Oslo' },
    { key: 'age', val: '30' }
  ];

  // Pre-compute all steps
  var steps = [];
  var placed = [];

  steps.push({ msg: 'A table with ' + OLD_CAP + ' buckets. Step through to insert entries and watch the load factor grow.', entries: [], cap: OLD_CAP, hl: -1, mode: 'single' });

  keysToInsert.forEach(function(kv, ki) {
    var cap = placed.length >= 3 ? NEW_CAP : OLD_CAP;
    // If this is the 3rd entry (index 2), it triggers resize — but we show it in OLD_CAP first
    if (ki < 3) {
      var idx = djb2(kv.key) % OLD_CAP;
      placed.push({ key: kv.key, val: kv.val, bucket: idx, oldBucket: idx });
      var lf = placed.length / OLD_CAP;
      if (lf >= 0.75) {
        // Step: insert that triggers threshold
        steps.push({ msg: '"' + kv.key + '" \u2192 bucket ' + idx + '. Load factor: ' + lf.toFixed(2) + ' \u2014 that\u2019s \u2265 0.75!', entries: placed.slice(), cap: OLD_CAP, hl: idx, mode: 'single' });
        // Step: show threshold crossed
        steps.push({ msg: 'Threshold crossed. Time to resize: ' + OLD_CAP + ' \u2192 ' + NEW_CAP + ' buckets. Every entry must be rehashed.', entries: placed.slice(), cap: OLD_CAP, hl: -1, mode: 'single' });
        // Step: show two-column empty new table
        var oldSnap = placed.map(function(e) { return { key: e.key, val: e.val, oldBucket: djb2(e.key) % OLD_CAP }; });
        steps.push({ msg: 'Old table on the left, new (empty) table on the right. Now rehashing one by one...', oldEntries: oldSnap, newEntries: [], cap: NEW_CAP, hl: -1, mode: 'split', rehashIdx: -1 });
        // Steps: rehash each entry
        var rehashed = [];
        oldSnap.forEach(function(en, ri) {
          var newB = djb2(en.key) % NEW_CAP;
          rehashed.push({ key: en.key, val: en.val, bucket: newB, oldBucket: en.oldBucket });
          var moved = en.oldBucket !== newB;
          steps.push({
            msg: '"' + en.key + '": % ' + OLD_CAP + ' = ' + en.oldBucket + '  \u2192  % ' + NEW_CAP + ' = ' + newB + (moved ? '  (moved!)' : '  (stayed)'),
            oldEntries: oldSnap, newEntries: rehashed.slice(), cap: NEW_CAP,
            hlOld: en.oldBucket, hlNew: newB, mode: 'split', rehashIdx: ri
          });
        });
        // Step: done rehashing
        steps.push({ msg: 'Done. All entries rehashed. Load factor back to ' + (rehashed.length / NEW_CAP).toFixed(2) + '.', oldEntries: oldSnap, newEntries: rehashed.slice(), cap: NEW_CAP, hl: -1, mode: 'split', rehashIdx: rehashed.length });
        // Transition to single-column new table
        placed = rehashed.map(function(e) { return { key: e.key, val: e.val, bucket: e.bucket }; });
        steps.push({ msg: 'Resized table with ' + NEW_CAP + ' buckets. Continuing inserts...', entries: placed.slice(), cap: NEW_CAP, hl: -1, mode: 'single' });
      } else {
        steps.push({ msg: '"' + kv.key + '" \u2192 bucket ' + idx + '. Load factor: ' + lf.toFixed(2) + '.', entries: placed.slice(), cap: OLD_CAP, hl: idx, mode: 'single' });
      }
    } else {
      var idx2 = djb2(kv.key) % NEW_CAP;
      placed.push({ key: kv.key, val: kv.val, bucket: idx2 });
      var lf2 = placed.length / NEW_CAP;
      steps.push({ msg: '"' + kv.key + '" \u2192 bucket ' + idx2 + '. Load factor: ' + lf2.toFixed(2) + '.', entries: placed.slice(), cap: NEW_CAP, hl: idx2, mode: 'single' });
    }
  });
  steps.push({ msg: 'All 6 entries inserted. The table resized once from ' + OLD_CAP + ' to ' + NEW_CAP + ' buckets, rehashing everything along the way.', entries: placed.slice(), cap: NEW_CAP, hl: -1, mode: 'single' });

  // --- DOM ---
  var step = 0;
  var wrap = el('div', { background: c.surface, border: '1px solid ' + c.border, borderRadius: '8px', padding: '24px', fontFamily: H.mono, fontSize: '14px' });

  var lfWrap = el('div', { marginBottom: '16px' });
  var lfLabel = el('div', { display: 'flex', justifyContent: 'space-between', marginBottom: '6px', fontSize: '13px' });
  var lfText = el('span', { color: c.warm300 });
  var lfThresh = el('span', { color: c.warm400 });
  lfThresh.textContent = 'threshold: 0.75';
  lfLabel.appendChild(lfText); lfLabel.appendChild(lfThresh);
  var lfBarOuter = el('div', { height: '8px', background: c.bg, borderRadius: '4px', overflow: 'hidden', position: 'relative' });
  var lfBarInner = el('div', { height: '100%', background: c.accent, borderRadius: '4px', transition: H.reducedMotion ? 'none' : 'width 0.3s ease', width: '0%' });
  var lfMarker = el('div', { position: 'absolute', left: '75%', top: '0', bottom: '0', width: '2px', background: c.heading, borderRadius: '1px' });
  lfBarOuter.appendChild(lfBarInner); lfBarOuter.appendChild(lfMarker);
  lfWrap.appendChild(lfLabel); lfWrap.appendChild(lfBarOuter);

  var narration = el('div', { color: c.text, lineHeight: '1.6', minHeight: '44px', marginBottom: '16px', fontFamily: "'Space Mono', monospace", fontSize: '16px' }, { 'aria-live': 'polite' });
  var contentArea = el('div', {});
  var bucketArea = el('div', { display: 'flex', flexDirection: 'column', gap: '3px' });
  var splitArea = el('div', { display: 'none', gap: '24px' });
  contentArea.appendChild(bucketArea);
  contentArea.appendChild(splitArea);

  var computeLog = el('div', { display: 'none', marginTop: '12px', padding: '12px', background: c.bg, borderRadius: '6px', border: '1px solid ' + c.border, fontSize: '13px', lineHeight: '1.8', maxHeight: '180px', overflowY: 'auto' });

  var controls = el('div', { display: 'flex', gap: '8px', marginTop: '16px', alignItems: 'center' });
  var prevBtn = H.btn('\u2190', function() { if (step > 0) { step--; render(); } }, true, 'Previous step');
  var playBtn = H.btn('\u25B6', function() { togglePlay(); }, true, 'Play');
  var nextBtn = H.btn('\u2192', function() { if (step < steps.length - 1) { step++; render(); } }, true, 'Next step');
  var resetBtn = H.btn('Reset', function() { step = 0; playing = false; playBtn.textContent = '\u25B6'; playBtn.setAttribute('aria-label', 'Play'); render(); }, true);
  var stepLabel = el('span', { color: c.warm300, fontSize: '13px', marginLeft: 'auto' });
  controls.appendChild(prevBtn); controls.appendChild(playBtn); controls.appendChild(nextBtn);
  controls.appendChild(resetBtn); controls.appendChild(stepLabel);

  wrap.appendChild(lfWrap); wrap.appendChild(narration); wrap.appendChild(contentArea);
  wrap.appendChild(computeLog); wrap.appendChild(controls);
  root.appendChild(wrap);

  var playing = false, playTimer = null;
  function togglePlay() {
    if (playing) { playing = false; clearTimeout(playTimer); playBtn.textContent = '\u25B6'; playBtn.setAttribute('aria-label', 'Play'); }
    else { playing = true; playBtn.textContent = '\u23F8'; playBtn.setAttribute('aria-label', 'Pause'); advancePlay(); }
  }
  function advancePlay() {
    if (!playing || step >= steps.length - 1) { playing = false; playBtn.textContent = '\u25B6'; playBtn.setAttribute('aria-label', 'Play'); return; }
    step++;
    render();
    playTimer = setTimeout(advancePlay, 1200);
  }

  function makeBucketColumn(cap, parent) {
    var rows = [];
    for (var i = 0; i < cap; i++) {
      var row = el('div', { display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', borderRadius: '4px', border: '1px solid ' + c.border, transition: 'all 0.15s', minHeight: '34px' });
      var idxSpan = el('span', { color: c.warm300, width: '20px', textAlign: 'right', fontSize: '13px', flexShrink: '0' });
      idxSpan.textContent = i;
      var content = el('span', { color: c.heading, fontSize: '14px', flex: '1', wordBreak: 'break-all' });
      row.appendChild(idxSpan); row.appendChild(content);
      rows.push({ row: row, content: content });
      parent.appendChild(row);
    }
    return rows;
  }

  function fillBuckets(rows, entries, hl) {
    rows.forEach(function(r) { r.content.textContent = ''; r.row.style.background = 'transparent'; r.row.style.borderColor = c.border; });
    entries.forEach(function(e) {
      var b = e.bucket !== undefined ? e.bucket : e.oldBucket;
      var r = rows[b]; if (!r) return;
      var t = r.content.textContent;
      r.content.textContent = t ? t + ' \u2192 ' + e.key : e.key;
    });
    if (hl >= 0 && hl < rows.length) { rows[hl].row.style.background = '#1f1b17'; rows[hl].row.style.borderColor = c.accent; }
  }

  function render() {
    var s = steps[step];
    narration.textContent = s.msg;
    stepLabel.textContent = (step + 1) + ' / ' + steps.length;
    prevBtn.style.opacity = step <= 0 ? '0.3' : '1'; prevBtn.disabled = step <= 0;
    nextBtn.style.opacity = step >= steps.length - 1 ? '0.3' : '1'; nextBtn.disabled = step >= steps.length - 1;

    // Load factor
    var entryCount = s.entries ? s.entries.length : (s.newEntries ? s.newEntries.length : 0);
    var cap = s.cap;
    var lf = entryCount / cap;
    lfText.textContent = 'load: ' + entryCount + '/' + cap + ' = ' + lf.toFixed(2);
    lfBarInner.style.width = Math.min(lf * 100, 100) + '%';
    lfBarInner.style.background = lf >= 0.75 ? '#e74c3c' : c.accent;

    if (s.mode === 'single') {
      splitArea.style.display = 'none';
      bucketArea.style.display = 'flex';
      computeLog.style.display = 'none';
      bucketArea.textContent = '';
      var rows = makeBucketColumn(cap, bucketArea);
      fillBuckets(rows, s.entries, s.hl !== undefined ? s.hl : -1);
    } else {
      // Split view
      bucketArea.style.display = 'none';
      splitArea.style.display = 'grid';
      splitArea.style.gridTemplateColumns = '1fr auto 1fr';
      splitArea.textContent = '';

      var oldCol = el('div', {});
      var oldLbl = el('div', { fontSize: '13px', color: c.warm300, marginBottom: '8px', textAlign: 'center' });
      oldLbl.textContent = 'old (' + OLD_CAP + ' buckets)';
      oldCol.appendChild(oldLbl);
      var oldWrap = el('div', { display: 'flex', flexDirection: 'column', gap: '3px' });
      var oldRows = makeBucketColumn(OLD_CAP, oldWrap);
      fillBuckets(oldRows, s.oldEntries, s.hlOld !== undefined ? s.hlOld : -1);
      oldCol.appendChild(oldWrap);

      var arrow = el('div', { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 8px', color: c.accent, fontSize: '20px' });
      arrow.textContent = '\u2192';

      var newCol = el('div', {});
      var newLbl = el('div', { fontSize: '13px', color: c.accent, marginBottom: '8px', textAlign: 'center' });
      newLbl.textContent = 'new (' + NEW_CAP + ' buckets)';
      newCol.appendChild(newLbl);
      var newWrap = el('div', { display: 'flex', flexDirection: 'column', gap: '3px' });
      var newRows = makeBucketColumn(NEW_CAP, newWrap);
      fillBuckets(newRows, s.newEntries, s.hlNew !== undefined ? s.hlNew : -1);
      newCol.appendChild(newWrap);

      splitArea.appendChild(oldCol); splitArea.appendChild(arrow); splitArea.appendChild(newCol);

      // Compute log — show all rehash entries up to current step
      computeLog.style.display = 'block';
      clr(computeLog);
      var logTitle = el('div', { color: c.warm300, marginBottom: '8px', fontSize: '13px', borderBottom: '1px dotted ' + c.border, paddingBottom: '6px' });
      logTitle.textContent = 'rehash log: key \u2192 % ' + OLD_CAP + ' \u2192 % ' + NEW_CAP;
      computeLog.appendChild(logTitle);

      if (s.newEntries) {
        s.newEntries.forEach(function(en) {
          var oldB = s.oldEntries.find(function(o) { return o.key === en.key; });
          if (!oldB) return;
          var moved = oldB.oldBucket !== en.bucket;
          var line = el('div', { fontSize: '13px', marginBottom: '3px' });
          line.appendChild(txt('"' + en.key + '"', c.heading));
          line.appendChild(txt('  % ' + OLD_CAP + ' = ' + oldB.oldBucket, c.warm300));
          line.appendChild(txt('  \u2192  % ' + NEW_CAP + ' = ' + en.bucket, c.accent));
          line.appendChild(txt(moved ? '  (moved)' : '  (same)', moved ? '#e74c3c' : c.muted));
          computeLog.appendChild(line);
        });
      }
      computeLog.scrollTop = computeLog.scrollHeight;
    }
  }

  render();
})();
`}</script>

See what happened there? The table doubled, every entry found a new home, and the load factor dropped back down. Chains got shorter. Performance restored.

This is why people say hash maps have **amortized** O(1) performance. _Most_ operations are instant. Every now and then, one unlucky insert triggers a resize that costs O(n). Each resize doubles the capacity, so you need _n_ more cheap inserts before the next expensive one. The total cost of all resizes is n + n/2 + n/4 + ... which is less than 2n. Spread across n inserts, that's O(1) each.

Pay a little now, save a lot later.

---

## Go break it

OK, theory's done. Here's a full hash map sandbox. You can `put`, `get`, and `del` keys. Try to cause a collision on purpose (hint: try `"name"` and `"city"`). Fill it up until it resizes. Look up a key that's buried deep in a chain and watch it traverse.

Or hit "flood bucket 0" and watch what happens when an attacker crafts keys that _all_ land in the same bucket. Your O(1) map becomes a linked list. This is a real attack vector: web frameworks parse query strings, JSON, and form data into hash maps, so an attacker controls the keys by controlling the HTTP request.

The fix is using a keyed hash function (like SipHash) with a per-process random secret, so an attacker can't predict which keys collide. Python added [hash randomization](https://www.ocert.org/advisories/ocert-2011-003.html) in 3.3 (2012) after a disclosure in late 2011, then switched to [SipHash in 3.4](https://en.wikipedia.org/wiki/SipHash#Usage) (2014).

<Figure n="05" label="Hash map sandbox">
  <div id="w-sandbox" class="not-prose" role="region" aria-label="Hash map sandbox">
    <noscript><p style="color:#9c9488;font-family:'Space Mono',monospace;font-size:13px;">This interactive widget requires JavaScript.</p></noscript>
  </div>
</Figure>

<script is:inline>{`
(function() {
  var root = document.getElementById('w-sandbox');
  if (!root) return;
  var H = window.__hm, djb2 = H.djb2, c = H.c, el = H.el, txt = H.text, clr = H.clearChildren;

  var capacity = 8, entries = [];
  var wrap = el('div', { background: c.surface, border: '1px solid ' + c.border, borderRadius: '8px', padding: '24px', fontFamily: H.mono, fontSize: '14px' });

  var insertRow = el('div', { display: 'flex', gap: '6px', marginBottom: '8px', flexWrap: 'wrap' });
  var keyIn = el('input', { flex: '1', minWidth: '80px', padding: '8px 10px', fontFamily: H.mono, fontSize: '14px', background: c.bg, color: c.heading, border: '1px solid ' + c.border, borderRadius: '6px', outline: 'none' }, { type: 'text', placeholder: 'key', autocomplete: 'off', spellcheck: 'false', 'aria-label': 'Key to insert' });
  var valIn = el('input', { flex: '1', minWidth: '80px', padding: '8px 10px', fontFamily: H.mono, fontSize: '14px', background: c.bg, color: c.heading, border: '1px solid ' + c.border, borderRadius: '6px', outline: 'none' }, { type: 'text', placeholder: 'value', autocomplete: 'off', spellcheck: 'false', 'aria-label': 'Value to insert' });
  var insertBtn = H.btn('put', function() { doInsert(); });
  insertRow.appendChild(keyIn); insertRow.appendChild(valIn); insertRow.appendChild(insertBtn);

  var lookupRow = el('div', { display: 'flex', gap: '6px', marginBottom: '16px', flexWrap: 'wrap' });
  var lookupIn = el('input', { flex: '1', minWidth: '80px', padding: '8px 10px', fontFamily: H.mono, fontSize: '14px', background: c.bg, color: c.heading, border: '1px solid ' + c.border, borderRadius: '6px', outline: 'none' }, { type: 'text', placeholder: 'key', autocomplete: 'off', spellcheck: 'false', 'aria-label': 'Key to look up or delete' });
  var getBtn = H.btn('get', function() { doGet(); });
  var delBtn = H.btn('del', function() { doDel(); });
  lookupRow.appendChild(lookupIn); lookupRow.appendChild(getBtn); lookupRow.appendChild(delBtn);

  var floodRow = el('div', { display: 'flex', gap: '6px', marginBottom: '16px', flexWrap: 'wrap', alignItems: 'center' });
  var floodBtn = H.btn('flood bucket 0', doFlood);
  floodBtn.style.fontSize = '11px';
  floodBtn.style.padding = '6px 12px';
  floodBtn.style.color = '#e74c3c';
  floodBtn.style.borderColor = '#5c564e';
  var floodHint = el('span', { fontSize: '13px', color: c.warm300 });
  floodHint.textContent = '8 crafted keys, all land in bucket 0. watch O(1) die.';
  var sandboxResetBtn = H.btn('Reset', function() {
    capacity = 8; entries = []; searching = false;
    buildRows(); renderBuckets(-1);
    clr(log);
    keyIn.value = ''; valIn.value = ''; lookupIn.value = '';
  }, true);
  floodRow.appendChild(floodBtn); floodRow.appendChild(sandboxResetBtn); floodRow.appendChild(floodHint);

  [keyIn, valIn, lookupIn].forEach(function(inp) {
    inp.addEventListener('focus', function() { inp.style.borderColor = c.accent; });
    inp.addEventListener('blur', function() { inp.style.borderColor = c.border; });
  });

  var stats = el('div', { display: 'flex', gap: '16px', marginBottom: '16px', fontSize: '13px', color: c.warm300, flexWrap: 'wrap' });
  var log = el('div', { marginBottom: '16px', minHeight: '22px', fontSize: '15px', color: c.text, fontFamily: "'Space Mono', monospace", lineHeight: '1.5' }, { 'aria-live': 'polite' });
  var bucketArea = el('div', { display: 'flex', flexDirection: 'column', gap: '3px' });

  wrap.appendChild(insertRow); wrap.appendChild(lookupRow); wrap.appendChild(floodRow);
  wrap.appendChild(stats); wrap.appendChild(log); wrap.appendChild(bucketArea);
  root.appendChild(wrap);

  var adversarial = ['ab','aj','ar','az','ba','bi','bq','by'];
  function doFlood() {
    capacity = 8; entries = [];
    buildRows();
    adversarial.forEach(function(k) {
      entries.push({ key: k, val: String(entries.length), bucket: djb2(k) % capacity });
    });
    clr(log);
    log.appendChild(txt('8 keys, ALL in bucket 0. Crafted to collide under djb2 mod 8. Resize skipped to show the worst case. Try get("by") and count the comparisons.', '#e74c3c'));
    renderBuckets(0);
  }

  var bucketRows = [];
  function buildRows() {
    bucketArea.textContent = '';
    bucketRows = [];
    for (var i = 0; i < capacity; i++) {
      var row = el('div', { display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', borderRadius: '4px', border: '1px solid ' + c.border, transition: 'all 0.2s', minHeight: '32px' });
      var idxSpan = el('span', { color: c.warm300, width: '20px', textAlign: 'right', fontSize: '13px', flexShrink: '0' });
      idxSpan.textContent = i;
      var content = el('span', { color: c.heading, fontSize: '14px', flex: '1', wordBreak: 'break-all' });
      row.appendChild(idxSpan); row.appendChild(content);
      bucketRows.push({ row: row, content: content }); bucketArea.appendChild(row);
    }
  }

  function renderBuckets(hl) {
    bucketRows.forEach(function(r) { r.content.textContent = ''; r.row.style.background = 'transparent'; r.row.style.borderColor = c.border; });
    entries.forEach(function(e) {
      var r = bucketRows[e.bucket]; var t = r.content.textContent;
      r.content.textContent = t ? t + ' \u2192 ' + e.key + ':' + e.val : e.key + ':' + e.val;
    });
    if (hl >= 0 && hl < bucketRows.length) { bucketRows[hl].row.style.background = '#1f1b17'; bucketRows[hl].row.style.borderColor = c.accent; }
    var lf = (entries.length / capacity).toFixed(2);
    var longest = 0, bMap = {};
    entries.forEach(function(e) { bMap[e.bucket] = (bMap[e.bucket] || 0) + 1; });
    Object.values(bMap).forEach(function(v) { if (v > longest) longest = v; });
    clr(stats);
    stats.appendChild(txt('entries: ' + entries.length));
    stats.appendChild(txt('buckets: ' + capacity));
    stats.appendChild(txt('load: ' + lf));
    stats.appendChild(txt('longest chain: ' + longest));
  }

  function maybeResize() {
    if (entries.length / capacity >= 0.75) {
      capacity *= 2;
      entries.forEach(function(e) { e.bucket = djb2(e.key) % capacity; });
      buildRows();
      clr(log);
      log.appendChild(txt('Resized to ' + capacity + ' buckets. All entries rehashed.', c.accent));
      renderBuckets(-1);
      return true;
    }
    return false;
  }

  function doInsert() {
    var k = keyIn.value.trim(), v = valIn.value.trim();
    if (!k) return;
    if (!v) v = '~';
    var idx = djb2(k) % capacity;
    var existing = entries.find(function(e) { return e.key === k; });
    var isNew = false;
    clr(log);
    if (existing) {
      existing.val = v;
      log.appendChild(txt('Updated "' + k + '" \u2192 "' + v + '" in bucket ' + idx + '.', c.accent));
    } else {
      isNew = true;
      entries.push({ key: k, val: v, bucket: idx });
      var chain = entries.filter(function(e) { return e.bucket === idx; });
      if (chain.length > 1) {
        log.appendChild(txt('Collision! ', c.accent));
        log.appendChild(txt('"' + k + '" chained in bucket ' + idx + ' (' + chain.length + ' entries).'));
      } else {
        log.appendChild(txt('"' + k + '": "' + v + '" \u2192 bucket ' + idx + '.'));
      }
    }
    renderBuckets(idx);
    keyIn.value = ''; valIn.value = ''; keyIn.focus();
    if (isNew) setTimeout(function() { maybeResize(); }, H.reducedMotion ? 10 : 400);
  }

  var searching = false;
  function doGet() {
    var k = lookupIn.value.trim();
    if (!k || searching) return;
    var idx = djb2(k) % capacity;
    var chain = entries.filter(function(e) { return e.bucket === idx; });
    renderBuckets(idx);
    clr(log);

    if (!chain.length) {
      log.appendChild(txt('Bucket ' + idx + ' is empty. Not found.', c.warm300));
      return;
    }

    // Animate the chain traversal
    searching = true;
    var d = H.reducedMotion ? 10 : 300;
    var i = 0;
    var startTime = performance.now();

    log.appendChild(txt('Bucket ' + idx + ' \u2192 ', c.warm300));
    var trailSpan = el('span', {});
    log.appendChild(trailSpan);
    var resultSpan = el('span', {});
    log.appendChild(resultSpan);

    // Comparisons counter
    var counterSpan = el('div', { marginTop: '6px', fontSize: '12px' });
    log.appendChild(counterSpan);

    function checkNext() {
      if (i >= chain.length) {
        // Not found
        resultSpan.appendChild(txt(' \u2192 not found.', c.warm300));
        var elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
        clr(counterSpan);
        counterSpan.appendChild(txt(i + ' comparisons, ' + elapsed + 's', c.warm400));
        searching = false;
        return;
      }
      var entry = chain[i];
      var isMatch = entry.key === k;

      // Show comparison
      if (i > 0) trailSpan.appendChild(txt(' \u2192 ', c.warm400));
      var keySpan = txt('"' + entry.key + '"', isMatch ? c.accent : c.warm300);
      if (isMatch) keySpan.style.fontWeight = '700';
      trailSpan.appendChild(keySpan);
      trailSpan.appendChild(txt(isMatch ? ' \u2713' : ' \u2717', isMatch ? c.accent : '#e74c3c'));

      // Update counter live
      clr(counterSpan);
      counterSpan.appendChild(txt((i + 1) + ' comparison' + (i > 0 ? 's' : '') + '...', c.warm400));

      if (isMatch) {
        var elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
        setTimeout(function() {
          clr(resultSpan);
          resultSpan.appendChild(txt(' \u2192 ', c.warm300));
          resultSpan.appendChild(txt('found "' + entry.val + '"', c.accent));
          clr(counterSpan);
          counterSpan.appendChild(txt((i + 1) + ' comparison' + (i > 0 ? 's' : '') + ', ' + elapsed + 's', c.warm400));
          if (i + 1 >= 4) {
            counterSpan.appendChild(txt('  (not so O(1) anymore...)', '#e74c3c'));
          }
          searching = false;
        }, d);
        return;
      }

      i++;
      setTimeout(checkNext, d);
    }
    setTimeout(checkNext, d);
  }

  function doDel() {
    var k = lookupIn.value.trim();
    if (!k) return;
    var idx = djb2(k) % capacity;
    var i = entries.findIndex(function(e) { return e.key === k; });
    clr(log);
    if (i >= 0) {
      entries.splice(i, 1);
      log.appendChild(txt('Deleted "' + k + '" from bucket ' + idx + '.', c.accent));
    } else {
      log.appendChild(txt('"' + k + '" not found.', c.warm300));
    }
    renderBuckets(idx);
    lookupIn.value = ''; lookupIn.focus();
  }

  keyIn.addEventListener('keydown', function(e) { if (e.key === 'Enter') doInsert(); });
  lookupIn.addEventListener('keydown', function(e) { if (e.key === 'Enter') doGet(); });

  buildRows(); renderBuckets(-1);
})();
`}</script>

The whole thing in one line:

> **key → hash(key) → index = hash % capacity → bucket[index] → walk chain → value**

Everything above is just unpacking this pipeline.

## What I left out (the rabbit hole goes deep)

What I showed you is **chaining**, the simplest collision strategy. Most production hash maps today actually use a different approach, and the optimizations go deep:

**Open addressing** is what most modern hash maps use instead of chaining. When there's a collision, you don't build a linked list, you just look for the next empty slot in the array itself. This has way better [cache locality](https://en.wikipedia.org/wiki/Locality_of_reference) because you're scanning contiguous memory instead of chasing pointers all over the heap. The tradeoff: deletion requires tombstones and is genuinely tricky, which is part of why chaining persisted as long as it did. [Robin Hood hashing](https://programming.guide/robin-hood-hashing.html) is a particularly beautiful variant that "steals from the rich" by letting entries with longer probe distances bump out entries with shorter ones. Cute name, clever algorithm.

(Java's `HashMap` still uses chaining but converts long chains into red-black trees at length 8, so its worst case is O(log n) not O(n). Clever hybrid.)

**SIMD probing** is what Google's [Swiss Table](https://abseil.io/about/design/swisstables) design does (implemented in Abseil C++ and, independently, in Rust's [hashbrown](https://github.com/rust-lang/hashbrown) which backs its standard `HashMap`). It uses CPU vector instructions to check 16 slots' metadata bytes in a single operation, then only does full key comparison on matches. Extremely fast. If you want to go deep, [Matt Kulukundis's CppCon talk](https://www.youtube.com/watch?v=ncHmEUmJZf4) is an absolute banger.

**Power-of-two sizing** is a neat trick: when your capacity is always a power of 2, you can replace the expensive modulo with a bitwise AND: `hash & (capacity - 1)` does the same thing, way faster. Most modern implementations do this.

**Better hash functions** fall into two camps. [SipHash](https://en.wikipedia.org/wiki/SipHash) is a keyed PRF designed for adversarial resistance: give it a per-process secret key and an attacker can't predict collisions. [xxHash](https://cyan4973.github.io/xxHash/) and [wyhash](https://github.com/wangyi-fudan/wyhash) are built for raw speed and better distribution, not security. Different tools for different problems.

---

The core idea never changes though: hash the key, find the bucket, handle collisions, resize when it gets full.

I wish I'd looked inside sooner.
