Wie kann ich sicherstellen, dass eine Division von ganzen Zahlen immer aufgerundet wird?


242

Ich möchte sicherstellen, dass eine Division von ganzen Zahlen bei Bedarf immer aufgerundet wird. Gibt es einen besseren Weg als diesen? Es wird viel gecastet. :-)

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

48
Können Sie klarer definieren, was Sie für "besser" halten? Schneller? Kürzer? Genauer? Robuster? Offensichtlicher richtig?
Eric Lippert

6
Sie haben immer viel Casting mit Mathematik in C # - deshalb ist es keine großartige Sprache für solche Dinge. Möchten Sie, dass die Werte aufgerundet oder von Null weggerundet werden - sollte -3,1 auf -3 (aufwärts) oder -4 (von null weg) gehen
Keith

9
Eric: Was meinst du mit "genauer? Robuster? Offensichtlicher richtig?" Eigentlich meinte ich nur "besser", ich würde den Leser die Bedeutung besser machen lassen. Also, wenn jemand einen kürzeren Code hatte, großartig, wenn ein anderer einen schnelleren hatte, auch großartig :-) Was ist mit dir? Hast du irgendwelche Vorschläge?
Karsten

1
Bin ich der einzige, der beim Lesen des Titels sagt: "Oh, es ist eine Art Zusammenfassung von C #?"
Matt Ball

6
Es ist wirklich erstaunlich, wie subtil schwierig diese Frage war und wie lehrreich die Diskussion war.
Justin Morgan

Antworten:


669

UPDATE: Diese Frage war das Thema meines Blogs im Januar 2013 . Danke für die tolle Frage!


Es ist schwierig, eine ganzzahlige Arithmetik richtig zu machen. Wie bisher ausführlich gezeigt wurde, stehen die Chancen gut, dass Sie einen Fehler gemacht haben, sobald Sie versuchen, einen "cleveren" Trick auszuführen. Und wenn ein Fehler gefunden wird, ist es keine gute Technik zur Problemlösung , den Code zu ändern, um den Fehler zu beheben, ohne zu berücksichtigen, ob der Fehler etwas anderes beschädigt. Bisher haben wir fünf verschiedene falsche ganzzahlige arithmetische Lösungen für dieses völlig nicht besonders schwierige Problem veröffentlicht.

Der richtige Weg, um ganzzahlige arithmetische Probleme anzugehen - das heißt, der Weg, der die Wahrscheinlichkeit erhöht, dass die Antwort beim ersten Mal richtig ist - besteht darin, sich dem Problem sorgfältig zu nähern, es Schritt für Schritt zu lösen und dabei gute technische Prinzipien anzuwenden so.

Lesen Sie zunächst die Spezifikation für das, was Sie ersetzen möchten. In der Spezifikation für die Ganzzahldivision heißt es eindeutig:

  1. Die Division rundet das Ergebnis gegen Null

  2. Das Ergebnis ist Null oder positiv, wenn die beiden Operanden das gleiche Vorzeichen haben, und Null oder negativ, wenn die beiden Operanden entgegengesetzte Vorzeichen haben

  3. Wenn der linke Operand der kleinste darstellbare int ist und der rechte Operand –1 ist, tritt ein Überlauf auf. [...] es ist implementierungsdefiniert, ob [eine ArithmeticException] ausgelöst wird oder der Überlauf nicht gemeldet wird, wobei der resultierende Wert der des linken Operanden ist.

  4. Wenn der Wert des rechten Operanden Null ist, wird eine System.DivideByZeroException ausgelöst.

Was wir wollen, ist eine ganzzahlige Divisionsfunktion, die den Quotienten berechnet, aber das Ergebnis immer nach oben rundet , nicht immer gegen Null .

Schreiben Sie also eine Spezifikation für diese Funktion. Für unsere Funktion int DivRoundUp(int dividend, int divisor)muss für jede mögliche Eingabe ein Verhalten definiert sein. Dieses undefinierte Verhalten ist zutiefst besorgniserregend, also lasst es uns beseitigen. Wir werden sagen, dass unser Betrieb diese Spezifikation hat:

  1. Operation wird ausgelöst, wenn der Divisor Null ist

  2. Operation wird ausgelöst, wenn die Dividende int.minval und der Divisor -1 ist

  3. Wenn es keinen Rest gibt - Division ist 'gerade' -, ist der Rückgabewert der integrale Quotient

  4. Andernfalls wird die kleinste Ganzzahl zurückgegeben, die größer als der Quotient ist, dh immer aufgerundet.

