В хеш-таблицах коллизии неизбежны. Для их разрешения используются два основных подхода: методы открытой и закрытой адресации. Каждый из этих подходов имеет свои особенности, преимущества и недостатки.

Открытая адресация

Открытая адресация (Open Addressing) — метод разрешения коллизий, при котором все элементы хранятся непосредственно в хеш-таблице. Если возникает коллизия, элемент перемещается в другое место в таблице согласно определённому алгоритму.

Основные методы открытой адресации

  1. Линейное пробегание (Linear Probing)

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

    int hashFunction(int key, int size) {
        return key % size;
    }
     
    int linearProbe(int hash, int i, int size) {
        return (hash + i) % size;
    }
  2. Квадратичное пробегание (Quadratic Probing)

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

    int quadraticProbe(int hash, int i, int size) {
        return (hash + i * i) % size;
    }
  3. Двойное хеширование (Double Hashing)

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

    int doubleHash(int hash1, int hash2, int i, int size) {
        return (hash1 + i * hash2) % size;
    }

Пример реализации открытой адресации с линейным пробеганием

#include <iostream>
#include <vector>
#include <utility>
 
class HashTable {
private:
    std::vector<std::pair<int, std::string>> table;
    std::vector<bool> occupied;
    int size;
 
    int hashFunction(int key) {
        return key % size;
    }
 
public:
    HashTable(int size) : size(size) {
        table.resize(size, std::make_pair(-1, ""));
        occupied.resize(size, false);
    }
 
    void insert(int key, std::string value) {
        int hash = hashFunction(key);
        int i = 0;
        while (occupied[(hash + i) % size]) {
            i++;
        }
        table[(hash + i) % size] = std::make_pair(key, value);
        occupied[(hash + i) % size] = true;
    }
 
    std::string search(int key) {
        int hash = hashFunction(key);
        int i = 0;
        while (occupied[(hash + i) % size]) {
            if (table[(hash + i) % size].first == key) {
                return table[(hash + i) % size].second;
            }
            i++;
        }
        return "Not found";
    }
 
    void remove(int key) {
        int hash = hashFunction(key);
        int i = 0;
        while (occupied[(hash + i) % size]) {
            if (table[(hash + i) % size].first == key) {
                table[(hash + i) % size] = std::make_pair(-1, "");
                occupied[(hash + i) % size] = false;
                return;
            }
            i++;
        }
    }
};
 
int main() {
    HashTable hashTable(10);
 
    hashTable.insert(1, "Data1");
    hashTable.insert(2, "Data2");
    hashTable.insert(12, "Data12"); // Это вызовет коллизию с ключом 2
 
    std::cout << hashTable.search(1) << std::endl;
    std::cout << hashTable.search(12) << std::endl;
    std::cout << hashTable.search(2) << std::endl;
 
    hashTable.remove(12);
    std::cout << hashTable.search(12) << std::endl;
 
    return 0;
}

Закрытая адресация

Закрытая адресация (Closed Addressing), также известная как метод цепочек (Separate Chaining), предполагает, что каждый бакет хеш-таблицы содержит список (или другую структуру данных), в который помещаются все элементы, хешируемые в данный бакет.

Основные аспекты метода цепочек

  1. Хеш-функция: Преобразует ключ в индекс бакета.
  2. Бакет: Содержит список элементов с одинаковым хеш-значением.

Преимущества метода цепочек

  1. Простота в реализации.
  2. Отсутствие необходимости в рехешировании при заполнении таблицы.
  3. Гибкость в управлении размером таблицы.

Недостатки метода цепочек

  1. Дополнительное потребление памяти на хранение списков.
  2. Увеличение времени поиска при длинных цепочках.

Пример реализации метода цепочек

#include <iostream>
#include <vector>
#include <list>
#include <utility> // Для std::pair
 
class HashTable {
private:
    std::vector<std::list<std::pair<int, std::string>>> table;
    int size;
 
    int hashFunction(int key) {
        return key % size;
    }
 
public:
    HashTable(int size) : size(size) {
        table.resize(size);
    }
 
    void insert(int key, std::string value) {
        int hash = hashFunction(key);
        table[hash].emplace_back(key, value);
    }
 
    std::string search(int key) {
        int hash = hashFunction(key);
        for (auto& pair : table[hash]) {
            if (pair.first == key) {
                return pair.second;
            }
        }
        return "Not found";
    }
 
    void remove(int key) {
        int hash = hashFunction(key);
        auto& cell = table[hash];
        cell.remove_if([key](const std::pair<int, std::string>& item) { return item.first == key; });
    }
};
 
int main() {
    HashTable hashTable(10);
 
    hashTable.insert(1, "Data1");
    hashTable.insert(2, "Data2");
    hashTable.insert(12, "Data12"); // Это вызовет коллизию с ключом 2
 
    std::cout << hashTable.search(1) << std::endl;
    std::cout << hashTable.search(12) << std::endl;
    std::cout << hashTable.search(2) << std::endl;
 
    hashTable.remove(12);
    std::cout << hashTable.search(12) << std::endl;
 
    return 0;
}

Сравнение методов

Открытая адресация:

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

Закрытая адресация:

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

Оба метода имеют свои достоинства и ограничения. Выбор между открытой и закрытой адресацией зависит от конкретных требований и условий задачи.