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

Как разрешать коллизии в словарях?

2.0 Middle🔥 91 комментариев
#Python Core

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Разрешение коллизий в словарях (Hash Collisions)

Коллизия в словаре (hash table) возникает, когда два разных ключа генерируют одинаковый хэш-код. Это естественное явление, и языки программирования должны иметь способ разрешения таких коллизий.

Что такое коллизия

В словаре ключи преобразуются в индексы массива через функцию хэширования. Когда два разных ключа дают одинаковый хэш, это коллизия:

ключ1: "apple"  → hash() → 5
ключ2: "orange" → hash() → 5  ← КОЛЛИЗИЯ!

Как Python разрешает коллизии

Python использует открытую адресацию (open addressing) с методом линейного зондирования (linear probing).

# Демонстрация хэширования в Python
print(hash("apple"))   # 1234567890
print(hash("orange"))  # 9876543210
print(hash(1))         # 1
print(hash(1.0))       # 1 - коллизия! int и float могут давать одинаковый хэш

Методы разрешения коллизий

1. Линейное зондирование (Linear Probing)

При коллизии переходим к следующей позиции в массиве:

Позиция 5: занята key1 → value1
При коллизии ищем позицию 6, 7, 8... пока не найдём свободную
# Пример реализации линейного зондирования
class SimpleHashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * size
    
    def _hash(self, key):
        """Простая функция хэширования"""
        return hash(key) % self.size
    
    def put(self, key, value):
        """Добавить или обновить значение"""
        index = self._hash(key)
        
        # Линейное зондирование
        while self.table[index] is not None:
            existing_key, _ = self.table[index]
            if existing_key == key:
                self.table[index] = (key, value)
                return
            index = (index + 1) % self.size
        
        self.table[index] = (key, value)
    
    def get(self, key):
        """Получить значение по ключу"""
        index = self._hash(key)
        
        while self.table[index] is not None:
            existing_key, value = self.table[index]
            if existing_key == key:
                return value
            index = (index + 1) % self.size
        
        return None

# Использование
ht = SimpleHashTable()
ht.put("apple", 5)
ht.put("orange", 3)  # Может быть коллизия
print(ht.get("apple"))   # 5
print(ht.get("orange"))  # 3

2. Цепочки (Chaining)

При коллизии создаём список (цепочку) значений:

# Реализация через цепочки
class ChainedHashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [[] for _ in range(size)]  # Список списков
    
    def _hash(self, key):
        return hash(key) % self.size
    
    def put(self, key, value):
        """Добавить значение в цепочку"""
        index = self._hash(key)
        
        # Ищем существующий ключ
        for i, (k, v) in enumerate(self.table[index]):
            if k == key:
                self.table[index][i] = (key, value)
                return
        
        # Добавляем новую пару
        self.table[index].append((key, value))
    
    def get(self, key):
        """Получить значение из цепочки"""
        index = self._hash(key)
        
        for k, v in self.table[index]:
            if k == key:
                return v
        
        return None

# Использование
ht = ChainedHashTable()
ht.put("apple", 5)
ht.put("orange", 3)
ht.put("grape", 2)
print(ht.get("apple"))   # 5
print(ht.get("orange"))  # 3

Python dict - реальная реализация

Python dict использует гибридный подход с оптимизациями:

# Python dict работает со средней сложностью O(1)
d = {}

# Добавление с потенциальной коллизией
d["apple"] = 5
d["orange"] = 3
d[1] = "число"
d[1.0] = "число с точкой"  # int и float имеют одинаковый хэш

print(d)  # {"apple": 5, "orange": 3, 1: "число с точкой"}
print(d[1] == d[1.0])  # True - это один и тот же ключ

Факторы, влияющие на коллизии

# 1. Качество функции хэширования
class BadHash:
    def __hash__(self):
        return 5  # ВСЕ объекты имеют одинаковый хэш!
    
    def __eq__(self, other):
        return isinstance(other, BadHash)

# Это вызовет деградацию производительности
bad_dict = {}
for i in range(1000):
    bad_dict[BadHash()] = i  # O(n) вместо O(1)!