Jetzt haben wir eine Spezifikation, damit wir wissen, dass wir ein testbares Design entwickeln können . Angenommen, wir fügen ein zusätzliches Entwurfskriterium hinzu, dass das Problem ausschließlich mit ganzzahliger Arithmetik gelöst werden soll, anstatt den Quotienten als Doppel zu berechnen, da die "doppelte" Lösung in der Problemstellung explizit abgelehnt wurde.

Was müssen wir also berechnen? Um unsere Spezifikation zu erfüllen und dabei ausschließlich in der Ganzzahlarithmetik zu bleiben, müssen wir drei Fakten kennen. Was war der ganzzahlige Quotient? Zweitens war die Division frei von Rest? Und drittens, wenn nicht, wurde der ganzzahlige Quotient durch Auf- oder Abrunden berechnet?

Nachdem wir eine Spezifikation und ein Design haben, können wir mit dem Schreiben von Code beginnen.

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

Ist das klug? Nicht hübsch? Kurz? Nein. Entsprechend der Spezifikation? Ich glaube schon, aber ich habe es nicht vollständig getestet. Es sieht aber ziemlich gut aus.

Wir sind Profis hier; Verwenden Sie gute technische Praktiken. Erforschen Sie Ihre Tools, geben Sie das gewünschte Verhalten an, betrachten Sie zuerst Fehlerfälle und schreiben Sie den Code, um seine offensichtliche Richtigkeit hervorzuheben. Und wenn Sie einen Fehler finden, prüfen Sie zunächst, ob Ihr Algorithmus stark fehlerhaft ist, bevor Sie zufällig die Vergleichsrichtungen vertauschen und bereits funktionierende Dinge auflösen.


44
Ausgezeichnete beispielhafte Antwort
Gavin Miller

61
Was mich interessiert, ist nicht das Verhalten; Jedes Verhalten scheint gerechtfertigt zu sein. Was mich interessiert ist, dass es nicht spezifiziert ist , was bedeutet, dass es nicht einfach getestet werden kann. In diesem Fall definieren wir unseren eigenen Operator, damit wir ein beliebiges Verhalten angeben können. Es ist mir egal, ob dieses Verhalten "werfen" oder "nicht werfen" ist, aber es ist mir wichtig, dass es angegeben wird.
Eric Lippert

68
Verdammt, Pedanterie scheitert :(
Jon Skeet

32
Mann - Könnten Sie bitte ein Buch darüber schreiben?
xtofl

76
@finnw: Ob ich es getestet habe oder nicht, ist irrelevant. Das Lösen dieses ganzzahligen Rechenproblems ist nicht mein Geschäftsproblem. Wenn es so wäre, würde ich es testen. Wenn jemand will , Code von Fremden aus dem Internet nehmen ihr Geschäft Problem zu lösen , dann ist die Beweislast auf sich , um es gründlich zu testen.
Eric Lippert

49

Alle Antworten hier scheinen bisher eher zu kompliziert.

In C # und Java müssen Sie für eine positive Dividende und einen Divisor einfach Folgendes tun:

( dividend + divisor - 1 ) / divisor 

Quelle: Number Conversion, Roland Backhouse, 2001


Genial. Sie hätten zwar die Klammern hinzufügen sollen, um die Unklarheiten zu beseitigen. Der Beweis dafür war etwas langwierig, aber Sie können es im Bauch spüren, dass es richtig ist, wenn Sie es nur ansehen.
Jörgen Sigvardsson

1
Hmmm ... was ist mit Dividende = 4, Divisor = (- 2) ??? 4 / (-2) = (-2) = (-2) nach dem Aufrunden. aber der von Ihnen angegebene Algorithmus (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0,5) = 0 nach dem Aufrunden.
Scott

1
@ Scott - Entschuldigung, ich habe nicht erwähnt, dass diese Lösung nur für positive Dividenden und Divisoren gilt. Ich habe meine Antwort aktualisiert, um dies zu verdeutlichen.
Ian Nelson

1
Ich mag es natürlich, Sie könnten einen etwas künstlichen Überlauf im Zähler als Nebenprodukt dieses Ansatzes haben ...
TCC

2
@PIntag: Die Idee ist gut, aber die Verwendung von Modulo ist falsch. Nehmen Sie 13 und 3. Erwartetes Ergebnis 5, aber ((13-1)%3)+1)1 als Ergebnis. Die richtige Art der Teilung 1+(dividend - 1)/divisorergibt das gleiche Ergebnis wie die Antwort für eine positive Dividende und einen Divisor. Auch keine Überlaufprobleme, wie künstlich sie auch sein mögen.
Lutz Lehmann

