web-dev-qa-db-fra.com

Dois-je traiter explicitement les nombres négatifs ou zéro lors de la sommation des chiffres au carré?

J'ai récemment passé un test dans ma classe. L'un des problèmes était le suivant:

Étant donné un nombre n, écrivez une fonction en C/C++ qui renvoie la somme des chiffres du nombre au carré . (Ce qui suit est important). La plage de n est [10 ^ 7), 10 ^ 7]. Exemple: Si --- (n = 123, votre fonction doit renvoyer 14 (1 ^ 2 + 2 ^ 2 + 3 ^ 2 = 14).

Voici la fonction que j'ai écrite:

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

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

    return s;
}

Il me semblait bien. Alors maintenant, le test est revenu et j'ai trouvé que le professeur ne m'a pas donné tous les points pour une raison que je ne comprends pas. Selon lui, pour que ma fonction soit complète, j'aurais dû ajouter le détail suivant:

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;
}

L'argument en est que le nombre n est dans la plage [10 ^ 7), 10 ^ 7], il peut donc être un nombre négatif. Mais je ne vois pas où ma propre version de la fonction échoue. Si je comprends bien, la signification de while(n) est while(n != 0), --- (paswhile (n > 0), donc dans ma version de la fonction le nombre - n ne manquerait pas d'entrer dans la boucle. Cela fonctionnerait tout de même.

Ensuite, j'ai essayé les deux versions de la fonction sur mon ordinateur à la maison et j'ai obtenu exactement les mêmes réponses pour tous les exemples que j'ai essayés. Ainsi, sum_of_digits_squared(-123) est égal à sum_of_digits_squared(123) (qui, encore une fois, est égal à 14) (Même sans les détails que j'aurais apparemment dû ajouter). En effet, si j'essaie d'imprimer à l'écran les chiffres du nombre (du moins au plus important), dans le cas 123 J'obtiens 3 2 1 Et dans le -123 cas, je reçois -3 -2 -1 (ce qui est en fait assez intéressant). Mais dans ce problème, cela n'aurait pas d'importance puisque nous quadrillons les chiffres.

Alors, qui a tort?

MODIFIER : Mon mauvais, j'ai oublié de préciser et je ne savais pas que c'était important. La version de C utilisée dans notre classe et nos tests doit être C99 ou plus récente. Donc je suppose (en lisant les commentaires) que ma version obtiendrait la bonne réponse de quelque façon que ce soit.

220
user010517720

