整数の除算が常に切り上げられるようにするにはどうすればよいですか?


242

必要に応じて、整数の除算が常に切り上げられるようにしたいと思います。これより良い方法はありますか?多くのキャストが進行中です。:-)

(int)Math.Ceiling((double)myInt1 / myInt2)

48
「より良い」と考えるものをより明確に定義できますか?もっと早く?もっと短い?より正確な?より堅牢ですか?より明らかに正しいですか?
Eric Lippert、

6
C#では常に数学を使ったキャストがたくさんあります。そのため、この種の言語にはあまり適していません。-3.1を-3(上)または-4(ゼロから)に移動する必要がありますか
キース

9
エリック:「より正確?より堅牢?より明らかに正しい?」とはどういう意味ですか?実は私が言ったことは単に「より良い」ということでした。読者に意味をよりよく伝えさせてください。したがって、誰かが短いコードを持っている場合は素晴らしいですが、別の誰かが速い場合は素晴らしい:-)あなたについて何か提案はありますか?
カルステン

1
しかし、タイトルを読んだときに「ああ、それはある種のC#のまとめです」というのは私だけですか。
マットボール

6
この質問がどれほど微妙に難しいことが判明し、議論がどれほど有益であるかは本当に驚くべきことです。
Justin Morgan

回答:


669

更新:この質問は、2013年1月に私のブログの主題でした。すばらしい質問をありがとう!


整数演算を正しく行うのは困難です。これまでに十分に説明されているように、「巧妙な」トリックを実行しようとした瞬間に、間違いを犯した確率は高くなります。そして、欠陥が見つかった場合、修正が他の何かを壊すかどうかを考慮せずに欠陥を修正するようにコードを変更します問題が解決さは、適切な問題解決手法ではありません。これまでのところ、投稿された完全に特に困難ではない問題に対する5つの異なる不正な整数演算ソリューションを考えてきました。

整数演算問題にアプローチする正しい方法、つまり、最初に正しい答えが得られる可能性を高める方法は、問題に注意深く取り組み、一度に1つずつ問題を解決し、優れたエンジニアリング原則を使用して実行することです。そう。

置き換えようとしているものの仕様を読むことから始めます。整数除算の仕様は明確に述べています:

  1. 除算は結果をゼロに向かって丸めます

  2. 2つのオペランドの符号が同じ場合の結果はゼロまたは正、2つのオペランドの符号が反対の場合の結果はゼロまたは負

  3. 左のオペランドが表現可能な最小の整数で、右のオペランドが–1の場合、オーバーフローが発生します。[...]これは、[ArithmeticException]がスローされるか、オーバーフローが報告されずに結果の値が左のオペランドの値になるかについて、実装によって定義されます。

  4. 右側のオペランドの値がゼロの場合、System.DivideByZeroExceptionがスローされます。

必要なのは、商を計算するが常にゼロに向かってではなく常に上に丸める整数除算関数です

その関数の仕様を書いてください。関数にint DivRoundUp(int dividend, int divisor)は、可能なすべての入力に対して定義された動作が必要です。その未定義の動作は非常に心配なので、それを排除しましょう。私たちの操作には次の仕様があると言います。

  1. 除数がゼロの場合、操作がスローされます

  2. 被除数がint.minvalで除数が-1の場合、演算がスローされます

  3. 余りがない場合-除算は「偶数」です-戻り値は整数の商です

  4. それ以外の場合は、商より大きい最小の整数を返します。つまり、常に切り上げます。

これで仕様ができたので、テスト可能な設計を考え出すことができます。。「二重」の解が問題ステートメントで明示的に拒否されているため、商をdoubleとして計算するのではなく、整数演算のみで問題を解決するという追加の設計基準を追加するとします。

では、何を計算する必要があるのでしょうか。明らかに、整数演算のみを行いながら仕様を満たすには、3つの事実を知る必要があります。まず、整数の商は何でしたか?第二に、部門には残りがありませんでしたか?そして、3番目の場合、そうでない場合、整数商は切り上げまたは切り捨てによって計算されましたか?

仕様とデザインができたので、コードの作成を開始できます。