48

Die endgültige int-basierte Antwort

Für vorzeichenbehaftete Ganzzahlen:

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

Für vorzeichenlose Ganzzahlen:

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

Die Begründung für diese Antwort

Die Ganzzahldivision ' /' wird definiert, um gegen Null zu runden (7.7.2 der Spezifikation), aber wir wollen aufrunden. Dies bedeutet, dass negative Antworten bereits korrekt gerundet sind, positive Antworten jedoch angepasst werden müssen.

Positive Antworten ungleich Null sind leicht zu erkennen, aber die Antwort Null ist etwas schwieriger, da dies entweder das Aufrunden eines negativen Werts oder das Abrunden eines positiven Werts sein kann.

Am sichersten ist es, zu erkennen, wann die Antwort positiv sein sollte, indem überprüft wird, ob die Vorzeichen beider Ganzzahlen identisch sind. Der ganzzahlige xor-Operator ' ^' für die beiden Werte führt in diesem Fall zu einem 0-Vorzeichenbit, was ein nicht negatives Ergebnis bedeutet. Bei der Prüfung wird daher (a ^ b) >= 0festgestellt, dass das Ergebnis vor dem Runden positiv sein sollte. Beachten Sie auch, dass für vorzeichenlose Ganzzahlen jede Antwort offensichtlich positiv ist, sodass diese Prüfung weggelassen werden kann.

Die einzige verbleibende Prüfung ist dann, ob eine Rundung aufgetreten ist, für die a % b != 0die Arbeit erledigt wird.

Gewonnene Erkenntnisse

Arithmetik (ganzzahlig oder anders) ist bei weitem nicht so einfach, wie es scheint. Immer sorgfältig überlegen.

Auch wenn meine endgültige Antwort vielleicht nicht so "einfach" oder "offensichtlich" oder vielleicht sogar "schnell" ist wie die Gleitkomma-Antworten, hat sie für mich eine sehr starke Erlösungsqualität. Ich habe jetzt die Antwort durchdacht , also bin ich mir tatsächlich sicher, dass sie richtig ist (bis mir jemand schlauer etwas anderes sagt - verstohlener Blick in Erics Richtung -).

Um das gleiche Gefühl der Gewissheit über die Gleitkommaantwort zu erhalten, müsste ich mehr (und möglicherweise komplizierter) darüber nachdenken, ob es Bedingungen gibt, unter denen die Gleitkommapräzision im Weg stehen könnte, und ob dies Math.Ceilingmöglicherweise der Fall ist etwas Unerwünschtes an "genau den richtigen" Eingängen.

Der Weg reiste

Ersetzen (Anmerkung: Ich habe die zweite myInt1durch ersetzt myInt2, vorausgesetzt, Sie haben das gemeint):

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

mit:

(myInt1 - 1 + myInt2) / myInt2

Die einzige Einschränkung besteht darin, dass myInt1 - 1 + myInt2Sie möglicherweise nicht das bekommen, was Sie erwarten , wenn der von Ihnen verwendete Integer-Typ überläuft.

Grund dafür ist falsch : -1000000 und 3999 sollten -250 ergeben, dies ergibt -249

BEARBEITEN:
Da dies den gleichen Fehler wie die andere ganzzahlige Lösung für negative myInt1Werte aufweist, ist es möglicherweise einfacher, Folgendes zu tun:

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

Dies sollte das richtige Ergebnis liefern, wenn divnur ganzzahlige Operationen verwendet werden.

