← Назад к вопросам

Как происходит разрешение коллизий в словаре 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:

  1. Open Addressing — поиск следующей свободной ячейки
  2. Quadratic Probing — специальная формула для распределения
  3. Automatic Resizing — увеличение размера словаря при заполнении
  4. Среднее время O(1) — благодаря хорошему дизайну и хеш-функции

Это один из примеров того, как Python заботится о производительности под капотом!