web-dev-qa-db-fra.com

Comment puis-je m'assurer qu'une division d'entiers est toujours arrondie?

Je veux m'assurer qu'une division de nombres entiers est toujours arrondie si nécessaire. Y a-t-il un meilleur moyen que cela? Il y a beaucoup de casting en cours. :-)

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

MISE À JOUR: Cette question était l'objet de mon blog en janvier 201 . Merci pour la bonne question!


Obtenir l'arithmétique entière correcte est difficile. Comme cela a été amplement démontré jusqu'à présent, au moment où vous essayez de faire un tour "intelligent", il y a de fortes chances que vous ayez commis une erreur. Et quand une faille est trouvée, changer le code pour la réparer sans considérer si le correctif casse autre chose n'est pas une bonne technique de résolution de problème. Jusqu'à présent, je pense que cinq solutions arithmétiques entières incorrectes différentes ont été affichées pour résoudre ce problème totalement peu difficile.

La bonne façon d’aborder les problèmes arithmétiques en nombres entiers - c’est-à-dire celle qui augmente les chances d’obtenir la bonne réponse du premier coup - consiste à aborder le problème avec précaution, à le résoudre étape par étape et à appliquer de bons principes d’ingénierie. alors.

Commencez par lire la spécification de ce que vous essayez de remplacer. La spécification de la division entière indique clairement:

  1. La division arrondit le résultat vers zéro

  2. Le résultat est zéro ou positif lorsque les deux opérandes ont le même signe et zéro ou négatif lorsque les deux opérandes ont des signes opposés.

  3. Si l'opérande gauche est le plus petit int représentable et que l'opérande droit est -1, un débordement se produit. [...] il est défini par l'implémentation de savoir si [une exception arithmétique] est levée ou si le dépassement de capacité n'est pas signalé, la valeur résultante étant celle de l'opérande de gauche.

  4. Si la valeur de l'opérande de droite est zéro, une exception System.DivideByZeroException est levée.

Ce que nous voulons, c'est une fonction de division entière qui calcule le quotient mais arrondit le résultat toujours vers le haut, pas toujours vers zéro.

Donc écrivez une spécification pour cette fonction. Notre fonction int DivRoundUp(int dividend, int divisor) doit avoir un comportement défini pour chaque entrée possible. Ce comportement indéfini est profondément préoccupant, éliminons-le. Nous dirons que notre opération a cette spécification:

  1. opération jette si diviseur est zéro

  2. opération lève si dividend est int.minval et diviseur est -1

  3. s'il n'y a pas de reste - la division est "même" - alors la valeur de retour est le quotient intégral

  4. Sinon, il retourne le nombre entier le plus petit ​​qui est supérieur au quotient, c'est-à-dire qu'il est toujours arrondi.

Maintenant que nous avons une spécification, nous savons donc que nous pouvons créer une conception testable . Supposons que nous ajoutions un critère de conception supplémentaire voulant que le problème soit résolu uniquement avec une arithmétique entière, plutôt que de calculer le quotient comme un double, car la solution "double" a été explicitement rejetée dans l'énoncé du problème.

Alors, que devons-nous calculer? Clairement, pour répondre à nos spécifications tout en restant uniquement en arithmétique entière, nous devons connaître trois faits. Premièrement, quel était le quotient entier? Deuxièmement, la division était-elle libre de reste? Et troisièmement, si non, le quotient entier a-t-il été calculé en arrondissant à la hausse ou à la baisse?

Maintenant que nous avons une spécification et une conception, nous pouvons commencer à écrire du code.

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

Est-ce intelligent? Pas beau? Non court? N ° correct selon le cahier des charges? Je crois que oui, mais je ne l'ai pas encore complètement testé. Ça a l'air plutôt bien.

Nous sommes des professionnels ici; utiliser de bonnes pratiques d'ingénierie. Recherchez vos outils, spécifiez le comportement souhaité, considérez d’abord les cas d’erreur et écrivez le code pour souligner son exactitude évidente. Et lorsque vous trouvez un bogue, considérez si votre algorithme est profondément défectueux avant de commencer simplement à échanger de manière aléatoire les instructions de comparaison et à casser des éléments qui fonctionnent déjà.

661
Eric Lippert

