Язык Си: проверка правописания по словарю


В конце пятой недели курса 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:


Запись опубликована в рубрике С (Си). Добавьте в закладки постоянную ссылку.

Добавить комментарий

🇬🇧 Attention! Comments with URLs/email are not allowed.
🇷🇺 Комментарии со ссылками/email удаляются автоматически.