Тестер анаграмм в C

Я пытаюсь реализовать тестер анаграмм на C. При вызове программы пользователь вводит два слова в двойных кавычках, например «слушай» и «молчи». Я почти заставил его работать, но у меня возникли проблемы с вспомогательной функцией, которую я написал, чтобы избавиться от пробелов в двух входных словах. Вот код этой функции:

void noSpaces(char word[100]) {
    /*
    This is a function to get rid of spaces in a word
    It does this by scanning for a space and shifting the
    array elements at indices > where the space is
    down by 1 as long as there is still a space
    there. 
    */
    for (int i = 0; i < 100; i++) {
        while (word[i] == ' ') {
            for (int j = i; j < 100; j++) {
                word[j] = word[j+1];
            }
        }
    }
}

Теперь он отлично работает, когда я передаю входное слово из функции main этому помощнику. Проблема заключается во втором вызове этой функции. Когда я вызываю эту функцию для второго ввода, если k — это количество пробелов в первом вводе, то функция стирает первые k буквы второго ввода. Например, ввод ./anagram " banana" "banana" даст мне ложный отрицательный результат, и если я добавлю оператор печати, чтобы увидеть, что происходит с входными данными после того, как на них вызывается noSpaces, я получаю следующее:

banana
anana

Вот код полной программы:

#include <stdio.h>

int main(int argc, char *argv[]) {
    //this if statement checks for empty entry
    if (isEmpty(argv[1]) == 0 || isEmpty(argv[2]) == 0) {
        //puts("one of these strings is empty");
        return 1;
    }
    //call to noSpaces to eliminate spaces in each word
    noSpaces(argv[1]);
    noSpaces(argv[2]);
    //call to sortWords
    sortWords(argv[1]);
    sortWords(argv[2]);
    int result = compare(argv[1], argv[2]);
    /*
    if (result == 1) {
        puts("Not anagrams");
    } else {
        puts("Anagrams");
    }
    */
    return result;
}

int compare(char word1[100], char word2[100]) {
    /*
    This is a function that accepts two sorted 
    char arrays (see 'sortWords' below) and
    returns 1 if it finds a different character
    at entry i in either array, or 0 if at no 
    index the arrays have a different character.
    */
    int counter = 0;
    while (word1[counter] != '\0' && word2[counter] != '\0') {
        if (word1[counter] != word2[counter]) {
            //printf("not anagrams\n");
            return 1;
        }
        counter++;
    }
    // printf("anagrams\n");
    return 0;
}

void sortWords(char word[100]) {
    /*
    This is a function to sort the input char arrays
    it's a simple bubble sort on the array elements.
    'sortWords' function accepts a char array and returns void,
    sorting the entries in alphabetical order
    being careful about ignoring the 'special character'
    '\0'.
    */
    for (int j = 0; j < 100; j++) {
        int i = 0;
        while (word[i + 1] != '\0') {
            if (word[i] > word[i + 1]) {
                char dummy = word[i + 1];
                word[i + 1] = word[i];
                word[i] = dummy;
            }
            i++;
        }
    }
}

void noSpaces(char word[100]) {
    /*
    This is a function to get rid of spaces in a word
    It does this by scanning for a space and shifting the
    array elements at indices > where the space is
    down by 1 as long as there is still a space there. 
    */
    for (int i = 0; i < 100; i++) {
        while (word[i] == ' ') {
            for (int j = i; j < 100; j++) {
                word[j] = word[j + 1];
            }
        }
    }
}

int isEmpty(char word[100]) {
    // if a word consists of the empty character, it's empty
    //otherwise, it isn't
    if (word[0] == '\0') {
        return 0;
    }
    return 1;
}

Я знаю, что есть библиотека, которая может работать со строками, но я действительно хотел бы избежать ее использования. Я уже зашел так далеко, не нуждаясь в этом, и я чувствую, что проблема в основном решена, но есть одна маленькая вещь, которую я не вижу.

Я пришел из java, и я новичок в C, если это объясняет какую ошибку я сделал.


person P. Gillich    schedule 16.02.2019    source источник
comment
Когда j равно 99, вы получаете доступ к word[j+1], то есть word[100]. Но word[100] нет, потому что word имеет только 100 записей.   -  person David Schwartz    schedule 16.02.2019
comment
Почему двойные кавычки? Это задание?   -  person n. 1.8e9-where's-my-share m.    schedule 16.02.2019
comment
@ Дэвид Шварц, спасибо, что поймал это, не понял. Однако, если слово содержит менее 100 символов, объясняет ли это странный эффект, который я наблюдаю?   -  person P. Gillich    schedule 16.02.2019
comment
@n.m да, я решил использовать аргументы командной строки вместо scanf, потому что подумал, что могут возникнуть проблемы с запуском второй части этого задания. Я просто скажу ТА заключать свои входные данные в двойные кавычки на случай, если у них есть записи с пробелами. Когда я впервые написал это, я не думал, что мне нужно лечить этот случай.   -  person P. Gillich    schedule 16.02.2019
comment
@P.Gillich Невозможно узнать. Эффект доступа за пределы может быть непредсказуемым.   -  person David Schwartz    schedule 17.02.2019


