web-dev-qa-db-ja.com

二乗数字を合計するときに負の数またはゼロを明示的に処理する必要がありますか?

最近、クラスでテストを受けました。問題の1つは次のとおりです。

数値nを指定して、数値の桁の合計を返す関数をC/C++で記述しますsquared。 (以下が重要です)。 rangenは[-(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)notwhile (n > 0)なので、私のバージョンでは関数番号nはループに入るのに失敗しません。同じように機能します。

次に、自宅のコンピューターで両方のバージョンの関数を試してみましたが、試したすべての例でまったく同じ答えが得られました。したがって、sum_of_digits_squared(-123)sum_of_digits_squared(123)と等しくなります(これも_14_と等しくなります)。確かに、数字の数字(重要度が最も低いものから最大のもの)を画面に印刷しようとすると、_123_の場合に_3 2 1_が、_-123_の場合に_-3 -2 -1_(実際には一種の興味深い)。しかし、この問題では、数字を2乗するので問題にはなりません。

だから、誰が間違っていますか?

[〜#〜] edit [〜#〜]:私の悪いところ、指定するのを忘れて、それが重要だとは知らなかった。クラスとテストで使用されるCのバージョンは、C99またはnewerでなければなりません。だから、(コメントを読んで)私のバージョンが何らかの形で正しい答えを得ると思います。

220
user010517720

コメントに浸透している議論を要約する:

  • _n == 0_を事前にテストする正当な理由はありません。 while(n)テストはそのケースを完全に処理します。
  • 負のオペランドを使用した_%_の結果が異なる方法で定義された場合、教師は以前よりも慣れている可能性があります。一部の古いシステム(特に、デニスリッチーが最初にCを開発したPDP-11の初期のUnixを含む)では、_a % b_の結果はalwaysの範囲で_[0 .. b-1]_でした-123%10が7であることを意味します。このようなシステムでは、_n < 0_の事前テストが必要になります。

ただし、2番目の箇条書きは以前の時間にのみ適用されます。 CおよびC++標準の両方の現在のバージョンでは、整数除算は0に向かって切り捨てるように定義されているため、nの最後の桁(負の場合もある)が_n % 10_によって保証されることがわかります。 nが負の場合でも。

したがって、質問への答え"while(n)?の意味は何ですか?""while(n != 0)"とまったく同じです、そして、「このコードはネガティブおよびポジティブn?に対して適切に機能しますか?」に対する答えは"はい、最新の標準準拠コンパイラの下で。"質問への答え「では、なぜインストラクターはそれを書き留めたのですか?」はおそらく、1999年にCに、2010年にC++に起こった重要な言語再定義を知らないということです。そう。

245
Steve Summit

あなたのコードは完璧です

あなたは絶対に正しいですし、あなたの教師は間違っています。結果にまったく影響を与えないため、このような複雑さを追加する理由はまったくありません。バグも発生します。 (下記参照)

まず、nがゼロであるかどうかの個別のチェックは明らかに完全に不要であり、これは非常に簡単に実現できます。正直に言うと、先生がこれについて異議を唱えているのかどうか、先生の能力に疑問を持っています。しかし、私は誰もが時々脳のおならを持つことができると思います。ただし、while(n)while(n != 0)に変更する必要があると思います。これは、余分な行を追加することなく、少しわかりやすくするためです。しかし、それは些細なことです。

2番目はもう少しわかりやすいですが、彼はまだ間違っています。

これが 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オペランドと同じ符号を持ちます。 2番目のオペランドは符号にまったく影響しません。たとえば、_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_で、かつアーキテクチャが2の補数であり、かつ符号ビットが1ですべての値ビットが0であるビットパターンがトラップ値ではない場合(トラップ値なしで2の補数を使用するのは非常に一般的です) 教師のコードは未定義の動作をもたらしますn = n * (-1)。あなたのコードは-少しでも-彼よりもbetterです。そして、コードを不必要に複雑にし、絶対にゼロの値を取得することで小さなバグを導入することを検討すると、あなたのコードははるかに優れていると思います。

言い換えると、INT_MIN = -32768のコンパイルでは(結果の関数が<-32768または> 32767の入力を受信できない場合でも)、valid-32768の入力は、-(-32768i16)の結果を16ビット整数として表現できないため、未定義の動作を引き起こします。 (実際、-(​​-32768i16)は通常-32768i16に評価され、プログラムは負の数を正しく処理するため、-32768はおそらく誤った結果を引き起こしません。)(SHRT_MINはコンパイラに応じて-32768または-32767になる可能性があります。)

しかし、先生はnの範囲は[-10 ^ 7; 10 ^ 7]。 16ビット整数は小さすぎます。 [少なくとも] 32ビット整数を使用する必要があります。 intが必ずしも32ビット整数であるとは限らないことを除いて、intを使用するとコードが安全になるように思えるかもしれません。 16ビットアーキテクチャ用にコンパイルする場合、両方のコードスニペットに欠陥があります。しかし、このシナリオでは、上記の_INT_MIN_のバージョンでバグが再導入されるため、コードはさらに改善されます。これを回避するには、どちらのアーキテクチャでも32ビット整数であるlongの代わりにintを記述できます。 longは、[-2147483647;の範囲の任意の値を保持できることが保証されています。 2147483647]。 C11 Standard 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は負の値を取得できますが、数字自体は正でも負でもありません。これを回避するにはいくつかの方法がありますが、これは本当に細かな作業であり、そのような細かい部分は気にしません。特に、最後の桁の個別の関数は、それを取りすぎています。皮肉なことに、これは教師のコードが実際に解決することの1つです。

  • 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と名前を付けてください。その変数名は有用な情報を提供しませんが、一方で、誤解を招くものでもありません。

しかし、正直なところ、この時点で、より重要な作業に進む必要があります。 :)

107
klutt

あなたのバージョンも先生のバージョンも完全に好きではありません。あなたの先生のバージョンは、あなたが正しく指摘する必要のない追加のテストを行います。 Cのmod演算子は適切な数学的modではありません:負の数mod 10は負の結果を生成します(適切な数学的係数は常に負ではありません)。しかし、とにかく二乗しているので、違いはありません。

しかし、これは明らかではないので、あなたのコードに教師のチェックではなく、なぜ機能するのかを説明する大きなコメントを追加します。例えば。:

/ *注:モジュラスは2乗するため、これは負の値に対して機能します* /

20

注:この答えを書いているとき、あなたはCを使用していることを明確にしました。私の答えの大部分はC++に関するものです。ただし、タイトルにはまだC++があり、質問にはまだC++のタグが付いているため、特にこれまで見てきた回答のほとんどがほとんど満足できないため、これが他の人にとっても有用である場合に備えて回答します。

現代のC++(注:Cがこれに基づいていることはよくわかりません)、あなたの教授は両方の点で間違っているようです。

最初はこの部分です。

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

C++では、この 基本的には同じもの

if (!n) {
        return 0;
}

つまり、whileは次のようなものに相当します。

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

つまり、whileがとにかく実行されない場合はifで終了するだけなので、ループの後とifで実行していることはいずれにしても同等であるため、ここにifを配置する理由はありません。私はそれが何らかの理由でこれらが異なっていたと言うべきですが、あなたはこれを持っている必要があります。

本当に、このifステートメントは、私が間違えない限り特に有用ではありません。

2番目の部分は、物事が毛むくじゃらになる場所です。

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

問題の核心は、負数のモジュラスの出力が何を出力するかです。

最新のC++では、- これはほとんど明確に定義されているようです

二項/演算子は商を生成し、二項%演算子は最初の式を2番目の式で除算した余りを生成します。 /または%の第2オペランドがゼロの場合、動作は未定義です。整数オペランドの場合、/演算子は、小数部分が破棄された代数商を生成します。商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 

唯一のキャッチは、最後の行です。

両方のオペランドが負でない場合、剰余は負ではありません。そうでない場合、剰余の符号は実装定義です。

つまり、このようなケースでは、signのみが実装定義のようです。とにかくこの値を二乗しているので、それはあなたのケースでは問題になりません。

ただし、これは必ずしも以前のバージョンのC++またはC99に適用されるわけではないことに注意してください。それがあなたの教授が使っているものであるなら、それが理由かもしれません。


EDIT:いいえ、間違っています。これは C99以降の場合も同様

C99では、a/bが表現可能な場合、以下が必要です。

(a/b)* b + a%bはaと等しくなります

そして 別の場所

整数が除算され、除算が不正確な場合、両方のオペランドが正の場合、/演算子の結果は代数商よりも小さい最大の整数であり、%演算子の結果は正です。いずれかのオペランドが負の場合、/演算子の結果が代数商より小さい最大整数か代数商より大きい最小整数かは、%演算子の結果の符号と同様に実装定義されます。商a/bが表現可能な場合、式(a/b)* b + a%bはaと等しくなります。

ANSI CまたはISO Cのどちらが-5%10になるべきかを指定しますか?

ええC99でも、これはあなたに影響を与えないようです。方程式は同じです。

10
Chipster

他の人が指摘したように、n == 0の特別な扱いはナンセンスです。なぜなら、すべての深刻なCプログラマーにとって、「while(n)」が仕事をすることは明らかだからです。

N <0の動作はそれほど明白ではないので、これらの2行のコードを見たいと思います。

if (n < 0) 
    n = -n;

または少なくともコメント:

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

正直なところ、nが負の値になる可能性があると考え始めたのはいつですか?コードを書くとき、または先生の発言を読むとき?

8
C.B.

これは、私が失敗した課題を思い出させます

90年代に遡ります。講師はループについて発芽していましたが、長い話を簡単に言えば、0を超える整数の桁数を返す関数を書くことでした。

したがって、たとえば、321の桁数は3になります。

割り当ては単に桁数を返す関数を記述するように言っていましたが、... 講義でカバーされているように、それを取得しますまで10で割るループを使用することが期待されていました。

しかし、ループの使用は明示的に記述されていないため、I:took the log, stripped away the decimals, added 1であり、その後クラス全体の前で中傷されました。

ポイントは、課題の目的は、講義中に学んだことの理解をテストすることでした。私が受け取った講義から、私はコンピューターの先生が少しジャークであることがわかりました(しかし、おそらく計画を立てたジャークですか?)


あなたの状況で:

c/C++で、2乗した数値の桁の合計を返す関数を作成します

私は間違いなく2つの答えを提供していたでしょう:

  • 正解(最初に数を二乗する)、および
  • 彼の幸せを保つために、例に沿った間違った答え;-)