Grund dafür ist falsch : -1 und -5 sollten 1 ergeben, dies ergibt 0

BEARBEITEN (noch einmal mit Gefühl):
Der Divisionsoperator rundet gegen Null; Für negative Ergebnisse ist dies genau richtig, sodass nur nicht negative Ergebnisse angepasst werden müssen. Wenn wir auch bedenken, dass DivRemnur a /und a ausgeführt %werden, überspringen wir den Anruf (und beginnen mit dem einfachen Vergleich, um eine Modulo-Berechnung zu vermeiden, wenn sie nicht benötigt wird):

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

Grund dafür ist falsch : -1 und 5 sollten 0 ergeben, dies ergibt 1

(Zu meiner eigenen Verteidigung des letzten Versuchs hätte ich niemals eine begründete Antwort versuchen sollen, während mein Verstand mir sagte, ich sei 2 Stunden zu spät zum Schlafen)


19

Perfekte Chance, eine Erweiterungsmethode zu verwenden:

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

Dies macht Ihren Code auch überlesbar:

int result = myInt.DivideByAndRoundUp(4);

1
Ähm, was? Ihr Code würde myInt.DivideByAndRoundUp () heißen und immer 1 zurückgeben, mit Ausnahme einer Eingabe von 0, die eine Ausnahme verursachen würde ...
Konfigurator

5
Epischer Fehltritt. (-2) .DivideByAndRoundUp (2) gibt 0 zurück.
Timwi

3
Ich bin wirklich zu spät zur Party, aber wird dieser Code kompiliert? Meine Math-Klasse enthält keine Ceiling-Methode, die zwei Argumente akzeptiert.
R. Martinho Fernandes

17

Sie könnten einen Helfer schreiben.

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

1
Immer noch die gleiche Menge an Casting
ChrisF

29
@Outlaw, nimm alles an, was du willst. Aber für mich, wenn sie es nicht in die Frage stellen, gehe ich im Allgemeinen davon aus, dass sie es nicht berücksichtigt haben.
JaredPar

1
Einen Helfer zu schreiben ist nutzlos, wenn es nur so ist. Schreiben Sie stattdessen einen Helfer mit einer umfassenden Testsuite.
Dolmen

3
@dolmen Kennen Sie das Konzept der Wiederverwendung von Code ? oO
Rushyo

4

Sie könnten so etwas wie das Folgende verwenden.

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

12
Dieser Code ist offensichtlich in zweierlei Hinsicht falsch. Zunächst einmal gibt es einen kleinen Fehler in der Syntax; Sie brauchen mehr Klammern. Noch wichtiger ist jedoch, dass das gewünschte Ergebnis nicht berechnet wird. Versuchen Sie beispielsweise, mit a = -1000000 und b = 3999 zu testen. Das Ergebnis der regulären Ganzzahldivision ist -250. Die doppelte Division ist -250.0625 ... Das gewünschte Verhalten ist das Aufrunden. Natürlich ist die korrekte Aufrundung von -250.0625 auf -250 aufzurunden, aber Ihr Code rundet auf -249 auf.
Eric Lippert

36
Es tut mir leid, dass ich das immer wieder sagen muss, aber Ihr Code ist NOCH FALSCH, Daniel. 1/2 sollte auf 1 aufrunden, aber Ihr Code rundet es auf 0 ab. Jedes Mal, wenn ich einen Fehler finde, "beheben" Sie ihn, indem Sie einen weiteren Fehler einführen. Mein Rat: Hör auf damit. Wenn jemand einen Fehler in Ihrem Code findet, schlagen Sie nicht einfach einen Fix zusammen, ohne klar zu überlegen, was den Fehler überhaupt verursacht hat. Verwenden Sie gute technische Praktiken. Finden Sie den Fehler im Algorithmus und beheben Sie ihn. Der Fehler in allen drei falschen Versionen Ihres Algorithmus besteht darin, dass Sie nicht richtig bestimmen, wann die Rundung "down" war.
Eric Lippert