Ответы (4)


В C строки представляют собой массивы char с завершающим нулем, то есть байт со значением 0, обычно представленный как '\0'. Вы не должны предполагать какую-либо конкретную длину, такую ​​как 100. Действительно, размер массива в аргументах прототипа функции игнорируется компилятором. Вы можете определить длину, просканировав нулевой терминатор, что эффективно делает strlen(), или вы можете написать код таким образом, чтобы избежать многократного сканирования, останавливаясь на нулевом терминаторе. Вы должны убедиться, что ваши функции работают с пустой строкой, которая представляет собой массив с одним нулевым байтом. Вот проблемы в вашем коде:

В функции noSpaces вы выполняете итерацию за концом строки, изменяя память, потенциально принадлежащую следующей строке. Программа имеет неопределенное поведение.

Вы должны остановиться в конце строки. Также используйте 2 переменные индекса для выполнения в линейном времени:

void noSpaces(char word[]) {
    /*
    This is a function to get rid of spaces in a word
    It does this by scanning for a space and shifting the
    array elements at indices > where the space is
    down by 1 as long as there is still a space
    there. 
    */
    int i, j;
    for (i = j = 0; word[i] != '\0'; i++) {
        if (word[i] != ' ') {
            word[j++] = word[i];
        }
    }
    word[j] = '\0';
}

Вы можете упростить compare, чтобы использовать в среднем треть тестов:

int compare(const char word1[], const char word2[]) {
    /*
    This is a function that accepts two sorted 
    char arrays (see 'sortWords' below) and
    returns 1 if it finds a different character
    at entry i in either array, or 0 if at no 
    index the arrays have a different character.
    */
    for (int i = 0; word1[i] == word2[i]; i++) {
        if (word1[i]) == '\0')
            //printf("anagrams\n");
            return 0;
        }
    }
    // printf("not anagrams\n");
    return 1;
}

sortWords имеет неопределенное поведение для пустой строки, потому что вы читаете char по индексу 1 за концом массива. Вот исправленная версия:

void sortWords(char word[]) {
    /*
    This is a function to sort the input char arrays
    it's a simple bubble sort on the array elements.
    'sortWords' function accepts a char array and returns void,
    sorting the entries in alphabetical order
    being careful about ignoring the 'special character'
    '\0'.
    */
    for (int j = 0; word[j] != '\0'; j++) {
        for (int i = 1; i < j; i++) {
            if (word[i - 1] > word[i]) {
                char dummy = word[i - 1];
                word[i - 1] = word[i];
                word[i] = dummy;
            }
        }
    }
}

Вы должны объявлять функции перед использованием или альтернативно определять их перед использованием. Ваш код компилируется, потому что компилятор принимает старый стиль C, где прототип для еще невидимых функций был выведен из аргументов, переданных в месте первого вызова. Эта практика подвержена ошибкам и устарела.

Ваша функция сортировки имеет квадратичную временную сложность, которая может быть очень медленной для очень длинных строк, но слова не должны быть слишком большими, так что это не проблема.

Было бы лучше не изменять строки аргументов. Вы можете выполнить тест с копией одной из строк с той же временной сложностью.

Вот прямой подход:

#include <stdio.h>

int check_anagrams(const char word1[], const char word2[]) {
    /*
       This function accepts two strings and returns 1 if they
       are anagrams of one another, ignoring spaces.
       The strings are not modified.
     */
    int i, j, len1, letters1, letters2;

    /* compute the length and number of letters of word1 */
    for (len1 = letters1 = 0; word1[len1] != '\0'; len1++) {
        if (word1[len1] != ' ')
            letters1++;
    }

    /* create a copy of word1 in automatic storage */
    char copy[len1];    /* this is an array, not a string */
    for (i = 0; i < len1; i++)
        copy[i] = word1[i];

    for (j = letters2 = 0; word2[j] != '\0'; j++) {
        char temp = word2[j];
        if (temp != ' ') {
            letters2++;
            for (i = 0; i < len1; i++) {
                if (copy[i] == temp) {
                    copy[i] = '\0';
                    break;
                }
            }
            if (i == len1) {
                /* letter was not found */
                return 0;
            }
        }
    }
    if (letters1 != letters2)
        return 0;
    return 1;
}

