Зачем Hash Map нужны tombstones, если можно просто очистить

Представим хеш-таблицу с открытой адресацией и линейным пробированием. Из-за коллизий несколько ключей могут оказаться в соседних ячейках.

Например: Индекс: 0 1 2 3 4 . A B C .

Допустим, мы хотим удалить B. Самое очевидное решение: просто очистить ячейку.

Индекс: 0 1 2 3 4 . A . C .

Но здесь появляется проблема.

Что произойдёт при поиске C?

Алгоритм вычисляет хеш ключа и начинает проверять ячейки по порядку. Доходит до ячейки с A, затем до пустой ячейки. Пустая ячейка обычно означает, что элемента в таблице нет и дальше искать бессмысленно.

Поэтому поиск остановится.

Хотя C на самом деле находится в следующей ячейке! Для этого и нужен tombstone

Вместо полного удаления элемента мы оставляем специальную отметку:

Индекс: 0 1 2 3 4 . A † C .

Теперь ячейка не считается пустой.

Она означает: здесь раньше находился элемент; поиск должен продолжаться;

при вставке сюда потенциально можно записать новый элемент. Поиск C проходит через tombstone и находит нужный ключ. Но tombstones тоже создают проблему Представим, что мы постоянно удаляем и добавляем элементы.

Со временем таблица может заполниться такими отметками:

Индекс: 0 1 2 3 4 5 6 † A † † C † . Даже если реальных элементов немного, поиск может проходить через множество tombstones.

Это увеличивает количество проверок и ухудшает производительность. Поэтому реализации хеш-таблиц могут периодически перестраивать таблицу (rehash), избавляясь от накопившихся отметок.

Главная идея: Пустая ячейка и удалённая ячейка - не одно и то же.

Empty: поиск можно остановить.

Tombstone: поиск нужно продолжить.

Именно поэтому в хеш-таблицах с открытой адресацией нельзя просто очищать ячейку после удаления элемента.

Кстати, это не означает, что Go map устроена именно так. Здесь мы рассматриваем классическую реализацию с open addressing и linear probing. Внутреннее устройство map в Go отличается.