public static int DivRoundUp(int dividend, int divisor)
{
  if (divisor == 0 ) throw ...
  if (divisor == -1 && dividend == Int32.MinValue) throw ...
  int roundedTowardsZeroQuotient = dividend / divisor;
  bool dividedEvenly = (dividend % divisor) == 0;
  if (dividedEvenly) 
    return roundedTowardsZeroQuotient;

  // At this point we know that divisor was not zero 
  // (because we would have thrown) and we know that 
  // dividend was not zero (because there would have been no remainder)
  // Therefore both are non-zero.  Either they are of the same sign, 
  // or opposite signs. If they're of opposite sign then we rounded 
  // UP towards zero so we're done. If they're of the same sign then 
  // we rounded DOWN towards zero, so we need to add one.

  bool wasRoundedDown = ((divisor > 0) == (dividend > 0));
  if (wasRoundedDown) 
    return roundedTowardsZeroQuotient + 1;
  else
    return roundedTowardsZeroQuotient;
}

これは賢いですか?いいえ。美しいですか?いいえ、短いですか?いいえ、仕様に合わせて修正しますか?私はそう信じていますが、完全にはテストしていません。それはかなりよさそうだ。

私たちはここで専門家です。適切なエンジニアリング手法を使用します。ツールを調査し、必要な動作を指定し、エラーのケースを最初に検討し、コードを記述してその明らかな正確さを強調します。そして、バグを見つけたら、比較の方向をランダムに入れ替えて、既に機能しているものを壊す前に、アルゴリズムに最初から深い欠陥があるかどうかを検討してください。


44
優れた模範的な回答
ギャビンミラー、

61
私が気にしているのは行動ではありません。どちらの動作も正当なようです。私が気にしているのは、指定されていないことです。つまり、簡単にテストすることはできません。この例では、独自の演算子を定義しているので、好きな動作を指定できます。その振る舞いが「投げる」か「投げない」かは気にしませんが、それが明記されていることは気にします。
Eric Lippert、

68
くそ、ペダントリー失敗:(
Jon Skeet

32
男-それについて本を書いてくれませんか?
xtofl 2009

76
@finnw:私がテストしたかどうかは関係ありません。この整数演算の問題を解決することは私のビジネスの問題ではありません。もしそうなら、私はそれをテストします。ビジネス上の問題を解決するために、見知らぬ人からコードをインターネットから取り出したい場合は、徹底的にテストする責任があります。
Eric Lippert、

49

ここまでのすべての答えは、かなり複雑すぎるようです。

C#とJavaでは、プラスの配当と除数を得るには、次のことを行うだけです。

( dividend + divisor - 1 ) / divisor 

出典:Number Conversion、Roland Backhouse、2001


驚くばかり。あいまいさをなくすために括弧を追加する必要がありましたが、その証拠は少し長かったですが、それを見るだけで、腸内でそれが正しいと感じることができます。
ヨルゲンSigvardsson

1
うーん...配当= 4、除数=(-2)はどうですか?4 /(-2)=(-2)=(-2)切り上げ後。ただし、四捨五入した後、指定したアルゴリズムは(4 +(-2)-1)/(-2)= 1 /(-2)=(-0.5)= 0です。
スコット

1
@スコット-申し訳ありませんが、この解決策はプラスの配当と除数に対してのみ成り立つことを省略しました これを明確にするために、回答を更新しました。
Ian Nelson

1
私はそれが好きです、もちろん、あなたはこのアプローチの副産物として分子のいくぶん人工的なオーバーフローを持っているかもしれません...
TCC

2
@PIntag:アイデアは良いですが、モジュロの使用は間違っています。13と3を取ります。期待される結果は5ですが((13-1)%3)+1)、結果として1を返します。適切な除算1+(dividend - 1)/divisorを行うと、正の配当と除数の答えと同じ結果になります。また、オーバーフローの問題はありませんが、人工的なものであってもかまいません。
Lutz Lehmann

48

最後のintベースの答え

符号付き整数の場合:

int div = a / b;
if (((a ^ b) >= 0) && (a % b != 0))
    div++;

符号なし整数の場合:

int div = a / b;
if (a % b != 0)
    div++;

この回答の理由

整数除算 ' /'はゼロ(仕様の7.7.2)に丸めるように定義されていますが、切り上げます。つまり、否定的な回答は既に正しく丸められていますが、肯定的な回答は調整する必要があります。

ゼロ以外の正の回答は簡単に検出できますが、回答0は少し難しいです。これは、負の値の切り上げまたは正の値の切り捨てのどちらかになる可能性があるためです。