5
SlowLearner

通常、割り当てでは、コードが機能するという理由だけですべてのマークが与えられるわけではありません。また、ソリューションを読みやすく、効率的かつエレガントにするためのマークも取得します。これらは常に相互に排他的ではありません。

私が十分に学習できないのは、"意味のある変数名を使用"です。

あなたの例では大きな違いはありませんが、数行のコードを読みやすいプロジェクトで作業している場合、非常に重要になります。

私がCコードで見がちなもう1つのことは、人々が賢く見ようとしていることです。 while(n!= 0)を使用するのではなく、while( n)同じことを意味するため。まあそれはあなたが持っているコンパイラで行いますが、あなたが示唆したようにあなたの先生の古いバージョンは同じ方法でそれを実装していません。

一般的な例は、配列のインデックスを参照しながら、同時にインクリメントします。 数字[i ++] = iPrime;

さて、コードに取り組む次のプログラマーは、誰かが自慢できるように、割り当ての前または後のどちらでインクリメントされるかを知る必要があります。

ディスクスペースのメガバイトは、トイレットペーパーのロールよりも安価であり、スペースを節約しようとするよりも明確にするために、あなたの仲間のプログラマーは幸せになります。

1
Paul McCarthy

問題のステートメントはわかりにくいですが、数値の例では2乗した数字の桁の合計の意味が明確になっています。改善されたバージョンは次のとおりです。

