Какой алгоритм действий при добавлении элемента в словарь в Python?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Алгоритм добавления элемента в словарь (dict) в Python
Словари в Python реализованы на основе хеш-таблиц. Процесс добавления элемента состоит из нескольких шагов с использованием хеширования.
Структура хеш-таблицы
Внутри словарь хранит:
- Массив ячеек (hash table) фиксированного размера
- Хеш-функция для преобразования ключа в индекс
- Разрешение коллизий для обработки совпадений
┌─────────────────────────────────────────┐
│ Словарь {'a': 1, 'b': 2, 'c': 3} │
├─────────────────────────────────────────┤
│ [0] → None │
│ [1] → [ключ='a', значение=1] │
│ [2] → [ключ='b', значение=2] │
│ [3] → None │
│ [4] → [ключ='c', значение=3] │
│ [5] → None │
│ [6] → None │
│ [7] → None │
└─────────────────────────────────────────┘
Шаг за шагом: добавление элемента
Операция:
d = {}
d['hello'] = 42
Шаг 1: Вычисление хеша ключа
hash_value = hash('hello')
# Результат: целое число (может быть очень большим)
# Например: hash('hello') = -9223372036854775789
Шаг 2: Преобразование хеша в индекс
# Размер таблицы: 8
size = 8
index = hash_value % size
index = (-9223372036854775789) % 8 = 3
Шаг 3: Проверка ячейки на коллизию
# Если ячейка [3] пуста → вставляем
# Если там уже есть элемент с тем же ключом → обновляем
# Если там элемент с другим ключом → коллизия (решаем ниже)
Шаг 4: Вставка в ячейку
table[3] = ('hello', 42) # (ключ, значение)
Полный пример процесса
# Начальный словарь пуст
d = {}
# Внутренний размер: 8 ячеек
# Шаг 1: Добавляем d['apple'] = 100
key = 'apple'
hash_val = hash('apple') # -6848352157346573405
index = hash_val % 8 # 3
# table[3] = ('apple', 100)
# Шаг 2: Добавляем d['banana'] = 200
hash_val = hash('banana') # 2783859921076103131
index = hash_val % 8 # 3 (КОЛЛИЗИЯ! Тот же индекс)
# Нужно разрешить коллизию (см. ниже)
# Шаг 3: Добавляем d['cherry'] = 300
hash_val = hash('cherry') # 8765432109876543210
index = hash_val % 8 # 2 (свободная ячейка)
# table[2] = ('cherry', 300)
Разрешение коллизий
Получается, что разные ключи могут иметь одинаковый индекс. Python использует открытую адресацию (linear probing):
Алгоритм:
- Вычислили индекс, он занят
- Проверяем следующую ячейку (index + 1)
- Если свободна — вставляем
- Если занята — ищем дальше
- Повторяем, пока не найдем свободную ячейку
def add_to_dict(table, key, value):
index = hash(key) % len(table)
# Линейное зондирование (linear probing)
while table[index] is not None:
existing_key, _ = table[index]
if existing_key == key:
# Ключ уже существует - обновляем значение
table[index] = (key, value)
return
# Коллизия - ищем следующую свободную ячейку
index = (index + 1) % len(table)
# Нашли свободную ячейку
table[index] = (key, value)
Визуально:
Добавляем 'banana' (коллизия на индекс 3):
Шаг 1: индекс 3 занят ('apple')
Шаг 2: индекс 4 свободен → вставляем 'banana'
┌────┬─────────┬─────────┬─────────┬─────────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │
├────┼─────────┼─────────┼─────────┼─────────┤
│ ∅ │ ∅ │ cherry │ apple │ banana │
└────┴─────────┴─────────┴─────────┴─────────┘
Перехеширование (Resizing)
Когда словарь становится слишком полным (коэффициент нагрузки > 2/3), Python перестраивает таблицу:
# Изначально размер 8
d = {}
for i in range(8):
d[f'key{i}'] = i
# Размер примерно 66% заполнен
# При добавлении 6-го элемента:
d['key5'] = 5
# Python создает НОВУЮ таблицу размером 16 (увеличивает вдвое)
# Все элементы перехешируются с новым размером:
# новый индекс = hash(key) % 16
Процесс перехеширования:
def resize_table(old_table, new_size):
new_table = [None] * new_size
# Перехешируем все элементы
for item in old_table:
if item is not None:
key, value = item
# Вычисляем новый индекс
new_index = hash(key) % new_size
new_table[new_index] = item
return new_table
# Использование
old_size = 8
new_size = 16
new_table = resize_table(table, new_size)
Полный процесс в коде Python
# Симуляция добавления элемента в dict
class SimpleDict:
def __init__(self, capacity=8):
self.capacity = capacity
self.size = 0
self.table = [None] * capacity
def __setitem__(self, key, value):
# Шаг 1: Проверяем нужно ли перехешировать
if self.size / self.capacity > 0.67:
self._resize()
# Шаг 2: Вычисляем индекс
index = hash(key) % self.capacity
# Шаг 3: Разрешаем коллизии (линейное зондирование)
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.capacity
# Шаг 4: Вставляем новый элемент
self.table[index] = (key, value)
self.size += 1
def __getitem__(self, key):
# Получение элемента (аналогичный процесс)
index = hash(key) % self.capacity
while self.table[index] is not None:
existing_key, value = self.table[index]
if existing_key == key:
return value
index = (index + 1) % self.capacity
raise KeyError(key)
def _resize(self):
# Перестраиваем таблицу
old_table = self.table
old_capacity = self.capacity
self.capacity *= 2
self.table = [None] * self.capacity
self.size = 0
# Перехешируем все элементы
for item in old_table:
if item is not None:
key, value = item
self[key] = value
# Использование
d = SimpleDict()
d['apple'] = 100
d['banana'] = 200
d['cherry'] = 300
print(d['apple']) # 100
print(d['banana']) # 200
Сложность операций
| Операция | Средний случай | Худший случай |
|---|---|---|
| Вставка | O(1) | O(n) - при плохой хеш-функции |
| Получение | O(1) | O(n) - при плохой хеш-функции |
| Удаление | O(1) | O(n) - при плохой хеш-функции |
О(1) в среднем — потому что коллизии редки при хорошей хеш-функции.
Требования к хешируемости
Объект должен быть хешируем:
- Строки, числа, кортежи → хешируемы
- Списки, словари → НЕ хешируемы (ошибка)
# ✅ Работает
d = {'key': 'value'}
d[(1, 2, 3)] = 'tuple' # Кортежи хешируемы
# ❌ Ошибка
d[[1, 2, 3]] = 'list' # TypeError: unhashable type: 'list'
# ❌ Ошибка
d[{'a': 1}] = 'dict' # TypeError: unhashable type: 'dict'
Почему требуется хешируемость?
Потому что нельзя перестраивать индекс каждый раз, когда ключ меняется. Хешируемость гарантирует, что hash(key) всегда вернет одно и то же значение.
Оптимизация: кеш хешей
Python кеширует хеши для строк и чисел:
# Первый раз
hash('hello') # Вычисляется и кешируется
# Второй раз
hash('hello') # Берется из кеша (намного быстрее)
Вывод
Алгоритм добавления в dict:
- hash() — вычислить хеш ключа
- % size — преобразовать в индекс (0..size-1)
- Проверить ячейку — может ли она принять элемент?
- Разрешить коллизию — если нужно (линейное зондирование)
- Вставить — поместить (ключ, значение) в ячейку
- Перехешировать — если словарь переполнен
Это объясняет, почему dict работает так быстро: O(1) в среднем, благодаря хеш-таблице и хорошей хеш-функции.