Résumant une discussion qui a été percolante dans les commentaires:

  • Il n'y a aucune bonne raison de tester à l'avance n == 0. Le test while(n) gérera parfaitement ce cas.
  • Il est probable que votre professeur soit encore habitué à des temps antérieurs, lorsque le résultat de % Avec des opérandes négatifs a été défini différemment. Sur certains anciens systèmes (y compris, notamment, le début d'Unix sur un PDP-11, où Dennis Ritchie a initialement développé C), le résultat de a % b Était toujours dans la plage [0 .. b-1], ce qui signifie que -123% 10 était 7. Sur un tel système, le test préalable de n < 0 serait nécessaire.

Mais la deuxième puce ne s'applique qu'aux temps antérieurs. Dans les versions actuelles des normes C et C++, la division entière est définie pour tronquer vers 0, il s'avère donc que n % 10 Est garanti pour vous donner le dernier chiffre (éventuellement négatif) de n même lorsque n est négatif.

Donc, la réponse à la question "Quelle est la signification de while(n)?" est "Exactement la même chose que while(n != 0)", et la réponse à "Ce code fonctionnera-t-il correctement aussi bien pour les négatifs que pour les positifs n?" est "Oui, sous tout compilateur moderne conforme aux normes." La réponse à la question "Alors pourquoi l'instructeur l'a-t-elle notée?" est probablement qu'ils ne sont pas au courant d'une redéfinition significative du langage qui est arrivée à C en 1999 et à C++ en 2010 ou alors.

245
Steve Summit

Votre code est parfaitement bien

Vous avez absolument raison et votre professeur a tort. Il n'y a absolument aucune raison d'ajouter cette complexité supplémentaire, car cela n'affecte pas du tout le résultat. Il introduit même un bug. (Voir ci-dessous)

Tout d'abord, la vérification séparée si n est zéro est évidemment complètement inutile et cela est très facile à réaliser. Pour être honnête, je remets en question la compétence de vos enseignants s'il a des objections à ce sujet. Mais je suppose que tout le monde peut avoir un pet de cerveau de temps en temps. Cependant, je pense que while(n) devrait être changé en while(n != 0) car cela ajoute un peu plus de clarté sans même coûter une ligne supplémentaire. C'est une chose mineure cependant.

Le second est un peu plus compréhensible, mais il a toujours tort.

C'est ce que dit le norme C11 6.5.5.p6 :

Si le quotient a/b est représentable, l'expression (a/b) * b + a% b doit être égale à a; sinon, le comportement de a/b et de% b n'est pas défini.

La note de bas de page dit ceci:

Ceci est souvent appelé "troncature vers zéro".

La troncature vers zéro signifie que la valeur absolue de a/b Est égale à la valeur absolue de (-a)/b Pour tous les a et b, ce qui signifie que votre le code est parfaitement bien.

Modulo est un calcul facile, mais peut être contre-intuitif

Cependant, votre professeur a un point que vous devez être prudent, car le fait que vous évaluez le résultat est en fait crucial ici. Calculer a%b Selon la définition ci-dessus est un calcul facile, mais cela pourrait aller à l'encontre de votre intuition. Pour la multiplication et la division, le résultat est positif si les opérandes ont un signe égal. Mais quand il s'agit de modulo, le résultat a le même signe que le premier opérande . Le deuxième opérande n'affecte pas du tout le signe. Par exemple, 7%3==1 Mais (-7)%(-3)==(-1).

Voici un extrait de démonstration:

$ 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

Donc, ironiquement, votre professeur a prouvé son point de vue en se trompant.

Le code de votre professeur est défectueux

Oui, en fait. Si l'entrée est INT_MIN ET que l'architecture est un complément à deux ET que le modèle de bits où le bit de signe est 1 et tous les bits de valeur sont 0 n'est PAS une valeur d'interruption (l'utilisation du complément à deux sans valeurs d'interruption est très courante), alors votre le code de l'enseignant donnera un comportement indéfini sur la ligne n = n * (-1). Votre code est - si jamais légèrement - meilleur que le sien. Et compte tenu de l'introduction d'un petit bug en rendant le code inutile et complexe et en obtenant une valeur absolument nulle, je dirais que votre code est BEAUCOUP mieux.

En d'autres termes, dans les compilations où INT_MIN = -32768 (même si la fonction résultante ne peut pas recevoir une entrée <-32768 ou> 32767), le valide l'entrée de -32768 provoque un comportement indéfini, car le résultat de - 32768i16) ne peut pas être exprimé sous la forme d'un entier 16 bits. (En fait, -32768 ne provoquerait probablement pas un résultat incorrect, car - (- 32768i16) est généralement évalué à -32768i16 et votre programme gère correctement les nombres négatifs.) (SHRT_MIN peut être -32768 ou -32767, selon le compilateur.)

Mais votre professeur a explicitement déclaré que n peut être compris entre [-10 ^ 7; 10 ^ 7]. Un entier de 16 bits est trop petit; vous devez utiliser [au moins] un entier 32 bits. L'utilisation de int peut sembler sécuriser son code, sauf que int n'est pas nécessairement un entier 32 bits. Si vous compilez pour une architecture 16 bits, vos deux extraits de code sont défectueux. Mais votre code est encore bien meilleur car ce scénario réintroduit le bogue avec INT_MIN Mentionné ci-dessus avec sa version. Pour éviter cela, vous pouvez écrire long au lieu de int, qui est un entier 32 bits sur l'une ou l'autre architecture. Un long est garanti pour pouvoir contenir n'importe quelle valeur dans la plage [-2147483647; 2147483647]. --- (C11 Standard 5.2.4.2.1LONG_MIN Est souvent -2147483648 Mais la valeur maximale (oui, maximum, c'est un nombre négatif) autorisée pour LONG_MIN est 2147483647.

Quelles modifications dois-je apporter à votre code?