整数のnを範囲とするCおよびC++の共通サブセットで関数を記述します[-107、107]は、10を基数とする表現の数字の2乗の合計を返します。例:n123の場合、関数は14(12 + 22 + 32 = 14)。

作成した関数は、次の2つの詳細を除いて問題ありません。

  • タイプlongは少なくとも31の値ビットを持つことがC標準によって保証されているため、引数は指定された範囲のすべての値に対応するためにタイプlongを持つ必要があります。 in [-107、107]。 (戻り値の型にはint型で十分であり、その最大値は568です。)
  • 負のオペランドに対する%の動作は直感的ではなく、その仕様はC99 Standardエディションと以前のエディションとで異なりました。否定的な入力に対してもアプローチが有効である理由を文書化する必要があります。

変更されたバージョンは次のとおりです。

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

先生の答えには複数の欠陥があります。

  • タイプintの値の範囲が不十分である可能性があります。
  • 0を特殊なケースにする必要はありません。
  • 負の値を否定することは不要であり、n = INT_MINに対して未定義の動作を引き起こす可能性があります。

問題ステートメントに追加の制約(C99およびnの値の範囲)があるため、最初の欠陥のみが問題です。余分なコードは依然として正しい答えを生成します。

このテストで良い点を取得する必要がありますが、負のnの問題に対する理解を示すために、筆記テストでは説明が必要です。口頭試験では、あなたは質問を受けて、あなたの答えはそれを釘付けにしたでしょう。

0
chqrlie

「%」の元の定義と現代の定義のどちらが優れているかについては議論しませんが、このような短い関数に2つのreturnステートメントを記述する人は、Cプログラミングをまったく教えるべきではありません。余分なreturnはgotoステートメントであり、Cではgotoを使用しません。さらに、ゼロチェックのないコードは同じ結果になり、余分なreturnは読みにくくなりました。

0
Peter Krassoi