Нужно ли явно обрабатывать отрицательные числа или ноль при суммировании квадратов? - PullRequest
213 голосов
/ 03 октября 2019

У меня недавно был тест в моем классе. Одной из проблем была следующая:

Учитывая число n , напишите функцию на C / C ++, которая возвращает сумму цифр числа в квадрате . (Важно следующее)Диапазон из n равен [- (10 ^ 7), 10 ^ 7]. Пример: если n = 123, ваша функция должна вернуть 14 (1 ^ 2 + 2 ^ 2 + 3 ^ 2 = 14).

Это функция, которую я написал:

int sum_of_digits_squared(int n) 
{
    int s = 0, c;

    while (n) {
        c = n % 10;
        s += (c * c);
        n /= 10;
    }

    return s;
}

Смотрел прямо на меня. Итак, теперь тест вернулся, и я обнаружил, что учитель не дал мне все пункты по причине, которую я не понимаю. По его словам, чтобы моя функция была завершена, я должен был добавить следующую деталь:

int sum_of_digits_squared(int n) 
 {
    int s = 0, c;

    if (n == 0) {      //
        return 0;      //
    }                  //
                       // THIS APPARENTLY SHOULD'VE 
    if (n < 0) {       // BEEN IN THE FUNCTION FOR IT
        n = n * (-1);  // TO BE CORRECT
    }                  //

    while (n) {
        c = n % 10;
        s += (c * c);
        n /= 10;
    }

    return s;
}

Аргументом для этого является то, что число n находится в диапазоне [- (10 ^ 7), 10 ^ 7], так что это может быть отрицательное число. Но я не вижу, где моя собственная версия функции терпит неудачу. Если я правильно понимаю, значение while(n) равно while(n != 0), , а не while (n > 0), поэтому в моей версии функции число n не может не войти впетля. Это будет работать точно так же.

Затем я попробовал обе версии функции на своем компьютере дома, и я получил абсолютно одинаковые ответы для всех примеров, которые я пробовал. Итак, sum_of_digits_squared(-123) равно sum_of_digits_squared(123) (что опять-таки равно 14) (даже без подробностей, которые я, очевидно, должен был добавить). Действительно, если я попытаюсь напечатать на экране цифры числа (от наименьшего до наибольшего значения), в случае 123 я получу 3 2 1, а в случае -123 я получу -3 -2 -1 (чтона самом деле вроде интересно). Но в этой проблеме это не имеет значения, так как мы возводим цифры в квадрат.

Итак, кто же не прав?

РЕДАКТИРОВАТЬ : Плохо, я забыл указать и не знал, что это важно. Версия C, используемая в нашем классе и тестах, должна быть C99 или новее . Поэтому я предполагаю (прочитав комментарии), что моя версия получит правильный ответ в любом случае.

Ответы [ 8 ]

242 голосов
/ 03 октября 2019

Подводя итоги обсуждения, которое просачивалось в комментариях:

  • Нет веских оснований для предварительного тестирования на n == 0. Тест while(n) отлично справится с этим случаем.
  • Вероятно, ваш учитель все еще привык к более раннему времени, когда результат % с отрицательными операндами был определен по-другому. В некоторых старых системах (включая, в частности, ранний Unix на PDP-11, где Деннис Ритчи первоначально разработал C), результат a % b был всегда в диапазоне [0 .. b-1], означая, что -123% 10 было 7. В такой системе был бы необходим предварительный тест для n < 0.

Но второй пункт применяется только к более ранним временам. В текущих версиях стандартов C и C ++ целочисленное деление определено для усечения до 0, поэтому получается, что n % 10 гарантированно даст вам (возможно, отрицательную) последнюю цифру n, даже когда nотрицательный.

Таким образом, ответ на вопрос «Что означает while(n) равен «Точно так же, как while(n != 0)» , иответ на «Будет ли этот код работать должным образом как для отрицательного, так и для положительного n равен «Да, в любом современном компиляторе, соответствующем стандартам». Ответ навопрос «Тогда почему инструктор отметил это?» вероятно, что они не знают о существенном переопределении языка, которое произошло с C в 1999 году и с C ++ в 2010 или около того.

104 голосов
/ 03 октября 2019

Ваш код в порядке

Вы абсолютно правы, а ваш учитель не прав. Нет абсолютно никакой причины добавлять эту дополнительную сложность, поскольку это никак не влияет на результат. Это даже вносит ошибку. (См. Ниже)