最も安全な方法は、両方の整数の符号が同じであることを確認することで、答えが正である場合を検出することです。整数XOR演算子 '^2つの値の 'は、負でない結果を意味する場合、0の符号ビットになります。したがって、チェック(a ^ b) >= 0は、丸める前に結果が正である必要があると判断します。また、符号なし整数の場合、すべての回答は明らかに正なので、このチェックは省略できます。

残っている唯一のチェックは、丸めが行われたかどうかです。 a % b != 0です。

学んだ教訓

算術(整数またはそれ以外)は、見かけほど単純ではありません。常に慎重に考えることが必要です。

また、私の最終的な答えは、おそらく浮動小数点の答えほど「単純」でも「明白」でも「高速」でもないかもしれませんが、私にとって非常に強力な償還品質があります。私は今、答えを通して推論したので、私は実際にそれが正しいと確信しています(より賢い誰かが私にそう言わない限り- エリックの方向をさらに一瞥する) -)。

浮動小数点の答えについて同じ確信を得るために、浮動小数点の精度が邪魔になる可能性のある条件があるかどうか、および Math.Ceilingおそらくそう「正しい」入力では望ましくない何か。

旅路

置き換え(2つ目myInt1をに置き換えたことに注意してくださいmyInt2

(int)Math.Ceiling((double)myInt1 / myInt2)

と:

(myInt1 - 1 + myInt2) / myInt2

唯一の注意点myInt1 - 1 + myInt2は、使用している整数型がオーバーフローすると、期待どおりの結果が得られない可能性があることです。

これが間違っている理由:-1000000と3999は-250を与え、これは-249を与えます

編集:
これが負のmyInt1値の他の整数解と同じエラーを持っていることを考えると、次のようなことを行う方が簡単かもしれません:

int rem;
int div = Math.DivRem(myInt1, myInt2, out rem);
if (rem > 0)
  div++;

これにより、div整数演算のみを使用した場合に正しい結果が得られます。

これが間違っている理由:-1と-5は1を与え、これは0を与えます

編集(1回以上、感覚あり):
除算演算子はゼロに向かって丸めます。否定的な結果の場合、これはまったく正しいので、否定的でない結果のみ調整が必要です。またことを考えるとDivRemちょうどない/%とにかく、レッツ・コールをスキップ(そしてそれが必要とされていない場合にモジュロ計算を避けるために、簡単に比較で始まります):

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

これが間違っている理由:-1と5は0を与え、これは1を与えます

(私の最後の試みの弁護において、私は私の心が睡眠のために2時間遅れていると私に告げている間、理にかなった答えを試みるべきではありませんでした)


19

拡張メソッドを使用する絶好のチャンス:

public static class Int32Methods
{
    public static int DivideByAndRoundUp(this int number, int divideBy)
    {                        
        return (int)Math.Ceiling((float)number / (float)divideBy);
    }
}

これにより、コードも読みやすくなります。

int result = myInt.DivideByAndRoundUp(4);

1
えっと、何?コードはmyInt.DivideByAndRoundUp()と呼ばれ、例外を引き起こす0の入力を除いて、常に1を返します...
configurator

5
壮大な失敗。(-2).DivideByAndRoundUp(2)戻り0
Timwi

3
私はパーティーに本当に遅れましたが、このコードはコンパイルされますか?私のMathクラスには、2つの引数を取るCeilingメソッドが含まれていません。
R.マルティーニョフェルナンデス

17

あなたはヘルパーを書くことができます。

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}

1
それでも、同じ量のキャストが行われています
ChrisF

29
@無法者、あなたが望むすべてを仮定します。しかし、私が彼らがそれを質問に入れない場合、私は一般的に彼らがそれを考慮しなかったと思います。
JaredPar 2009

1
それだけだとヘルパーを書くのは無意味です。代わりに、包括的なテストスイートを備えたヘルパーを記述します。
ドルメン2011

3
@dolmen コードの 再利用の概念に精通していますか?オブジェクト指向
Rushyo

4

次のようなものを使用できます。

a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)

12
このコードは明らかに2つの点で間違っています。まず、構文に小さなエラーがあります。より多くの括弧が必要です。しかし、より重要なのは、それが望ましい結果を計算しないことです。たとえば、a = -1000000およびb = 3999でテストしてみます。通常の整数除算結果は-250です。二重除算は-250.0625です...望ましい動作は切り上げです。明らかに、-250.0625からの正しい切り上げは-250に切り上げることですが、コードは-249に切り上げます。
Eric Lippert、

