Como garantir que uma divisão de números inteiros seja sempre arredondada?

242

Quero garantir que uma divisão de números inteiros seja sempre arredondada, se necessário. Existe uma maneira melhor do que isso? Há muitos lançamentos acontecendo. :-)

(int)Math.Ceiling((double)myInt1 / myInt2)
Karsten
fonte
48
Você pode definir mais claramente o que considera "melhor"? Mais rápido? Mais curta? Mais preciso? Mais robusto? Mais obviamente correto?
Eric Lippert
6
Você sempre usa muito elenco com matemática em C # - é por isso que não é uma ótima linguagem para esse tipo de coisa. Quer os valores arredondados para cima ou para longe de zero - deve -3.1 ir para -3 (para cima) ou 4 (longe de zero)
Keith
9
Eric: O que você quer dizer com "Mais preciso? Mais robusto? Mais obviamente correto?" Na verdade, o que eu quis dizer foi apenas "melhor", deixaria o leitor colocar o significado em melhor. Então, se alguém tiver um código mais curto, ótimo, se outro tiver um código mais rápido, também ótimo :-) E você, você tem alguma sugestão?
291 Karsten
1
Eu sou o único que, ao ler o título, "Oh, é algum tipo de resumo de C #?"
Matt Ball
6
É realmente incrível o quão sutilmente difícil essa pergunta acabou sendo, e quão instrutiva a discussão tem sido.
Justin Morgan

Respostas:

669

ATUALIZAÇÃO: Esta pergunta foi o assunto do meu blog em janeiro de 2013 . Obrigado pela ótima pergunta!


Obter aritmética inteira correta é difícil. Como foi demonstrado amplamente até agora, no momento em que você tenta fazer um truque "inteligente", é bem provável que você tenha cometido um erro. E quando uma falha é encontrada, alterar o código para corrigi-la sem considerar se a correção quebra outra coisa não é uma boa técnica de solução de problemas. Até agora, achamos que foram postadas cinco soluções aritméticas inteiras incorretas diferentes para este problema completamente não particularmente difícil.

A maneira correta de abordar problemas aritméticos inteiros - ou seja, a maneira que aumenta a probabilidade de obter a resposta certa da primeira vez - é abordar o problema com cuidado, resolvê-lo um passo de cada vez e usar bons princípios de engenharia para fazer isso. tão.

Comece lendo a especificação para o que você está tentando substituir. A especificação para divisão inteira afirma claramente:

  1. A divisão arredonda o resultado para zero

  2. O resultado é zero ou positivo quando os dois operandos têm o mesmo sinal e zero ou negativo quando os dois operandos têm sinais opostos

  3. Se o operando esquerdo for o menor int representável e o operando direito for –1, ocorrerá um estouro. [...] é definido pela implementação se [uma ArithmeticException] é lançada ou se o estouro não é relatado, com o valor resultante sendo o do operando esquerdo.

  4. Se o valor do operando certo for zero, será lançado um System.DivideByZeroException.

O que queremos é uma função de divisão inteira que calcule o quociente, mas arredonde o resultado sempre para cima , nem sempre para zero .

Então escreva uma especificação para essa função. Nossa função int DivRoundUp(int dividend, int divisor)deve ter um comportamento definido para todas as entradas possíveis. Esse comportamento indefinido é profundamente preocupante, então vamos eliminá-lo. Diremos que nossa operação tem esta especificação:

  1. operação lança se divisor é zero

  2. A operação será lançada se o dividendo for int.minval e o divisor for -1

  3. se não houver resto - a divisão é 'par' - então o valor de retorno é o quociente integral

  4. Caso contrário, ele retorna o menor número inteiro maior que o quociente, ou seja, sempre arredonda para cima.

Agora temos uma especificação, para que possamos criar um design testável . Suponha que adicionemos um critério de design adicional para que o problema seja resolvido apenas com aritmética inteira, em vez de computar o quociente como um duplo, pois a solução "duplo" foi explicitamente rejeitada na declaração do problema.

Então, o que devemos calcular? Claramente, para atender às nossas especificações e permanecer unicamente na aritmética inteira, precisamos conhecer três fatos. Primeiro, qual foi o quociente inteiro? Segundo, a divisão estava livre do restante? E terceiro, se não, o quociente inteiro foi calculado arredondando para cima ou para baixo?

