2008-11-06 16 views
69

Me gustaría implementar una gran clase int en C++ como un ejercicio de programación — una clase que puede manejar números más grandes que un int largo. Sé que ya hay varias implementaciones de código abierto, pero me gustaría escribir las mías. Estoy tratando de tener una idea de cuál es el enfoque correcto.Cómo implementar big int en C++

Entiendo que la estrategia general es obtener el número como una cadena, y luego dividirlo en números más pequeños (por ejemplo, dígitos únicos) y colocarlos en una matriz. En este punto, debería ser relativamente simple implementar los diversos operadores de comparación. Mi principal preocupación es cómo implementaría cosas como la suma y la multiplicación.

Estoy buscando un enfoque general y consejos en lugar de código de trabajo real.

+10

si se trata de un ejercicio de programación, comience a codificar. ¡De lo contrario no puedes hacer ningún ejercicio! –

+3

Primero, las cadenas de dígitos están bien, pero piense en términos de base 2^32 (4 mil millones de dígitos distintos impares). Tal vez incluso base 2^64 en estos días. En segundo lugar, siempre trabaje con "dígitos" enteros sin signo. Puede hacer el complemento de dos para enteros grandes firmados usted mismo, pero si intenta hacer su manejo de desbordamiento, etc. con enteros con signo, se encontrará con problemas de comportamiento no definidos por las normas. – Steve314

+3

En cuanto a los algoritmos: para una biblioteca básica, los que aprendió en la escuela son correctos. – Steve314

Respuesta

35

Cosas a tener en cuenta para una gran clase int:

  1. Los operadores matemáticos: +, -, /, *,% No se olvide que su clase puede estar en cualquier lado del operador , que los operadores pueden encadenados, que uno de los operandos podría ser un int, float, double, etc.

  2. de E/S operadores: >>, < < Ésta es en el que encontrar la manera de adecuada crea tu clase a partir de la entrada del usuario y cómo formatearla para su salida también.

  3. Conversiones/elencos: Averiguar lo tipos/clases de su gran clase int debe ser convertible a, y cómo manejar adecuadamente la conversión . Una lista rápida sería incluye double y float, y puede include int (con los límites apropiados checking) y complex (suponiendo que puede manejar el rango).

+1

