JavaRush /Курсы /Harvard CS50 /Практическая работа. Постановка задачи и рекомендации

Практическая работа. Постановка задачи и рекомендации

Harvard CS50
5 уровень , 9 лекция
Открыта

Проверка орфографии

Итак, перед нами стоит следующая задача: реализовать load, check, size и unload настолько эффективно, насколько это возможно, чтобы минимизировать TIME IN load, TIME IN check, TIME IN size, и TIME IN unload.

Возможно, не совсем понятно, что значит минимизовать. Особенно с учётом того, что эти показатели будут меняться в зависимости от того, какой словарь dictionary и текст text вы загружаете в speller. Это и есть самая соль задачи, самое сложное, но при этом — очень увлекательное затруднение. К его решению можно подойти творчески.

Разумеется, желательно минимизировать объем используемой памяти, но главное — уменьшить время выполнения. Прежде, чем погрузиться в решение, пробегитесь по следующим рекомендациям.

  • Вы не можете менять speller.c.
  • Вы можете менять dictionary.c (по сути, вы должны его менять, чтобы завершить реализацию load, check, size и unload, но при этом нельзя изменять объявления load, check, size и unload).
  • Вы можете изменять dictionary.h, но нельзя изменять объявления load, check, size и unload.
  • Вы можете менять Makefile.
  • Вы можете добавить функции в dictionary.c или в ваши собственные файлы. Только проследите, чтобы при этом ваш код успешно компилировался с помощью make.
  • Ваша реализация check должна быть нечувствительна к регистру. Другими словами, если foo есть в словаре, check должна возвращать значение true для любого слова, которые отличаются от foo только регистром символов. Все слова foo, foO, fOo, fOO, fOO, Foo, FoO, FOo и FOO нужно считать правильными.
  • Помимо регистра, ваша реализация check должна возвращать значение true для слов, которые действительно есть в dictionary. Осторожнее с часто употребляемыми словами (например, с the), если мы передали dictionary без них. Более того, разрешены только те конкретные слова, которые явно заданы в словаре dictionary, а их формы — нет. Иначе говоря, даже если foo есть в dictionary, команда check должна вернуть значение false для foo’s в случае если foo’s нет в dictionary.
  • Считайте, что check проходит строки только по символам алфавита и/или апострофам.
  • Считайте, что любой словарь в вашей программе будет структурирован точно так, как наши: лексикографически выстроен сверху вниз по одному слову в строке, и все строки заканчиваются знаком \n. Также примите, что dictionary должен содержать по крайней мере одно слово и все слова не должны быть длиннее LENGTH (эта константа определена в dictionary.h). Ни одно слово не должно повторяться и все они должны содержать строчные символы алфавита и, возможно, апострофы.
  • Ваша программа проверки правописания должна принимать на вход только text и, дополнительно, может принимать словарь dictionary. Невзирая на то, что вы, скорее всего захотите предварительно “доработать” наш словарь по умолчанию с целью определить для него лучшую хеш-функцию, вы не сможете сохранить на диск результат любой подобной доработки с тем, чтобы повторно загрузить его в память для более быстрых дальнейших вызовов.
  • Вы можете использовать хеш-функции из книг или интернета, при условии, что вы даете ссылку на оригинал любой хеш-функции, которую вы интегрируете в ваш код.

Начинаем

