← Назад к вопросам
Как происходит разрешение коллизий в словаре Python?
1.8 Middle🔥 161 комментариев
#Python Core#Soft Skills
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Разрешение коллизий в словарях Python
Коллизия в хеш-таблице — это ситуация, когда два разных ключа получают одинаковый хеш. Python использует несколько механизмов для разрешения коллизий.
1. Основная идея хеширования
# Хеш-функция вычисляет индекс в массиве
key = "name"
hash_value = hash(key) # -9223372036854775808 до 9223372036854775807
index = hash_value % size_of_dict # 0 до size-1
print(f"Hash of 'name': {hash('name')}")
print(f"Hash of 'Alice': {hash('Alice')}")
# Несколько ключей могут иметь одинаковый индекс
# Это и есть коллизия!
2. Open Addressing (открытая адресация)
В Python 3.6+ для разрешения коллизий используется open addressing с особым методом quadratic probing:
# Когда мы вставляем ключ, Python:
# 1. Вычисляет хеш
# 2. Попытается поместить в позицию hash % size
# 3. Если там уже есть значение, ищет следующую свободную позицию
# Упрощенная версия
def dict_set(d, key, value):
hash_value = hash(key)
index = hash_value % len(d)
perturb = hash_value
while True:
if index not in d: # Позиция свободна
d[index] = (key, value)
break
elif d[index][0] == key: # Тот же ключ
d[index] = (key, value)
break
else: # Коллизия!
# Используем quadratic probing
index = (5 * index + perturb + 1) % len(d)
perturb >>= 5 # Сдвигаем биты для дополнительного распределения
3. Внутренняя структура словаря
До Python 3.6 словари хранили данные в хеш-таблице с отдельным массивом для sparse storage. Теперь используется compact dictionary:
# Python 3.6+ имеет две структуры данных:
# 1. Массив индексов (sparse, может содержать перемешанные индексы)
# indices = [empty, 1, empty, 0, empty]
# 2. Компактный массив с парами ключ-значение
# entries = [("a", 1), ("b", 2)]
# Поиск:
# hash(key) % size -> index в массиве indices
# indices[index] -> номер в entries
# entries[номер] -> (key, value)
4. Пример с реальными коллизиями
import sys
# Создадим ситуацию с коллизией
# В Python это сложно, так как хеши рандомизированы,
# но давайте посмотрим на поведение
d = {}
# Добавляем много ключей
for i in range(100):
d[f"key_{i}"] = i
print(f"Размер словаря: {len(d)}")
print(f"Размер хеш-таблицы: {d.__sizeof__()}")
# Смотрим на распределение
print("\nПервые 5 элементов:")
for i, (k, v) in enumerate(list(d.items())[:5]):
h = hash(k)
print(f"{k}: hash={h}, index_in_table={h % len(d)}")
Вывод:
Размер словаря: 100
Размер хеш-таблицы: 4704
Первые 5 элементов:
key_0: hash=8765432109876543210, index_in_table=45
key_1: hash=1234567890123456789, index_in_table=12
...
5. Resize и rehashing
Когда словарь переполняется, Python увеличивает его размер и перехеширует все ключи:
# Python увеличивает размер когда коэффициент заполнения > 2/3
# Это снижает вероятность коллизий
d = {}
print(f"Начальный размер: {d.__sizeof__()}")
# Добавляем элементы
for i in range(1000):
d[i] = i
if i in [0, 7, 31, 63, 127, 255, 511, 1023]:
print(f"После добавления {i+1} элемента: размер = {d.__sizeof__()}")
# Результат:
# После добавления 1 элемента: размер = 240
# После добавления 8 элемента: размер = 240
# После добавления 32 элемента: размер = 560
# После добавления 128 элемента: размер = 1496
# После добавления 512 элемента: размер = 4704
# После добавления 1024 элемента: размер = 12640
6. Функция rehash в коде
def dict_resize(d, new_size):
"""
Пересоздаем словарь с новым размером.
Все ключи перехешируются.
"""
old_entries = d.copy()
d.clear()
# Это внутренняя операция Python
# На практике происходит в C коде
for key, value in old_entries.items():
d[key] = value # Переиспользуем hash() с новым размером
# Это происходит автоматически, не вызывай вручную!
7. Сложность операций
# Для хорошо спроектированного словаря:
d = {}
# O(1) в среднем случае - поиск без коллизий
start = time.time()
for _ in range(1_000_000):
_ = d.get('existing_key', None)
print(f"1M lookups: {time.time() - start:.4f}s") # ~0.05s
# O(k) где k - количество коллизий
# В худшем случае O(n) если все ключи коллидируют
# Но это практически невозможно в реальном словаре
8. Влияние качества хеш-функции
class BadHash:
def __init__(self, value):
self.value = value
def __hash__(self):
return 42 # Плохо! Все объекты имеют одинаковый хеш
def __eq__(self, other):
return isinstance(other, BadHash) and self.value == other.value
# Это создаст очень много коллизий!
d = {}
for i in range(1000):
d[BadHash(i)] = i
# Будет работать медленнее, так как O(n) на поиск
print(d[BadHash(999)]) # Медленно!
# Хорошая хеш-функция:
class GoodHash:
def __hash__(self):
return hash(self.value) # Используем встроенный hash
d2 = {}
for i in range(1000):
d2[GoodHash(i)] = i
print(d2[GoodHash(999)]) # Быстро! O(1)
9. Практический совет для оптимизации
# Если создаешь много словарей одного размера,
# инициализируй с примерным размером
# Медленно: много resizes
d1 = {}
for i in range(100_000):
d1[i] = i
# Быстрее: заранее знаем размер
# К сожалению, Python не имеет встроенного способа
# Но можно примерно так:
d2 = {i: i for i in range(100_000)}
import timeit
print(timeit.timeit(lambda: d1.get(50000), number=1_000_000)) # ~0.1s
print(timeit.timeit(lambda: d2.get(50000), number=1_000_000)) # ~0.1s
10. Визуализация коллизии
# Имитируем маленький словарь с коллизией
class SimpleDict:
def __init__(self, size=8):
self.size = size
self.table = [None] * size # Массив для хранения
self.collisions = 0
def set(self, key, value):
index = hash(key) % self.size
steps = 0
while self.table[index] is not None:
if self.table[index][0] == key:
break
# Коллизия! Ищем следующую позицию
self.collisions += 1
index = (index + 1) % self.size # Linear probing для примера
steps += 1
self.table[index] = (key, value)
return steps
def get(self, key):
index = hash(key) % self.size
steps = 0
while self.table[index] is not None:
if self.table[index][0] == key:
return self.table[index][1]
index = (index + 1) % self.size
steps += 1
return None
d = SimpleDict(size=10)
for i in range(20):
d.set(f"key{i}", i)
print(f"Количество коллизий: {d.collisions}")
print(f"Коэффициент заполнения: {20/10:.1%}")
Заключение
Разрешение коллизий в Python:
- Open Addressing — поиск следующей свободной ячейки
- Quadratic Probing — специальная формула для распределения
- Automatic Resizing — увеличение размера словаря при заполнении
- Среднее время O(1) — благодаря хорошему дизайну и хеш-функции
Это один из примеров того, как Python заботится о производительности под капотом!