int main(int argc, char *argv[]) {
    const char *s1 = " listen";
    const char *s2 = "silent   ";
    if (argc >= 3) {
        s1 = argv[1];
        s2 = argv[2];
    }
    int result = check_anagrams(s1, s2);
    if (result == 0) {
        printf("\"%s\" and \"%s\" are not anagrams\n", s1, s2);
    } else {
        printf("\"%s\" and \"%s\" are anagrams\n", s1, s2);
    }
    return result;
}
person chqrlie    schedule 16.02.2019
comment
спасибо за подробный ответ. Как может быть, что я модифицирую память, принадлежащую следующему массиву (при условии, что я не совершаю ошибку «не на единицу»)? Оба массива состоят из 100 элементов, и я ни в коем случае не выхожу за пределы индекса (опять же, исключая ошибку «один за другим»). Это задание, предполагающее короткие входные данные, и хотя я понимаю, насколько неэффективна моя сортировка, все, что мне нужно, это чтобы она работала. Меня не беспокоит, что алгоритм занимает на 4 мс больше, чем мог бы. То же самое для функций, определяемых после вызова из main; действительно ли это имеет значение с точки зрения компилятора? - person P. Gillich; 16.02.2019
comment
Я не реализовал все предложенные вами исправления, но, похоже, я решил свою конкретную проблему после написания функции, которая задает длину входных слов, и не повторял ее в последующих функциях. Возможно, мое понимание здесь ошибочно в отношении поведения C, но мне интересно, почему это вообще имело значение, поскольку, насколько я знаю, каждому массиву выделяется 100 слотов памяти, поэтому даже если я внес изменения в пустые слоты по индексам ‹ 100, почему это повлияет на другую ячейку памяти? Просто любопытно. - person P. Gillich; 16.02.2019
comment
@P.Gillich: Боюсь, ваше понимание ошибочно: где вы узнали, что каждому массиву выделяется 100 элементов? Это совсем не так. Массивы могут быть выделены любого размера, следует избегать доступа к элементам строки C за пределами оканчивающегося нулем, поскольку это имеет неопределенное поведение. Например, строка C, выделенная с помощью malloc(), может иметь только необходимые слоты и вызывать ошибку сегментации при доступе к элементам с недопустимыми индексами. - person chqrlie; 16.02.2019
comment
я имею в виду, что если я объявлю массив размером 100, не означает ли это, что где-то зарезервирован кусок памяти с достаточным пространством для хранения до 100 элементов? Таким образом, даже если я внесу изменения в индексы помимо того, который хранит последний символ в массиве, до тех пор, пока я не попытаюсь изменить индекс, выходящий за пределы массива, почему это должно повлиять на другой массив? - person P. Gillich; 17.02.2019
comment
@P.Gillich: Объявление размера массива в аргументе функции вообще не влияет. Функция просто получает указатель на первый элемент массива, переданный вызывающей стороной. Размер массива такой, какой он есть в месте вызова. Доступ к элементам за пределами конца массива имеет неопределенное поведение, тем более их изменение с возможным побочным эффектом, таким как изменение другого массива или что-то еще хуже. - person chqrlie; 18.02.2019

Вы делаете логическую ошибку в своей вспомогательной функции. Вы начинаете копирование с word[j], а не с начала второго слова, поэтому вы собираетесь удалить столько начальных символов, сколько начальных пробелов, как вы видите в выводе.

Обратите внимание, что j=i и i подсчитывают количество начальных пробелов из внешнего цикла.

Кстати у вас должно получиться всего две петли. Поместите условие while в первый цикл for следующим образом: for (int i = 0; i<100 && word[i]==' '; i++).

Чтобы исправить вашу логическую ошибку, вам нужно использовать другой итератор k, инициализированный нулем в самом внутреннем цикле, и использовать word[k] = word[j+1]. Я думаю, это сработает.

person okovko    schedule 16.02.2019
comment
Я не понимаю, разве i и j не должны сбрасываться при разных вызовах функции? Разве я не работаю от 0 до 99, и каждый раз, когда слово [i] является пробелом, мы входим во второй цикл? - person P. Gillich; 16.02.2019
comment
@P.Gillich Я не говорю об отдельных вызовах функции, я говорю об одном вызове. И нет, циклы работают не так. Для i=0 вы повторяете 0 <= j < 100. Итак, до i=99. - person okovko; 16.02.2019
comment
@P.Gillich У меня может быть время посмотреть остальную часть вашего кода позже. Удачи. - person okovko; 16.02.2019
comment
Так почему же он отлично работает при первом вызове? Я не понимаю, я вижу желаемый результат, если я вызываю функцию один раз, но махинации только на второй. - person P. Gillich; 16.02.2019
comment
@P.Gillich Совпадение, наверное. Происходит постоянно. Попробуйте еще несколько случаев. - person okovko; 16.02.2019
comment
Ладно, утром попробую. Большое спасибо! - person P. Gillich; 16.02.2019
comment
Спасибо за предложение, но в конечном итоге логическая ошибка заключалась в том, что я не был осторожен с длиной массива символов, переданного в функцию. Я не думаю, что то, что вы говорите, правильно; Я подаю функцию только по одному массиву символов за раз, и каждый раз, когда я вызываю функцию, индексы i и j сбрасываются, поэтому, в частности, не должно иметь значения, что я не инициализирую индекс внутреннего цикла нулем, на самом деле он должен быть инициализирован тем, что я есть в данный момент. - person P. Gillich; 16.02.2019
comment
@P.Gillich Вы неправильно поняли, что я написал, я не знаю, что еще сказать. Я предлагал вам использовать совсем другую переменную. Я рад, что ты разобрался со своей проблемой. - person okovko; 17.02.2019

