Внедряем хэш-таблицы с нуля: от теории к работающему коду на Python

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

В основе хэш-таблицы лежат два компонента: функция хэширования и массив (список) «корзин» (buckets). Функция принимает ключ (например, строку или число) и возвращает целочисленный хэш-код. Идеальная функция равномерно распределяет ключи по корзинам. Затем этот хэш-код преобразуется в индекс массива (обычно через операцию взятия остатка от деления на размер массива). Если два разных ключа дают один и тот же индекс, возникает коллизия. Существует несколько методов их разрешения; мы реализуем метод цепочек, где каждая корзина содержит список (цепочку) пар ключ-значение.

Начнем с создания класса. Определим фиксированный начальный размер внутреннего массива, например, 8. Каждый элемент этого массива будет пустым списком.

class HashTable:
 def __init__(self, capacity=8):
 self.capacity = capacity
 self.buckets = [[] for _ in range(capacity)]  # Создаем список пустых списков
 self.size = 0  # Количество фактически хранимых элементов

Теперь нужна простая хэш-функция. В Python есть встроенная функция hash(), но для демонстрации и воспроизводимости создадим свою. Например, для строкового ключа просуммируем коды символов, умноженные на простое число.

 def _hash(self, key):
 if isinstance(key, str):
 hash_value = 0
 prime = 31
 for char in key:
 hash_value = (hash_value * prime + ord(char)) % self.capacity
 return hash_value
 else:
 # Для числовых ключей
 return hash(key) % self.capacity

Метод для вставки (put или __setitem__). Сначала вычисляем индекс корзины. Затем проходим по цепочке в этой корзине, проверяя, не существует ли уже такой ключ. Если ключ найден, обновляем его значение. Если не найден, добавляем новую пару в конец цепочки и увеличиваем счетчик size.

 def put(self, key, value):
 index = self._hash(key)
 bucket = self.buckets[index]
 for i, (k, v) in enumerate(bucket):
 if k == key:
 bucket[i] = (key, value)  # Обновление
 return
 bucket.append((key, value))
 self.size += 1

Метод для получения значения (get или __getitem__). Аналогично вычисляем индекс, ищем ключ в цепочке. Если нашли — возвращаем значение. Если нет — вызываем исключение KeyError.

 def get(self, key):
 index = self._hash(key)
 bucket = self.buckets[index]
 for k, v in bucket:
 if k == key:
 return v
 raise KeyError(f"Key '{key}' not found")

Метод удаления (delete или __delitem__). Находим ключ в цепочке и удаляем эту пару, уменьшая size.

 def delete(self, key):
 index = self._hash(key)
 bucket = self.buckets[index]
 for i, (k, v) in enumerate(bucket):
 if k == key:
 del bucket[i]
 self.size -= 1
 return
 raise KeyError(f"Key '{key}' not found")

Наша базовая реализация работает, но у нее есть серьезный недостаток: при большом количестве элементов производительность деградирует до O(n), если все ключи попадают в одну корзину. Необходима возможность увеличения размера внутреннего массива (rehashing) при достижении определенного коэффициента загрузки (load factor = size / capacity). Обычно порог выбирают 0.75.

Добавим приватный метод _resize, который будет создавать новый массив корзин в два раза больше, пересчитывать хэши для всех существующих пар и перемещать их в новые корзины. Вызов этого метода нужно добавить в конец метода put.

 def _resize(self):
 old_buckets = self.buckets
 self.capacity *= 2
 self.buckets = [[] for _ in range(self.capacity)]
 self.size = 0  # Сбросим, put его восстановит
 for bucket in old_buckets:
 for key, value in bucket:
 self.put(key, value)  # Перехэширование

 def put(self, key, value):
 # ... (старый код поиска и обновления/добавления)
 # После добавления нового элемента:
 if self.size / self.capacity > 0.75:  # Проверка коэффициента загрузки
 self._resize()

Теперь наша хэш-таблица стала динамической. Давайте протестируем ее работу:

ht = HashTable(4)
ht.put("apple", 10)
ht.put("banana", 20)
ht.put("orange", 30)
print(ht.get("banana"))  # 20
ht.put("banana", 25)  # Обновление
print(ht.get("banana"))  # 25
ht.delete("apple")
try:
 print(ht.get("apple"))
except KeyError as e:
 print(e)  # Key 'apple' not found
print(f"Size: {ht.size}, Capacity: {ht.capacity}")

В результате вы увидите, как таблица автоматически увеличивает capacity при добавлении элементов. Эта реализация, конечно, упрощена: она не учитывает многие оптимизации (например, использование кортежей вместо списков для пар), не реализует итераторы и другие специальные методы Python в полной мере. Однако она идеально раскрывает суть: хэш-функция, корзины, цепочки, разрешение коллизий и рехеширование. Создав такую структуру самостоятельно, вы глубже поймете, как работают встроенные ассоциативные массивы в любом языке программирования, и сможете лучше оценивать их производительность в ваших проектах.
166 2

Комментарии (8)

avatar
6ia545jcl3 28.03.2026
Практика — лучший учитель. После такого кода структура данных запоминается намертво!
avatar
xantepbljd 29.03.2026
Реализация понятная, но в продакшене лучше использовать встроенный dict — он оптимизирован и надежнее.
avatar
bmkg09usdfmk 29.03.2026
Хорошо, что затронули тему хэш-функций. Можно ли добавить сравнение разных методов разрешения коллизий?
avatar
8a564l 30.03.2026
Спасибо за пошаговый разбор. Теперь гораздо яснее, как работают словари под капотом в Python.
avatar
rxqu1nzq 30.03.2026
Отличная статья для понимания основ! Особенно полезно разобрать коллизии и рехеширование на практике.
avatar
mxu4g0 30.03.2026
Не хватает бенчмарков: сравните производительность вашей хэш-таблицы со словарем на больших данных.
avatar
f1bicd0nm 31.03.2026
Хотелось бы больше примеров, как выбирать начальный размер массива и коэффициент расширения для разных задач.
avatar
8dktn4ppi4 31.03.2026
А есть ли смысл в своей реализации, кроме учебного? Кажется, это лишь академическое упражнение.
Вы просмотрели все комментарии