Как реализовать образец хеш-таблицы на C/C++
Введение
Хэш-таблица в C/C++ — это структура данных, которая сопоставляет ключи со значениями. Хеш-таблица использует хэш-функцию для вычисления индексов для ключа. Вы можете сохранить значение в соответствующем месте на основе индекса хеш-таблицы.
Преимущество использования хэш-таблицы заключается в очень быстром времени доступа. Как правило, временная сложность (амортизированная временная сложность) представляет собой постоянное O(1)
время доступа.
Если два разных ключа получают один и тот же индекс, вам нужно будет использовать другие структуры данных (сегменты) для учета этих коллизий. Если вы выберете очень хорошую хэш-функцию, вероятность коллизии может быть незначительной.
C++ STL (Стандартная библиотека шаблонов) имеет структуру данных std::unordered_map()
.
В этой статье вы создадите хеш-таблицу с нуля, состоящую из:
- Хеш-функция для сопоставления ключей со значениями.
- Структура данных хэш-таблицы, поддерживающая операции
insert
,search
иdelete
. - Структура данных для учета конфликта ключей.
Выбор хеш-функции
Первый шаг — выбрать достаточно хорошую хеш-функцию с низкой вероятностью коллизий. Однако для целей этого руководства будет применена плохая хэш-функция, чтобы лучше проиллюстрировать коллизии хэшей. В этом ограниченном примере также будут использоваться только строки (или массивы символов в C).
#define CAPACITY 50000 // Size of the HashTable.
unsigned long hash_function(char* str)
{
unsigned long i = 0;
for (int j = 0; str[j]; j++)
i += str[j];
return i % CAPACITY;
}
Запустите этот код и протестируйте разные строки на возможные коллизии. Например, строки Hel
и Cau
будут конфликтовать, поскольку они имеют одинаковое значение ASCII.
Этот код должен возвращать число в пределах емкости хеш-таблицы. В противном случае он может получить доступ к несвязанной ячейке памяти, что приведет к ошибке.
Определение структур данных хеш-таблицы
Хэш-таблица — это массив элементов, представляющих собой пары { ключ: значение
.
Во-первых, определите структуру элемента:
// Defines the HashTable item.
typedef struct Ht_item
{
char* key;
char* value;
} Ht_item;
Теперь в хеш-таблице есть массив указателей, указывающих на Ht_item
, так что это двойной указатель.
// Defines the HashTable.
typedef struct HashTable
{
// Contains an array of pointers to items.
Ht_item** items;
int size;
int count;
} HashTable;
Ваша хеш-таблица должна будет возвращать количество элементов в хеш-таблице с помощью count
и размер хэш-таблицы с помощью size
.
Создание хеш-таблицы и элементов хеш-таблицы
Далее создайте функции для выделения памяти и создания элементов.
Создайте элементы, выделив память для ключа и значения, и верните указатель на элемент:
Ht_item* create_item(char* key, char* value)
{
// Creates a pointer to a new HashTable item.
Ht_item* item = (Ht_item*) malloc(sizeof(Ht_item));
item->key = (char*) malloc(strlen(key) + 1);
item->value = (char*) malloc(strlen(value) + 1);
strcpy(item->key, key);
strcpy(item->value, value);
return item;
}
Создайте таблицу, выделив память и установив size
, count
и items
:
HashTable* create_table(int size)
{
// Creates a new HashTable.
HashTable* table = (HashTable*) malloc(sizeof(HashTable));
table->size = size;
table->count = 0;
table->items = (Ht_item**) calloc(table->size, sizeof(Ht_item*));
for (int i = 0; i < table->size; i++)
table->items[i] = NULL;
return table;
}
В предыдущем примере выделяется память для структуры-оболочки HashTable
и присваивается всем элементам значение NULL
.
Освобождение памяти — лучшая практика C/C++. Освободите память, выделенную в куче, с помощью malloc()
и calloc()
.
Напишите функции, которые освобождают элемент таблицы и всю таблицу.
void free_item(Ht_item* item)
{
// Frees an item.
free(item->key);
free(item->value);
free(item);
}
void free_table(HashTable* table)
{
// Frees the table.
for (int i = 0; i < table->size; i++)
{
Ht_item* item = table->items[i];
if (item != NULL)
free_item(item);
}
free(table->items);
free(table);
}
Добавьте print_table()
для отображения index
, key
и value
для каждого элемента:
void print_table(HashTable* table)
{
printf("\nHash Table\n-------------------\n");
for (int i = 0; i < table->size; i++)
{
if (table->items[i])
{
printf("Index:%d, Key:%s, Value:%s\n", i, table->items[i] -> key, table->items[i]->value);
}
}
printf("-------------------\n\n");
}
На этом основная функциональность вашей пользовательской хеш-таблицы заканчивается. Теперь вы будете писать функции вставки, поиска и удаления.
Вставка в хэш-таблицу
Создайте функцию ht_insert()
, которая выполняет вставки.
Функция принимает указатель HashTable
, key
и value
в качестве параметров:
void ht_insert(HashTable* table, char* key, char* value)
{
...
}
Теперь есть определенные шаги, связанные с функцией ht_insert()
.
- Создайте элемент на основе пары
{ key: value
. - Вычислить индекс на основе хеш-функции.
- Проверьте, занят ли уже индекс, сравнив
key
.- Если он не занят, вы можете напрямую вставить его в
index
. - В противном случае это коллизия, и вам нужно будет с ней справиться.
В этом руководстве рассматривается обработка коллизий после создания исходной модели.
Сначала создайте элемент:
create_item(key, value)
Затем вычислите индекс:
int index = hash_function(key);
При вставке ключа в первый раз элемент должен быть
NULL
:// Creates the item. Ht_item* item = create_item(key, value); // Computes the index. int index = hash_function(key); Ht_item* current_item = table->items[index]; if (current_item == NULL) { // Key does not exist. if (table->count == table->size) { // HashTable is full. printf("Insert Error: Hash Table is full\n"); free_item(item); return; } // Insert directly. table->items[index] = item; table->count++; }
Рассмотрим сценарий, в котором пара
{ key: value
уже существует, поскольку тот же самый элемент был вставлен в хеш-таблицу. Чтобы решить эту проблему, код должен обновить значение элемента до нового:if (current_item == NULL) { ... } else { // Scenario 1: Update the value. if (strcmp(current_item->key, key) == 0) { strcpy(table->items[index] -> value, value); return; } }
Рассмотрим сценарий, в котором необходимо обработать коллизию. Для решения этой проблемы был добавлен заполнитель:
void handle_collision(HashTable* table, Ht_item* item) { } void ht_insert(HashTable* table, char* key, char* value) { ... if (current_item == NULL) { ... } else { // Scenario 1: Update the value. if (strcmp(current_item->key, key) == 0) { ... } else { // Scenario 2: Handle the collision. handle_collision(table, item); return; } } }
Теперь ваша функция
ht_insert()
завершена.Поиск элементов в хеш-таблице
Создайте функцию
ht_search()
, которая проверяет, существует ли ключ, и возвращает соответствующее значение, если оно существует.Функция принимает указатель
HashTable
иkey
в качестве параметров:char* ht_search(HashTable* table, char* key) { ... }
Найдите элемент с
ключом
вHashTable
. Если элемент не может быть найден вHashTable
, возвращаетсяNULL
.char* ht_search(HashTable* table, char* key) { // Searches for the key in the HashTable. // Returns NULL if it doesn't exist. int index = hash_function(key); Ht_item* item = table->items[index]; // Provide only non-NULL values. if (item != NULL) { if (strcmp(item->key, key) == 0) return item->value; } return NULL; }
Добавьте
print_search()
для отображения элемента, соответствующегоключу
:void print_search(HashTable* table, char* key) { char* val; if ((val = ht_search(table, key)) == NULL) { printf("Key:%s does not exist\n", key); return; } else { printf("Key:%s, Value:%s\n", key, val); } }
Теперь ваша функция
ht_search()
завершена.Обработка столкновений
Существуют различные способы разрешения коллизии. В этом руководстве будет использоваться метод под названием «Разделение цепочки», целью которого является создание независимых цепочек для всех элементов с одинаковым хэш-индексом. Реализация в этом руководстве создаст эти цепочки с использованием связанных списков.
Всякий раз, когда происходит конфликт, дополнительные элементы, которые сталкиваются с одним и тем же индексом, добавляются в список переполнения. Таким образом, вам не придется удалять какие-либо существующие записи в хеш-таблице.
Из-за того, что связанные списки имеют
O(n)
временную сложность для вставки, поиска и удаления, в случае коллизии у вас будет время доступа в худшем случаеO(n)
тоже. Преимущество этого метода в том, что это хороший выбор, если ваша хеш-таблица имеет небольшую емкость.Реализуйте список переполнения:
// Defines the LinkedList. typedef struct LinkedList { Ht_item* item; struct LinkedList* next; } LinkedList;; LinkedList* allocate_list() { // Allocates memory for a LinkedList pointer. LinkedList* list = (LinkedList*) malloc(sizeof(LinkedList)); return list; } LinkedList* linkedlist_insert(LinkedList* list, Ht_item* item) { // Inserts the item onto the LinkedList. if (!list) { LinkedList* head = allocate_list(); head->item = item; head->next = NULL; list = head; return list; } else if (list->next == NULL) { LinkedList* node = allocate_list(); node->item = item; node->next = NULL; list->next = node; return list; } LinkedList* temp = list; while (temp->next->next) { temp = temp->next; } LinkedList* node = allocate_list(); node->item = item; node->next = NULL; temp->next = node; return list; } Ht_item* linkedlist_remove(LinkedList* list) { // Removes the head from the LinkedList. // Returns the item of the popped element. if (!list) return NULL; if (!list->next) return NULL; LinkedList* node = list->next; LinkedList* temp = list; temp->next = NULL; list = node; Ht_item* it = NULL; memcpy(temp->item, it, sizeof(Ht_item)); free(temp->item->key); free(temp->item->value); free(temp->item); free(temp); return it; } void free_linkedlist(LinkedList* list) { LinkedList* temp = list; while (list) { temp = list; list = list->next; free(temp->item->key); free(temp->item->value); free(temp->item); free(temp); } }
Теперь добавьте эти дополнительные списки сегментов в свою
HashTable
. У каждого элемента будет цепочка, поэтому вся таблица представляет собой массив указателейLinkedList
.typedef struct HashTable HashTable; // Defines the HashTable. struct HashTable { // Contains an array of pointers to items. Ht_item** items; LinkedList** overflow_buckets; int size; int count; };
Теперь, когда определены
overflow_buckets
, добавьте функции для их создания и удаления. Вам также потребуется учитывать их в функцияхcreate_table()
иfree_table()
.LinkedList** create_overflow_buckets(HashTable* table) { // Create the overflow buckets; an array of LinkedLists. LinkedList** buckets = (LinkedList**) calloc(table->size, sizeof(LinkedList*)); for (int i = 0; i < table->size; i++) buckets[i] = NULL; return buckets; } void free_overflow_buckets(HashTable* table) { // Free all the overflow bucket lists. LinkedList** buckets = table->overflow_buckets; for (int i = 0; i < table->size; i++) free_linkedlist(buckets[i]); free(buckets); } HashTable* create_table(int size) { // Creates a new HashTable. HashTable* table = (HashTable*) malloc(sizeof(HashTable)); table->size = size; table->count = 0; table->items = (Ht_item**) calloc(table->size, sizeof(Ht_item*)); for (int i = 0; i < table->size; i++) table->items[i] = NULL; table->overflow_buckets = create_overflow_buckets(table); return table; } void free_table(HashTable* table) { // Frees the table. for (int i = 0; i < table->size; i++) { Ht_item* item = table->items[i]; if (item != NULL) free_item(item); } // Free the overflow bucket lists and its items. free_overflow_buckets(table); free(table->items); free(table); }
Если список переполнения для элемента не существует, создайте список и добавьте в него элемент.
Обновите
handle_collision()
для вставок:void handle_collision(HashTable* table, unsigned long index, Ht_item* item) { LinkedList* head = table->overflow_buckets[index]; if (head == NULL) { // Creates the list. head = allocate_list(); head->item = item; table->overflow_buckets[index] = head; return; } else { // Insert to the list. table->overflow_buckets[index] = linkedlist_insert(head, item); return; } }
И звонок:
void ht_insert(HashTable* table, char* key, char* value) { ... if (current_item == NULL) { ... } else { // Scenario 1: Update the value. if (strcmp(current_item->key, key) == 0) { ... } else { // Scenario 2: Handle the collision. handle_collision(table, index, item); return; } } }
Теперь обновите метод поиска, чтобы использовать сегменты переполнения:
char* ht_search(HashTable* table, char* key) { // Searches for the key in the HashTable. // Returns NULL if it doesn't exist. int index = hash_function(key); Ht_item* item = table->items[index]; LinkedList* head = table->overflow_buckets[index]; // Provide only non-NULL values. if (item != NULL) { if (strcmp(item->key, key) == 0) return item->value; if (head == NULL) return NULL; item = head->item; head = head->next; } return NULL; }
Наконец, коллизии теперь обрабатываются в
insert()
иsearch()
!Удаление из хеш-таблицы
Давайте теперь, наконец, посмотрим на функцию удаления:
void ht_delete(HashTable* table, char* key) { ... }
Опять же, метод аналогичен вставке.
- Вычислить хэш-индекс и получить элемент.
- Если это
NULL
, ничего не делайте. - В противном случае после сравнения ключей, если для этого индекса нет цепочки коллизий, удалите элемент из таблицы.
- Если существует цепочка коллизий, удалите этот элемент и соответствующим образом переместите ссылки.
void ht_delete(HashTable* table, char* key) { // Deletes an item from the table. int index = hash_function(key); Ht_item* item = table->items[index]; LinkedList* head = table->overflow_buckets[index]; if (item == NULL) { // Does not exist. return; } else { if (head == NULL && strcmp(item->key, key) == 0) { // Collision chain does not exist. // Remove the item. // Set table index to NULL. table->items[index] = NULL; free_item(item); table->count--; return; } else if (head != NULL) { // Collision chain exists. if (strcmp(item->key, key) == 0) { // Remove this item. // Set the head of the list as the new item. free_item(item); LinkedList* node = head; head = head->next; node->next = NULL; table->items[index] = create_item(node->item->key, node->item->value); free_linkedlist(node); table->overflow_buckets[index] = head; return; } LinkedList* curr = head; LinkedList* prev = NULL; while (curr) { if (strcmp(curr->item->key, key) == 0) { if (prev == NULL) { // First element of the chain. // Remove the chain. free_linkedlist(head); table->overflow_buckets[index] = NULL; return; } else { // This is somewhere in the chain. prev->next = curr->next; curr->next = NULL; free_linkedlist(curr); table->overflow_buckets[index] = head; return; } } curr = curr->next; prev = curr; } } } }
#include <stdio.h> #include <stdlib.h> #include <string.h> #define CAPACITY 50000 // Size of the HashTable. unsigned long hash_function(char *str) { unsigned long i = 0; for (int j = 0; str[j]; j++) i += str[j]; return i % CAPACITY; } // Defines the HashTable item. typedef struct Ht_item { char *key; char *value; } Ht_item; // Defines the LinkedList. typedef struct LinkedList { Ht_item *item; LinkedList *next; } LinkedList; // Defines the HashTable. typedef struct HashTable { // Contains an array of pointers to items. Ht_item **items; LinkedList **overflow_buckets; int size; int count; } HashTable; LinkedList *allocate_list() { // Allocates memory for a LinkedList pointer. LinkedList *list = (LinkedList *)malloc(sizeof(LinkedList)); return list; } LinkedList *linkedlist_insert(LinkedList *list, Ht_item *item) { // Inserts the item onto the LinkedList. if (!list) { LinkedList *head = allocate_list(); head->item = item; head->next = NULL; list = head; return list; } else if (list->next == NULL) { LinkedList *node = allocate_list(); node->item = item; node->next = NULL; list->next = node; return list; } LinkedList *temp = list; while (temp->next->next) { temp = temp->next; } LinkedList *node = allocate_list(); node->item = item; node->next = NULL; temp->next = node; return list; } Ht_item *linkedlist_remove(LinkedList *list) { // Removes the head from the LinkedList. // Returns the item of the popped element. if (!list) return NULL; if (!list->next) return NULL; LinkedList *node = list->next; LinkedList *temp = list; temp->next = NULL; list = node; Ht_item *it = NULL; memcpy(temp->item, it, sizeof(Ht_item)); free(temp->item->key); free(temp->item->value); free(temp->item); free(temp); return it; } void free_linkedlist(LinkedList *list) { LinkedList *temp = list; while (list) { temp = list; list = list->next; free(temp->item->key); free(temp->item->value); free(temp->item); free(temp); } } LinkedList **create_overflow_buckets(HashTable *table) { // Create the overflow buckets; an array of LinkedLists. LinkedList **buckets = (LinkedList **)calloc(table->size, sizeof(LinkedList *)); for (int i = 0; i < table->size; i++) buckets[i] = NULL; return buckets; } void free_overflow_buckets(HashTable *table) { // Free all the overflow bucket lists. LinkedList **buckets = table->overflow_buckets; for (int i = 0; i < table->size; i++) free_linkedlist(buckets[i]); free(buckets); } Ht_item *create_item(char *key, char *value) { // Creates a pointer to a new HashTable item. Ht_item *item = (Ht_item *)malloc(sizeof(Ht_item)); item->key = (char *)malloc(strlen(key) + 1); item->value = (char *)malloc(strlen(value) + 1); strcpy(item->key, key); strcpy(item->value, value); return item; } HashTable *create_table(int size) { // Creates a new HashTable. HashTable *table = (HashTable *)malloc(sizeof(HashTable)); table->size = size; table->count = 0; table->items = (Ht_item **)calloc(table->size, sizeof(Ht_item *)); for (int i = 0; i < table->size; i++) table->items[i] = NULL; table->overflow_buckets = create_overflow_buckets(table); return table; } void free_item(Ht_item *item) { // Frees an item. free(item->key); free(item->value); free(item); } void free_table(HashTable *table) { // Frees the table. for (int i = 0; i < table->size; i++) { Ht_item *item = table->items[i]; if (item != NULL) free_item(item); } // Free the overflow bucket lists and its items. free_overflow_buckets(table); free(table->items); free(table); } void handle_collision(HashTable *table, unsigned long index, Ht_item *item) { LinkedList *head = table->overflow_buckets[index]; if (head == NULL) { // Creates the list. head = allocate_list(); head->item = item; table->overflow_buckets[index] = head; return; } else { // Insert to the list. table->overflow_buckets[index] = linkedlist_insert(head, item); return; } } void ht_insert(HashTable *table, char *key, char *value) { // Creates the item. Ht_item *item = create_item(key, value); // Computes the index. int index = hash_function(key); Ht_item *current_item = table->items[index]; if (current_item == NULL) { // Key does not exist. if (table->count == table->size) { // HashTable is full. printf("Insert Error: Hash Table is full\n"); free_item(item); return; } // Insert directly. table->items[index] = item; table->count++; } else { // Scenario 1: Update the value. if (strcmp(current_item->key, key) == 0) { strcpy(table->items[index]->value, value); return; } else { // Scenario 2: Handle the collision. handle_collision(table, index, item); return; } } } char *ht_search(HashTable *table, char *key) { // Searches for the key in the HashTable. // Returns NULL if it doesn't exist. int index = hash_function(key); Ht_item *item = table->items[index]; LinkedList *head = table->overflow_buckets[index]; // Provide only non-NULL values. if (item != NULL) { if (strcmp(item->key, key) == 0) return item->value; if (head == NULL) return NULL; item = head->item; head = head->next; } return NULL; } void ht_delete(HashTable *table, char *key) { // Deletes an item from the table. int index = hash_function(key); Ht_item *item = table->items[index]; LinkedList *head = table->overflow_buckets[index]; if (item == NULL) { // Does not exist. return; } else { if (head == NULL && strcmp(item->key, key) == 0) { // Collision chain does not exist. // Remove the item. // Set table index to NULL. table->items[index] = NULL; free_item(item); table->count--; return; } else if (head != NULL) { // Collision chain exists. if (strcmp(item->key, key) == 0) { // Remove this item. // Set the head of the list as the new item. free_item(item); LinkedList *node = head; head = head->next; node->next = NULL; table->items[index] = create_item(node->item->key, node->item->value); free_linkedlist(node); table->overflow_buckets[index] = head; return; } LinkedList *curr = head; LinkedList *prev = NULL; while (curr) { if (strcmp(curr->item->key, key) == 0) { if (prev == NULL) { // First element of the chain. // Remove the chain. free_linkedlist(head); table->overflow_buckets[index] = NULL; return; } else { // This is somewhere in the chain. prev->next = curr->next; curr->next = NULL; free_linkedlist(curr); table->overflow_buckets[index] = head; return; } } curr = curr->next; prev = curr; } } } } void print_search(HashTable *table, char *key) { char *val; if ((val = ht_search(table, key)) == NULL) { printf("Key:%s does not exist\n", key); return; } else { printf("Key:%s, Value:%s\n", key, val); } } void print_table(HashTable *table) { printf("\nHash Table\n-------------------\n"); for (int i = 0; i < table -> size; i++) { if (table -> items[i]) { printf("Index:%d, Key:%s, Value:%s\n", i, table -> items[i] -> key, table -> items[i] -> value); } } printf("-------------------\n\n"); } int main() { HashTable *ht = create_table(CAPACITY); ht_insert(ht, (char *)"1", (char *)"First address"); ht_insert(ht, (char *)"2", (char *)"Second address"); ht_insert(ht, (char *)"Hel", (char *)"Third address"); ht_insert(ht, (char *)"Cau", (char *)"Fourth address"); print_search(ht, (char *)"1"); print_search(ht, (char *)"2"); print_search(ht, (char *)"3"); print_search(ht, (char *)"Hel"); print_search(ht, (char *)"Cau"); // Collision! print_table(ht); ht_delete(ht, (char *)"1"); ht_delete(ht, (char *)"Cau"); print_table(ht); free_table(ht); return 0; }
Этот код выдаст следующий результат:
OutputKey:1, Value:First address Key:2, Value:Second address Key:3 does not exist Key:Hel, Value:Third address Key:Cau does not exist Hash Table ------------------- Index:49, Key:1, Value:First address Index:50, Key:2, Value:Second address Index:281, Key:Hel, Value:Third address ------------------- Hash Table ------------------- Index:50, Key:2, Value:Second address Index:281, Key:Hel, Value:Third address -------------------ht_insert()
,ht_search()
иht_delete
ведут себя так, как ожидалось.Заключение
В этой статье вы реализовали хеш-таблицу с нуля на C/C++.
Поэкспериментируйте с другими алгоритмами обработки столкновений и другими хеш-функциями. Продолжите свое обучение с помощью дополнительных руководств по C++
- Если он не занят, вы можете напрямую вставить его в