# 2. Factor load (коэффициент заполнения)
# Когда словарь переполняется, он автоматически расширяется
d = {}
print(len(d.__sizeof__()))  # Размер в памяти

for i in range(1000000):
    d[i] = i
# Python автоматически переходит на больший размер при увеличении нагрузки

Хорошая vs плохая хэш-функция

# ПЛОХАЯ хэш-функция
class BadHashClass:
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):
        return 1  # Все объекты одинаковый хэш
    
    def __eq__(self, other):
        return self.value == other.value

# ХОРОШАЯ хэш-функция
class GoodHashClass:
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):
        return hash(self.value)  # Хэш зависит от значения
    
    def __eq__(self, other):
        return self.value == other.value

# Тестирование
import time

# Плохой случай
start = time.time()
bad_dict = {}
for i in range(10000):
    bad_dict[BadHashClass(i)] = i
print(f"Плохая хэш-функция: {time.time() - start:.4f}s")  # Медленно

# Хороший случай
start = time.time()
good_dict = {}
for i in range(10000):
    good_dict[GoodHashClass(i)] = i
print(f"Хорошая хэш-функция: {time.time() - start:.4f}s")  # Быстро

Избежание проблем с коллизиями

# 1. Используй неизменяемые типы как ключи
# ХОРОШО
d = {}
d[(1, 2)] = "tuple"      # Кортежи неизменяемы
d["key"] = "string"      # Строки неизменяемы
d[frozenset([1, 2])] = "value"  # Замороженное множество

# ПЛОХО - списки не хэшируются
# d[[1, 2, 3]] = "value"  # TypeError!

# 2. Правильно реализуй __hash__ и __eq__
class User:
    def __init__(self, user_id, name):
        self.user_id = user_id
        self.name = name
    
    def __hash__(self):
        # Хэш должен зависеть от тех же полей что __eq__
        return hash(self.user_id)
    
    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return self.user_id == other.user_id

# Использование
users = {}
user1 = User(1, "John")
users[user1] = "data"

user1_copy = User(1, "John")
print(users[user1_copy])  # "data" - работает, потому что __eq__ совпадает

Проверка коллизий на практике

import sys

def check_collisions(keys):
    """Проверить коллизии для набора ключей"""
    hashes = {}
    collisions = []
    
    for key in keys:
        h = hash(key)
        if h in hashes:
            collisions.append((hashes[h], key))
        else:
            hashes[h] = key
    
    return collisions

# Пример
keys = ["apple", "orange", "grape", "banana", 1, 2, 3]
collisions = check_collisions(keys)

if collisions:
    print(f"Найдено коллизий: {len(collisions)}")
    for key1, key2 in collisions:
        print(f"  {key1} и {key2} имеют одинаковый хэш")
else:
    print("Коллизий не найдено")

Производительность при коллизиях

import time

# Хорошо распределённые ключи
good_dict = {}
for i in range(1000000):
    good_dict[i] = i  # O(1) в среднем

start = time.time()
for i in range(100000):
    _ = good_dict[i]
print(f"С хорошим хэшем: {time.time() - start:.6f}s")

# Плохо распределённые ключи
bad_dict = {}
for i in range(1000):
    bad_dict[i % 10] = i  # Много коллизий

start = time.time()
for i in range(100000):
    _ = bad_dict[i % 10]  # Деградирует к O(n)
print(f"С плохим хэшем: {time.time() - start:.6f}s")

Вывод

Разрешение коллизий — это критическая часть реализации словарей:

  • Python dict использует оптимизированную версию открытой адресации
  • Коллизии неизбежны, но алгоритмы разработаны для минимизации их влияния
  • Хэш-функция должна распределять значения равномерно
  • Используй неизменяемые типы как ключи словарей
  • Правильно реализуй __hash__ и __eq__ в пользовательских классах
  • Средняя сложность O(1) достигается при хорошем разрешении коллизий

Понимание коллизий помогает писать быстрый и надёжный код.

Как разрешать коллизии в словарях? | PrepBro