Votre code est très bien tel qu'il est, donc ce ne sont pas vraiment des plaintes. C'est plus comme ça si j'ai vraiment, vraiment besoin de dire quelque chose sur votre code, il y a quelques petites choses qui pourraient le rendre un peu plus clair.

  • Les noms des variables pourraient être un peu meilleurs, mais c'est une fonction courte qui est facile à comprendre, donc ce n'est pas grave.
  • Vous pouvez modifier la condition de n à n!=0. Sémantiquement, c'est 100% équivalent, mais cela le rend un peu plus clair.
  • Déplacez la déclaration de c (que j'ai renommée digit) à l'intérieur de la boucle while car elle n'y est utilisée que.
  • Remplacez le type d'argument par long pour vous assurer qu'il peut gérer l'ensemble des entrées.
int sum_of_digits_squared(long n) 
{
    long sum = 0;

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

    return sum;
}

En fait, cela peut être un peu trompeur car - comme mentionné ci-dessus - la variable digit peut obtenir une valeur négative, mais un chiffre en soi n'est jamais ni positif ni négatif. Il y a plusieurs façons de contourner cela, mais c'est vraiment une piqûre, et je ne me soucierais pas de ces petits détails. En particulier, la fonction séparée pour le dernier chiffre va trop loin. Ironiquement, c'est l'une des choses que le code de vos enseignants résout réellement.

  • Remplacez sum += (digit * digit) par sum += ((n%10)*(n%10)) et ignorez complètement la variable digit.
  • Modifiez le signe de digit s'il est négatif. Mais je déconseille fortement de rendre le code plus complexe juste pour donner un sens à un nom de variable. C'est une très forte odeur de code.
  • Créez une fonction distincte qui extrait le dernier chiffre. int last_digit(long n) { int digit=n%10; if (digit>=0) return digit; else return -digit; } Ceci est utile si vous souhaitez utiliser cette fonction ailleurs.
  • Nommez-le simplement c comme vous le faites à l'origine. Ce nom de variable ne donne aucune information utile, mais d'un autre côté, il n'est pas trompeur non plus.

Mais pour être honnête, à ce stade, vous devez passer à un travail plus important. :)

107
klutt

Je n'aime pas complètement votre version ou votre professeur. La version de votre professeur effectue les tests supplémentaires que vous signalez correctement comme inutiles. L'opérateur de mod de C n'est pas un mod mathématique approprié: un nombre négatif mod 10 produira un résultat négatif (le module mathématique correct est toujours non négatif). Mais puisque vous le quadrillez de toute façon, aucune différence.

Mais cela est loin d'être évident, donc j'ajouterais à votre code non pas les vérifications de votre professeur, mais un gros commentaire qui explique pourquoi cela fonctionne. Par exemple.:

/ * REMARQUE: cela fonctionne pour les valeurs négatives, car le module devient carré * /

20

REMARQUE: Tandis que j'écrivais cette réponse, vous avez précisé que vous utilisez C. La majorité de ma réponse concerne le C++. Cependant, comme votre titre contient toujours C++ et que la question est toujours étiquetée C++, j'ai choisi de répondre de toute façon au cas où cela serait encore utile à d'autres personnes, d'autant plus que la plupart des réponses que j'ai vues jusqu'à présent sont pour la plupart insatisfaisantes.

En C++ moderne (Remarque: je ne sais pas vraiment où se situe C à ce sujet), votre professeur semble avoir tort sur les deux points.

La première est cette partie ici:

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

En C++, ceci est fondamentalement la même chose que :

if (!n) {
        return 0;
}

Cela signifie que votre temps équivaut à quelque chose comme ceci:

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

Cela signifie que puisque vous quittez simplement votre if si le moment ne s'exécute pas de toute façon, il n'y a vraiment aucune raison de le mettre ici, car ce que vous faites après la boucle et dans le if sont de toute façon équivalents. Bien que je devrais dire que c'est pour une raison quelconque, ils étaient différents, vous auriez besoin de cela si.

Donc vraiment, cette déclaration if n'est pas particulièrement utile, sauf erreur.

La deuxième partie est l'endroit où les choses deviennent velues:

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

Le cœur du problème est de savoir ce que la sortie du module d'un nombre négatif produit.

En C++ moderne, cela semble être généralement bien défini :