Во-первых, отдельная проверка, если n равно нулю, очевидно, совершенно не нужна, и это очень легко реализовать. Если честно, я на самом деле подвергаю сомнению вашу компетентность учителя, если у него есть возражения по этому поводу. Но я думаю, что каждый может время от времени пукнуть. Тем не менее, я ДЕЙСТВИТЕЛЬНО считаю, что while(n) следует изменить на while(n != 0), потому что это добавляет немного большей ясности, даже не стоя дополнительной строки. Это незначительная вещь.

Второй немного более понятен, но он все еще не прав.

Это то, что C11 стандарт 6.5.5.p6 говорит:

Если частное a / b представимо, выражение (a / b) * b + a% b должно быть равно a;в противном случае поведение a / b и a% b не определено.

Сноска гласит:

Это часто называют «усечением до нуля».

Усечение до нуля означает, что абсолютное значение для a/b равно абсолютному значению для (-a)/b для всех a и b, что, в свою очередь, означает, что ваш код идеальнохорошо. Тем не менее, ваш учитель имеет смысл, что вы должны быть осторожны, потому что факт, что вы возводите в квадрат результат, на самом деле имеет решающее значение. Вычисление a%b в соответствии с приведенным выше определением - простая математика, но это может пойти вразрез с вашей интуицией. Для умножения и деления результат является положительным, если операнды имеют знак равенства. Но когда дело доходит до модуля, результат имеет тот же знак, что и операнд first . Например, 7%3==1, но (-7)%(-3)==(-1). Вот фрагмент, демонстрирующий это:

$ cat > main.c 
#include <stdio.h>

void f(int a, int b) 
{
    printf("a: %2d b: %2d a/b: %2d a\%b: %2d (a%b)^2: %2d (a/b)*b+a%b==a: %5s\n",
           a, b ,a/b, a%b, (a%b)*(a%b), (a/b)*b+a%b == a ? "true" : "false");
}

int main(void)
{
    int a=7, b=3;
    f(a,b);
    f(-a,b);
    f(a,-b);
    f(-a,-b);
}

$ gcc main.c -Wall -Wextra -pedantic -std=c99

$ ./a.out
a:  7 b:  3 a/b:  2 a%b:  1 (a%b)^2:  1 (a/b)*b+a%b==a:  true
a: -7 b:  3 a/b: -2 a%b: -1 (a%b)^2:  1 (a/b)*b+a%b==a:  true
a:  7 b: -3 a/b: -2 a%b:  1 (a%b)^2:  1 (a/b)*b+a%b==a:  true
a: -7 b: -3 a/b:  2 a%b: -1 (a%b)^2:  1 (a/b)*b+a%b==a:  true

Итак, по иронии судьбы, ваш учитель доказал свою точку зрения, ошибаясь.

Код вашего учителя ошибочен

Да, это на самом делеявляется. Если входное значение равно INT_MIN И архитектура является дополнением до двух И шаблон битов, где бит знака равен 1, а все биты значения равны 0, НЕ является значением ловушки (использование дополнения до двух без значений прерываний очень распространено), тогда ваш Код учителя выдаст неопределенное поведение в строке n = n * (-1). Ваш код - если даже немного - лучше , чем его. И если учесть небольшую ошибку, сделав код ненужным сложным и получив абсолютно нулевое значение, я бы сказал, что ваш код НАМНОГО лучше.

Другими словами, в компиляциях, где INT_MIN = -32768 (даже еслирезультирующая функция не может получить входные данные <-32768 или> 32767), ввод valid -32768 вызывает неопределенное поведение, поскольку результат - (- 32768i16) не может быть выражен как 16-разрядное целое число,(На самом деле, -32768, вероятно, не приведет к неправильному результату, потому что - (- - 32768i16) обычно оценивается как -32768i16, и ваша программа корректно обрабатывает отрицательные числа.) (SHRT_MIN может быть -32768 или -32767, в зависимости от компилятора.)

