2014-01-18 4 views
0

Я внедрил class NaturalNum для представления натурального числа «бесконечного» размера (до 4 ГБ).Самый точный способ вычисления числителя и знаменателя двойного

Я также внедрил class RationalNum для представления рационального числа с бесконечной точностью. Он хранит числитель и знаменатель рационального числа, оба из которых являются экземплярами NaturalNum и полагаются на них при выполнении любой арифметической операции, выданной пользователем.

Единственное место, где точность «отбрасывается в определенной степени», при печати, так как существует предел (предоставленный пользователем) до количества цифр, которые появляются после десятичной (или десятичной) точки.

Мой вопрос касается одного из конструкторов class RationalNum. А именно, конструктор, который принимает значение double и вычисляет соответствующий числитель и знаменатель.

Мой код приведен ниже, и я хотел бы знать, если кто-нибудь увидит более точный способ для вычисления их:

RationalNum::RationalNum(double value) 
{ 
    if (value == value+1) 
     throw "Infinite Value"; 
    if (value != value) 
     throw "Undefined Value"; 

    m_sign  = false; 
    m_numerator = 0; 
    m_denominator = 1; 

    if (value < 0) 
    { 
     m_sign = true; 
     value = -value; 
    } 

    // Here is the actual computation 
    while (value > 0) 
    { 
     unsigned int floor = (unsigned int)value; 
     value   -= floor; 
     m_numerator += floor; 
     value   *= 2; 
     m_numerator *= 2; 
     m_denominator *= 2; 
    } 

    NaturalNum gcd = GCD(m_numerator,m_denominator); 
    m_numerator /= gcd; 
    m_denominator /= gcd; 
} 

Примечание: переменные, начинающиеся с «m_» являются переменными членами.

Благодаря

+4

Разложите поплавок на знак, мантисса и экспонента, а затем у вас достаточно информации, чтобы точно представлять его. –

+0

Разве я не буду рисковать возможностью различных представлений FP на разных компиляторах (или архитектурах процессоров)? –

+1

@barakmanos Вы программируете для мейнфрейма IBM или суперкомпьютера Cray? Если вы этого не сделаете, тогда забудьте о представлениях, отличных от IEEE 754. –

ответ

3

Стандартная библиотека содержит функцию для получения мантиссу и экспоненту, frexp.

Просто умножьте значение, чтобы получить все биты перед десятичной точкой и установить соответствующий знаменатель. Просто не забывайте, что значение нормализуется в пределах от 0,5 до 1 (я бы рассмотрел от 1 до 2 более естественных, но что бы то ни было) и что у него есть 53 значащих бита для IEEE double (практически нет используемых платформ, которые использовали бы различные плавающие точечный формат).

+0

Thanks; Задавая тот же вопрос, что и в одном из комментариев выше: разве я не буду рисковать возможностью представления разных FP на разных компиляторах (или на архитектуре ЦП)? Или 'frexp' гарантированно будет реализован соответствующим образом в любом случае? –

+0

Значимые (а не мантиссы, знаменатели линейны, но мантиссы логарифмичны) имеют 53 значащих бита, а не 52. 52 - это просто количество бит, которые явно закодированы. –

+0

@EricPostpischil: Спасибо, обновлено (по какой-то причине я думал, что показатель составляет 12, а не 11 бит). –

1

Я не уверен на 100% в математике, которую вы имеете для фактического вычисления, только потому, что я ее не изучил, но я думаю, что метод ниже устраняет необходимость использования функции GCD, которая может принести некоторые ненужное время работы.

Вот класс, который я придумал. У меня нет полностью протестировал его, но я выпустил пару миллиардов случайных удвоений, и утверждения никогда не срабатывали, поэтому я уверенно уверен в его удобстве использования, но я все равно еще проверил бы крайние случаи вокруг INT64_MAX.

Если я не ошибаюсь, сложность времени выполнения этого алгоритма линейна относительно размера в битах ввода.

#include <iostream> 
#include <cmath> 
#include <cassert> 
#include <limits> 

