Come posso garantire che una divisione di numeri interi sia sempre arrotondata per eccesso?


243

Voglio garantire che una divisione di numeri interi sia sempre arrotondata per eccesso, se necessario. C'è un modo migliore di questo? C'è un sacco di casting in corso. :-)

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

48
Puoi definire più chiaramente ciò che consideri "migliore"? Più veloce? Più breve? Più accurato? Più robusto? Più ovviamente corretto?
Eric Lippert,

6
Hai sempre un sacco di casting con la matematica in C # - ecco perché non è un linguaggio eccezionale per questo genere di cose. Vuoi i valori arrotondati per eccesso o per difetto da zero - dovrebbe -3.1 andare a -3 (su) o -4 (lontano da zero)
Keith

9
Eric: Cosa intendi con "Più preciso? Più robusto? Più ovviamente corretto?" In realtà, ciò che intendevo dire era semplicemente "migliore", avrei lasciato che il lettore mettesse in risalto il significato. Quindi, se qualcuno avesse un codice più breve, fantastico, se un altro avesse un codice più veloce, ottimo anche :-) Che ne dici di te hai qualche suggerimento?
Karsten,

1
Sono l'unico che, dopo aver letto il titolo, però, "Oh, è una specie di raccolta di C #?"
Matt Ball,

6
È davvero sorprendente quanto sia stata delicatamente difficile questa domanda e quanto sia stata istruttiva la discussione.
Justin Morgan,

Risposte:


670

AGGIORNAMENTO: Questa domanda è stata l'oggetto del mio blog a gennaio 2013 . Grazie per l'ottima domanda!


Ottenere l'aritmetica intera corretta è difficile. Come è stato ampiamente dimostrato finora, nel momento in cui provi a fare un trucco "intelligente", le probabilità sono buone che tu abbia commesso un errore. E quando viene trovato un difetto, cambiare il codice per correggere l'errore senza considerare se la correzione rompe qualcos'altro non è una buona tecnica di risoluzione dei problemi. Finora abbiamo pensato a cinque diverse soluzioni aritmetiche intere errate a questo problema completamente non particolarmente difficile pubblicato.

Il modo giusto di affrontare i problemi aritmetici interi, ovvero il modo in cui aumenta la probabilità di ottenere la risposta giusta la prima volta, è quello di affrontare il problema con attenzione, risolverlo un passo alla volta e utilizzare buoni principi ingegneristici nel fare così.