L'opérateur binaire/donne le quotient, et l'opérateur binaire% donne le reste de la division de la première expression par la seconde. Si le deuxième opérande de/ou% est nul, le comportement n'est pas défini. Pour les opérandes intégraux, l'opérateur/fournit le quotient algébrique avec toute partie fractionnaire rejetée; si le quotient a/b est représentable dans le type du résultat, (a/b) * b + a% b est égal à a.

Et ensuite:

Si les deux opérandes sont non négatifs, le reste est non négatif; sinon, le signe du reste est défini par l'implémentation.

Comme l'a correctement montré l'affiche de la réponse citée, la partie importante de cette équation ici:

(a/b) * b + a% b

En prenant un exemple de votre cas, vous obtiendrez quelque chose comme ceci:

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

Le seul hic, c'est cette dernière ligne:

Si les deux opérandes sont non négatifs, le reste est non négatif; sinon, le signe du reste est défini par l'implémentation.

Cela signifie que dans un cas comme celui-ci, seul le signe semble être défini par l'implémentation. Cela ne devrait pas être un problème dans votre cas parce que, parce que vous évaluez cette valeur de toute façon.

Cela dit, gardez à l'esprit que cela ne s'applique pas nécessairement aux versions antérieures de C++ ou C99. Si c'est ce que votre professeur utilise, cela pourrait être la raison.


EDIT: Non, je me trompe. Cela semble être le cas pour C99 ou version ultérieure également :

C99 exige que lorsque a/b est représentable:

(a/b) * b + a% b est égal à a

Et n autre endroit :

Lorsque les entiers sont divisés et que la division est inexacte, si les deux opérandes sont positifs, le résultat de l'opérateur/est le plus grand entier inférieur au quotient algébrique et le résultat de l'opérateur% est positif. Si l'un des opérandes est négatif, que le résultat de l'opérateur/soit le plus grand entier inférieur au quotient algébrique ou le plus petit entier supérieur au quotient algébrique est défini par l'implémentation, tout comme le signe du résultat de l'opérateur%. Si le quotient a/b est représentable, l'expression (a/b) * b + a% b doit être égale à a.

ANSI C ou ISO C spécifie-t-il ce que -5% 10 devrait être?

Donc voilà. Même en C99, cela ne semble pas vous affecter. L'équation est la même.

10
Chipster

Comme d'autres l'ont souligné, le traitement spécial pour n == 0 est un non-sens, car pour tout programmeur C sérieux, il est évident que "while (n)" fait le travail.

Le comportement pour n <0 n'est pas si évident, c'est pourquoi je préférerais voir ces 2 lignes de code:

if (n < 0) 
    n = -n;

ou au moins un commentaire:

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

Honnêtement, à quelle heure avez-vous commencé à considérer que n pourrait être négatif? Lors de l'écriture du code ou lors de la lecture des remarques de votre professeur?

8
C.B.

Cela me rappelle une mission que j'ai échoué

Retour dans les années 90. Le conférencier avait poussé sur les boucles et, pour faire court, notre mission était d'écrire une fonction qui retournerait le nombre de chiffres pour un entier donné> 0.

Ainsi, par exemple, le nombre de chiffres dans 321 serait 3.

Bien que l'affectation ait simplement dit d'écrire une fonction qui renvoyait le nombre de chiffres, l'attente était que nous utiliserions une boucle qui divise par 10 jusqu'à ce que ... vous l'obteniez, comme couvert par le conférence .

Mais l'utilisation de boucles n'était pas explicitement indiquée, donc je: took the log, stripped away the decimals, added 1 et a ensuite été fustigé devant toute la classe.

Le point est, le but de la mission était de tester notre compréhension de ce que nous avions appris au cours des conférences. De la conférence que j'ai reçue, j'ai appris que le professeur d'informatique était un peu idiot (mais peut-être un idiot avec un plan?)


Dans votre situation:

écrire une fonction en C/C++ qui retourne la somme des chiffres du nombre au carré

