Получить доступ

Как устроена хеш-таблица?

JuniorСтруктуры данных
Ещё спрашивают так:
  • Как решаются коллизии в хеш-таблице?
  • Что такое коэффициент загрузки?
  • Чем метод цепочек отличается от открытой адресации?
Коротко

Хеш-таблица — массив «корзин»: хеш-функция превращает ключ в номер корзины, поэтому поиск, вставка и удаление в среднем — O(1).

  • Коллизия — разные ключи в одной корзине: решают цепочками или открытой адресацией
  • Коэффициент загрузки (элементов на корзину) растёт — таблицу увеличивают и перераспределяют ключи

А ещё Худший случай O(n) — при плохом хеше или атаке на коллизии

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Попробуйте сами

Хеш-таблица: цепочки и открытая адресация, коллизии, расширение

Подробно

Принцип. Хеш-функция превращает ключ в число, а остаток от деления на размер массива даёт индекс корзины. Вместо перебора мы вычисляем, где искать, — отсюда средняя сложность O(1).

Требования к хеш-функции: детерминированность (одинаковый ключ — одинаковый хеш), равномерное распределение по корзинам, быстрота. Равные ключи обязаны давать равный хеш.

Коллизии неизбежны: ключей больше, чем корзин. Два основных способа:

  • метод цепочек — в корзине хранится список (или дерево) записей; при поиске идём по нему и сравниваем ключи. Так устроены HashMap в Java и unordered_map в C++;
  • открытая адресация — все записи прямо в массиве; если ячейка занята, пробуем следующие по правилу: линейное пробирование (+1), квадратичное, двойное хеширование. Так устроен dict в Python. Удаление требует пометки «здесь было» (tombstone), иначе разорвётся цепочка поиска.

Коэффициент загрузки — отношение числа элементов к числу корзин. Чем он больше, тем больше коллизий. При превышении порога (около 0,75 для цепочек, 2/3 у Python) таблица увеличивается, обычно вдвое, и все ключи перераспределяются — дорого, но редко, поэтому вставка амортизированно O(1).

Худший случай — все ключи в одной корзине: O(n). Бывает при плохой хеш-функции или намеренной атаке; поэтому для строк используют хеш со случайной солью, а Java превращает длинные цепочки в деревья.

Пример

class HashTable:
    def __init__(self, capacity=8):
        self.buckets = [[] for _ in range(capacity)]
        self.size = 0

    def put(self, key, value):
        bucket = self.buckets[hash(key) % len(self.buckets)]
        for i, (k, _) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return
        bucket.append((key, value))
        self.size += 1
        if self.size / len(self.buckets) > 0.75:
            self._resize()

    def get(self, key):
        bucket = self.buckets[hash(key) % len(self.buckets)]
        return next((v for k, v in bucket if k == key), None)

    def _resize(self):
        old = [pair for b in self.buckets for pair in b]
        self.buckets = [[] for _ in range(len(self.buckets) * 2)]
        self.size = 0
        for k, v in old:
            self.put(k, v)

Что хотят услышать

  • Хеш-функция → индекс корзины → средняя O(1).
  • Два способа разрешения коллизий и их отличия.
  • Коэффициент загрузки и расширение.
  • Худший случай и защита от него.

Частые ошибки

  • «Поиск всегда O(1)» без оговорки про средний случай.
  • Забыть, что равные ключи должны давать одинаковый хеш.

Что спросят дальше

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем