É tudo uma questão de armazenamento e algoritmos adequados para tratar os números como partes menores. Vamos supor que você tenha um compilador no qual um int
pode ser apenas de 0 a 99 e você deseja lidar com números até 999999 (vamos nos preocupar apenas com números positivos aqui para mantê-lo simples).
Você faz isso dando a cada número três se int
usando as mesmas regras que você (deveria ter) aprendido na escola primária para adição, subtração e outras operações básicas.
Em uma biblioteca de precisão arbitrária, não há limite fixo para o número de tipos básicos usados para representar nossos números, apenas o que a memória pode conter.
Adição, por exemplo 123456 + 78
::
12 34 56
78
-- -- --
12 35 34
Trabalhando a partir da extremidade menos significativa:
- transporte inicial = 0.
- 56 + 78 + 0 transporte = 134 = 34 com 1 transporte
- 34 + 00 + 1 transporte = 35 = 35 com 0 transporte
- 12 + 00 + 0 transporte = 12 = 12 com 0 transporte
Esta é, de fato, como a adição geralmente funciona no nível de bits dentro de sua CPU.
A subtração é semelhante (usando subtração do tipo de base e emprestar em vez de carregar), a multiplicação pode ser feita com adições repetidas (muito lentas) ou produtos cruzados (mais rápido) e a divisão é mais complicada, mas pode ser feita por deslocamento e subtração dos números envolvido (a longa divisão que você teria aprendido quando criança).
Na verdade, escrevi bibliotecas para fazer esse tipo de coisa usando as potências máximas de dez que podem caber em um inteiro quando ao quadrado (para evitar estouro ao multiplicar dois int
s juntos, como um de 16 bits int
sendo limitado de 0 a 99 a gerar 9.801 (<32.768) quando elevado ao quadrado, ou 32 bits int
usando 0 a 9.999 para gerar 99.980.001 (<2.147.483.648)), o que facilitou muito os algoritmos.
Alguns truques a serem observados.
1 / Ao somar ou multiplicar números, pré-aloque o espaço máximo necessário e reduza depois se achar que é muito. Por exemplo, adicionar dois números de 100 "dígitos" (onde dígito é um int
) nunca fornecerá mais de 101 dígitos. Multiplicar um número de 12 dígitos por um número de 3 dígitos nunca gerará mais de 15 dígitos (adicione as contagens de dígitos).
2 / Para aumentar a velocidade, normalize (reduza o armazenamento necessário para) os números apenas se for absolutamente necessário - minha biblioteca tinha isso como uma chamada separada para que o usuário pudesse decidir entre as questões de velocidade e armazenamento.
3 / A adição de um número positivo e um negativo é uma subtração, e subtrair um número negativo é o mesmo que adicionar o positivo equivalente. Você pode economizar bastante código fazendo com que os métodos add e subtract chamem uns aos outros após ajustar os sinais.
4 / Evite subtrair números grandes de pequenos, pois você invariavelmente acaba com números como:
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
Em vez disso, subtraia 10 de 11 e, em seguida, negue:
11
10-
--
1 (then negate to get -1).
Aqui estão os comentários (transformados em texto) de uma das bibliotecas para as quais tive que fazer isso. O código em si é, infelizmente, protegido por direitos autorais, mas você pode conseguir obter informações suficientes para lidar com as quatro operações básicas. Assuma a seguir que -a
e -b
representam números negativos e a
e b
são zero ou números positivos.
Para adição , se os sinais forem diferentes, use a subtração da negação:
-a + b becomes b - a
a + -b becomes a - b
Para subtração , se os sinais forem diferentes, use a adição da negação:
a - -b becomes a + b
-a - b becomes -(a + b)
Também tratamento especial para garantir que estamos subtraindo números pequenos de grandes:
small - big becomes -(big - small)
A multiplicação usa matemática básica da seguinte maneira:
475(a) x 32(b) = 475 x (30 + 2)
= 475 x 30 + 475 x 2
= 4750 x 3 + 475 x 2
= 4750 + 4750 + 4750 + 475 + 475
A maneira como isso é obtido envolve a extração de cada um dos dígitos de 32, um de cada vez (para trás), em seguida, usando add para calcular um valor a ser adicionado ao resultado (inicialmente zero).
ShiftLeft
e as ShiftRight
operações são usadas para multiplicar ou dividir rapidamente a LongInt
pelo valor de agrupamento (10 para matemática "real"). No exemplo acima, adicionamos 475 a zero 2 vezes (o último dígito de 32) para obter 950 (resultado = 0 + 950 = 950).
Em seguida, deslocamos à esquerda 475 para obter 4750 e à direita 32 para obter 3. Adicione 4750 a zero 3 vezes para obter 14.250 e, em seguida, adicione ao resultado 950 para obter 15200.
Desloque para a esquerda 4750 para obter 47500, deslocamento para a direita 3 para obter 0. Como o deslocado 32 para a direita agora é zero, terminamos e, de fato, 475 x 32 é igual a 15200.
A divisão também é complicada, mas baseada na aritmética primitiva (o método "gazinta" para "entra em"). Considere a seguinte divisão longa para 12345 / 27
:
457
+-------
27 | 12345 27 is larger than 1 or 12 so we first use 123.
108 27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
---
154 Bring down 4.
135 27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
---
195 Bring down 5.
189 27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
---
6 Nothing more to bring down, so stop.
Portanto, 12345 / 27
é 457
com resto 6
. Verificar:
457 x 27 + 6
= 12339 + 6
= 12345
Isso é implementado usando uma variável de redução (inicialmente zero) para reduzir os segmentos de 12345, um de cada vez, até que seja maior ou igual a 27.
Então, simplesmente subtraímos 27 disso até chegarmos abaixo de 27 - o número de subtrações é o segmento adicionado à linha superior.
Quando não houver mais segmentos para derrubar, temos nosso resultado.
Lembre-se de que esses são algoritmos bastante básicos. Existem maneiras muito melhores de fazer aritmética complexa se seus números forem particularmente grandes. Você pode olhar para algo como GNU Multiple Precision Arithmetic Library - é substancialmente melhor e mais rápido do que minhas próprias bibliotecas.
Ele tem a característica infeliz de que ele simplesmente será encerrado se ficar sem memória (uma falha fatal para uma biblioteca de uso geral, na minha opinião), mas, se você puder ignorar isso, é muito bom no que faz.
Se você não pode usá-lo por motivos de licenciamento (ou porque não deseja que seu aplicativo seja encerrado sem motivo aparente), você poderia pelo menos obter os algoritmos de lá para integração em seu próprio código.
Eu também descobri que os bods do MPIR (um fork do GMP) são mais receptivos a discussões sobre mudanças potenciais - eles parecem um grupo mais amigável ao desenvolvedor.