Но ваш учитель прямо заявил, что n может быть в диапазоне [-10 ^ 7;10 ^ 7]. 16-битное целое число слишком мало;вам придется использовать [как минимум] 32-разрядное целое число. Использование int может показаться безопасным для его кода, за исключением того, что int не обязательно является 32-разрядным целым числом. Если вы компилируете для 16-битной архитектуры, оба ваших фрагмента кода имеют недостатки. Но ваш код все еще намного лучше, потому что этот сценарий повторно вводит ошибку с INT_MIN, упомянутую выше с его версией. Чтобы избежать этого, вы можете написать long вместо int, что является 32-разрядным целым числом в любой архитектуре. A long гарантированно может содержать любое значение в диапазоне [-2147483647;2147483647]. C11 Стандарт 5.2.4.2.1 LONG_MIN часто -2147483648, но максимально (да, максимум, это отрицательное число) допустимое значение для LONG_MIN равно 2147483647.

Какие изменения я бы сделал в вашем коде?

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

  • Имена переменных могут быть немногонемного лучше, но это короткая функция, которую легко понять, поэтому это не имеет большого значения.
  • Вы можете изменить условие с n на n!=0. Семантически это на 100% эквивалентно, но делает его немного понятнее.
  • Переместите объявление c (которое я переименовал в digit) внутри цикла while, поскольку оно используется только там.
  • Измените тип аргумента на long, чтобы он мог обрабатывать весь входной набор.
int sum_of_digits_squared(long n) 
{
    long sum = 0;

    while (n != 0) {
        int digit = n % 10;
        sum += (digit * digit);
        n /= 10;
    }

    return sum;
}

На самом деле, это может немного вводить в заблуждение, поскольку - как упоминалось выше - переменнаяdigit может получить отрицательное значение, но сама по себе цифра никогда не бывает ни положительной, ни отрицательной. Есть несколько способов обойти это, но это ДЕЙСТВИТЕЛЬНО придирчиво, и мне было бы наплевать на такие мелкие детали. Особенно отдельная функция для последней цифры заходит слишком далеко. По иронии судьбы, это одна из вещей, которую на самом деле решает код вашего учителя.

  • Измените sum += (digit * digit) на sum += ((n%10)*(n%10)) и полностью пропустите переменную digit.
  • Измените знакdigit если отрицательный.
  • Создать отдельную функцию, которая извлекает последнюю цифру. int last_digit(long n) { int digit=n%10; if (digit>=0) return digit; else return -digit; }
  • Просто назовите его c, как вы это делаете изначально. Это имя переменной не дает никакой полезной информации, но, с другой стороны, оно также не вводит в заблуждение.

Но, если честно, в этот момент вам следует перейти к более важной работе. :)

19 голосов
/ 03 октября 2019

Мне не совсем нравится ни ваша версия, ни версия вашего учителя. Версия вашего учителя делает дополнительные тесты, которые вы правильно указали, не нужны. Оператор мода C не является правильным математическим модом: мод с отрицательным числом 10 даст отрицательный результат (правильный математический модуль всегда неотрицателен). Но так как вы все равно возводите в квадрат, без разницы.

Но это далеко не очевидно, поэтому я бы добавил к вашему коду не проверки вашего учителя, а большой комментарий, объясняющий, почему он работает. Например:

/ * ПРИМЕЧАНИЕ. Это работает для отрицательных значений, поскольку модуль получает квадрат * /

10 голосов
/ 03 октября 2019

ПРИМЕЧАНИЕ: Поскольку я писал этот ответ, вы разъяснили, что используете C. Большая часть моего ответа касается C ++. Однако, поскольку в вашем заголовке по-прежнему есть C ++, а вопрос все еще помечен как C ++, я решил ответить в любом случае, если это все еще полезно для других людей, тем более что большинство ответов, которые я до сих пор видел, в основном неудовлетворительные.

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

Во-первых, эта часть праваздесь:

if (n == 0) {
        return 0;
}

В C ++ это в основном то же самое, что и :

if (!n) {
        return 0;
}

Это означает, что ваше while эквивалентно чему-то вроде этого:

while(n != 0) {
    // some implementation
}

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

На самом деле, это утверждение if не особенно полезно, если я не ошибаюсь.

Во второй части все становится волосатым:

if (n < 0) {
    n = n * (-1);
}  

Суть проблемы в том, что вывод модуля отрицательного числа выводит.

В современном C ++, это, кажется, в основном хорошо определено :

Двоичный / оператор дает частное, а двоичный оператор% возвращает остаток от деления первого выражения на второе,Если второй операнд / или% равен нулю, поведение не определено. Для целочисленных операндов оператор / дает алгебраический фактор с любой отброшенной дробной частью;если частное a / b представимо в типе результата, (a / b) * b + a% b равно a.

И позже:

Если оба операнда неотрицательны, то остаток неотрицателен;если нет, знак остатка определяется реализацией.