La réponse finale en int

Pour les entiers signés:

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

Pour les entiers non signés:

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

Le raisonnement de cette réponse

Division entière '/ 'est défini pour arrondir à zéro (7.7.2 de la spécification), mais nous voulons arrondir. Cela signifie que les réponses négatives sont déjà arrondies correctement, mais que les réponses positives doivent être ajustées.

Les réponses positives non nul sont faciles à détecter, mais la réponse zéro est un peu plus délicate, car cela peut être soit l’arrondi d’une valeur négative, soit celui arrondi d’une valeur positive.

Le pari le plus sûr est de détecter le moment où la réponse doit être positive en vérifiant que les signes des deux nombres entiers sont identiques. Opérateur xor entier '^ 'sur les deux valeurs donnera un bit de signe 0 lorsque c'est le cas, ce qui signifie un résultat non négatif, donc le contrôle (a ^ b) >= 0 détermine que le résultat aurait dû être positif avant arrondi. Notez également que pour les entiers non signés, chaque réponse est évidemment positive, cette vérification peut donc être omise.

Il ne reste plus qu'à vérifier s'il y a eu des arrondis, pour lesquels a % b != 0 fera le travail.

Leçons apprises

L'arithmétique (entière ou autre) n'est pas aussi simple qu'il y paraît. Penser soigneusement requis à tout moment.

De plus, bien que ma réponse finale ne soit peut-être pas aussi simple, évidente ou même rapide que la virgule flottante, elle a une très forte qualité rédemptrice pour moi; J'ai maintenant raisonné à travers la réponse, donc je suis en fait certain que c'est correct (jusqu'à ce que quelqu'un de plus intelligent me dise le contraire - un regard furtif dans la direction d'Eric -).

Pour obtenir le même sentiment de certitude à propos de la réponse en virgule flottante, il faudrait que je réfléchisse davantage (et peut-être plus compliqué) à la question de savoir s'il existe des conditions dans lesquelles la précision de la virgule flottante pourrait gêner et si Math.Ceiling peut-être fait quelque chose de indésirable avec les entrées "juste".

Le chemin parcouru

Remplacer (notez que j'ai remplacé le second myInt1 avec myInt2, en supposant que c’était ce que vous vouliez dire:

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

avec:

(myInt1 - 1 + myInt2) / myInt2

La seule réserve est que si myInt1 - 1 + myInt2 _ déborde le type entier que vous utilisez, vous pourriez ne pas obtenir ce que vous attendiez.

Raison pour laquelle c'est faux : -1000000 et 3999 devraient donner -250, cela donne -249

EDIT:
Considérant que cela a la même erreur que l'autre solution entière pour négatif myInt1 valeurs, il pourrait être plus facile de faire quelque chose comme:

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

Cela devrait donner le résultat correct dans div en n'utilisant que des opérations entières.

Raison pour laquelle c'est faux : -1 et -5 devraient donner 1, cela donne 0

EDIT (encore une fois, avec sentiment):
L'opérateur de division arrondit vers zéro; pour les résultats négatifs, cela est tout à fait exact, de sorte que seuls les résultats non négatifs nécessitent un ajustement. Considérant également que DivRem ne fait qu'un / et un % de toute façon, passons l’appel (et commençons par la comparaison facile pour éviter le calcul modulo quand il n’est pas nécessaire):

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

Raison pour laquelle c'est faux : -1 et 5 doivent donner 0, cela donne 1

(Pour ma propre défense de la dernière tentative, je n'aurais jamais dû tenter une réponse motivée alors que mon esprit me disait que j'avais 2 heures de retard pour dormir)

46
jerryjvl

Toutes les réponses semblent jusqu'ici plutôt compliquées.

En C # et Java, pour un dividende positif et un diviseur, il vous suffit de faire:

( dividend + divisor - 1 ) / divisor 

Source: Conversion de nombre, Roland Backhouse, 2001

46
Ian Nelson

Occasion parfaite d'utiliser une méthode d'extension:

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

Cela rend également votre code très lisible:

int result = myInt.DivideByAndRoundUp(4);
20
joshcomley

Vous pouvez écrire une aide.

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

Vous pouvez utiliser quelque chose comme ce qui suit.

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