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

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

Сегодня я продемонстрирую, как создать эту структуру данных на C ++. Это мое первое приложение, написанное на этом языке, поэтому вся критика и отзывы, которые помогут мне стать лучше, приветствуются. :)

Начиная с малого

Первое, что нам нужно сделать, это создать класс Node для хранения нашей пары ключ-значение.

class Node
{
    private:
        string key;
        string value;
        Node *next;
    public:
        Node();
        Node(string, string);
        string get_key();
        string get_value();
        Node *get_next();
        void set_key(string);
        void set_value(string);
        void set_next(Node *);
};

Вы заметите, что у этого класса есть два перегруженных конструктора и сеттеры / геттеры для каждого значения. У каждого узла будут строки в качестве ключей и значений, а также указатель на следующий узел на случай конфликта.

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

#хеш-таблица

Далее мы создадим наш класс Hash_Table.

class Hash_Table
{
    private:
        unsigned int size;
        Node **array;
    public:
        Hash_Table(unsigned int);
        unsigned int get_size();
        unsigned long int hash_djb2(string);
        void addNode(string, string);
        string get_value(string);        
        void printHash();
        void delete_table();
};

Наш класс Hash Table будет иметь размер, определяемый пользователем, и массив узлов в качестве частных атрибутов. Что касается наших методов, у нас есть функции, которые будут индексировать нашу строку, добавлять новые узлы, извлекать значение с заданным ключом, распечатывать все содержимое хеш-таблицы и удалять хеш-таблицу. Мы подробно рассмотрим все эти методы.

Тестирование кода

В нашем основном файле давайте начнем с динамического выделения памяти для хеш-таблицы.

<main.cpp>
int main()
{
    unsigned int s = 1024;
    Hash_Table *ht = new Hash_Table(s);
}

Это вызывает наш конструктор, который выполняет следующие действия:

Hash_Table::Hash_Table(unsigned int size)
{
    unsigned int i;
    if (!size)
        throw "Value must be over 0";
    this->size = size;
    this->array = new Node * [size];
    for (i = 0; i < size; i++)
        this->array[i] = nullptr;
}

В этом методе мы проверяем, что пользователь не ввел 0 в качестве размера, а затем мы создаем массив с пустыми узлами, которые все указывают на null. Давайте проверим, правильно ли мы это сделали.

cout << "Address of hash table is " << &ht << endl;
/* Address of hash table is 0x7ffee06eca20 */

Алгоритм djb2

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

unsigned long int Hash_Table::hash_djb2(string str)
{
    unsigned long int hash;
    int c;
    
    hash = 5381;
    for (auto x: str)
    {
        c = x;
        hash = ((hash << 5) + hash) + c; 
        /* hash * 33 + c */
    }
    return (hash % this->size);
}

Давайте посмотрим на это в действии.

<main.cpp>
string str = "c++isfun";
cout << "Hash index of " << str << " is " << ht->hash_djb2(str) << endl;
/* Hash index of c++isfun is 611 */
str = "oopiscool";
cout << "Hash index of " << str << " is " << ht->hash_djb2(str) << endl;
/* Hash index of oopiscool is 124 */

Добавление узлов

Следующий метод, который мы рассмотрим, - это добавление узлов.

void Hash_Table::addNode(string key, string value)
{
    Node *curr;
    unsigned long int idx;
    if (key.empty())
        throw "Invalid Key";
    
    idx = this->hash_djb2(key);
    curr = this->array[idx];
    while (curr)
    {
        if (curr->get_key() == key)
        {
            cout << "Collision detected!" << endl;
            curr->set_value(value);
            return;
        }
        curr = curr->get_next();
    }
    Node *n = new Node(key, value);
    n->set_next(this->array[idx]);
    this->array[idx] = n;
}

Цикл while в середине проверяет каждый индекс, если уже существует ключ с тем же именем, что и входная строка. Если есть, он заменит значение в этом узле новым строковым значением. В противном случае узел с новым ключом и значением будет вставлен в начало каждого связанного списка по хешированному индексу. Чтобы это проверить, нам понадобится метод печати.

cout - твой друг

void Hash_Table::printHash()
{
    unsigned long int i;
    Node *curr;
    bool first = false;
    cout << "{";
    for (i = 0; i < this->size; i++)
    {
        if ((curr = this->array[i]))
        {
             while (curr)
             {
                 if (first)
                     cout << ", ";
                     cout << ""' << curr->get_key() << "': '" <<    curr->get_value() << "'";
                 first = true;
                 curr = curr->get_next();
             }
        }
    }
    cout << "}" << endl;
}

Собираем все вместе

Чтобы проверить, что наши методы работают правильно, нам нужно добавить пару узлов, некоторые из которых, возможно, придется перезаписать, а некоторые будут конфликтовать.

<main.cpp>
ht->addNode("betty", "holberton");
ht->addNode("stylist", "kevin");
ht->addNode("stylist", "shawna");
ht->addNode("subgenera", "chicken");
/* stylist and subgenera will collide at index 225 */
ht->printHash();
/*
 
{'betty': 'holberton', 'subgenera': 'chicken', 'stylist': 'shawna'}
*/

Как видите, «стилист» имеет правильное значение «шона», потому что оно было добавлено позже. Кроме того, наш список печати может печатать как «subgenerea», так и «stylist», даже если они имеют один и тот же индекс, благодаря его способности перемещаться по связному списку.

Покажи мне ценность

Что, если нам просто нужно значение данного ключа, без необходимости распечатывать всю хеш-таблицу? Точно так же, когда мы получаем доступ к значению словаря в Python или объекту в Javascript, когда мы вводим:

print(hash_table["betty"])

, мы можем написать метод, который будет получать это значение за нас.

string Hash_Table::get_value(string key)
{
    unsigned long int idx;
    Node *curr;
    if (key.empty())
        throw "Invalid key";
    idx = this->hash_djb2(key);
    curr = this->array[idx];
    
    while (curr)
    {
        if (curr->get_key() == key)
            return curr->get_value();
        curr = curr->get_next();
    }
    return nullptr;
}

Теперь, чтобы проверить это:

<main.cpp>
string key = "stylist";
cout << key << "'s value is " << ht->get_value(key) << endl;
/* stylist's value is shawna */
key = "betty";
cout << key << "'s value is " << ht->get_value(key) << endl;
/* betty's value is holberton */

Все бесплатно!

И последнее, но не менее важное: нам нужна функция, которая удалит все выделения памяти в куче.

void Hash_Table::delete_table()
{
    Node *tmp, *curr;
    unsigned long int i;
    
    for (i = 0; i < this->size; i++)
    {
        if ((curr = this->array[i]))
        {
            while (curr)
            {
                tmp = curr;
                curr = curr->get_next();
                delete tmp;
            }
         }
     }
     delete[] this->array;
}

Также нам нужно освободить экземпляр класса в нашем main:

<main.cpp>
ht->delete_table();
delete ht;

Это все, ребята. Удачного хеширования и спасибо за чтение!