Вот что вам нужно сделать:

  • Реализовать load. Советуем вам подготовить словари, меньше 143 091 слов, с которыми вы будете тестировать ваш код во время разработки.
  • Реализовать check. Советуем вам подготовить несколько малых файлов для проверки правописания, перед началом. «Войну и мир», например.
  • Реализовать size.
  • Реализовать unload. Обязательно высвободите память (free), которую вы выделили под load!
  • Еще немного подсказок:

    Не забывайте, valgrind — ваш лучший приятель! Он отслеживает потерю доступа к памяти, пока ваша программа работает. Убедитесь, что аргументы в программной строке передаются, если вы хотите, чтобы функция valgrind анализировала speller, пока вы используете определенный словарь и/или текст. Лучше всего работать с небольшим текстом, в противном случае valgrind потребуется значительное количество времени.

    valgrind — leak-check=full ./speller texts/ralph.txt

    Если вы запускаете valgrind без указания text для speller, ваша реализация load и unload не будет вызываться (и анализироваться).

    Если вы не уверены, как следует интерпретировать результат valgrind, запустите help50:

    help50 valgrind — leak-check=full ./speller texts/ralph.txt

    Проверка

    Как понять, что программа выбрала верные слова с опечатками? Во-первых, вы можете посмотреть ответы в директории keys. Она находится внутри директории speller. Например, в keys/austinpowers.txt должны быть собраны все слова, которые ваша программа считает написанными неправильно.

    Или вы можете запустить программу над текстом в одном окне.

    ./speller ~cs50/pset5/texts/austinpowers.txt

    А потом запустите версию, созданную сотрудниками курса, с тем же самым текстом в другом окне, как показано ниже:

    ~cs50/pset5/speller ~cs50/pset5/texts/austinpowers.txt 

    и провести визуальное сравнение визуально, строка за строкой. Правда, такая работа очень быстро надоедает. Вместо этого лучше перенаправить результат работы программы в отдельный файл (вы уже так делали с generate в третьем задачнике):

    ./speller ~cs50/pset5/texts/austinpowers.txt > student.txt
    ~cs50/pset5/speller ~cs50/pset5/texts/austinpowers.txt > staff.txt

    Вы можете сравнить оба файла, открыв их в одном окне с помощью программы diff:

    diff -y student.txt staff.txt

    Или ещё один вариант: для экономии времени, вы можете сравнить результат работы вашей программы (предположим, что вы перенаправляете его в, например, student.txt) с одним из файлов ответов:

    diff -y student.txt ~cs50/pset5/keys/austinpowers.txt

    Если результат работы вашей программы совпадает с нашей версией, diff выведет на экран два идентичных столбца. Отличие возможно только во времени продолжительности работы, указанной внизу.

    Если столбцы будут отличаться, вы увидите знаки > или |, там, где они отличаются. Например,

    MISSPELLED WORDS                                                MISSPELLED WORDS
    
    FOTTAGE                                                         FOTTAGE
    INT                                                             INT
                                                                  > EVIL'S
    s                                                               s
                                                                  > EVIL'S
    Farbissina                                                      Farbissina

    Это означает что программа (её вывод — слева) не считает, что EVIL’s не верен, а вот наша программа считает именно так, что представляется отсутствием EVIL’s в левой колонке и присутствием его в правой.

    check50

    Чтобы проверить свой код в автоматическом режиме (правда, не полностью), вы можете выполнить следующее:

    check50 2015.fall.pset5.speller dictionary.c dictionary.h Makefile

    Обратите внимание, check50 не отслеживает утечки памяти, поэтому обязательно используйте также valgrind.

    Как понять, достаточно ли быстрая ваша версия?

    Как всегда, вы можете запустить версию, созданную сотрудниками курса, и сравнить результат её работы с вашим:

    ~cs50/pset5/speller ~cs50/pset5/texts/austinpowers.txt
    Комментарии (65)
    ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
    ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
    FeatHonnar Уровень 16
    6 января 2023
    Что ж. Работа действительно на 1 взгляд кажется очень сложной, но по мере изучения указателей и памяти, оказалось, что алгоритмы довольно примитивные. По крайней мере, у меня. В целом, скорость не очень плохая, но чтобы ускорить check, как я понимаю, надо переделывать принцип работы load, ибо то, что я сделал, мне кажется наиболее экономным в моей ситуации вариантом. Снизу прикреплю ответы на вопросы и реализацию функкций (выбрал я trie). Из интересного: Все говорили, что реализация load самая сложная, а далее все очень просто. Я согласен, но вот над тем, чтобы придумать принцип работиы uload, пришлось конкретно так запариться. Надеюсь, то, что я реализовал это в двух функциях, не является читерством. Ибо как тут вообще можно, кроме как рекурсией?,,, Так же надеюсь, что моя реализация size, которая просто возвращает число, уже высчитанное в load, тоже читерством можно не считать :) Внимательности тем, кто начинает. Не пугайтесь, если трудно. В конце вы все равно будете думать "Блин.. мне реализация кажется такой простой, потому что я поумнел или потому что она реально простая?" Результат программы:
    
    ./speller texts/kjv.txt
    
    WORDS IN DICTIONARY:  143091
    WORDS IN TEXT:        799460
    TIME IN load:         0.08
    TIME IN check:        1.20
    TIME IN size:         0.00
    TIME IN unload:       0.06
    TIME IN TOTAL:        1.34
    
    FeatHonnar Уровень 16
    6 января 2023
    0. Что такое pneumonoultramicroscopicsilicovolcanoconiosis? (0_0 Google точно знает!) -Самое длинное слово в английском, синоним к названию болезни "силикоз". 1. Каково предназначение getrusage, согласно странице справочника man? -getrusage возвращает подробную информацию о том, как и сколько памяти у нас в данный момент используется. Вся информация сохраняется в структуру rusage, где есть множество переменных, отвечающих за различную информацию о потреблении памяти (например, long ru_maxrss хранит информацию о том, сколько всего используется памяти на данный момент). 2. Согласно этой же странице, сколько полей в переменной типа struct rusage? -16. 3. Как вы думаете, почему мы передаем параметры before и after по ссылке (вместо передачи по значению) в calculate, несмотря на то, что мы не изменяем их содержание? -Потому что так мы экономим ресурсы компьютера. Без амперсанта программа бы сперва создала копию before и after, загрузила в функцию calculate, и далее бы функция работала с копиями, занимающими отдельные место в памяти. С амперсантом же мы передаем адрес переменных, после чего функция обращается непосредственно сразу к оригинальным before и after.
    FeatHonnar Уровень 16
    6 января 2023
    4. Объясните максимально точно, каким образом main может считывать слова из файла. Другими словами, убедите нас, что вы действительно понимаете, как работает цикл с параметром функции main. -Сначала мы запускаем цикл for и с каждым повторением читаем символ из текста, сдвигая курсор вперёд. Если прочитанный символ является буквой или апострофом, мы сохраняем его в массив word (в слово) под определенным индексом и увеличиваем этот индекс на 1. Если индекс стал больше допустимого размера слова, то дальнейшие символы (относящиеся к данному слову) для нас не имеют ценности, поэтому мы аннулируем их и индекс Если символ является цифрой, то мы аннулируем все дальнейшие цифры, являющиеся частью данного числа, и индекс. Если индекс на данном этапе уже больше нуля (и, раз мы дошли до этого этапа, символ не является буквой или числом), то мы заканчиваем наше слово, обновляем счетчик слов, проверяем его на орфографию и аннулируем индекс, создавая новое слово. Стоит упомянуть, что если символ является не буквой, апострофом или числом, а, например, пробелом, точкой или иным знаком, то мы просто пропускаем этот символ и повторяем цикл.
    FeatHonnar Уровень 16
    6 января 2023
    5. Как вы думаете, почему мы использовали fgetc и считываем каждый раз одну букву в слове, вместо того, чтобы использовать fscanf со строкой формата “%s” для чтения всего слова сразу? Подумайте, какие проблемы могут возникнуть, если использовать только fscanf? -Насколько мне известно, fscanf можно использовать для чтения символов. В таком случае разницы между fgetc и fscanf быть не должно. Но речь о чтении целого слова сразу. Тогда всплывает целый ряд проблем: Во-первых, функция не очень точно умеет определять конец слова, поэтому может занести в массив иные знаки, помимо букв. Во-вторых, если наше слово окажется длиннее заданной длины массива, то функция начнет перезаписывать иные данные в памяти, что легко может привести к ошибке сегментации. Ну и в-третьих, мы теряем достаточно много контроля, ведь обязаны сперва читать целое слово и только потом читать его посимвольно (если возникает такая нужда), а не взаимодействовать с каждым символов сразу 6. Почему мы объявили параметры для check и load как константы (const)? -Потому что это параметры, которые нет смысла как-либо менять? Ну а на практике это может быть полезно, чтобы случайно не поломать значения этих данных в ходе работы.
    FeatHonnar Уровень 16
    6 января 2023
    
    /**
     * dictionary.c
     *
     * Computer Science 50
     * Problem Set 5
     *
     * Implements a dictionary's functionality.
     */
    
    #include <stdbool.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <ctype.h>
    
    #include "dictionary.h"
    
    
    //Создать структуру trie и указатели типа trie
    typedef struct trie {
        bool is_word;
        struct trie* sym[27];
    } trie;
    
    // Создать указатели типа trie: главный(main), нынешний(this).
    trie* mainPref = NULL;
    trie* thisPref = NULL;
    
    unsigned int words;
    
    
    /**
     * Загружает словарь в память. Возвращает true, если успешно, иначе false.
     */
    bool load(const char* dictionary)
    {
        // Выделить память (инициализированную как 0) и присвоить ее адрес указателю главного дерева
        mainPref = calloc(1, sizeof(trie));
    
        thisPref = mainPref;
    
        // Открыть файл словаря для чтения
        const char* name = dictionary;
        FILE* dict = fopen(name, "r");
    
        // Прекратить, если не удалось открыть файл
        if (!dict)
        {
            return false;
        }
    
    FeatHonnar Уровень 16
    6 января 2023
    
        // Сохранить словарь в память
        while (!feof(dict))
        {
            // Создать массив слова
            char word[LENGTH];
    
            // Записать в этот массив слово из словаря
            fscanf(dict, "%s\n", word);
    
            // Сохранить в память это слово в виде префиксного дерева
            for (int index, i = 0; word[i] != '\0'; i++)
            {
                // Считаем индекс символа
                if (word[i] == '\'')
                {
                    index = 27;
                }
                else
                {
                    index = tolower(word[i]) - 'a';
                }
    
                // Если указатель sym[index] ни на что не указывает
                if (!thisPref->sym[index])
                {
                    // Выделить память (инициализированную как 0) и присвоить её адрес данному укзаателю
                    thisPref->sym[index] = calloc(1, sizeof(trie));
                }
                // Перейти к следующему указателю, как к нынешнему
                thisPref = thisPref->sym[index];
            }
            // Если цикл окончен, то мы дошли до конца слова
    
            thisPref->is_word = true;
    
            words++;
    
            thisPref = mainPref;
        }
        // Если цикл окончен, то мы дошли до конца файла словаря
    
        thisPref = mainPref;
    
        return true;
    }
    
    FeatHonnar Уровень 16
    6 января 2023
    
    /**
     * Возвращает true, если слово есть в словаре, иначе false.
     */
    bool check(const char* word)
    {
        thisPref = mainPref;
    
        // Проверяем каждый символ слова на наличие в словаре
        for (int index, i = 0; word[i] != '\0'; i++)
        {
            // Считаем индекс символа
            if (word[i] == '\'')
            {
                index = 27;
            }
            else
            {
                index = tolower(word[i]) - 'a';
            }
    
            // Проверяем, указывает ли на что-то sym[index] (Да - продолжаем. Нет - возвращаем false)
            if (!thisPref->sym[index])
            {
                return false;
            }
            else
            {
                thisPref = thisPref->sym[index];
            }
        }
    
        // Проверяем, является ли последний символ слова в словаре концом какого-либо слова
        if (thisPref->is_word == false)
        {
            return false;
        }
    
        return true;
    }
    
    
    /**
     * Возвращает количество слов в словаре, если он загружен, иначе 0 (если не загружен).
     */
    unsigned int size(void)
    {
        if (!mainPref)
        {
            return false;
        }
        else
        {
            return words;
        }
    }
    
    
    // Проходит дерево в глубину, освобождая память каждого поддерева (для функции unload)
    void nodeunload (trie **node)
    {
        if (node && *node)
        {
            // Рекурсивный обход всего дерева в глубину
            for (int index = 0; index < 28; index++)
            {
                nodeunload(&((*node)->sym[index]));
            }
    
            free(*node);
    
            *node = NULL;
        }
    }
    
    /**
     * Выгружает словарь из памяти. Возвращает true в случае успеха, иначе false.
     */
    bool unload(void)
    {
        nodeunload(&mainPref);
    
        return !mainPref;
    }
    
    Fatima Уровень 2
    13 августа 2023
    Ты изучаешь C или Java?)
    FeatHonnar Уровень 16
    17 августа 2023
    Сначала изучал С по этому курсу (CS50). Затем забросил, т.к. курс перестал устраивать и в начала лета купил подписку на JavaRush
    Karen Уровень 1
    21 мая 2022
    
    
    ./speller texts/kjv.txt
    
    WORDS MISSPELLED:   775290
    WORDS IN DICTIONARY:  143091
    WORDS IN TEXT:        799460
    TIME IN load:         0.18
    TIME IN check:        0.23
    TIME IN size:         0.01
    TIME IN unload:       0.00
    TIME IN TOTAL:        0.42
    
    valgrind --leak-check=full ./speller texts/kjv.txt
    
    WORDS MISSPELLED:   775290
    WORDS IN DICTIONARY:  143091
    WORDS IN TEXT:        799460
    TIME IN load:         2.25
    TIME IN check:        8.43
    TIME IN size:         0.64
    TIME IN unload:       0.00
    TIME IN TOTAL:        11.32
    
    Александр # Уровень 0
    6 мая 2021
    Парни (и девушки), а кто понял, как посмотреть или запустить версии преподавателей курса?
    Александр # Уровень 0
    6 мая 2021
    Может кому пригодится, очень понятный пример принципа реализации TRIE: https://www.techiedelight.com/trie-implementation-insert-search-delete/ На всякий случай, если кто-то не знает, в сети есть перевод на русский коротких видео от преподавателей CS50: https://www.youtube.com/c/OnlineUniver/playlists Если Вы пока ещё на 1-й стадии депрессии - знайте, это скоро пройдёт :) Задание сознательно построено с прицелом, что студенту придётся сначала поизучать теорию и справочники, прежде чем он поймет детали задания. Я также как и многие, несколько дней потратил просто на чтение.
    mak7imt Уровень 0
    4 февраля 2021
    GAME OVER
    Ихтияр Кадыров Уровень 1
    19 ноября 2020
    Без valgrind: Результат вместе с Valgrind:
    All Cool Уровень 1
    11 октября 2020
    долго я бился над этой задачей решил через хеш таблицу
    Artem Уровень 0
    12 декабря 2020
    All Cool, как пишут в подготовке к практической работе, в словаре 143 091 слово. как у вас получилось 143 092 ?
    xacker Уровень 5
    26 июня 2020
    На три варианта программы (две с хэш-таблицами, одна - с деревом) у меня ушло где-то две недели времени. Да, задание сложное для тех, кто недавно услышал про указатели, структуры, хэш-таблицы и т.п., но и в процессе решения скилл прокачивается соответственно.
    Даниил Уровень 41 Master
    14 марта 2020
    Ну и задание... Думал будет проще и быстрее. Сначала прочитал большую часть того что советовали из литературы - потратил грубо говоря день. Долго не мог въехать что от меня конкретно хотят. Потом вроде как осознав, то начал делать. Сначала решил делать через hash-таблицу, но что-то у меня не пошло (уже не помню что именно так как делал я подходами с относительно большими перерывами) и по этому решил сделать через Префиксное дерево (trie). Самое сложное - это было реализовать метод load() так как делая его ты должен придумать самую главную вещь - в каком виде и как будут организованы данные в твоей реализации и исходя из этого уже будут делаться другие методы (с него и стоит начинать написание решения). На него я потратил дня 2, не меньше. В итоге в сумме потратил наверное целых дня 3 или 4 на реализацию полноценного рабочего решения которое освобождало все ресурсы (без листика и ручки не получилось найти ошибку где я не освобождал память). Получился такой результат на файле kjv.txt:
    
    WORDS MISSPELLED:     33441
    WORDS IN DICTIONARY:  143091
    WORDS IN TEXT:        799460
    TIME IN load:         0.05
    TIME IN check:        0.68
    TIME IN size:         0.00
    TIME IN unload:       0.01
    TIME IN TOTAL:        0.74
    
    оптимизировать я уже не буду, ну его в баню.
    Даниил Уровень 41 Master
    15 марта 2020
    Решил всё-таки для отработки реализовать вариант с hash-таблицей. Потратил в сумме целый день на реализацию и попытку исправить ошибки. В итоге все ошибки я исправить так и не смог((( Так и не получилось освободить всю память и почему-то на тексте alice.txt всё было нормально, а на том же kjv.txt на 3 слова меньше находило (хотя сравнивая вручную и через текстовые редакторы не получилось вычислить эти несовпадения). Общее время работы стало быстрее.
    
    WORDS MISSPELLED:     33438
    WORDS IN DICTIONARY:  143091
    WORDS IN TEXT:        799460
    TIME IN load:         0.08
    TIME IN check:        0.49
    TIME IN size:         0.00
    TIME IN unload:       0.05
    TIME IN TOTAL:        0.63
    
    Елена Уровень 19
    25 февраля 2020
    Долго не могла понять как создавать hash table. Мне помогла вот эта статья, может кому-то пригодится, все разложено по полочкам: https://stackoverflow.com/questions/31930046/what-is-a-hash-table-and-how-do-you-make-it-in-c