¿Cómo puedo asegurarme de que una división de enteros siempre se redondea?

242

Quiero asegurarme de que una división de enteros siempre se redondea si es necesario. ¿Hay una mejor manera que esta? Hay mucho casting en marcha. :-)

(int)Math.Ceiling((double)myInt1 / myInt2)
Karsten
fuente
48
¿Puedes definir más claramente lo que consideras "mejor"? ¿Más rápido? ¿Corta? ¿Más preciso? ¿Más robusto? ¿Más obviamente correcto?
Eric Lippert
66
Siempre tienes mucho casting con matemáticas en C #, es por eso que no es un gran lenguaje para este tipo de cosas. ¿Desea que los valores se redondeen hacia arriba o lejos de cero? ¿Debería -3.1 ir a -3 (arriba) o -4 (lejos de cero)
Keith
99
Eric: ¿Qué quieres decir con "más preciso? ¿Más robusto? ¿Más obviamente correcto?" En realidad, lo que quise decir fue simplemente "mejor", dejaría que el lector le diera un significado mejor. Entonces, si alguien tenía un código más corto, genial, si otro tenía un código más rápido, también genial :-) ¿Y usted tiene alguna sugerencia?
Karsten
1
¿Soy el único que, al leer el título, "Oh, es una especie de resumen de C #?"
Matt Ball
66
Es realmente sorprendente lo sutilmente difícil que resultó ser esta pregunta y lo instructiva que ha sido la discusión.
Justin Morgan

Respuestas:

669

ACTUALIZACIÓN: Esta pregunta fue el tema de mi blog en enero de 2013 . Gracias por la gran pregunta!


Obtener aritmética de enteros correcto es difícil. Como se ha demostrado ampliamente hasta el momento, en el momento en que intentas hacer un truco "inteligente", es muy probable que hayas cometido un error. Y cuando se encuentra una falla, cambiar el código para corregir la falla sin considerar si la reparación rompe algo más no es una buena técnica para resolver problemas. Hasta ahora hemos tenido, creo que publicaron cinco soluciones aritméticas enteras incorrectas diferentes para este problema completamente no particularmente difícil.

La forma correcta de abordar los problemas aritméticos de enteros, es decir, la forma en que aumenta la probabilidad de obtener la respuesta correcta la primera vez, es abordar el problema con cuidado, resolverlo paso a paso y utilizar buenos principios de ingeniería al hacer entonces.

Comience leyendo la especificación de lo que está tratando de reemplazar. La especificación para la división de enteros establece claramente:

  1. La división redondea el resultado hacia cero.

  2. El resultado es cero o positivo cuando los dos operandos tienen el mismo signo y cero o negativo cuando los dos operandos tienen signos opuestos.

  3. Si el operando izquierdo es el int representable más pequeño y el operando derecho es –1, se produce un desbordamiento. [...] está definido por la implementación en cuanto a si se lanza [una Excepción Aritmética] o si el desbordamiento no se informa con el valor resultante que es el del operando izquierdo.

  4. Si el valor del operando correcto es cero, se genera una excepción System.DivideByZeroException.

Lo que queremos es una función de división entera que calcule el cociente pero redondea el resultado siempre hacia arriba , no siempre hacia cero .

Entonces escriba una especificación para esa función. Nuestra función int DivRoundUp(int dividend, int divisor)debe tener un comportamiento definido para cada entrada posible. Ese comportamiento indefinido es profundamente preocupante, así que eliminémoslo. Diremos que nuestra operación tiene esta especificación:

  1. la operación arroja si el divisor es cero

  2. la operación arroja si el dividendo es int.minval y el divisor es -1

  3. Si no hay resto - la división es 'par' - entonces el valor de retorno es el cociente integral

  4. De lo contrario, devuelve el entero más pequeño que es mayor que el cociente, es decir, siempre se redondea.

Ahora tenemos una especificación, por lo que sabemos que podemos llegar a un diseño comprobable . Supongamos que agregamos un criterio de diseño adicional para que el problema se resuelva únicamente con la aritmética de enteros, en lugar de calcular el cociente como un doble, ya que la solución "doble" se ha rechazado explícitamente en la declaración del problema.

Entonces, ¿qué debemos calcular? Claramente, para cumplir con nuestras especificaciones mientras permanecemos únicamente en aritmética de enteros, necesitamos conocer tres hechos. Primero, ¿cuál fue el cociente entero? Segundo, ¿fue la división libre de residuos? Y tercero, si no, ¿se calculó el cociente entero redondeando hacia arriba o hacia abajo?

Ahora que tenemos una especificación y un diseño, podemos comenzar a escribir código.

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

¿Es esto inteligente? ¿No Hermosa? No. corto? No. ¿Correcto según la especificación? Creo que sí, pero no lo he probado completamente. Sin embargo, se ve bastante bien.

Somos profesionales aquí; use buenas prácticas de ingeniería. Investigue sus herramientas, especifique el comportamiento deseado, considere primero los casos de error y escriba el código para enfatizar su corrección obvia. Y cuando encuentre un error, considere si su algoritmo es profundamente defectuoso para comenzar antes de comenzar a intercambiar aleatoriamente las direcciones de las comparaciones y romper cosas que ya funcionan.

Eric Lippert
fuente
44
Excelente respuesta ejemplar
Gavin Miller,
61
Lo que me importa no es el comportamiento; Cualquier comportamiento parece justificable. Lo que me importa es que no está especificado , lo que significa que no se puede probar fácilmente. En este caso, estamos definiendo nuestro propio operador, por lo que podemos especificar cualquier comportamiento que nos guste. No me importa si ese comportamiento es "tirar" o "no tirar", pero me importa que se diga.
Eric Lippert
68
Maldición, la pedantería falla :(
Jon Skeet
32
Hombre - ¿Podrías escribir un libro sobre eso, por favor?
xtofl
76
@finnw: si lo he probado o no es irrelevante. Resolver este problema aritmético de enteros no es mi problema comercial; si lo fuera, entonces lo estaría probando. Si alguien quiere quitar el código de extraños de Internet para resolver su problema comercial, entonces es responsabilidad de ellos probarlo a fondo.
Eric Lippert
49

Todas las respuestas aquí hasta ahora parecen demasiado complicadas.

En C # y Java, para dividendos y divisores positivos, simplemente debe hacer:

( dividend + divisor - 1 ) / divisor 

Fuente: Conversión de números, Roland Backhouse, 2001

Ian Nelson
fuente
Increíble. Aunque, debería haber agregado los paréntesis, para eliminar las ambigüedades. La prueba de ello fue un poco larga, pero puede sentirlo en el intestino, que es correcto, solo con mirarlo.
Jörgen Sigvardsson
1
Hmmm ... ¿qué pasa con dividendo = 4, divisor = (- 2) ??? 4 / (-2) = (-2) = (-2) después de ser redondeado. pero el algoritmo que proporcionó (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0.5) = 0 después de ser redondeado.
Scott
1
@Scott: lo siento, omití mencionar que esta solución solo es válida para dividendos y divisores positivos. He actualizado mi respuesta para mencionar aclarar esto.
Ian Nelson
1
Me gusta, por supuesto, podría tener un desbordamiento artificial en el numerador como un subproducto de este enfoque ...
TCC
2
@PIntag: La idea es buena, pero el uso del módulo es incorrecto. Tome 13 y 3. Resultado esperado 5, pero ((13-1)%3)+1)da 1 como resultado. Tomar el tipo correcto de división, 1+(dividend - 1)/divisorda el mismo resultado que la respuesta para el dividendo positivo y el divisor. Además, no hay problemas de desbordamiento, por muy artificiales que puedan ser.
Lutz Lehmann
48

La respuesta final basada en int

Para enteros con signo:

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

Para enteros sin signo:

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

El razonamiento para esta respuesta

La división de enteros ' /' se define para redondear hacia cero (7.7.2 de la especificación), pero queremos redondear. Esto significa que las respuestas negativas ya están redondeadas correctamente, pero las respuestas positivas deben ajustarse.

Las respuestas positivas que no son cero son fáciles de detectar, pero la respuesta cero es un poco más complicada, ya que puede ser el redondeo de un valor negativo o el redondeo de uno positivo.

La apuesta más segura es detectar cuándo la respuesta debe ser positiva al verificar que los signos de ambos enteros sean idénticos. El operador entero xor ' ^' en los dos valores dará como resultado un bit de signo 0 cuando este sea el caso, lo que significa un resultado no negativo, por lo que la comprobación (a ^ b) >= 0determina que el resultado debería haber sido positivo antes del redondeo. También tenga en cuenta que para enteros sin signo, cada respuesta es obviamente positiva, por lo que esta verificación se puede omitir.

