EasyТеория13 min

Основы хеширования

Хеш-функции, разрешение коллизий: chaining и open addressing

Что такое хеш-таблица?

Хеш-таблица — структура данных, которая связывает ключи со значениями через хеш-функцию. Обеспечивает доступ, вставку и удаление в среднем за O(1).

Ключ "apple" ---> hash("apple") = 3 ---> bucket[3] = "яблоко"
Ключ "banana" --> hash("banana") = 7 ---> bucket[7] = "банан"
Ключ "cherry" --> hash("cherry") = 3 ---> КОЛЛИЗИЯ! bucket[3] уже занят

Хеш-функция

Хеш-функция преобразует ключ произвольного размера в число фиксированного размера (индекс в массиве).

Требования к хорошей хеш-функции

  1. Детерминированность — один и тот же вход всегда даёт один и тот же выход
  2. Равномерное распределение — ключи распределяются по бакетам равномерно
  3. Скорость — вычисляется быстро
  4. Лавинный эффект — маленькое изменение ключа сильно меняет хеш

Примеры хеш-функций

<?php
declare(strict_types=1);

// Simple hash function for strings
function simpleHash(string $key, int $tableSize): int
{
    $hashValue = 0;
    for ($i = 0; $i < strlen($key); $i++) {
        $hashValue += ord($key[$i]);
    }
    return $hashValue % $tableSize;
}

// Better quality (polynomial rolling hash)
function polynomialHash(string $key, int $tableSize, int $p = 31): int
{
    $hashValue = 0;
    $pPower = 1;
    for ($i = 0; $i < strlen($key); $i++) {
        $hashValue = ($hashValue + ord($key[$i]) * $pPower) % $tableSize;
        $pPower = ($pPower * $p) % $tableSize;
    }
    return $hashValue;
}
### Что используют реальные языки
  • PHP array: DJB33A (DJBX33A) хеш-функция для строковых ключей
  • Python dict: модифицированный SipHash (криптографический, защита от collision attacks)
  • Go map: AES-based hash на поддерживаемых CPU
  • Java HashMap: hashCode() объекта + дополнительное перемешивание

Коллизии

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

Метод 1: Chaining (цепочки)

Каждый бакет — это связный список (или массив). При коллизии просто добавляем элемент в список.

Бакеты:
[0] -> None
[1] -> ("cat", 1) -> ("bat", 2) -> None
[2] -> None
[3] -> ("apple", 5) -> ("cherry", 8) -> None
[4] -> ("dog", 3) -> None
<?php
declare(strict_types=1);

final class HashTableChaining
{
    /** @var array<int, array<int, array{string, mixed}>> */
    private array $buckets;
    private int $count = 0;

    public function __construct(private int $size = 16)
    {
        $this->buckets = array_fill(0, $this->size, []);
    }

    private function hash(string $key): int
    {
        return crc32($key) % $this->size;
    }

    public function put(string $key, mixed $value): void
    {
        $idx = $this->hash($key);
        // Check if key already exists
        foreach ($this->buckets[$idx] as $i => [$k, $v]) {
            if ($k === $key) {
                $this->buckets[$idx][$i] = [$key, $value];
                return;
            }
        }
        $this->buckets[$idx][] = [$key, $value];
        $this->count++;
        // Check load factor
        if ($this->count / $this->size > 0.75) {
            $this->resize();
        }
    }

    public function get(string $key): mixed
    {
        $idx = $this->hash($key);
        foreach ($this->buckets[$idx] as [$k, $v]) {
            if ($k === $key) {
                return $v;
            }
        }
        throw new \RuntimeException("Key not found: {$key}");
    }

    public function delete(string $key): void
    {
        $idx = $this->hash($key);
        foreach ($this->buckets[$idx] as $i => [$k, $v]) {
            if ($k === $key) {
                array_splice($this->buckets[$idx], $i, 1);
                $this->count--;
                return;
            }
        }
        throw new \RuntimeException("Key not found: {$key}");
    }

    private function resize(): void
    {
        $oldBuckets = $this->buckets;
        $this->size *= 2;
        $this->buckets = array_fill(0, $this->size, []);
        $this->count = 0;
        foreach ($oldBuckets as $bucket) {
            foreach ($bucket as [$key, $value]) {
                $this->put($key, $value);
            }
        }
    }
}
### Метод 2: Open Addressing (открытая адресация)

При коллизии ищем следующее свободное место в самом массиве.

Linear Probing: проверяем следующий бакет по очереди.

hash("apple") = 3
hash("cherry") = 3  <- коллизия! Проверяем 4... занят, 5... свободен!

Бакеты:
[0] [1] [2] [3:apple] [4:banana] [5:cherry] [6] [7]
                ^                     ^
           оба имеют hash=3     нашли свободное место
