В конце пятой недели курса CS50 Harvard мы изучили хэш-таблицы, с помощью которых можно решить задачу хэширования слов из текста, чтобы проверить правильность их написания по словарю (speller.c). Дается словарь, в котором на каждой новой строке перечислены слова. В отдельном файле дается текст, который программа должна проверить — какие слова в тексте написаны не правильно. Считается, что все слова, которых нет в словаре, — это неправильные слова.
Нужно не только создать код программы, который сверяет слова из текста со словарем, но и минимизировать скорость выполнения этой программы. Чтобы программа работала быстро, нужно придумать свою хэш-функцию и подобрать оптимальное количество «ведерок» в хэш-таблице (изначально предлагается 26 «ведерок»). Задание находится в файле словаря (dictionary.c) и состоит из четырех функций: загрузка словаря (load), хэш-функция (hash), проверка слов (check) и освобождение памяти (unload).
Хэш-функция
Чтобы лучше понять, что такое хэш-функцию и зачем она нужна, рассмотрим пример: предположим, что у вас есть объект, и вы хотите присвоить ему некий ключ, чтобы упростить поиск. Для решения такой задачи можно использовать простой массив и хранить там пары данных (объект и его ключ). Если же ключи длинные и их нельзя использовать напрямую в качестве индекса, следует использовать хеширование.
При хешировании длинныее ключи преобразуются в короткие с помощью хеш-функций. Затем их значения сохраняются в структуре данных, называемой хеш-таблицей . Почему хэширование позволяет сделать поиск быстрым? Дело в том, что при хешировании все записи пар (объект / ключ) распределяются равномерно по массиву и доступ к ним идет через рассчитываемое хэш-код (целое число). Чтобы рассчитать хэш-код для объекта используется алгоритм (хеш-функция), по ней вычисляет индекс, который показывает, где можно найти или вставить запись.
Как сделать хорошую хэш-функцию?
Эту функцию нужно сделать самостоятельно так, чтобы программа работала как можно быстрее. Есть разные подходы к тому, как писать быстрые хэш-функции, здесь нужно провести свое исследование и выбрать подходящий вариант. Можно ориентироваться на буквы в слове или на математические операции с буквами и другими имеющимися параметрами. Тестируя готовый вариант, можно подобрать оптимальную хэш-функцию и количество «ведерок» (ячеек).
Есть подход, в котором создается хэш-функция, дающая всегда уникальный результат. В нашей задаче нет такой необходимости, так как мы делаем «цепочки». Тем более, что даже в хорошей хэш-функции могут быть коллизии (пересечения). Поэтому можно создать хэш-функцию, которая будет быстро работать и минимизировать коллизии. Самый простой способ: сложить все буквы и умножить на какое-нибудь число (например, strlen) или прибавить фиксированное значение (например, длину слова), а потом взять от него модуль от количества ячеек.
Функция загрузки словаря
Оперирует функцией открытия файла FILE *x = fopen(dictionary, "r")
, через которую мы открываем файл словаря. Далее построчно читаем каждое слово из файла, используя функцию fscanf(x, "%s", word)
. Для каждого слова динамически выделяем память через malloc(sizeof(node))
, применяем хэш-функцию и копируем слово в созданный узел. Затем вставляем узел в нужное место хэш-таблицы. В этой же функции можно сделать подсчет количества загруженных слов из словаря.
Функция проверки
Основа этого блока — функция strcasecmp()
, которая сравнивает аргументы без чувствительности к регистру. Нужно возвратить true
, если слова одинаковые (то есть слово из текста есть в словаре, следовательно, оно правильное)! В этом моменте я запуталась и долго не могла понять, почему функция не правильно сравнивает слова Оказалось, что я возвращала false
там, где нужно было вернуть true
.
Функция освобождения памяти
Чтобы освободить память для каждого узла, нужно создать временную переменную, которая указывает на начало связанного списка (cursor) и еще одну переменную (tmp), которая изначально никуда не указывает. Затем даем tmp значение cursor, а сам cursor передвигаем на один шаг вперед. И освобождаем память tmp. Продолжая таким образом освобождать память, мы не потеряем ни одной связи.
Код программы (Игроглаз)
// Implements a dictionary's functionality #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <stdbool.h> #include <string.h> #include <strings.h> #include "dictionary.h" // Represents a node in a hash table typedef struct node { char word[LENGTH + 1]; struct node *next; } node; // Choose number of buckets in hash table const unsigned int N = 4001; // 26 (alphabet) * 45 (length) = 1170 // Hash table node *table[N]; // global variable for size_count() unsigned int counter = 0; // global flag for check_hash recursion search bool flag; // go through dictionary branch with hash-index and check is it exist there bool check_hash(node *tabb, const char *word) { if (tabb == NULL) // base case return false; check_hash(tabb->next, word); // recursion // compare strings and if word found - flag it up if (strcasecmp(tabb->word, word) == 0) flag = true; return flag; } // Returns true if word is in dictionary, else false bool check(const char *word) { // hash word and get the unsigned int x = hash(word); // reset flag for check_hash flag = false; if (table[x] == NULL) // paranoia: such hash doesn't exist in the dictionary return false; else // hash exist, so lets find is the word exist in the hash's branch return check_hash(table[x], word); } // Hashes word to a number unsigned int hash(const char *word) { unsigned int hash = 0; for (int h = 0; h < LENGTH; h++) // compare with number of letters to take { if (word[h] == '\0') break; if (isalpha(word[h])) hash += toupper(word[h]) - 'A'; } hash *= strlen(word); hash %= N; // paranoia if (hash < 0) hash = 0; else if (hash > N) hash = N; return hash; } // Loads dictionary into memory, returning true if successful, else false bool load(const char *dictionary) { char word[LENGTH+1]; unsigned int i = 0; // will contain hash number FILE *file = fopen(dictionary, "rb"); if (file == NULL) return false; while (fscanf(file, "%s", word) != EOF) // return EOF if it reach end of file.. while? //filepointer from fopen format character array where we put the word { node *n = (node *)malloc(sizeof(node)); if (n == NULL) return false; strcpy(n->word, word); // copy from buffer to node n->next = NULL; // point to NULL i = hash(word); // hash word // hash is array of linked list... table[26] // 1) add new node to a linked list.. // start new branch of linked list if (table[i] == NULL) { table[i] = n; counter++; } // join to existing branch else { // 1st connection to existing node if (table[i]->next == NULL) { table[i]->next = n; counter++; } // node already got children else { n->next = table[i]->next; table[i]->next = n; counter++; } } } fclose(file); return true; } // Returns number of words in dictionary if loaded, else 0 if not yet loaded unsigned int size(void) { return counter; } // Unloads dictionary from memory, returning true if successful, else false bool unload(void) { node *cursor = NULL; // points to node which we look into node *tmp = NULL; // so we won't loose child address for (int i = 0; i < N; i++) // 76 total, starting from 0.. so from 0 to 75 { if (table[i] != NULL) { cursor = table[i]; // init tmp = table[i]; while (cursor->next != NULL) { cursor = tmp->next; free(tmp); tmp = cursor; } free(cursor); } } return true; }
Вывод командой ./speller texts/lalaland.txt
:
... < list of misspelled words >.. WORDS MISSPELLED: 955 WORDS IN DICTIONARY: 143091 WORDS IN TEXT: 17756 TIME IN load: 0.03 TIME IN check: 0.18 TIME IN size: 0.00 TIME IN unload: 0.01 TIME IN TOTAL: 0.21
Код программы (Штукенция)
// Implements a dictionary's functionality #include <ctype.h> #include <stdbool.h> #include <string.h> #include <strings.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include "dictionary.h" int size_words = 0; // Keep track of the number of words added // Represents a node in a hash table typedef struct node { char word[LENGTH + 1]; struct node *next; } node; // TODO: Choose number of buckets in hash table const unsigned int N = 10000; // Hash table node *table[N]; // Returns true if word is in dictionary, else false bool check(const char *word) { // TODO // Convert word to lowercase // Hash the word int hash_index = hash(word); int result; // Create variable that points to the start of linked list node *cursor = table[hash_index]; while (cursor != NULL) { result = strcasecmp(word, cursor->word); // compare without case sensitivity if (result == 0) { return true; } cursor = cursor->next; // check next word in linked list } return false; } // Hashes word to a number unsigned int hash(const char *word) { // TODO: Improve this hash function // The output should be between 0 and N-1 unsigned int result = 0; unsigned int word_lenth = strlen(word); for (int i = 0; i <= word_lenth; i++) { result += tolower(word[i]) + word_lenth / 2; } return result % N; } // Loads dictionary into memory, returning true if successful, else false bool load(const char *dictionary) { // TODO // Open the dictionary file FILE *dict = fopen(dictionary, "r"); if (dict == NULL) { printf("Could not open dictionary.\n"); return false; } // Read strings from file one at a time char word[LENGTH + 1]; while (fscanf(dict, "%s", word) != EOF) { // Create a new node for each word node *n = malloc(sizeof(node)); if (n == NULL) { printf("Not enough memory.\n"); fclose(dict); return false; } // Hash word and get hash-value int hash_index = hash(word); strcpy(n->word, word); // Copy word // Insert node into hash table at that location node *head = table[hash_index]; // Create variable that points to the start of linked list if (head != NULL) { n->next = table[hash_index]; } else { n->next = NULL; } table[hash_index] = n; size_words ++; } fclose(dict); return true; } // Returns number of words in dictionary if loaded, else 0 if not yet loaded unsigned int size(void) { // TODO return size_words; } // Unloads dictionary from memory, returning true if successful, else false bool unload(void) { // TODO for (int i = 0; i < N ; i++) { node *cursor = table[i]; // Create variable that points to the start of linked list node *tmp = NULL; while (cursor != NULL) { tmp = cursor; cursor = cursor->next; free(tmp); } } return true; }
Результат работы программы на примере ./speller texts/lalaland.txt
:
Для сравнения можно использовать решение команды CS50: