Todo es cuestión de almacenamiento y algoritmos adecuados para tratar los números como partes más pequeñas. Supongamos que tiene un compilador en el que an int
solo puede ser de 0 a 99 y desea manejar números hasta 999999 (aquí solo nos preocuparemos por los números positivos para que sea más simple).
Lo haces dando a cada número tres int
y usando las mismas reglas que (deberías haber) aprendido en la escuela primaria para la suma, resta y las otras operaciones básicas.
En una biblioteca de precisión arbitraria, no hay un límite fijo en la cantidad de tipos de base utilizados para representar nuestros números, solo lo que la memoria pueda contener.
Adición por ejemplo 123456 + 78
:
12 34 56
78
-- -- --
12 35 34
Trabajando desde el extremo menos significativo:
- acarreo inicial = 0.
- 56 + 78 + 0 acarreo = 134 = 34 con 1 acarreo
- 34 + 00 + 1 acarreo = 35 = 35 con 0 acarreo
- 12 + 00 + 0 acarreo = 12 = 12 con 0 acarreo
De hecho, así es como funciona generalmente la suma a nivel de bits dentro de su CPU.
La resta es similar (usando la resta del tipo base y pedir prestado en lugar de llevar), la multiplicación se puede hacer con sumas repetidas (muy lento) o productos cruzados (más rápido) y la división es más complicada, pero se puede hacer cambiando y restando los números. involucrados (la división larga que habrías aprendido de niño).
De hecho, he escrito bibliotecas para hacer este tipo de cosas usando las potencias máximas de diez que pueden encajar en un número entero cuando se eleva al cuadrado (para evitar el desbordamiento al multiplicar dos int
s juntos, como un 16 bits int
limitado a 0 a 99 para generar 9.801 (<32.768) cuando se eleva al cuadrado, o 32 bits int
utilizando 0 a 9.999 para generar 99.980.001 (<2.147.483.648)) lo que facilitó enormemente los algoritmos.
Algunos trucos a tener en cuenta.
1 / Al sumar o multiplicar números, pre-asigne el espacio máximo necesario y luego reduzca más tarde si encuentra que es demasiado. Por ejemplo, agregar dos números de 100 "dígitos" (donde el dígito es un int
) nunca le dará más de 101 dígitos. Multiplicar un número de 12 dígitos por un número de 3 dígitos nunca generará más de 15 dígitos (sume los recuentos de dígitos).
2 / Para mayor velocidad, normalice (reduzca el almacenamiento requerido para) los números solo si es absolutamente necesario; mi biblioteca tenía esto como una llamada separada para que el usuario pueda decidir entre problemas de velocidad y almacenamiento.
3 / La suma de un número positivo y negativo es una resta, y restar un número negativo es lo mismo que sumar el equivalente positivo. Puede guardar bastante código haciendo que los métodos de suma y resta se llamen entre sí después de ajustar los signos.
4 / Evite restar números grandes de pequeños, ya que invariablemente terminará con números como:
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
En cambio, reste 10 de 11, luego néguelo:
11
10-
--
1 (then negate to get -1).
Aquí están los comentarios (convertidos en texto) de una de las bibliotecas para las que tuve que hacer esto. Desafortunadamente, el código en sí está protegido por derechos de autor, pero es posible que pueda seleccionar suficiente información para manejar las cuatro operaciones básicas. Suponga en lo siguiente que -a
y -b
representan números negativos y a
y b
son cero o números positivos.
Para la suma , si los signos son diferentes, use la resta de la negación:
-a + b becomes b - a
a + -b becomes a - b
Para la resta , si los signos son diferentes, use la suma de la negación:
a - -b becomes a + b
-a - b becomes -(a + b)
También un manejo especial para asegurarnos de que restamos números pequeños de grandes:
small - big becomes -(big - small)
La multiplicación utiliza matemáticas de nivel de entrada de la siguiente manera:
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
La forma en que esto se logra implica extraer cada uno de los dígitos de 32 uno a la vez (hacia atrás) y luego usar sumar para calcular un valor que se agregará al resultado (inicialmente cero).
ShiftLeft
y las ShiftRight
operaciones se utilizan para multiplicar o dividir rápidamente a LongInt
por el valor de ajuste (10 para matemáticas "reales"). En el ejemplo anterior, sumamos 475 a cero 2 veces (el último dígito de 32) para obtener 950 (resultado = 0 + 950 = 950).
Luego, dejamos el cambio 475 para obtener 4750 y el de la derecha 32 para obtener 3. Suma 4750 a cero 3 veces para obtener 14250 y luego suma al resultado de 950 para obtener 15200.
Desplazamiento a la izquierda 4750 para obtener 47500, desplazamiento a la derecha 3 para obtener 0. Dado que el desplazamiento a la derecha 32 ahora es cero, hemos terminado y, de hecho, 475 x 32 es igual a 15200.
La división también es complicada pero se basa en la aritmética temprana (el método "gazinta" para "entra"). Considere la siguiente división larga 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.
Por tanto 12345 / 27
es 457
con resto 6
. Verificar:
457 x 27 + 6
= 12339 + 6
= 12345
Esto se implementa mediante el uso de una variable de reducción (inicialmente cero) para reducir los segmentos de 12345 uno a la vez hasta que sea mayor o igual a 27.
Luego, simplemente restamos 27 de eso hasta que lleguemos por debajo de 27: el número de restas es el segmento agregado a la línea superior.
Cuando no hay más segmentos para derribar, tenemos nuestro resultado.
Tenga en cuenta que estos son algoritmos bastante básicos. Hay formas mucho mejores de hacer aritmética compleja si sus números van a ser particularmente grandes. Puede buscar algo como la biblioteca aritmética de precisión múltiple de GNU : es sustancialmente mejor y más rápido que mis propias bibliotecas.
Tiene el desafortunado error de que simplemente se cerrará si se queda sin memoria (una falla bastante fatal para una biblioteca de propósito general en mi opinión) pero, si puede mirar más allá de eso, es bastante bueno en lo que hace.
Si no puede usarlo por razones de licencia (o porque no desea que su aplicación simplemente salga sin razón aparente), al menos podría obtener los algoritmos de allí para integrarlos en su propio código.
También descubrí que los cuerpos de MPIR (una bifurcación de GMP) son más receptivos a las discusiones sobre cambios potenciales; parecen un grupo más amigable para los desarrolladores.