J'aurais certainement fourni deux réponses:

  • la bonne réponse (en mettant d'abord le nombre au carré), et
  • la réponse incorrecte conformément à l'exemple, juste pour le garder heureux ;-)
5
SlowLearner

Généralement, dans les affectations, toutes les marques ne sont pas attribuées simplement parce que le code fonctionne. Vous obtenez également des notes pour rendre une solution facile à lire, efficace et élégante. Ces choses ne s'excluent pas toujours mutuellement.

Celui que je ne peux pas assez stries est "utiliser des noms de variables significatifs".

Dans votre exemple, cela ne fait pas beaucoup de différence, mais si vous travaillez sur un projet avec un million de lignes de lisibilité du code devient très important.

Une autre chose que j'ai tendance à voir avec le code C, c'est que les gens essaient de paraître intelligents. Plutôt que d'utiliser while (n! = 0) je montrerai à tout le monde à quel point je suis intelligent en écrivant while ( n) car cela signifie la même chose. C'est le cas dans le compilateur que vous avez, mais comme vous l'avez suggéré, l'ancienne version de votre professeur ne l'a pas implémentée de la même manière.

Un exemple courant consiste à référencer un index dans un tableau tout en l'incrémentant en même temps; Nombres [i ++] = iPrime;

Maintenant, le prochain programmeur qui travaille sur le code doit savoir si je suis incrémenté avant ou après l'affectation, juste pour que quelqu'un puisse se montrer.

Un mégaoctet d'espace disque est moins cher qu'un rouleau de papier toilette, optez pour la clarté plutôt que d'essayer d'économiser de l'espace, vos collègues programmeurs seront plus heureux.

1
Paul McCarthy

L'énoncé du problème prête à confusion, mais l'exemple numérique clarifie la signification de la somme des chiffres du nombre au carré. Voici une version améliorée:

Écrivez une fonction dans le sous-ensemble commun de C et C++ qui prend un entier n dans la plage [- 107, dix7] et renvoie la somme des carrés des chiffres de sa représentation en base 10. Exemple: si n est 123, votre fonction devrait retourner 14 (12 + 22 + 32 = 14).

La fonction que vous avez écrite est correcte sauf pour 2 détails:

  • L'argument doit avoir le type long pour s'adapter à toutes les valeurs de la plage spécifiée, car le type long est garanti par la norme C d'avoir au moins 31 bits de valeur, d'où une plage suffisante pour représenter toutes les valeurs en [- 107, dix7]. (Notez que le type int est suffisant pour le type de retour, dont la valeur maximale est 568.)
  • Le comportement de % pour les opérandes négatifs n'est pas intuitif et ses spécifications variaient entre la norme C99 et les éditions précédentes. Vous devez documenter pourquoi votre approche est valable même pour les entrées négatives.

Voici une version modifiée:

int sum_of_digits_squared(long n) {
    int s = 0;

    while (n != 0) {
        /* Since integer division is defined to truncate toward 0 in C99 and C++98 and later,
           the remainder of this division is positive for positive `n`
           and negative for negative `n`, and its absolute value is the last digit
           of the representation of `n` in base 10.
           Squaring this value yields the expected result for both positive and negative `c`.
           dividing `n` by 10 effectively drops the last digit in both cases.
           The loop will not be entered for `n == 0`, producing the correct result `s = 0`.
         */
        int c = n % 10;
        s += c * c;
        n /= 10;
    }
    return s;
}

La réponse de l'enseignant a plusieurs défauts:

  • type int peut avoir une plage de valeurs insuffisante.
  • il n'est pas nécessaire de cas particulier la valeur 0.
  • la négation des valeurs négatives n'est pas nécessaire et peut avoir un comportement indéfini pour n = INT_MIN.

Étant donné les contraintes supplémentaires dans l'énoncé du problème (C99 et plage de valeurs pour n), seul le premier défaut est un problème. Le code supplémentaire produit toujours les bonnes réponses.

Vous devriez obtenir une bonne note dans ce test, mais l'explication est requise dans un test écrit pour montrer votre compréhension des problèmes de n négatifs, sinon l'enseignant peut supposer que vous n'étiez pas au courant et que vous avez juste eu de la chance. Lors d'un examen oral, vous auriez obtenu une question et votre réponse l'aurait clouée.

Je ne dirais pas si la définition originale ou moderne de '%' est meilleure, mais quiconque écrit deux déclarations de retour dans une fonction aussi courte ne devrait pas du tout enseigner la programmation C. Extra return est une instruction goto et nous n'utilisons pas goto en C. De plus, le code sans la vérification zéro aurait le même résultat, le retour supplémentaire en a rendu la lecture plus difficile.

0
Peter Krassoi