awesome-everything EN
↑ Обратно к восхождению

Алгоритмы с нуля

Хеширование: чтение кода

Суть Читаем реальные сниппеты hash table и hash map, предсказываем поведение и выбираем фикс: chaining-поиск, resize удвоением, баги дополнения и частотной map.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Баги хеширования прячутся в коде, который выглядит корректным. Прочитайте каждый сниппет, предскажите, что он на самом деле делает под нагрузкой или на крайних случаях, и выберите фикс, который аккуратный инженер сделает первым.

Цель

Отработайте цикл, который вы прогоняете на каждой задаче с hash map: прочитать код, протрассировать на неудобном входе и взять точный фикс — а не отмахнуться расплывчатым «используй hash map».

Сниппет 1 — вставка и поиск с chaining

class HashTable {
  constructor(size = 8) {
    this.buckets = Array.from({ length: size }, () => []);
  }
  hash(key) {
    let h = 0;
    for (const ch of key) h = (h * 31 + ch.charCodeAt(0)) % this.buckets.length;
    return h;
  }
  set(key, value) {
    const b = this.buckets[this.hash(key)];
    b.push([key, value]);          // всегда push
  }
  get(key) {
    const b = this.buckets[this.hash(key)];
    for (const [k, v] of b) if (k === key) return v;
    return undefined;
  }
}
Викторина

Вы вызываете set('a', 1), затем set('a', 2), затем get('a'). Что вернётся и в чём баг?

Сниппет 2 — resize по load factor

set(key, value) {
  if ((this.count + 1) / this.buckets.length > 0.75) this.resize();
  const b = this.buckets[this.hash(key)];
  b.push([key, value]);
  this.count++;
}
resize() {
  const old = this.buckets;
  this.buckets = Array.from({ length: old.length * 2 }, () => []);
  for (const bucket of old)
    for (const [k, v] of bucket) {
      const i = this.hash(k);       // перехешируем по НОВОЙ длине
      this.buckets[i].push([k, v]);
    }
}
Викторина

Почему resize обязан перехешировать каждый ключ по новой длине массива, а не копировать бакеты напрямую?

Сниппет 3 — two-sum через паттерн дополнения

function twoSum(nums, target) {
  const seen = new Map();           // значение -> индекс
  for (let i = 0; i < nums.length; i++) {
    const need = target - nums[i];
    if (seen.has(need)) return [seen.get(need), i];
    seen.set(nums[i], i);           // записываем ПОСЛЕ проверки
  }
  return null;
}
Викторина

Почему проверка need идёт ДО seen.set(nums[i], i), и что сломается, если поменять две строки местами?

Сниппет 4 — группировка анаграмм по частотному ключу

function groupAnagrams(words) {
  const groups = new Map();
  for (const w of words) {
    const key = w.split('').sort().join('');   // канонический ключ
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(w);
  }
  return [...groups.values()];
}
Викторина

Это корректно группирует анаграммы. Для длинных строк сколько стоит ключ, и какой более дешёвый канонический ключ даёт тот же результат группировки?

Итог

Каждая задача с hash map читается в коде: set с chaining должен обновлять существующий ключ на месте, а не слепо делать push; resize должен перехешировать по новому модулю, иначе поиски уходят не в тот бакет; паттерн дополнения записывает после проверки, чтобы элемент не паровался сам с собой; а группировке нужен канонический ключ, инвариантный по группе — счётчик символов бьёт сортировку на горячем пути. Сначала прочитайте неудобный вход, потом выберите точный фикс.

Продолжить восхождение ↑Хеширование: соберите hash map с resize
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.