Agora que temos uma especificação e um design, podemos começar a escrever o 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;
}

Isso é inteligente? Não é bonito? Não. Curto? Não. Correto de acordo com a especificação? Acredito que sim, mas ainda não o testei completamente. Parece muito bom embora.

Somos profissionais aqui; use boas práticas de engenharia. Pesquise suas ferramentas, especifique o comportamento desejado, considere os casos de erro primeiro e escreva o código para enfatizar sua correção óbvia. E quando você encontrar um bug, considere se o seu algoritmo é profundamente defeituoso antes de começar aleatoriamente a trocar as direções das comparações e quebrar as coisas que já funcionam.

Eric Lippert
fonte
44
Excelente resposta exemplar
Gavin Miller
61
O que me interessa não é o comportamento; qualquer comportamento parece justificável. O que me interessa é que não seja especificado , o que significa que não pode ser facilmente testado. Nesse caso, estamos definindo nosso próprio operador, para que possamos especificar o comportamento que desejar. não me importo se esse comportamento é "jogar" ou "não jogar", mas me importo que seja declarado.
30511 Eric Lippert
68
Darn-lo, pedantismo falhar :(
Jon Skeet
32
Homem - Você poderia escrever um livro sobre isso, por favor?
xtofl 8/09/09
76
@ finnw: Se eu testei ou não, é irrelevante. Resolver esse problema aritmético inteiro não é meu problema comercial; se fosse, então eu estaria testando. Se alguém quiser tirar o código de estranhos da Internet para resolver seus problemas de negócios, o ônus é deles para testá-lo completamente.
Eric Lippert
49

Todas as respostas aqui até agora parecem bastante complicadas.

Em C # e Java, para dividendo positivo e divisor, você simplesmente precisa:

( dividend + divisor - 1 ) / divisor 

Fonte: Conversão de números, Roland Backhouse, 2001

Ian Nelson
fonte
Impressionante. Embora você devesse ter adicionado os parênteses, para remover as ambiguidades. A prova disso foi um pouco demorada, mas você pode sentir no estômago, que está certo, apenas olhando para ele.
Jörgen Sigvardsson
1
Hmmm ... e quanto ao dividendo = 4, divisor = (- 2) ??? 4 / (-2) = (-2) = (-2) depois de arredondado. mas o algoritmo que você forneceu (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0,5) = 0 após ser arredondado.
Scott
1
@ Scott - desculpe, omiti mencionar que esta solução é válida apenas para dividendos e divisores positivos. Eu atualizei minha resposta para mencionar esclarecer isso.
Ian Nelson
1
Eu gosto dele, é claro que você pode ter um estouro de um pouco artificial no numerador como um subproduto desta abordagem ...
TCC
2
@ PinIntag: A idéia é boa, mas o uso do módulo está errado. Tome 13 e 3. Resultado esperado 5, mas ((13-1)%3)+1)fornece 1 como resultado. Tomando o tipo certo de divisão, 1+(dividend - 1)/divisordá o mesmo resultado que a resposta para dividendos e divisores positivos. Além disso, não há problemas de transbordamento, por mais artificiais que sejam.
Lutz Lehmann
48

A resposta final baseada em int

Para números inteiros assinados:

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

Para números inteiros não assinados:

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

O raciocínio para esta resposta

A divisão inteira ' /' é definida para arredondar para zero (7.7.2 da especificação), mas queremos arredondar para cima. Isso significa que as respostas negativas já estão arredondadas corretamente, mas as respostas positivas precisam ser ajustadas.

Respostas positivas diferentes de zero são fáceis de detectar, mas responder zero é um pouco mais difícil, pois pode ser o arredondamento de um valor negativo ou o arredondamento de um valor positivo.

A aposta mais segura é detectar quando a resposta deve ser positiva, verificando se os sinais dos dois números inteiros são idênticos. O operador xor inteiro ' ^' nos dois valores resultará em um bit de sinal 0 quando for o caso, significando um resultado não negativo, portanto, a verificação (a ^ b) >= 0determina que o resultado deve ter sido positivo antes do arredondamento. Observe também que, para números inteiros não assinados, todas as respostas são obviamente positivas, portanto, essa verificação pode ser omitida.

A única verificação restante é, então, se ocorreu algum arredondamento, para o qual a % b != 0fará o trabalho.

Lições aprendidas

Aritmética (número inteiro ou não) não é tão simples quanto parece. Pensar cuidadosamente exigido em todos os momentos.

Além disso, embora minha resposta final talvez não seja tão 'simples' ou 'óbvia' ou talvez até 'rápida' como o ponto flutuante responde, ela tem uma qualidade redentora muito forte para mim; Eu já raciocinei a resposta, então estou realmente certo de que está correto (até que alguém mais inteligente me diga o contrário - um olhar furtivo na direção de Eric -).

Para obter o mesmo sentimento de certeza sobre a resposta de ponto flutuante, que eu teria que fazer mais (e possivelmente mais complicado) pensar sobre se há quaisquer condições em que a precisão de ponto flutuante pode ficar no caminho, e se Math.Ceilingtalvez não algo indesejável nas entradas 'just right'.

O caminho percorrido

Substitua (note que substituí o segundo myInt1por myInt2, assumindo que foi isso que você quis dizer):

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

com:

(myInt1 - 1 + myInt2) / myInt2

A única ressalva é que, se myInt1 - 1 + myInt2exceder o tipo de número inteiro que você está usando, talvez você não consiga o que espera.

Razão pela qual está errado : -1000000 e 3999 devem dar -250, isso dá -249

EDIT:
Considerando que este tem o mesmo erro que a outra solução inteira para myInt1valores negativos , pode ser mais fácil fazer algo como:

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

Isso deve fornecer o resultado correto divusando apenas operações inteiras.

Razão pela qual está errado : -1 e -5 devem dar 1, isso fornece 0

EDIT (mais uma vez, com sentimento):
O operador de divisão se volta para zero; para resultados negativos, isso é exatamente correto; portanto, apenas resultados não negativos precisam de ajustes. Também considerando que DivRemapenas faz a /e a de %qualquer maneira, vamos pular a chamada (e começar com a comparação fácil para evitar o cálculo do módulo quando não for necessário):

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

Razão pela qual está errado : -1 e 5 devem dar 0, isso fornece 1

(Em minha defesa da última tentativa, eu nunca deveria ter tentado uma resposta fundamentada enquanto minha mente me dizia que eu estava duas horas atrasada para dormir)

jerryjvl
fonte
19

Oportunidade perfeita para usar um método de extensão:

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

Isso torna seu código super legível também:

int result = myInt.DivideByAndRoundUp(4);
Joshcomley
fonte
1
Erm, o que? Seu código seria chamado myInt.DivideByAndRoundUp () e sempre retornaria 1, exceto por uma entrada de 0 que causaria uma exceção ...
configurator
5
Falha épica. (-2) .DivideByAndRoundUp (2) retorna 0. #
Timwi
3
Estou realmente atrasado para a festa, mas esse código é compilado? Minha classe Math não contém um método Ceiling que usa dois argumentos.
R. Martinho Fernandes
17

Você poderia escrever um ajudante.

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}
JaredPar
fonte
1
Ainda a mesma quantidade de elenco acontecendo embora
ChrisF
29
@ Outlaw, presuma tudo o que quiser. Mas para mim, se eles não colocam isso em questão, geralmente presumo que eles não o consideraram.
JaredPar 28/08/09
1
Escrever um ajudante é inútil se for exatamente isso. Em vez disso, escreva um auxiliar com um conjunto de testes abrangente.
Dolmen
3
@dolmen Você está familiarizado com o conceito de reutilização de código ? oO
Rushyo
4

Você pode usar algo como o seguinte.

