c++ - sirven - ¿Por qué los números racionales no se implementan y almacenan como fracciones sin pérdida de información?
propiedades de los numeros racionales (8)
Sé que esto es un poco hipotético, pero me pregunto por qué ningún idioma que conozco lo hace.
Por ejemplo, desea almacenar 1/3. Dale al programador una opción para especificarlo como 1/3, y almacena 1 y 3. Algo como
struct float {
int numerator;
int denominator;
};
¡La aritmética numérica racional se vuelve realmente fácil y considerablemente más precisa!
Esto resolvería tantos problemas relacionados con la precisión y las limitaciones de almacenamiento de los números de punto flotante, ¡y tampoco veo que se presenten nuevos problemas!
De ahí mi pregunta: ¿por qué no se implementan los números racionales y se almacenan como fracciones sin pérdida de información?
Como Joe preguntó, y otros también podrían señalar, no quiero decir esto para reemplazar el sistema existente, sino para complementarlo.
P: ¿Cómo almacenas pi
?
R: Muchas veces, solo estoy almacenando 1/3
y no pi
. pi
puede almacenarse a la manera antigua, y 1/3
a la manera nueva.
"De ahí mi pregunta: ¿por qué no se implementan los números racionales y se almacenan como fracciones sin pérdida de información?"
La biblioteca estándar de C ++ carece de muchos tipos prácticamente necesarios que ofrecen otros idiomas y bibliotecas. La biblioteca Boost ya ofrece una implementación de número racional . Y appears que en breve también puede ofrecer los tipos decimales propuestos , por ejemplo, para el manejo de importes de moneda.
En cuanto a por qué faltan esos tipos, la biblioteca estándar de C ++ generalmente solo proporciona bloques básicos generales, no las cosas más útiles en la práctica que uno podría construir con esos bloques. Es decir, es minimalista. La biblioteca Boost es solo un poco menos minimalista, y luego, por ejemplo, la biblioteca Poco se parece más a las bibliotecas estándar más ricas en otros lenguajes.
¡La aritmética de números reales se vuelve realmente fácil y considerablemente más precisa!
No, no lo hace. La estructura que usted describe solo maneja números racionales , es decir, aquellos que pueden expresarse como fracciones. El conjunto de números reales incluye números tanto racionales como irracionales. La mayoría de los cálculos del mundo real se realizan con números reales, por lo que no puede limitarse a los racionales y esperar que todo esté bien.
Me pregunto por qué ningún idioma que conozco lo hace.
La mayoría de los idiomas en los que puedo pensar hacen posible hacer exactamente lo que usted describe. En C, puede crear una estructura que contenga numerador y denominador, y puede definir un grupo de funciones que operan en dichas estructuras. C ++ facilita mucho las cosas al permitirle definir una clase y operaciones en esa clase: la misma idea, una sintaxis mucho más agradable, etc. De hecho, a menudo se usan diferentes conjuntos de números como ejemplos en los idiomas OO: puede comenzar por definir una Clase racional, y luego extiéndalo para incluir números imaginarios, y así sucesivamente.
Supongo que la razón por la que no hay más idiomas con soporte incorporado para tipos exactos probablemente tenga que ver con el hecho de que los procesadores no admiten directamente tales operaciones. Los procesadores modernos incluyen instrucciones que implementan operaciones aritméticas para tipos de punto flotante, por lo que es fácil incluirlos en cualquier idioma. Soportar tipos exactos significaría construir una biblioteca matemática en el idioma, y probablemente sea mejor en varios niveles dejar la biblioteca matemática fuera del idioma y dejar que aquellos que la necesitan la incorporen en su software.
Si se va a tomar todas las molestias para producir resultados exactos, probablemente no quiera limitarse solo a los racionales, por lo que la estructura que da como ejemplo no la va a cortar. Ser capaz de hacer cálculos exactos sobre los racionales no es muy útil si recurres a resultados inexactos la primera vez que aparece un número irracional. Afortunadamente, hay sistemas matemáticos sofisticados por ahí. Mathematica es un ejemplo bien conocido.
C ++ al menos incluye una biblioteca aritmética racional de tiempo de compilación. Aquí hay un ejemplo:
#include <ratio>
#include <iostream>
int main() {
using a = std::ratio<3,5>;
using b = std::ratio<7,6>;
using c = std::ratio_multiply<a,b>::type;
std::cout << c::num << ''/'' << c::den << ''/n''; // prints 7/10
}
Aquí multiplicamos 3/5 por 7/6 y obtenemos 7/10.
Contrariamente a muchas de las respuestas que recibió, su idea es buena para muchos tipos de problemas. Especialmente problemas como este en los que sabe que habrá muchas cancelaciones y desea una respuesta exacta. Por esta razón, Python tiene el módulo de fractions
, y como @ bames53 señala, C ++ tiene <ratio>
.
Hay unas pocas razones:
- No hay apoyo de la arquitectura común. Esto es comprensible, ya que la fracción siempre viene con simplificación. Esto no es trivial para ser implementado a nivel de hardware, o más bien, será una instrucción sin mucha aplicación.
- No se pueden manejar números muy grandes o muy pequeños. O BigInteger debe involucrar. Y para números muy grandes o muy pequeños, generalmente no necesitamos la mayor parte de la precisión proporcionada.
- Si este es un tipo admitido en el nivel de idioma, debe admitir la conversión con otros tipos numéricos. Debe decidir cuándo devolver el tipo flotante si la representación interna tiene una precisión fija (en caso de multiplicación).
En un idioma, la decisión de apoyar algo generalmente se decide por su aplicación (o la justificación del lenguaje). Si la aplicación es pequeña, tiene menos posibilidades de ser compatible.
La razón por la que no se almacenan de esta manera por defecto es que el rango de valores válidos que pueden caber en un conjunto fijo de bits es menor. Su clase float
puede almacenar números entre 1 / MAXINT y MAXINT (más o menos). El float
AC / C ++ puede representar números entre 1E + 37 y 1E-37 (más o menos). En otras palabras, un float
estándar puede representar valores 26 órdenes de magnitud más grandes y 26 órdenes de magnitud más pequeños que los suyos a pesar de tomar la mitad del número de bits. En general, es más conveniente poder representar valores muy grandes y muy pequeños que ser perfectamente precisos. Esto es especialmente cierto ya que el redondeo tiende a darnos las respuestas correctas con fracciones pequeñas como 1/3. En g ++, lo siguiente da 1:
std::cout << ((1.0/3.0) * 3.0) << std::endl;
Recuerde que los tipos en C ++ tienen un tamaño fijo en bits. Por lo tanto, un tipo de datos en 32 bits tiene como máximo MAX_UINT valores. Si cambia la forma en que se representa, solo está cambiando qué valores se pueden representar con precisión, no incrementándolos. No se puede meter más y, por lo tanto, no puede ser "más preciso". Usted comercia siendo capaz de representar 1/3 precisamente por no poder representar otros valores con precisión, como 5.4235E + 25.
Es cierto que su float
puede representar valores de manera más precisa entre 1E-9 y 1E + 9 (suponiendo 32 ints de bit) pero a un costo de no poder representar valores fuera de este rango. Peor aún, mientras que el float
estándar siempre tiene 6 dígitos de precisión, su float
tendría una precisión que varía según el nivel de cero de los valores. (Y tenga en cuenta que está utilizando el doble de bits que el float
).
(Supongo que int
. De 32 bits. El mismo argumento se aplica para int
. De 64 bits.)
Edición : también tenga en cuenta que la mayoría de los datos de personas que usan float
s no son precisos de todos modos Si está leyendo datos fuera de un sensor, ya tiene imprecisión, por lo que estar a punto de representar "perfectamente" el valor no tiene sentido. Si está utilizando un float
en cualquier tipo de contexto informático, no va a importar. No tiene sentido describir perfectamente "1/3" si su propósito es mostrar un poco de texto 1/3 del camino a través de la pantalla.
Las únicas personas que realmente necesitan una precisión perfecta son los matemáticos, y generalmente tienen un software que les ofrece esto. Muy pocos necesitan precisión más allá de lo que da el double
.
Muchas CPU tienen un tratamiento especial para los puntos flotantes ( ver Wikipedia ) y el tipo de datos float
en el idioma garantiza que los programas puedan utilizar la FPU de una manera fácil. Por otro lado, no conozco ningún CPU que pueda manejar Fracciones con instrucciones de ensamblador especiales para que las fracciones puedan implementarse de manera fácil y eficiente dentro de una biblioteca y no tengan que ser una característica de lenguaje. Si desea usar fracciones en C ++, puede usar Boost.Rational .
La razón por la que las CPU modernas implementan aritmética de punto flotante en lugar de manejar fracciones es que los puntos flotantes se pueden implementar mucho más fácilmente. Para implementar las operaciones de float
básicas, básicamente necesita poder sumar, restar multiplicar y dividir enteros y hacer algunos cambios de bits. Para comparar con las fracciones, por otro lado, necesitas encontrar el mayor divisor común de dos ints, que es mucho más difícil de implementar en hardware.
Póngase la correa en los cascos, porque estamos a punto de llegar a la teoría aquí.
Cualquier estudiante de matemáticas podría darle una explicación detallada de la prueba de Cantor de la incontable cardinalidad de los números reales. Para una explicación más larga, vaya here.
Pero como señaló Caleb, el campo de los números reales contiene números tanto racionales como irracionales. Esto significa que algún subconjunto del campo de número real nunca se podrá representar como un par de numerador / denominador. ¿Qué tan grande es este subconjunto? Como resultado, la mayoría de los números reales son irracionales, porque el conjunto de los racionales es contable.
Aquí está la línea de remate: Almacenar números de esta manera sería muy tonto porque la mayoría de los resultados de las funciones de valores reales no se pueden almacenar como un numerador y un denominador.
Esto puede parecer difícil de creer, pero piense en funciones trascendentales comunes, por ejemplo, pecado, cos, log. La mayoría de los resultados de estas funciones no son racionales, y los tipos que escribieron IEEE 754 y otras cosas de FP temprana lo sabían. Se dieron cuenta de que lidiar con una pequeña cantidad de error a cambio de la posibilidad de representar (con algún truncamiento) una porción mucho mayor del campo de números reales era un buen compromiso de diseño.