tl; dr: решение проблемы кодирования, которая берет слова в словарь и проверяет, правильно ли они написаны, в кратчайшие сроки

Неделя 5 была посвящена структурам данных, разыменованию и общему низкоуровневому доступу к памяти компьютера. Идея хеш-таблиц и связанных списков стала для меня уникальным просветительским моментом, учитывая количество реальных примеров, в которых используется эта концепция. Нельзя сказать, что концепции, которые я изучил до сих пор, были абстрактными, но я думаю, что со стратегической точки зрения это было хорошей нотой, чтобы закончить C.

Итак, на первый взгляд правописание кажется довольно забавным. Суть этой проблемы состоит в том, чтобы реализовать максимально быструю проверку орфографии. Самое быстрое здесь значение в секундах, как в реальном применении алгоритмов O (n).

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

часть 0: определения

// Implements a dictionary's functionality
#include <stdbool.h>
#include "dictionary.h"
#include <stdio.h>
#include <cs50.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
//represents a node in a hash table
typedef struct node
{
    char word[LENGTH + 1];
    struct node *next;
}
node;
// no. of buckets in hash table
const unsigned int N = 26;
// initialise positive hash value using unsigned int 
unsigned int hash_value;
// initialise (positive) hash table word count 
unsigned int word_count;
// hash table
node *table[N];

Вставляя сюда эту «часть 0» ретроспективно, как я понял после завершения оставшейся части этого пошагового руководства, что я ничего не упомянул об инициализации функций! Это просто вопрос использования unsigned int (для положительных целых чисел) для инициализации различных значений, которые нам нужны, и предварительной установки N = 26, чтобы каждый алфавит получил ведро. Подробнее об этом читайте в части 2: хеш!

часть 1: загрузка

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

malloc принимает аргумент (количество байтов, которые вы хотите выделить) и возвращает этот фрагмент памяти в виде первого адреса, хранящегося в указателе. Это позволяет выделять динамическую память. Здесь malloc используется для каждого нового слова в файле.

Теперь о фактической загрузке.

  1. Возьмите в качестве аргумента char * или строку, которая является файлом словаря, который нужно открыть и прочитать из
  2. Используйте fopen, чтобы открыть файл
  3. Вернуть истину, если выполнено успешно
  4. Если возвращаемое значение NULL, вернуть false (ошибка выделения памяти)
  5. Используйте fscanf для чтения строк в файле до EOF (конца файла).
  6. Используйте malloc для создания и выделения памяти для каждого нового узла.
  7. Если он возвращает NULL, вернуть false
  8. Скопируйте слово в узел с помощью strcpy
  9. Используйте хеш, чтобы взять слова и вернуть хеш-значение / индекс.
  10. Вставить узел в хеш-таблицу, установив указатель на последовательные узлы
  11. Подсчитайте количество вводимых слов
// Loads dict into memory, returning true if successful else false
bool load(const char *dictionary)
{
    // Open dict 
    FILE *file = fopen(dictionary, "r");
// If file is not opened, return false
    if (file == NULL)
    {
        return false;
    }
    // storage space for word
    char word[LENGTH + 1];
// Scan dict for strings that are not the end of the file
    while (fscanf(file, "%s", word) != EOF)
    {
        // Allocate memory for new node
        node *n = malloc(sizeof(node));
// If malloc returns NULL, return false
        if (n == NULL)
        {
            return false;
        }
// Pointer to next node and word itself
        strcpy(n->word, word);
        // Hash the word to get hash value
        hash_value = hash(word);
        // Set new pointer
        n->next = table[hash_value];
        // Set head to new pointer
        table[hash_value] = n;
        // Increment word count
        word_count++;
    }
// Close file
    fclose(file);
// If dict is loaded, return true
    return true;
}

часть 2: хеш

Хеш-функция принимает слово и возвращает хеш-значение или числовое значение слова, чтобы проверить его правильность.

  1. принимать входные данные, которые представляют собой алфавитные символы, независимо от заглавных букв и, возможно, с апострофами
  2. вывод должен быть числовым индексом от 0 до N-1 включительно
  3. детерминированный