8
Unglaublich, wie viele Fehler in diesem kleinen Code enthalten sein können. Ich hatte nie viel Zeit darüber nachzudenken - das Ergebnis zeigt sich in den Kommentaren. (1) a * b> 0 wäre korrekt, wenn es nicht überlaufen würde. Es gibt 9 Kombinationen für das Vorzeichen von a und b - [-1, 0, +1] x [-1, 0, +1]. Wir können den Fall b == 0 ignorieren und die 6 Fälle [-1, 0, +1] x [-1, +1] belassen. a / b rundet gegen Null, dh für negative Ergebnisse aufrunden und für positive Ergebnisse abrunden. Daher muss die Anpassung durchgeführt werden, wenn a und b das gleiche Vorzeichen haben und nicht beide Null sind.
Daniel Brückner

5
Diese Antwort ist wahrscheinlich das Schlimmste, was ich auf SO geschrieben habe ... und sie wird jetzt von Erics Blog verlinkt ... Nun, meine Absicht war es nicht, eine lesbare Lösung zu geben; Ich war wirklich auf einen kurzen und schnellen Hack eingestellt. Und um meine Lösung wieder zu verteidigen, hatte ich gleich beim ersten Mal die richtige Idee, dachte aber nicht an Überläufe. Es war offensichtlich mein Fehler, den Code zu veröffentlichen, ohne ihn in VisualStudio zu schreiben und zu testen. Die "Korrekturen" sind noch schlimmer - ich wusste nicht, dass es sich um ein Überlaufproblem handelt und dachte, ich hätte einen logischen Fehler gemacht. Infolgedessen haben die ersten "Korrekturen" nichts geändert; Ich habe gerade den
Daniel Brückner

10
Logik und schob den Fehler herum. Hier habe ich die nächsten Fehler gemacht; Wie Eric bereits erwähnte, habe ich den Fehler nicht wirklich analysiert und nur das erste getan, was richtig schien. Und ich habe VisualStudio immer noch nicht verwendet. Okay, ich hatte es eilig und verbrachte nicht mehr als fünf Minuten mit dem "Fix", aber das sollte keine Entschuldigung sein. Nachdem Eric wiederholt auf den Fehler hingewiesen hatte, startete ich VisualStudio und fand das eigentliche Problem. Das Update mit Sign () macht das Ding noch unleserlicher und verwandelt es in Code, den Sie nicht wirklich pflegen möchten. Ich habe meine Lektion gelernt und werde nicht länger unterschätzen, wie schwierig es ist
Daniel Brückner

-2

Einige der oben genannten Antworten verwenden Floats. Dies ist ineffizient und wirklich nicht erforderlich. Für Ints ohne Vorzeichen ist dies eine effiziente Antwort für int1 / int2:

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

Für signierte Ints ist dies nicht korrekt


Nicht das, was das OP an erster Stelle gefragt hat, trägt nicht wirklich zu den anderen Antworten bei.
Santamanno

-4

Das Problem bei allen Lösungen hier ist entweder, dass sie eine Besetzung benötigen oder dass sie ein numerisches Problem haben. Casting auf Float oder Double ist immer eine Option, aber wir können es besser machen.

Wenn Sie den Code der Antwort von @jerryjvl verwenden

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

Es liegt ein Rundungsfehler vor. 1/5 würde aufrunden, weil 1% 5! = 0. Dies ist jedoch falsch, da nur gerundet wird, wenn Sie die 1 durch eine 3 ersetzen, sodass das Ergebnis 0,6 ist. Wir müssen einen Weg finden, um aufzurunden, wenn die Berechnung einen Wert größer oder gleich 0,5 ergibt. Das Ergebnis des Modulo-Operators im oberen Beispiel hat einen Bereich von 0 bis myInt2-1. Die Rundung erfolgt nur, wenn der Rest mehr als 50% des Divisors beträgt. Der angepasste Code sieht also folgendermaßen aus:

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

Natürlich haben wir auch bei myInt2 / 2 ein Rundungsproblem, aber dieses Ergebnis bietet Ihnen eine bessere Rundungslösung als die anderen auf dieser Site.


"Wir müssen einen Weg finden, um aufzurunden, wenn die Berechnung einen Wert größer oder gleich 0,5 ergibt" - Sie haben den Punkt dieser Frage verpasst - oder immer aufzurunden, dh das OP möchte 0,001 auf 1 aufrunden.
Grhm
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.