Vea [aquí] (http://stackoverflow.com/questions/4421706/operator-overloading) las formas idiomáticas de hacer los operadores. –

+3

Para enteros, el operador << and >> son operaciones de cambio de bit. Interpretarlos como E/S sería un mal diseño. – Dave

+1

@Dave: Excepto que es el estándar C++ para usar 'operator <<' y 'operator >>' con 'iostream's para E/S. – Hurkyl

5

Una vez que tenga los dígitos del número en una matriz, puede hacer la suma y la multiplicación exactamente como lo haría a mano.

0

Usa los algoritmos que aprendiste en 1 ° a 4 ° grado.
Comience con las columnas, luego las decenas, y así sucesivamente.

4

No olvide que no necesita limitarse a 0-9 como dígitos, es decir, usar bytes como dígitos (0-255) y aún puede hacer aritmética de mano larga de la misma manera que lo haría con los dígitos decimales . Incluso podría usar una matriz de largo.

+0

Si desea representar los números en decimales (es decir, para meros mortales), el 0-9 por algoritmo de nibble es más fácil. Solo deja el almacenamiento. – dmckee

+0

¿Crees que es más fácil hacer algoritmos BCD que sus contrapartes binarias regulares? – Eclipse

+2

La base AFAIK 10 se usa a menudo porque convertir números grandes en la base 255 (o cualquier cosa que no sea una potencia de 10) de/a la base 10 es costoso, y la entrada y salida de los programas generalmente estará en la base 10. – Tobi

5

Curiosamente, acabo de ver un video sobre esto antes de ver su pregunta. Here is the link. Cortesía del Congreso de Comunicación del Caos.

+0

http://dl.fefe.de/bignum.pdf ¡parecen las diapositivas del video! –

+0

Pero considera que no hay explicación sobre la división ... –

2

Al igual que otros dijeron, hacerlo a manera de largo mano pasada de moda, pero lejos de hacer todo esto en base 10. Yo sugeriría hacerlo todo en la base de 65536, y el almacenamiento de las cosas en una serie de pantalones largos.

1

Si la arquitectura de destino admite BCD (decimal codificado en binario) representación de los números, se puede obtener un poco de hardware soporte para la multiplicación/adición a mano que necesita hacer. Hacer que el compilador emita instrucciones BCD es algo que tendrá que leer en ...

Los chips de la serie Motorola 68K tenían esto. No es que sea amargo ni nada por el estilo.

3

No estoy convencido de que usar una cuerda sea el camino correcto, aunque nunca he escrito código, creo que usar una matriz de tipo numérico básico podría ser una mejor solución. La idea es que simplemente amplíes lo que ya obtuviste de la misma forma en que la CPU amplía un solo bit en un entero.

Por ejemplo, si usted tiene una estructura

typedef struct { 
    int high, low; 
} BiggerInt; 

continuación, puede realizar manualmente las operaciones nativas en cada uno de los "dígitos" (alta y baja, en este caso), siendo consciente de las condiciones de desbordamiento:

BiggerInt add(const BiggerInt *lhs, const BiggerInt *rhs) { 
    BiggerInt ret; 

    /* Ideally, you'd want a better way to check for overflow conditions */ 
    if (rhs->high < INT_MAX - lhs->high) { 
     /* With a variable-length (a real) BigInt, you'd allocate some more room here */ 
    } 

    ret.high = lhs->high + rhs->high; 

    if (rhs->low < INT_MAX - lhs->low) { 
     /* No overflow */ 
     ret.low = lhs->low + rhs->low; 
    } 
    else { 
     /* Overflow */ 
     ret.high += 1; 
     ret.low = lhs->low - (INT_MAX - rhs->low); /* Right? */ 
    } 

    return ret; 
} 

Es un ejemplo simplista, pero debería ser bastante obvio cómo extender a una estructura que tenía un número variable de cualquier clase numérica básica que esté utilizando.

+0

Por cadena, el OP significa tomar una cadena que contiene el número que desea en su representación numérica (en cualquier base) e inicializar BigInt con el valor. – KTC

+0

STLPLUS usa cadena para contener un entero grande. – lsalamon

0

Mi inicio sería tener una matriz de enteros de tamaño arbitrario, utilizando 31 bits y 32n como desbordamiento.

El op inicial sería ADD, y luego, MAKE-NEGATIVE, utilizando el complemento de 2. Después de eso, la resta fluye trivialmente, y una vez que tienes add/sub, todo lo demás es factible.

Probablemente haya enfoques más sofisticados. Pero este sería el enfoque ingenuo de la lógica digital.

2

Eche un vistazo a here para ver cómo GMP implementa sus algoritmos.

25

Hay una sección completa sobre esto: [The Art of Computer Programming, vol.2: Algoritmos seminumerical, sección 4.3 Aritmética de precisión múltiple, pp. 265-318 (ed.3)]. Puede encontrar otro material interesante en el Capítulo 4, Aritmética.

Si realmente no desea ver otra implementación, ¿ha considerado qué es lo que está aprendiendo? Hay innumerables errores que cometer y descubrirlos es instructivo y también peligroso. También existen desafíos para identificar economías computacionales importantes y tener estructuras de almacenamiento adecuadas para evitar problemas graves de rendimiento.

Una pregunta de desafío para usted: ¿Cómo pretende probar su implementación y cómo propone demostrar que su aritmética es la correcta?

Es posible que desee probar otra implementación (sin tener en cuenta cómo lo hace), pero se necesitará más que eso para poder generalizar sin esperar un nivel de prueba excrutiante. No olvide considerar los modos de falla (problemas de falta de memoria, fuera de la pila, demasiado tiempo, etc.).

¡Diviértete!

+1

Comparar con alguna implementación de referencia no lo llevará más allá, porque entonces tiene otro problema: ¿cómo probar si la implementación de referencia también es correcta? El mismo problema es probar el conocimiento en general: si un hombre tiene que probar a otro, ¿quién lo probará? No hay forma de salir de este problema excepto uno, inventado hace mucho tiempo: probar a partir de axiomas. Si el conjunto de axiomas se considera correcto (sin contradicciones), y la prueba se deriva adecuadamente de acuerdo con las reglas de la lógica, no puede estar mal, incluso para un número infinito de casos que nadie podría probar. – SasQ

40

Un desafío divertido.:)

Supongo que quieres enteros de longitud arbitraria. Sugiero el siguiente enfoque:

Considere la naturaleza binaria del tipo de datos "int". Piensa en usar operaciones binarias simples para emular lo que hacen los circuitos en tu CPU cuando agregan cosas. En caso de que esté interesado más en profundidad, considere leer this wikipedia article on half-adders and full-adders. Harás algo similar a eso, pero puedes bajar de nivel tan bajo como ese, pero al ser flojo, pensé que podría renunciar y encontrar una solución aún más simple.

Pero antes de entrar en cualquier detalle algorítmico sobre cómo sumar, restar, multiplicar, busquemos alguna estructura de datos. Una manera simple, por supuesto, es almacenar cosas en un std :: vector.

template< class BaseType > 
class BigInt 
{ 
typedef typename BaseType BT; 
protected: std::vector<BaseType> value_; 
}; 