Чем больше N, тем эффективнее будет работать программа. Если функция заканчивается значением больше N, можно использовать % N для получения значения в подходящем диапазоне.

Я сыграл просто, выбрав хэш-функцию «первая буква» с N = 26 корзинами. Итак, я порылся на Reddit, чтобы найти хэш-функции, и нашел следующее:

Пришлось немного адаптировать его для собственного использования, но до этого мне действительно нужно было понять, что вообще делает хеш-функция. Оказывается, что хеш «5» (оператор сдвига влево) означает, что хеш сдвигается влево (в двоичном формате) на 5 бит (как указано правым операндом). Не знаю, как Дэн Бернштейн пришел к этому, но ему спасибо, и я просто воспользуюсь этой функцией и посмотрю, что из этого получится.

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

// Hashes word to a number
unsigned int hash(const char *word)
{
    unsigned long hash = 5381;
    int c;
    while ((c = toupper(*word++)))
    {
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
    }
    return hash % N;
}

часть 3: размер

Чтобы вернуть количество слов, программа должна отслеживать количество добавленных слов в реальном времени. word_count уже был определен в load, поэтому оставалось только вернуть его.

// Returns number of words in dictionary if loaded, else 0 if not yet loaded
unsigned int size(void)
{
    // Check if there are any words
    if (word_count > 0)
    {
        // Return count
        return word_count;
    }
    // Else
    return 0;
}

часть 4: проверьте

Проверьте, есть ли слово в словаре, независимо от использования заглавных букв. Вывод имеет форму логического значения.

  1. Хеш-слово для получения хеш-значения
  2. Доступ к связанному списку по этому хэш-значению по этому конкретному индексу в хеш-таблице
  3. Просматривайте связанный список, просматривая по одному узлу за конкретным словом.
  4. Чтобы пройти, установите курсор или указатель на первый узел в связанном списке
  5. Используйте strcasecmp, который похож на strcmp, но нечувствителен к регистру, чтобы проверить, есть ли слово в словаре.
  6. Если это не то слово, введите курсор = курсор - ›далее, чтобы перейти к следующему узлу.
  7. Делайте это до тех пор, пока курсор не станет равным NULL (eof), а если слово не найдено, верните false.
// Returns true if word is in dictionary else false
bool check(const char *word)
{
    //hash the word to get hash value
    hash_value = hash(word);
    //access the linked list  
    node *cursor = table[hash_value];
//go through the linked list
    while (cursor != NULL)
    {
        //check if the word matches
        if (strcasecmp(word, cursor->word) == 0)
        {
            return true;
        }
//move cursor to next node
        cursor = cursor->next;
    }
return false;
}

часть 5: разгрузка

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

  1. Пройдите по каждому ведру и установите курсор в его местоположение.
  2. Создайте временную переменную, указывающую на первый узел
  3. Переместите фактический курсор к следующему узлу в списке
  4. Освободите временную переменную
  5. Повторяйте, пока курсор не достигнет последнего сегмента и не станет NULL
  6. Вернуть истину
// Unloads dictionary from memory, returning true if successful else false
bool unload(void)
{
    // Iterate through buckets
    for (int i = 0; i < N; i++)
    {
        // Set cursor to this each bucket location  
        node *cursor = table[i];
// If cursor is not NULL, free  
        while (cursor)
        {
            // Create temp 
            node *tmp = cursor;
            // Move cursor to next node
            cursor = cursor->next;
            // Free up temp
            free(tmp);
        }
// If cursor is NULL
        if (i == N - 1 && cursor == NULL)
        {
            return true;
        }
    }
    return false;
}

Когда код был готов, настало время для реального тестирования времени. Я запустил его на koran.txt и должен признать, что сводная таблица всплывала довольно долго.

Сплошная разница в 18 секунд! Я увеличил N с 26 до 1000000 (почему бы и нет) и снизил его до 0,18 га.