a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)
Daniel Brückner
fonte
12
Esse código está obviamente errado de duas maneiras. Primeiro, há um pequeno erro na sintaxe; você precisa de mais parênteses. Mais importante, porém, não calcula o resultado desejado. Por exemplo, tente testar com a = -1000000 eb = 3999. O resultado regular da divisão inteira é -250. A divisão dupla é -250,0625 ... O comportamento desejado é arredondar para cima. Claramente, o arredondamento correto de -250,0625 é arredondado para -250, mas seu código é arredondado para -249.
Eric Lippert
36
Lamento ter que continuar dizendo isso, mas seu código ainda está errado Daniel. 1/2 deve arredondar para cima para 1, mas seu código o reduz para 0. Cada vez que encontro um bug, você o "corrige" introduzindo outro bug. Meu conselho: pare de fazer isso. Quando alguém encontrar um bug no seu código, não apenas dê uma correção sem pensar claramente no que causou o bug. Use boas práticas de engenharia; encontre a falha no algoritmo e corrija-a. A falha nas três versões incorretas do seu algoritmo é que você não está determinando corretamente quando o arredondamento foi "inativo".
Eric Lippert
8
Inacreditável quantos bugs podem existir neste pequeno pedaço de código. Eu nunca tive muito tempo para pensar sobre isso - o resultado se manifesta nos comentários. (1) a * b> 0 estaria correto se não transbordasse. Existem 9 combinações para o sinal de aeb - [-1, 0, +1] x [-1, 0, +1]. Podemos ignorar o caso b == 0 deixando os 6 casos [-1, 0, +1] x [-1, +1]. a / b arredonda para zero, ou seja, arredondando para cima para resultados negativos e arredondando para baixo para resultados positivos. Portanto, o ajuste deve ser realizado se aeb tiverem o mesmo sinal e não forem zero.
Daniel Brückner
5
Essa resposta é provavelmente a pior coisa que escrevi no SO ... e agora está vinculada ao blog de Eric ... Bem, minha intenção não era fornecer uma solução legível; Eu estava realmente travando para um hack curto e rápido. E para defender minha solução novamente, acertei a ideia da primeira vez, mas não pensei em estouros. Obviamente, foi um erro meu publicar o código sem escrevê-lo e testá-lo no VisualStudio. As "correções" são ainda piores - não percebi que era um problema de estouro e pensei que cometi um erro lógico. Em conseqüência, as primeiras "correções" não mudaram nada; Acabei de inverter o
Daniel Brückner
10
lógica e empurrou o bug ao redor. Aqui eu cometi os próximos erros; como Eric já mencionou, eu realmente não analisei o bug e fiz a primeira coisa que parecia certa. E ainda não usei o VisualStudio. Ok, eu estava com pressa e não gastei mais de cinco minutos na "correção", mas isso não deveria ser uma desculpa. Depois que Eric apontou repetidamente o bug, iniciei o VisualStudio e encontrei o problema real. A correção usando Sign () torna a coisa ainda mais ilegível e a transforma em código que você realmente não deseja manter. Aprendi minha lição e não vou mais subestimar o quão difícil é
Daniel Brückner
-2

Algumas das respostas acima usam carros alegóricos, isso é ineficiente e realmente não é necessário. Para entradas não assinadas, essa é uma resposta eficiente para int1 / int2:

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

Para entradas assinadas, isso não estará correto

Tom Mensink
fonte
Não o que o OP pediu em primeiro lugar, realmente não contribui para as outras respostas.
23919 santamanno
-4

O problema com todas as soluções aqui é que elas precisam de uma conversão ou têm um problema numérico. Moldar para flutuar ou dobrar é sempre uma opção, mas podemos fazer melhor.

Quando você usa o código da resposta de @jerryjvl

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

há um erro de arredondamento. 1/5 seria arredondado, porque 1% 5! = 0. Mas isso está errado, porque o arredondamento só ocorrerá se você substituir o 1 por um 3, portanto o resultado é 0,6. Precisamos encontrar uma maneira de arredondar quando o cálculo nos fornecer um valor maior ou igual a 0,5. O resultado do operador módulo no exemplo superior tem um intervalo de 0 a myInt2-1. O arredondamento ocorrerá apenas se o restante for maior que 50% do divisor. Portanto, o código ajustado fica assim:

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

É claro que também temos um problema de arredondamento no myInt2 / 2, mas esse resultado fornecerá uma solução de arredondamento melhor do que os outros neste site.

raboni
fonte
"Precisamos encontrar uma maneira de arredondar quando o cálculo nos fornecer um valor maior ou igual a 0,5" - você perdeu o ponto desta pergunta - ou sempre arredonda, ou seja, o OP deseja arredondar 0,001 para 1.
Grhm