<?php
declare(strict_types=1);

final class HashTableOpenAddr
{
    private const string DELETED = '__DELETED__';

    /** @var array<int, string|null> */
    private array $keys;
    /** @var array<int, mixed> */
    private array $values;
    private int $count = 0;

    public function __construct(private int $size = 16)
    {
        $this->keys = array_fill(0, $this->size, null);
        $this->values = array_fill(0, $this->size, null);
    }

    private function hash(string $key): int
    {
        return abs(crc32($key)) % $this->size;
    }

    /** Find position for key using linear probing */
    private function probe(string $key): int
    {
        $idx = $this->hash($key);
        $firstDeleted = null;

        for ($i = 0; $i < $this->size; $i++) {
            if ($this->keys[$idx] === null) {
                return $firstDeleted ?? $idx;
            }
            if ($this->keys[$idx] === self::DELETED) {
                $firstDeleted ??= $idx;
            } elseif ($this->keys[$idx] === $key) {
                return $idx;
            }
            $idx = ($idx + 1) % $this->size;
        }

        return $firstDeleted ?? throw new \OverflowException('Table is full');
    }

    public function put(string $key, mixed $value): void
    {
        if ($this->count / $this->size > 0.7) {
            $this->resize();
        }
        $idx = $this->probe($key);
        if ($this->keys[$idx] !== $key) {
            $this->count++;
        }
        $this->keys[$idx] = $key;
        $this->values[$idx] = $value;
    }

    public function get(string $key): mixed
    {
        $idx = $this->probe($key);
        if ($this->keys[$idx] === $key) {
            return $this->values[$idx];
        }
        throw new \RuntimeException("Key not found: {$key}");
    }

    private function resize(): void
    {
        $oldKeys = $this->keys;
        $oldValues = $this->values;
        $this->size *= 2;
        $this->keys = array_fill(0, $this->size, null);
        $this->values = array_fill(0, $this->size, null);
        $this->count = 0;
        foreach ($oldKeys as $i => $key) {
            if ($key !== null && $key !== self::DELETED) {
                $this->put($key, $oldValues[$i]);
            }
        }
    }
}
### Сравнение методов
Свойство Chaining Open Addressing
Реализация Проще Сложнее
Кеш-эффективность Хуже (указатели) Лучше (линейная)
Load factor Может быть > 1 Строго < 1
Удаление Просто Нужен DELETED
Кластеризация Нет Проблема
Используется в Java HashMap, PHP array Python dict

Load Factor

Load factor = количество_элементов / размер_таблицы.

Load factor = 0.5: таблица заполнена наполовину
Load factor = 0.75: типичный порог для resize
Load factor = 1.0: таблица полностью заполнена (для open addressing — плохо!)

Когда load factor превышает порог (обычно 0.75):

  1. Создаём новую таблицу в 2 раза больше
  2. Перехешируем все элементы
  3. Это стоит O(n), но происходит редко — амортизированно O(1)

Сложность операций

Операция Среднее Худшее (все коллизии)
Вставка O(1) O(n)
Поиск O(1) O(n)
Удаление O(1) O(n)

Худший случай наступает, когда все ключи попадают в один бакет (плохая хеш-функция или атака).

Хеш-таблицы в PHP

Array (ассоциативный массив)

<?php
declare(strict_types=1);

// PHP array IS a hash table (ordered hash map)
$d = [];
$d['name'] = 'Alice';         // O(1)
echo $d['name'];               // O(1)
unset($d['name']);             // O(1)
isset($d['name']);             // O(1)
array_key_exists('name', $d); // O(1)

// Iteration — O(n), insertion order is preserved!
foreach ($d as $key => $value) {
    echo "{$key}: {$value}\n";
}
> **Запомни:** Хеш-таблица — это массив + хеш-функция. Среднее O(1) для всех операций. Коллизии неизбежны, важно как их разрешать. Load factor > 0.75 = время для resize. PHP array гарантирует порядок вставки и использует chaining для разрешения коллизий.

Итоги

  1. Хеш-функция: ключ -> индекс в массиве
  2. Коллизии: chaining (списки) vs open addressing (проба)
  3. Среднее O(1), худшее O(n)
  4. Load factor ~ 0.75 — порог для resize
  5. Resize = O(n), но амортизированно O(1) на операцию

Проверь себя

5 из 7

Как устроено разрешение коллизий в `map` в Go?

В open addressing при удалении элемента используется маркер DELETED. Почему нельзя просто записать null?

Какова сложность resize (увеличения) хеш-таблицы и почему это не проблема?

Какой метод разрешения коллизий используется в PHP array?

Что такое load factor хеш-таблицы и почему 0.75 — типичный порог?