class Real; 

namespace std { 
    inline bool isnan(const Real& r); 
    inline bool isinf(const Real& r); 
} 

class Real { 
public: 
    Real(double val) 
     : _val(val) 
    { 
     if (std::isnan(val)) { return; } 
     if (std::isinf(val)) { return; } 

     double d; 

     if (modf(val, &d) == 0) { 
      // already a whole number 
      _num = val; 
      _den = 1.0; 
      return; 
     } 

     int exponent; 
     double significand = frexp(val, &exponent); // val = significand * 2^exponent 
     double numerator = val; 
     double denominator = 1; 

     // 0.5 <= significand < 1.0 
     // significand is a fraction, multiply it by two until it's a whole number 
     // subtract exponent appropriately to maintain val = significand * 2^exponent 
     do { 
      significand *= 2; 
      --exponent; 
      assert(std::ldexp(significand, exponent) == val); 
     } while (modf(significand, &d) != 0); 

     assert(exponent <= 0); 

     // significand is now a whole number 
     _num = significand; 
     _den = 1.0/std::ldexp(1.0, exponent); 

     assert(_val == _num/_den); 
    } 

    friend std::ostream& operator<<(std::ostream &os, const Real& rhs); 
    friend bool std::isnan(const Real& r); 
    friend bool std::isinf(const Real& r); 

private: 
    double _val = 0; 
    double _num = 0; 
    double _den = 0; 
}; 

std::ostream& operator<<(std::ostream &os, const Real& rhs) { 
    if (std::isnan(rhs) || std::isinf(rhs)) { 
     return os << rhs._val; 
    } 
    if (rhs._den == 1.0) { 
     return os << rhs._num; 
    } 
    return os << rhs._num << "/" << rhs._den; 
} 

namespace std { 
    inline bool isnan(const Real& r) { return std::isnan(r._val); } 
    inline bool isinf(const Real& r) { return std::isinf(r._val); } 
} 

#include <iomanip> 

int main() { 

    #define PRINT_REAL(num) \ 
     std::cout << std::setprecision(100) << #num << " = " << num << " = " << Real(num) << std::endl 

    PRINT_REAL(1.5); 
    PRINT_REAL(123.875); 
    PRINT_REAL(0.125); 

    // double precision issues 
    PRINT_REAL(-10000000000000023.219238745); 
    PRINT_REAL(-100000000000000000000000000000000000000000.5); 

    return 0; 
} 

При взгляде на ваш код немного больше, есть проблема с вашим тестированием для бесконечных значений. Обратите внимание на следующую программу:

#include <numeric> 
#include <cassert> 
#include <cmath> 

int main() { 
    { 
     double d = std::numeric_limits<double>::max(); // about 1.7976931348623e+308 

     assert(!std::isnan(d)); 
     assert(!std::isinf(d)); 

     // assert(d != d + 1); // fires 
    } 

    { 
     double d = std::ldexp(1.0, 500); // 2^700 
     assert(!std::isnan(d)); 
     assert(!std::isinf(d)); 

     // assert(d != d + 1); // fires 
    } 
} 

В дополнение к этому, если ваша функция GCD не поддерживает двойников, то вы будете ограничивать себя в терминах значений, которые можно импортировать в качестве двойников. Попробуйте любое число> INT64_MAX, и функция GCD может не работать.

+0

Итак? Теперь я застрял с еще одним значением «double» ... или это значение всегда будет 0,5? –

+0

@barakmanos: Значимость возвращается как «двойная», но легко преобразуется в другие формы, поскольку ее масштаб известен. Если у вас есть 64-битные целые числа, просто умножьте их на 2 ** 53 и преобразуйте в целое число. –

+0

Оказывается, это намного сложнее, чем я думал, я начал реализовывать свое собственное решение, но он уродлив, и мне это не нравится. Если бы мне приходилось это делать, я бы использовал библиотеку рациональных чисел Boost: http://www.boost.org/doc/libs/1_55_0/libs/rational/rational.html – vmrob

Смежные вопросы