La única comprobación restante es si se ha producido algún redondeo, para lo cual a % b != 0hará el trabajo.

Lecciones aprendidas

La aritmética (entera o no) no es tan simple como parece. Pensando cuidadosamente requiere en todo momento.

Además, aunque mi respuesta final tal vez no sea tan 'simple' o 'obvia' o tal vez incluso 'rápida' como las respuestas de coma flotante, tiene una cualidad redentora muy fuerte para mí; Ahora he razonado a través de la respuesta, así que estoy realmente seguro de que es correcta (hasta que alguien más inteligente me diga lo contrario, una mirada furtiva en la dirección de Eric ).

Para tener la misma sensación de certeza sobre la respuesta de punto flotante, tendría que pensar más (y posiblemente más complicado) si hay alguna condición bajo la cual la precisión del punto flotante podría interferir, y si Math.Ceilingtal vez algo indeseable en las entradas 'correctas'.

El camino recorrido

Reemplazar (tenga en cuenta que reemplacé el segundo myInt1con myInt2, asumiendo que eso era lo que quiso decir):

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

con:

(myInt1 - 1 + myInt2) / myInt2

La única advertencia es que si se myInt1 - 1 + myInt2desborda el tipo entero que está utilizando, es posible que no obtenga lo que espera.

Razón por la que esto está mal : -1000000 y 3999 deberían dar -250, esto da -249

EDITAR:
Teniendo en cuenta que esto tiene el mismo error que la otra solución entera para myInt1valores negativos , podría ser más fácil hacer algo como:

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

Eso debería dar el resultado correcto al divusar solo operaciones enteras.

Razón por la que esto está mal : -1 y -5 deberían dar 1, esto da 0

EDITAR (una vez más, con sentimiento):
el operador de división se redondea hacia cero; para resultados negativos esto es exactamente correcto, por lo que solo los resultados no negativos necesitan un ajuste. También teniendo en cuenta que DivRemsolo hace ay /a de %todos modos, omita la llamada (y comencemos con la comparación fácil para evitar el cálculo del módulo cuando no sea necesario):

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

Razón por la que esto está mal : -1 y 5 deberían dar 0, esto da 1

(En mi propia defensa del último intento, nunca debería haber intentado una respuesta razonada mientras mi mente me decía que llegaba 2 horas tarde para dormir)

jerryjvl
fuente
19

Posibilidad perfecta de usar un método de extensión:

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

Esto también hace que su código sea súper legible:

int result = myInt.DivideByAndRoundUp(4);
joshcomley
fuente
1
Erm, que? El código sería llamado myInt.DivideByAndRoundUp () y siempre devuelve 1 a excepción de una entrada de 0 lo que causaría una excepción ...
configurador
55
Fallo épico. (-2) .DivideByAndRoundUp (2) devuelve 0.
Timwi
3
Realmente llego tarde a la fiesta, pero ¿se compila este código? Mi clase de Matemáticas no contiene un método de techo que toma dos argumentos.
R. Martinho Fernandes
17

Podrías escribir un ayudante.

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}
JaredPar
fuente
1
Sin embargo, sigue la misma cantidad de casting
ChrisF
29
@ Fuera de la ley, presume todo lo que quieras. Pero para mí, si no lo ponen en la pregunta, generalmente asumo que no lo consideraron.
JaredPar
1
Escribir un ayudante es inútil si es solo eso. En cambio, escriba un ayudante con un conjunto completo de pruebas.
dolmen
3
@dolmen ¿Está familiarizado con el concepto de reutilización de código ? oO
Rushyo
4

Podrías usar algo como lo siguiente.