У вас есть проблема с переполнением буфера для argv[1] и argv[2], если длина буфера argv[1] меньше 100. Поэтому я думаю, что вам следует использовать цикл for с strlen(word), этого достаточно. Когда вы используете статическую длину со 100 в цикле for, иногда слово получает данные из другого места памяти и делает вашу программу в неопределенном поведении. И другие функции имеют ту же проблему. Я имею в виду функции sortWords и compare.

Вот моя модификация в вашей функции noSpaces, она должна работать.

void noSpaces(char word [100]){
    /*
    This is a function to get rid of spaces in a word
    It does this by scanning for a space and shifting the
    array elements at indices > where the space is
    down by 1 as long as there is still a space
    there.
    */
    for(int i =0; i<strlen(word)-1; i++){
        while(word[i]==' '){
            for(int j = i ; j<strlen(word); j++){
                word[j] = word [j+1];
            }
        }
    }
}
person Loc Tran    schedule 16.02.2019

Вместо того, чтобы пытаться удалить пробелы и отсортировать, что составляет O (N lg N) время выполнения. Вы можете выполнить операцию O(N), просто создав массив, представляющий количество каждой буквы в слове. И просто игнорируйте пробелы при этом.

// Iterate over each character in the string
// For each char in string, increment the count of that character
// in the lettercount array.
// Return the number of unfiltered letters that were counted
int fillLetterCountTable(const char* string, int* lettercount)
{
    int len = strlen(string);
    int valid = 0;

    for (int i = 0; i < len; i++)
    {
       unsigned char index = (unsigned char)(string1[i]);
       if (index ==  ' ')  // ignore spaces
       {
           continue;
       }
       counts[index] += 1;
       valid++;
    }

    return valid;
}

// compare if two strings are anagrams of each other
// return true if string1 and string2 are anagrams, false otherwise
bool compare(const char* string1, const char* string2)
{
    int lettercount1[256] = {0};
    int lettercount2[256] = {0};

    int valid1 = fillLetterCountTable(string1, lettercount1);
    int valid2 = fillLetterCountTable(string2, lettercount2);

    if (valid1 != valid2)
        return false;

    // memcmp(lettercount1, lettercount2, sizeof(lettercount1));
    for (int i = 0; i < 256; i++)
    {
        if (counts1[i] != counts2[i])
            return false;
    }
    return true;
}
person selbie    schedule 16.02.2019
comment
К вашему сведению: это не работает с текстом Unicode, поэтому его практически невозможно использовать в реальных приложениях. - person Alexander; 16.02.2019
comment
OP явно использует тип char. Я рассматриваю это решение как честную игру. Для 16-битного Unicode (аля Windows) просто переключитесь с char на wchar_t и с 256 на 65536). Для 32-битного Unicode (аля Mac) вы были бы правы - нужен лучший подход к хеш-таблице, чтобы не взорвать стек. Подход для этого состоял бы в том, чтобы иметь алгоритм отсортированной хеш-таблицы, который можно было бы использовать как набор, и способ сравнить два набора на равенство. В C++ вы просто использовали бы std::set<wchar_t> и покончили с этим. - person selbie; 16.02.2019
comment
В более общем случае, я думаю, это в основном вопрос о его текущей реализации, и речь идет о C, а не о тестировании анаграмм. Хотя есть ценность в предоставлении хорошего решения! - person okovko; 16.02.2019
comment
@Alexander Александр Сортировка символов внутри слов также не будет работать для текста Unicode. В любом случае анаграммы в целом могут быть плохо определены для Unicode. - person jamesdlin; 16.02.2019
comment
@selbie, хотя мне нравится это решение, и оно пришло мне в голову, я подумал, что было бы более поучительно сделать это так, как я это сделал. В итоге я заставил его работать, кстати. Спасибо! - person P. Gillich; 16.02.2019
comment
@jamesdlin Да, есть несколько возможных интерпретаций анаграммы для Unicode, но, вероятно, наиболее разумной будет иметь одинаковое количество каждого расширенного кластера графем, что пользователь в конечном итоге видит как символ - person Alexander; 16.02.2019