36
これを言わなくてはならないのは残念ですが、あなたのコードはダニエルをまだ間違っています。1/2は1に切り上げますが、コードは0に切り下げます。バグを見つけるたびに、別のバグを導入して「修正」します。私のアドバイス:それをやめなさい。誰かがあなたのコードでバグを見つけたとき、そもそもバグの原因を明確に考えずに修正を平手打ちしないでください。適切なエンジニアリング手法を使用します。アルゴリズムの欠陥を見つけて修正します。アルゴリズムの3つの誤ったバージョンすべての問題は、丸めがいつ「ダウン」されたかを正しく判別できないことです。
Eric Lippert、

8
この小さなコードにいくつのバグがあるか、信じられないほどです。私はそれについて考える時間があまりなかった-結果はコメントで明らかになった。(1)a * b> 0は、オーバーフローしない場合は正しいでしょう。aとbの符号には9つの組み合わせがあります-[-1、0、+1] x [-1、0、+1]。ケースb == 0を無視して、6つのケース[-1、0、+1] x [-1、+1]を残すことができます。a / bはゼロに向かって丸めます。つまり、否定的な結果の場合は切り上げ、ポジティブな結果の場合は切り捨てます。したがって、aとbの符号が同じで、両方がゼロでない場合は、調整を実行する必要があります。
ダニエル・ブリュックナー

5
この答えはおそらく私がSOで書いた中で最悪のものです...そして今ではエリックのブログにリンクされています... 私は短くて速いハックのために本当にロックしていました。そして、自分の解決策を再び守るために、私は最初にその考えを正しく理解しましたが、オーバーフローについては考えていませんでした。VisualStudioでコードを記述およびテストせずにコードをポストするのは明らかに私の間違いでした。「修正」はさらに悪いことです-私はそれがオーバーフローの問題であることを理解していなかったので、論理的な間違いを犯したと思いました。結果として、最初の「修正」は何も変更しませんでした。私はちょうど反転しました
ダニエル・ブリュックナー

10
ロジックとバグを押しのけました。ここで私は次の間違いを犯しました。エリックがすでに述べたように、私は実際にはバグを分析せず、最初に正しいと思われることだけを行いました。そして、私はまだVisualStudioを使用していません。さて、私は急いでいて、「修正」に5分以上費やしませんでしたが、これは言い訳にはなりません。Ericが繰り返しバグを指摘した後、VisualStudioを起動して実際の問題を見つけました。Sign()を使用した修正により、事態がさら​​に読みにくくなり、維持したくないコードに変換されます。私は自分のレッスンを学びました。トリッキーなことを過小評価することはもうありません
ダニエル・ブリュックナー

-2

上記の回答のいくつかは浮動小数点数を使用していますが、これは非効率で実際には必要ありません。符号なし整数の場合、これはint1 / int2の効率的な答えです。

(int1 == 0) ? 0 : (int1 - 1) / int2 + 1;

署名されたintの場合、これは正しくありません


OPが最初に求めたものではなく、他の答えを実際に追加するものではありません。
santamanno

-4

ここでのすべてのソリューションの問題は、キャストが必要か、数値的な問題があることです。floatまたはdoubleへのキャストは常にオプションですが、もっとうまくやることができます。

@jerryjvlからの回答のコードを使用する場合

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

丸め誤差があります。1/5は、1%5!= 0なので切り上げられます。ただし、1を3に置き換えた場合にのみ丸めが行われるため、結果は0.6になるため、これは誤りです。計算で0.5以上の値が得られた場合は、切り上げる方法を見つける必要があります。上の例のモジュロ演算子の結果の範囲は0〜myInt2-1です。丸めは、余りが除数の50%を超える場合にのみ行われます。したがって、調整後のコードは次のようになります。

int div = myInt1 / myInt2;
if (myInt1 % myInt2 >= myInt2 / 2)
    div++;

もちろん、myInt2 / 2でも丸めの問題がありますが、この結果により、このサイトの他の丸めよりも優れた丸めソリューションが得られます。


「計算により0.5以上の値が得られた場合、切り上げる方法を見つける必要があります」-この質問の要点を逃した-または常に切り上げます。つまり、OPは0.001から1に切り上げたいと考えています。
Grhm
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.