web-dev-qa-db-ja.com

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

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

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

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


整数演算を正しくするのは難しいです。これまでに十分に実証されているように、「巧妙な」トリックを実行しようとすると、ミスを犯した可能性が高くなります。そして、欠陥が見つかった場合、欠陥を修正するためにコードを変更する修正が他の何かを壊すかどうかを考慮せずには、良い問題解決テクニックではありません。これまでのところ、この完全に特に困難ではない問題に対する5つの異なる誤った整数演算ソリューションが投稿されたと思いました。

整数算術問題に取り組む正しい方法、つまり、最初に正しい答えを得る可能性を高める方法は、問題に慎重に取り組み、一度に1ステップずつ解決し、適切なエンジニアリング原則を使用することです。そう。

まず、置き換えようとしているものの仕様を読んでください。整数除算の仕様には、次のように明記されています。

  1. 除算は結果をゼロに丸めます

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

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

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

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

その関数の仕様を記述します。関数int DivRoundUp(int dividend, int divisor)は、可能なすべての入力に対して定義された動作を持たなければなりません。その未定義の動作は非常に心配なので、削除しましょう。オペレーションには次の仕様があります。

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

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

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

  4. それ以外の場合は、商よりもgreaterであるsmallest整数を返します。つまり、常に切り上げます。

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

それでは、何を計算する必要がありますか?明らかに、整数演算のみに留まりながら仕様を満たすには、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;
}

これは賢いですか?いや、美しい?いいえ、短いですか?いいえ。仕様に従って正しいですか? そうだと思いますが、完全にはテストしていません。かなりよさそうです。

私たちはここの専門家です。優れたエンジニアリング手法を使用します。ツールを調べ、目的の動作を指定し、最初にエラーケースを考慮し、コードを書いてその明確な正確さを強調します。そして、バグを見つけたら、考慮しますランダムに比較の方向を入れ替えて、すでに機能しているものを壊す前に、アルゴリズムに根本的な欠陥があるかどうか。

661
Eric Lippert

最終的な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)に丸めるように定義されていますが、切り上げたいと考えています。これは、否定的な回答がすでに正しく丸められているが、肯定的な回答を調整する必要があることを意味します。

ゼロ以外の正の答えは簡単に検出できますが、負の値の切り上げまたは正の値の切り捨てのいずれかになる可能性があるため、ゼロの答えは少し複雑です。

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

残っている唯一のチェックは、丸めが発生したかどうかであり、a % b != 0がジョブを実行します。

学んだ教訓

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

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

浮動小数点の答えについて同じ確信を得るには、浮動小数点の精度が邪魔になる条件があるかどうか、そしてMath.Ceilingは、おそらく「ちょうどいい」入力に対して望ましくないことを行います。

旅した道

置換(2番目のmyInt1myInt2に置き換えたことに注意してください。これはあなたが意図したことを前提としています):

(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を与える

編集(もう一度、気分で):
除算演算子はゼロに向かって丸めます。負の結果の場合、これは正確に正しいため、非負の結果のみを調整する必要があります。また、DivRem/%を行うだけであることを考慮して、呼び出しをスキップしましょう(そして、不要な場合はモジュロ計算を避けるために簡単な比較から始めましょう):

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

これが間違っている理由:-1と5は0を与えるべきであり、これは1を与える

(私の最後の試みの防御では、睡眠が2時間遅れていたと私の心が私に言っていた間、私は理性的な答えを決して試みるべきではなかった)

46
jerryjvl

ここでのすべての答えは、かなり複雑に思えます。

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

( dividend + divisor - 1 ) / divisor 

出典: 数値変換、Roland Backhouse、2001

46
Ian Nelson

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

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);
20
joshcomley

ヘルパーを書くことができます。

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

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

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