¿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.
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:
La división redondea el resultado hacia cero.
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.
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.
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:
la operación arroja si el divisor es cero
la operación arroja si el dividendo es int.minval y el divisor es -1
Si no hay resto - la división es 'par' - entonces el valor de retorno es el cociente integral
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.
publicstaticintDivRoundUp(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;elsereturn 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.
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:
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)
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:
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.
"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.
Respuestas:
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:
La división redondea el resultado hacia cero.
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.
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.
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:la operación arroja si el divisor es cero
la operación arroja si el dividendo es int.minval y el divisor es -1
Si no hay resto - la división es 'par' - entonces el valor de retorno es el cociente integral
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.
¿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.
fuente
Todas las respuestas aquí hasta ahora parecen demasiado complicadas.
En C # y Java, para dividendos y divisores positivos, simplemente debe hacer:
Fuente: Conversión de números, Roland Backhouse, 2001
fuente
((13-1)%3)+1)
da 1 como resultado. Tomar el tipo correcto de división,1+(dividend - 1)/divisor
da 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.La respuesta final basada en int
Para enteros con signo:
Para enteros sin signo:
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) >= 0
determina 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 != 0
hará 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.Ceiling
tal vez algo indeseable en las entradas 'correctas'.El camino recorrido
Reemplazar (tenga en cuenta que reemplacé el segundo
myInt1
conmyInt2
, asumiendo que eso era lo que quiso decir):con:
La única advertencia es que si se
myInt1 - 1 + myInt2
desborda 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
myInt1
valores negativos , podría ser más fácil hacer algo como:Eso debería dar el resultado correcto al
div
usar 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
DivRem
solo 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):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)
fuente
Posibilidad perfecta de usar un método de extensión:
Esto también hace que su código sea súper legible:
fuente
Podrías escribir un ayudante.
fuente
Podrías usar algo como lo siguiente.
fuente
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:
Para entradas firmadas, esto no será correcto
fuente
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
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í:
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.
fuente