Es posible que desee considerar si desea hacer el vector de un tamaño fijo y si una asignación previa a la misma. La razón es que para diversas operaciones, tendrá que pasar por cada elemento del vector - O (n). Es posible que desee saber de primera mano qué tan compleja será una operación y una n fija lo hace.

Pero ahora a algunos algoritmos para operar en los números. Podrías hacerlo en un nivel lógico, pero usaremos esa potencia mágica de CPU para calcular los resultados. Pero lo que haremos cargo de la lógica-ilustración de Half- y FullAdders es la forma en que maneja carry. Como ejemplo, considere cómo implementaría el operador + =. Para cada número en BigInt <> :: value_, debe agregar esos y ver si el resultado produce alguna forma de acarreo. No lo haremos por bits, sino que confiaremos en la naturaleza de nuestro BaseType (ya sea largo o int o corto o lo que sea): se desborda.

Seguramente, si agrega dos números, el resultado debe ser mayor que el mayor de esos números, ¿verdad? Si no es así, entonces el resultado se desbordó.

template< class BaseType > 
BigInt<BaseType>& BigInt<BaseType>::operator += (BigInt<BaseType> const& operand) 
{ 
    BT count, carry = 0; 
    for (count = 0; count < std::max(value_.size(), operand.value_.size(); count++) 
    { 
    BT op0 = count < value_.size() ? value_.at(count) : 0, 
     op1 = count < operand.value_.size() ? operand.value_.at(count) : 0; 
    BT digits_result = op0 + op1 + carry; 
    if (digits_result-carry < std::max(op0, op1) 
    { 
     BT carry_old = carry; 
     carry = digits_result; 
     digits_result = (op0 + op1 + carry) >> sizeof(BT)*8; // NOTE [1] 
    } 
    else carry = 0; 
    } 

    return *this; 
} 
// NOTE 1: I did not test this code. And I am not sure if this will work; if it does 
//   not, then you must restrict BaseType to be the second biggest type 
//   available, i.e. a 32-bit int when you have a 64-bit long. Then use 
//   a temporary or a cast to the mightier type and retrieve the upper bits. 
//   Or you do it bitwise. ;-) 

La otra operación aritmética es análoga. Diablos, incluso podría usar stl-functors std :: plus y std :: minus, std :: times y std :: divide, ..., pero tenga en cuenta el carry. :) También puede implementar la multiplicación y la división usando los operadores más y menos, pero eso es muy lento, porque eso recalcula los resultados que ya calculó en llamadas anteriores a más y menos en cada iteración. Hay muchos buenos algoritmos para esta simple tarea, usewikipedia o la web.

Y, por supuesto, se deben implementar los operadores estándar, tales como operator<< (simplemente cambiar cada valor en VALUE_ a la izquierda de n bits, empezando por el value_.size()-1 ... ah y recordar el equipaje :), operator< - incluso se puede optimice un poco aquí, primero verifique el número aproximado de dígitos con size(). Y así. Luego haz que tu clase sea útil, por befriendig std :: ostream operator<<.

Espero que este enfoque sea útil.

+4

"int" (como en firmado) es una mala idea. El comportamiento indefinido de los estándares en los desbordamientos hace que sea difícil (si no imposible) obtener la lógica correcta, al menos de manera portátil. Sin embargo, es bastante fácil trabajar en dos complementos con enteros sin signo, donde el comportamiento de desbordamiento se define estrictamente como el resultado del módulo 2^n. – Steve314

-1

reste 48 de su cadena de números enteros e imprima para obtener el número de dígitos grandes. luego realice la operación matemática básica. de lo contrario, proporcionaré una solución completa.

0

podría intentar implementar algo como esto:

http://www.docjar.org/html/api/java/math/BigInteger.java.html

usted sólo necesita 4 bits para un solo dígito 0 - 9

lo tanto un valor int permitiría hasta 8 dígitos cada uno. Decidí que me quedaría con una serie de caracteres, así que uso el doble de memoria, pero para mí solo se usa una vez.

Además, cuando se almacenan todos los dígitos en un solo int, se complica demasiado y, en todo caso, puede incluso ralentizarlo.

No tengo ninguna prueba de velocidad, pero mirando la versión java de BigInteger parece que está haciendo un montón de trabajo.

Para que yo hace el siguiente

//Number = 100,000.00, Number Digits = 32, Decimal Digits = 2. 
BigDecimal *decimal = new BigDecimal("100000.00", 32, 2); 
decimal += "1000.99"; 
cout << decimal->GetValue(0x1 | 0x2) << endl; //Format and show decimals. 
//Prints: 101,000.99 
+0

OP nunca dijo que quiere centrarse en los dígitos decimales. – einpoklum