Inizia leggendo le specifiche per ciò che stai cercando di sostituire. Le specifiche per la divisione dei numeri interi indicano chiaramente:

  1. La divisione arrotonda il risultato allo zero

  2. Il risultato è zero o positivo quando i due operandi hanno lo stesso segno e zero o negativo quando i due operandi hanno segni opposti

  3. Se l'operando di sinistra è l'int più piccolo rappresentabile e l'operando di destra è -1, si verifica un overflow. [...] è definito dall'implementazione se viene lanciata [un'eccezione aritmetica] o se l'overflow non viene segnalato con il valore risultante quello dell'operando di sinistra.

  4. Se il valore dell'operando destro è zero, viene generata una System.DivideByZeroException.

Ciò che vogliamo è una funzione di divisione di numeri interi che calcola il quoziente ma arrotonda il risultato sempre verso l'alto , non sempre verso zero .

Quindi scrivi una specifica per quella funzione. La nostra funzione int DivRoundUp(int dividend, int divisor)deve avere un comportamento definito per ogni possibile input. Quel comportamento indefinito è profondamente preoccupante, quindi eliminiamolo. Diremo che la nostra operazione ha questa specifica:

  1. l'operazione viene generata se il divisore è zero

  2. l'operazione viene generata se il dividendo è int.minval e il divisore è -1

  3. se non c'è resto - la divisione è 'pari' - allora il valore di ritorno è il quoziente integrale

  4. Altrimenti restituisce il numero intero più piccolo che è maggiore del quoziente, ovvero viene sempre arrotondato per eccesso.

Ora abbiamo una specifica, quindi sappiamo che possiamo elaborare un design verificabile . Supponiamo di aggiungere un ulteriore criterio di progettazione secondo cui il problema deve essere risolto esclusivamente con l'aritmetica dei numeri interi, anziché calcolare il quoziente come doppio, poiché la soluzione "doppia" è stata esplicitamente respinta nella dichiarazione del problema.

Quindi cosa dobbiamo calcolare? Chiaramente, per soddisfare le nostre specifiche pur rimanendo esclusivamente nell'aritmetica dei numeri interi, dobbiamo conoscere tre fatti. Innanzitutto, qual era il quoziente intero? In secondo luogo, la divisione era libera dal resto? E terzo, in caso contrario, il quoziente intero è stato calcolato arrotondando per eccesso o per difetto?

Ora che abbiamo una specifica e un design, possiamo iniziare a scrivere codice.

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

È intelligente? No. Bello? No. Breve? No. Corretto secondo le specifiche? Credo di si, ma non l'ho ancora testato. Sembra abbastanza buono però.

Siamo professionisti qui; usare buone pratiche ingegneristiche. Cerca i tuoi strumenti, specifica il comportamento desiderato, considera prima i casi di errore e scrivi il codice per enfatizzare la sua ovvia correttezza. E quando trovi un bug, considera se il tuo algoritmo è profondamente imperfetto prima di iniziare a scambiare casualmente le direzioni dei confronti e rompere cose che già funzionano.


45
Ottima risposta esemplare
Gavin Miller,

62
Quello che mi interessa non è il comportamento; entrambi i comportamenti sembrano giustificabili. Quello che mi interessa è che non sia specificato , il che significa che non può essere facilmente testato. In questo caso, stiamo definendo il nostro operatore, quindi possiamo specificare qualunque comportamento ci piaccia. non mi importa se quel comportamento è "gettare" o "non gettare", ma mi interessa che sia dichiarato.
Eric Lippert,

69
Maledizione, la pedanteria fallisce :(
Jon Skeet,

33
Amico, potresti scriverci un libro, per favore?
xtofl,

77
@finnw: se l'ho provato o no è irrilevante. Risolvere questo problema aritmetico intero non è un mio problema commerciale; se lo fosse, lo testerei. Se qualcuno vuole prendere il codice da estranei da Internet per risolvere il loro problema aziendale, spetta a loro testarlo a fondo.
Eric Lippert,

49

Tutte le risposte qui finora sembrano piuttosto complicate.

In C # e Java, per dividendi e divisori positivi, devi semplicemente fare:

( dividend + divisor - 1 ) / divisor 

Fonte: conversione numerica, Roland Backhouse, 2001


Eccezionale. Tuttavia, avresti dovuto aggiungere le parentesi per rimuovere le ambiguità. La prova era un po 'lunga, ma puoi sentirla nell'intestino, che è giusto, solo guardandola.
Jörgen Sigvardsson,

1
Hmmm ... che dire del dividendo = 4, divisore = (- 2) ??? 4 / (-2) = (-2) = (-2) dopo essere stato arrotondato per eccesso. ma l'algoritmo fornito (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0.5) = 0 dopo essere stato arrotondato per eccesso.
Scott,

1
@Scott - scusate, ho omesso di menzionare che questa soluzione vale solo per dividendi e divisori positivi. Ho aggiornato la mia risposta per menzionare questo chiarimento.
Ian Nelson,

1
mi piace, ovviamente potresti avere un overflow un po 'artificiale nel numeratore come sottoprodotto di questo approccio ...
TCC

2
@PIntag: L'idea è buona, ma l'uso del modulo è sbagliato. Prendi 13 e 3. Risultato previsto 5, ma ((13-1)%3)+1)dà 1 come risultato. Prendendo il giusto tipo di divisione, si 1+(dividend - 1)/divisorottiene lo stesso risultato della risposta per dividendi e divisori positivi. Inoltre, nessun problema di trabocco, per quanto artificiale possa essere.
Lutz Lehmann

48

L'ultima risposta basata su int

Per numeri interi con segno:

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

Per numeri interi senza segno:

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

Il ragionamento per questa risposta

La divisione intera ' /' è definita per arrotondare verso zero (7.7.2 della specifica), ma vogliamo arrotondare per eccesso. Ciò significa che le risposte negative sono già arrotondate correttamente, ma le risposte positive devono essere adattate.

Le risposte positive diverse da zero sono facili da rilevare, ma la risposta zero è un po 'più complicata, dal momento che può essere l'arrotondamento di un valore negativo o l'arrotondamento di uno positivo.

La scommessa più sicura è rilevare quando la risposta dovrebbe essere positiva verificando che i segni di entrambi i numeri siano identici. L'operatore xo intero ' ^' sui due valori genererà un bit di segno 0 in questo caso, il che significa un risultato non negativo, quindi il controllo (a ^ b) >= 0determina che il risultato avrebbe dovuto essere positivo prima dell'arrotondamento. Si noti inoltre che per numeri interi senza segno, ogni risposta è ovviamente positiva, quindi questo controllo può essere omesso.

L'unico controllo rimanente è quindi se si è verificato un arrotondamento, per il quale a % b != 0farà il lavoro.

Lezioni imparate

L'aritmetica (numero intero o altro) non è così semplice come sembra. Pensare attentamente richiesto in ogni momento.

Inoltre, sebbene la mia risposta finale non sia forse "semplice" o "ovvio" o forse "veloce" come risponde il virgola mobile, ha una qualità di riscatto molto forte per me; Ora ho ragionato sulla risposta, quindi in realtà sono sicuro che sia corretta (fino a quando qualcuno più intelligente non mi dirà diversamente - sguardo furtivo nella direzione di Eric -).

Per avere la stessa sensazione di certezza sulla risposta in virgola mobile, dovrei fare di più (e forse più complicato) pensando se ci sono condizioni in cui la precisione in virgola mobile potrebbe interferire e se Math.Ceilingforse lo fa qualcosa di indesiderabile sugli input "giusti".

Il percorso ha viaggiato

Sostituisci (nota che ho sostituito il secondo myInt1con myInt2, supponendo che fosse quello che volevi dire):

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

con:

(myInt1 - 1 + myInt2) / myInt2

L'unica avvertenza è che se myInt1 - 1 + myInt2trabocca il tipo intero che stai usando, potresti non ottenere quello che ti aspetti.

Motivo per cui è sbagliato : -1000000 e 3999 dovrebbero dare -250, questo dà -249

EDIT:
considerando che questo ha lo stesso errore dell'altra soluzione intera per myInt1valori negativi , potrebbe essere più facile fare qualcosa del tipo:

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

Questo dovrebbe dare il risultato corretto divutilizzando solo operazioni interi.

Motivo per cui è sbagliato : -1 e -5 dovrebbero dare 1, questo dà 0

EDIT (ancora una volta, con sentimento):
l'operatore di divisione arrotonda verso zero; per risultati negativi questo è esattamente giusto, quindi solo i risultati non negativi devono essere adattati. Considerando anche che DivRemfa solo una /e una %, saltiamo la chiamata (e iniziamo con il confronto semplice per evitare il calcolo del modulo quando non è necessario):

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

Motivo per cui è sbagliato : -1 e 5 dovrebbero dare 0, questo da 1

(A mia difesa dell'ultimo tentativo non avrei mai dovuto tentare una risposta motivata mentre la mia mente mi stava dicendo che ero in ritardo di 2 ore per dormire)


19

Perfetta possibilità di utilizzare un metodo di estensione:

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

Questo rende anche leggibile il tuo codice:

int result = myInt.DivideByAndRoundUp(4);

1
Ehm, cosa? Il tuo codice si chiamerebbe myInt.DivideByAndRoundUp () e restituirebbe sempre 1 tranne per un input di 0 che causerebbe un'eccezione ...
configuratore

5
Fallimento epico. (-2) .DivideByAndRoundUp (2) restituisce 0.
Timwi,

3
Sono davvero in ritardo alla festa, ma questo codice viene compilato? La mia classe di matematica non contiene un metodo Ceiling che accetta due argomenti.
R. Martinho Fernandes,

17

Potresti scrivere un aiutante.

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

1
Tuttavia, la stessa quantità di casting è in corso
ChrisF

29
@Outlaw, presumi tutto quello che vuoi. Ma per me se non lo mettono nella questione, generalmente suppongo che non lo abbiano preso in considerazione.
JaredPar,

1
Scrivere un aiuto è inutile se è proprio questo. Invece, scrivi un aiutante con una suite di test completa.
dolmen,

3
@dolmen Conosci il concetto di riutilizzo del codice ? oO
Rushyo,

4

Puoi usare qualcosa di simile al seguente.

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

12
Questo codice è ovviamente sbagliato in due modi. Prima di tutto, c'è un piccolo errore nella sintassi; hai bisogno di più parentesi. Ma soprattutto, non calcola il risultato desiderato. Ad esempio, prova a provare con a = -1000000 eb = 3999. Il risultato della divisione intera regolare è -250. La doppia divisione è -250.0625 ... Il comportamento desiderato è arrotondare per eccesso. Chiaramente il corretto arrotondamento per eccesso da -250.0625 è arrotondare per eccesso a -250, ma il codice arrotonda per eccesso a -249.
Eric Lippert,

36
Mi dispiace dover continuare a dirlo ma il tuo codice è ANCORA SBAGLIATO Daniel. 1/2 dovrebbe arrotondare UP a 1, ma il codice lo arrotonda DOWN a 0. Ogni volta che trovo un bug lo "risolvi" introducendo un altro bug. Il mio consiglio: smetti di farlo. Quando qualcuno trova un bug nel tuo codice, non limitarti a mettere insieme una soluzione senza pensare chiaramente a cosa ha causato il bug in primo luogo. Utilizzare buone pratiche di ingegneria; trova il difetto nell'algoritmo e correggilo. Il difetto di tutte e tre le versioni errate del tuo algoritmo è che non stai determinando correttamente quando l'arrotondamento era "inattivo".
Eric Lippert,

8
Incredibile quanti bug possono esserci in questo piccolo pezzo di codice. Non ho mai avuto molto tempo per pensarci - il risultato si manifesta nei commenti. (1) a * b> 0 sarebbe corretto se non traboccasse. Esistono 9 combinazioni per il segno di aeb - [-1, 0, +1] x [-1, 0, +1]. Possiamo ignorare il caso b == 0 lasciando i 6 casi [-1, 0, +1] x [-1, +1]. a / b arrotonda verso zero, ovvero arrotondando per risultati negativi e arrotondando per risultati positivi. Quindi la regolazione deve essere eseguita se aeb hanno lo stesso segno e non sono entrambi zero.
Daniel Brückner,

5
Questa risposta è probabilmente la cosa peggiore che ho scritto su SO ... ed è ora collegata dal blog di Eric ... Beh, la mia intenzione non era quella di dare una soluzione leggibile; Stavo davvero bloccando un hack breve e veloce. E per difendere di nuovo la mia soluzione, ho avuto l'idea giusta la prima volta, ma non ho pensato agli overflow. È stato ovviamente un mio errore pubblicare il codice senza scriverlo e testarlo in VisualStudio. Le "correzioni" sono anche peggiori: non mi rendevo conto che si trattava di un problema di overflow e pensavo di aver fatto un errore logico. Di conseguenza le prime "correzioni" non hanno cambiato nulla; Ho appena invertito
Daniel Brückner il

10
logica e ha spinto il bug in giro. Qui ho fatto i prossimi errori; come già accennato da Eric, non ho davvero analizzato il bug e ho fatto solo la prima cosa che sembrava giusta. E ancora non ho usato VisualStudio. Okay, avevo fretta e non ho trascorso più di cinque minuti nella "correzione", ma questa non dovrebbe essere una scusa. Dopo che Eric ha ripetutamente segnalato il bug, ho acceso VisualStudio e ho trovato il vero problema. La correzione usando Sign () rende la cosa ancora più illeggibile e la trasforma in codice che non vuoi davvero mantenere. Ho imparato la lezione e non sottostimerò più quanto sia complicato
Daniel Brückner,

-2

Alcune delle risposte sopra usano float, questo è inefficiente e davvero non necessario. Per gli integ non firmati questa è una risposta efficace per int1 / int2:

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

Per gli integri firmati questo non sarà corretto


Non quello che l'OP ha chiesto in primo luogo, non aggiunge realmente alle altre risposte.
santamanno,

-4

Il problema con tutte le soluzioni qui è che hanno bisogno di un cast o hanno un problema numerico. Lanciare in modalità float o double è sempre un'opzione, ma possiamo fare di meglio.

Quando si utilizza il codice della risposta da @jerryjvl

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

c'è un errore di arrotondamento. 1/5 arrotonderebbe per eccesso, perché 1% 5! = 0. Ma questo è sbagliato, perché l'arrotondamento avverrà solo se si sostituisce 1 con un 3, quindi il risultato è 0,6. Dobbiamo trovare un modo per arrotondare quando il calcolo ci fornisce un valore maggiore o uguale a 0,5. Il risultato dell'operatore modulo nell'esempio superiore ha un intervallo compreso tra 0 e myInt2-1. L'arrotondamento avverrà solo se il resto è maggiore del 50% del divisore. Quindi il codice modificato è simile al seguente:

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

Ovviamente abbiamo un problema di arrotondamento anche su myInt2 / 2, ma questo risultato ti darà una soluzione di arrotondamento migliore rispetto agli altri su questo sito.


"Dobbiamo trovare un modo per arrotondare quando il calcolo ci fornisce un valore maggiore o uguale a 0,5" - hai perso il punto di questa domanda - o arrotondare sempre, ovvero l'OP vuole arrotondare da 0,001 a 1.
Grhm,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.