a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)
Daniel Brückner
fuente
12
Este código obviamente es incorrecto de dos maneras. En primer lugar, hay un pequeño error en la sintaxis; Necesitas más paréntesis. Pero lo más importante, no calcula el resultado deseado. Por ejemplo, intente probar con a = -1000000 yb = 3999. El resultado de la división entera normal es -250. La doble división es -250.0625 ... El comportamiento deseado es redondear. Claramente, el redondeo correcto desde -250.0625 es redondear hasta -250, pero su código se redondea hasta -249.
Eric Lippert
36
Lamento tener que seguir diciendo esto, pero su código TODAVÍA está EQUIVOCADO Daniel. 1/2 debería redondear hacia ARRIBA a 1, pero su código lo redondea hacia ABAJO a 0. Cada vez que encuentro un error, lo "arregla" introduciendo otro error. Mi consejo: deja de hacer eso. Cuando alguien encuentre un error en su código, no solo intente solucionarlo sin pensar claramente qué causó el error en primer lugar. Use buenas prácticas de ingeniería; encuentra la falla en el algoritmo y arréglalo. La falla en las tres versiones incorrectas de su algoritmo es que no está determinando correctamente cuándo el redondeo estaba "abajo".
Eric Lippert
8
Increíble cuántos errores puede haber en este pequeño fragmento de código. Nunca tuve mucho tiempo para pensarlo, el resultado se manifiesta en los comentarios. (1) a * b> 0 sería correcto si no se desbordara. Hay 9 combinaciones para el signo de a y b - [-1, 0, +1] x [-1, 0, +1]. Podemos ignorar el caso b == 0 dejando los 6 casos [-1, 0, +1] x [-1, +1]. a / b se redondea hacia cero, es decir, redondeando hacia arriba para obtener resultados negativos y redondeando hacia abajo para obtener resultados positivos. Por lo tanto, el ajuste debe realizarse si ayb tienen el mismo signo y no son ambos cero.
Daniel Brückner
55
Esta respuesta es probablemente lo peor que escribí en SO ... y ahora está vinculada por el blog de Eric ... Bueno, mi intención no era dar una solución legible; Estaba realmente bloqueado por un hack corto y rápido. Y para defender mi solución nuevamente, tuve la idea correcta la primera vez, pero no pensé en los desbordamientos. Obviamente fue mi error publicar el código sin escribirlo y probarlo en VisualStudio. Las "soluciones" son aún peores: no me di cuenta de que se trataba de un problema de desbordamiento y pensé que había cometido un error lógico. En consecuencia, las primeras "soluciones" no cambiaron nada; Acabo de invertir el
Daniel Brückner
10
lógica y empujó el error. Aquí cometí los siguientes errores; como Eric ya mencionó, realmente no analicé el error e hice lo primero que parecía correcto. Y todavía no usé VisualStudio. Bien, tenía prisa y no pasé más de cinco minutos en el "arreglo", pero esto no debería ser una excusa. Después de que Eric señaló repetidamente el error, encendí VisualStudio y encontré el verdadero problema. La solución que usa Sign () hace que la cosa sea aún más ilegible y la convierte en un código que realmente no desea mantener. Aprendí mi lección y ya no subestimaré lo difícil que es
Daniel Brückner
-2

Algunas de las respuestas anteriores usan flotadores, esto es ineficiente y realmente no es necesario. Para entradas sin signo, esta es una respuesta eficiente para int1 / int2:

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

Para entradas firmadas, esto no será correcto

Tom Mensink
fuente
No es lo que el OP preguntó en primer lugar, realmente no se suma a las otras respuestas.
santamanno
-4

El problema con todas las soluciones aquí es que necesitan un yeso o que tienen un problema numérico. Lanzar para flotar o duplicar siempre es una opción, pero podemos hacerlo mejor.

Cuando usas el código de la respuesta de @jerryjvl

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

Hay un error de redondeo. 1/5 se redondearía hacia arriba, porque 1% 5! = 0. Pero esto está mal, porque el redondeo solo ocurrirá si reemplaza el 1 con un 3, por lo que el resultado es 0.6. Necesitamos encontrar una manera de redondear cuando el cálculo nos da un valor mayor o igual a 0.5. El resultado del operador de módulo en el ejemplo superior tiene un rango de 0 a myInt2-1. El redondeo solo ocurrirá si el resto es mayor al 50% del divisor. Entonces el código ajustado se ve así:

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

Por supuesto, también tenemos un problema de redondeo en myInt2 / 2, pero este resultado le dará una mejor solución de redondeo que las otras en este sitio.

raboni
fuente
"Necesitamos encontrar una manera de redondear cuando el cálculo nos da un valor mayor o igual a 0.5", ha omitido el punto de esta pregunta, o siempre redondear, es decir, el OP quiere redondear 0.001 a 1.
Grhm