Как правильно указывает автор цитируемого ответа, важная часть этого уравнения здесь:

(a / b) * b + a% b

Принимая пример вашего случая, вы получите что-то вроде этого:

-13/ 10 = -1 (integer truncation)
-1 * 10 = -10
-13 - (-10) = -13 + 10 = -3 

Единственноеcatch - это последняя строка:

Если оба операнда неотрицательны, то остаток неотрицателен;в противном случае знак остатка определяется реализацией.

Это означает, что в подобном случае только знак , по-видимому, определяется реализацией. Это не должно быть проблемой в вашем случае, потому что, поскольку вы все равно возводите это значение в квадрат.

При этом имейте в виду, что это не обязательно относится к более ранним версиям C ++ или C99. Если это то, что использует ваш профессор, это может быть причиной.


РЕДАКТИРОВАТЬ: Нет, я не прав. Похоже, что относится к C99 или более поздней версии. :

C99 требует, чтобы при представлении a / b:

(a / b) *b + a% b должно равняться

А другому месту :

Когда целые числа делятся и деление является неточным, еслиоба операнда положительны, результат оператора / является наибольшим целым числом, меньшим алгебраического отношения, а результат оператора% положителен. Если любой из операндов отрицателен, то, является ли результат оператора / наибольшим целым числом, меньшим, чем алгебраический фактор, или наименьшим целым числом, большим, чем алгебраический фактор, определяется реализацией, как и знак результата оператора%. Если частное a / b представимо, выражение (a / b) * b + a% b должно равняться a.

Указывает ли ANSI C или ISO C, что -5%10 должно быть?

Итак, да. Даже в C99 это, кажется, не влияет на вас. Уравнение то же самое.

7 голосов
/ 16 октября 2019

Как уже отмечали другие, специальная обработка для n == 0 бессмысленна, поскольку для каждого серьезного программиста на C очевидно, что "while (n)" выполняет свою работу.

Поведение для n<0 не так уж очевидно, поэтому я бы предпочел увидеть эти две строки кода: </p>

if (n < 0) 
    n = -n;

или хотя бы комментарий:

// don't worry, works for n < 0 as well

Честно говоря, в какое времяВы начинаете считать, что п может быть отрицательным? При написании кода или при чтении замечаний вашего учителя?

4 голосов
/ 16 октября 2019

Это напоминает мне о задании, которое я провалил

В 90-х. Лектор рассказывал о циклах, и, короче говоря, нашим заданием было написать функцию, которая возвращала бы число цифр для любого заданного целого числа> 0.

Так, например, количество цифрв 321 будет 3.

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

Но использование циклов не было явно указано, поэтому я: took the log, stripped away the decimals, added 1 и впоследствии подвергся критике перед всем классом.

Дело в том, что целью задания было проверить наше понимание того, что мы узнали во время лекций . Из лекции, которую я получил, я узнал, что учитель информатики был немного придурком (но, может быть, подонком с планом?)


В вашей ситуации:

написатьфункция на C / C ++, которая возвращает сумму цифр в квадрате числа

Я бы определенно дал два ответа:

  • правильный ответ (сначала возводя квадрат в квадрат) и
  • неправильный ответ в соответствии с примером, просто чтобы он был счастлив ;-)
1 голос
/ 17 октября 2019

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

Я не могу понять, что достаточно «использовать значимые имена переменных» .

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

Еще одна вещь, которую я склонен видеть в коде C, это люди, которые стараются выглядеть умными. Вместо использования while (n! = 0) я покажу всем, насколько я умен, написав while (n) , потому что это означает то же самое. Что ж, так и есть в вашем компиляторе, но, как вы и предполагали, более старая версия вашего учителя не реализовала его таким же образом.

Типичным примером является ссылка на индекс в массиве при одновременном его увеличении; Numbers [i ++] = iPrime;

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

Мегабайт дискового пространства дешевле, чем рулон туалетной бумаги, для ясности, а не для экономии места, ваши коллеги-программисты будут счастливее.

0 голосов
/ 16 октября 2019

Я бы не стал спорить о том, лучше ли оригинальное или современное определение «%», но любой, кто пишет два оператора return в такую ​​короткую функцию, вообще не должен учить программированию на С. Дополнительный возврат - это оператор goto, и мы не используем goto в C. Более того, код без проверки нуля будет иметь тот же результат, дополнительный возврат затруднит чтение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...