You are on page 1of 6

Arytmetyka liczb wymiernych w j˛ezyku C++

Monika Zagała

Wydział Inżynierii Mechanicznej i Informatyki


Kierunek Informatyka, Rok V
m_zagala@o2.pl

Streszczenie
Poniższa praca przedstawia projekt oraz implementacj˛e nowego typu danych
mzRational dla j˛ezyka C++, służacego
˛ do prostych operacji arytmetycznych na licz-
bach wymiernych. Artykuł wykazuje jego słabe i mocne strony, na podstawie po-
równania z liczbami zmiennoprzecinkowymi (dost˛epnymi w standardzie j˛ezyka),
precyzji wyników otrzymanych dla prostych działań matematycznych. Ponadto, zo-
stała zaproponowana arytmetyka mieszana mi˛edzy liczbami wymiernymi, a liczbami
zmiennopozycyjnymi oraz omówione zostały problemy, jakie si˛e z tym wiaż˛ a.˛

1 Wst˛ep
Wraz, z pojawieniem si˛e pierwszych maszyn liczacych,
˛ czynności zwiazane
˛ z pobieraniem
i przetwarzaniem danych liczbowych, zostały zautomatyzowane. Do wykonywania działań
arytmetycznych stosowany jest powszechnie typ zmiennopozycyjny. Niestety, w wielu
przypadkach, obliczenia wykonywane przy jego pomocy, daja˛ przybliżone rezultaty. Wy-
st˛epujace
˛ bł˛edy, sa˛ spowodowane min. brakiem skończonego rozwini˛ecia dziesi˛etnego,
dla niektórych ułamków zwykłych [1]. Inna˛ istotna˛ sprawa˛ jest kolejność wykonywania
działań. Ma ona duży wpływ na precyzj˛e otrzymywanych wyników [2].
Fakt, że reprezentacja liczb zmiennoprzecinkowych w pami˛eci komputera nie zawsze
jest precyzyjna, nasuwa ide˛e zastosowania zamiennie danych, w postaci wymiernej. Dzi˛eki
temu, że sa˛ one przedstawiane za pomoca˛ pary liczb: licznika i mianownika, uniknać ˛
można np. bł˛edów zaokraglenia
˛ liczb, które towarzysza˛ postaci dziesi˛etnej implementacji.
Ze wzgl˛edu na brak ogólnodost˛epnego typu liczb wymiernych dla j˛ezyka C++ oraz
przez wzglad ˛ na jego duże zapotrzebowanie w wielu dziedzinach nauki i techniki, została
zaprojektowana biblioteka zawierajaca ˛ zestaw algorytmów i funkcji, umożliwiajacych
wykonywanie operacji arytmetycznych, na liczbach reprezentowanych w postaci ułamków
zwykłych.
Praca zorganizowana jest w nastepujacy ˛ sposób: W rozdziale drugim przedstawiona
została reprezentacja liczb zmiennoprzecinkowych, wraz z charakterystyka˛ najcz˛eściej
spotykanych bł˛edów wystepujacych ˛ w obliczeniach. Rozdział trzeci zawiera definicj˛e
typu mzRational oraz jego porównanie z typami zmiennopozycyjnymi, na podstawie pro-
stych przykładów działań arytmetycznych. Rozdział czwarty określa zasady arytmetyki
mieszanej opracowanego typu liczb wymiernych, z istniejacymi ˛ postaciami reprezentacji
liczb rzeczywistych.

1
2 Reprezentacja liczb zmiennoprzecinkowych
Liczba zmiennoprzecinkowa (ang. floating–point number) służy do przedstawienia ogra-
niczonego przedziału liczb rzeczywistych w pami˛eci komputera. Wszystkie założenia,
zwiazane
˛ z reprezentacja˛ tego typu, zdefiniowane zostały przez standard IEEE 754 [3].
W praktyce stosowane sa˛ trzy metody wyświetlania liczb zmiennoprzecinkowych: dzie-
si˛etna, naukowa oraz inżynierska. Najbardziej popularnym zapisem jest notacja nauko-
wa[4]. Stosujac˛ dynamiczne przesuni˛ecie przecinka oraz używajac
˛ pot˛egi podstawy do o-
kreślenia jego rzeczywistego położenia, możemy reprezentować dowolne liczby za po-
moca˛ kilku cyfr [5]. Ogólny wzór wyglada˛ nast˛epujaco:
˛

zm M ∗ βzcC (1)

gdzie : M – mantysa liczby (ang. mantissa) , C – cecha (ang. exponent) , β – używana


podstawa systemu liczbowego (ang. base) , zm – znak mantysy, zc – znak cechy.
Zarówno, dla mantysy, jak i wykładnika ilość cyfr jest z góry ustalona. Zatem, dana
liczba jest reprezentowana, z pewna˛ skończona˛ dokładnościa˛ i należy, do policzalnego
zbioru wartości [2].
Przy obliczeniach, wykonywanych na liczbach zmiennopozycyjnych, można napotkać
podstawowe rodzaje błedów :

• Bł˛edy danych wejściowych – wyst˛epuja˛ wówczas, gdy dane liczbowe wprowadzone


do pami˛eci, lub rejestrów maszyny cyfrowej, odbiegaja˛ od dokładnych ich wartości.

• Bł˛edy reprezentacji – problem


√ isnieje, w przypadku reprezentacji wszystkich liczb
niewymiernych np. Π, 3, liczb o nieskończonym rozwini˛eciu dziesi˛etnym np. 1/3,
1/6, 1/7 oraz dla ułamków dziesi˛etnych o nieskończonym rozwini˛eciu binarnym
np. 0.1, 0.2. Nieuniknione jest wówczas zaokraglenie.
˛

• Bł˛edy obci˛ecia – powstaja˛ podczas obliczeń, na skutek zmniejszania liczby działań.


Na przykład, podczas dodawania bardzo małej i bardzo dużej liczby, ze wzgl˛edu
na ograniczona˛ reprezentacj˛e mantysy wyniku, jej przesuni˛ecie wzgl˛edem tych sa-
mych cech, powoduje brak dodania liczb, a otrzymanym wynikiem b˛edzie wartość
liczby wi˛ekszej.

• Bł˛edy zaokragleń
˛ – pojawiaja˛ si˛e podczas obliczeń, na skutek konieczności zao-
kraglania
˛ wartości, ze wzgledu na ograniczona˛ długość słów binarnych. Bł˛edy te
można czasem zmniejszyć, ustalajac ˛ umiej˛etnie sposób i kolejność wykonywania
działań.

Liczb˛e zmiennoprzecinkowa˛ można potraktować, jako sum˛e wartości dokładnej oraz


poprawki do wartości liczby zmiennoprzecinkowej [3]:

f =d+p (2)

gdzie: f – wartość zmiennoprzecinkowa; d – wartość dokładna, która˛ reprezentuje liczba f ;


p – poprawka wartości d do wartości f , zwana również bł˛edem zaokraglenia ˛ (może
przyjmować wartości dodatnie oraz ujemne).
Dla przykładu, liczby:

2
float d1 = 123456.78
float d2 = 103.6003
sa˛ reprezentowane jako:
f 1 = d1 + p1 i f 2 = d2 + p2,
przy czym: f1 = 123456.781250, f2 = 103.600304 natomiast bł˛edy zaokraglenia
˛ wy-
nosza˛ odpowiednio: p1 = 0.00125 , p2 = -0.000004.
Podczas dodawania dwóch liczb zmiennoprzecinkowych mamy do czynienia, z sumo-
waniem si˛e bł˛edów:

f 1 + f 2 = d1 + p1 + d2 + p2 = (d1 + d2) + (p1 + p2); (3)


| {z }
bład
˛
Jeżeli, poprawki: p1 i p2 maja˛ przeciwne znaki, wówczas bład
˛ może być nieco mniejszy.
Teoretycznie, po podstawieniu do wzoru liczb otrzymamy:
f = 123560.381554
d = 123560.3803
p = 0.001254
Wyniki otrzymane, przy użyciu kompilatora dla j˛ezyka C++, różnia˛ si˛e od przedsta-
wionych wyżej, gdyż dochodza˛ jeszcze bł˛edy reprezentacji poszczególnych składników
działań arytmetycznych oraz otrzymanego wyniku. Stad,
˛ f = 123560.382812, natomiast
poprawka p = 0.002512.
Mnożenie dwóch liczb zmiennoprzecinkowych, przedstawia poniższe równanie:

f 1 ∗ f 2 = (d1 + p1) ∗ (d2 + p2) = (d1 ∗ d2) + (d1 ∗ p2 + d2 ∗ p1 + p1 ∗ p2); (4)


| {z }
bład
˛
Dodajac,
˛ do wartości uj˛etej w nawias klamrowy (z wzoru (4) ), bł˛edy numeryczne, wyni-
kajace
˛ z niedokładnej reprezentacji tych liczb, uzyskany bład˛ całkowity może być duży.
Biorac
˛ pod uwag˛e, że jest to jedynie pojedyncza operacja, warto zastanowić si˛e, kiedy
dokonywanie bardziej skomplikowanych operacji arytmetycznych ma w ogóle sens [2].

3 Działania arytmetyczne na liczbach wymiernych


Z poprzedniego rozdziału wynika, że typ zmiennopozycyjny niesie ze soba˛ wiele nie-
doskonałości. Można łatwo uzyskać bezużyteczne wyniki, czyli takie, które obarczone sa˛
bardzo dużym bł˛edem. Zastosowanie wi˛ekszej precyzji liczb zmiennoprzecinkowych, jest
jedna˛ z metod osłabiajac
˛ a˛ ryzyko uzyskania niedokładnych wyników [2]. Jednak, w wielu
laboratoriach naukowych, technicznych, czy przemysłowych, gdzie jakość obliczeń ma
bardzo duże znaczenie, arytmetyka zmiennopozycyjna może okazać si˛e zawodna.
Fakt ten, przyczynił si˛e do prac nad nowym typem danych zwanym ogólnie Rational.
Głównym założeniem jest przedstawienie liczb rzeczywistych, wymiernych, za pomoca˛
ułamków zwykłych. Licznik i mianownik sa˛ zapisywane w postaci liczb całkowitych,
i dlatego podstawowe działania matematyczne wykonywane sa˛ z pełna˛ precyzja.˛ Na przy-
kład dla j˛ezyków takich jak: Java, czy Python istnieja˛ odpowiedniki takiej biblioteki.

3
Na stronie internetowej Boost’a [6] można znależć implementacj˛e typu rational dla j˛e-
zyka C++, wraz z podstawowymi algorytmami i funkcjami. Brakuje jednak operandów
dla arytmetyki mieszanej i możliwosci rzutowania typu zmiennopozycyjnego, na typ wy-
mierny. Pakiet ten jest biblioteka˛ "otwarta",˛ wciaż
˛ opracowywana.˛ Na jego podstawie
została zaprojektowana własna biblioteka mzRational, z operandami: dodawania, odejmo-
wania, mnożenia i dzielenia, a także relacji porównania. Dodatkowo zostały przeciażone
˛
operatory typów zmiennoprzecinkowych oraz zdefionowana została ich konwersja
do mzRational, wraz z funkcjami dla całej arytmetyki mieszanej.
Na podstawie przykładowych dwóch liczb: a = 1223334444.5 i b = 2e-8 zostało
dokonane porównanie operacji dodawania i mnożenia pomi˛edzy danymi typu mzRational,
gdzie poszczególne składowe ułamka zwykłego zdefiniowane zostały jako long long int
oraz liczbami zmiennoprzecinkowymi typu double.
Otrzymane wyniki były nast˛epujace:

operacja mzRational double


+ (61166722225000001/50000000) 1223334444.500000000000000000
∗ (2446668889/100000000) 24.4666890000000356

dla wymiernej reprezentacji rezultat był prawidłowy, zarówno dla operacji dodawania
oraz mnożenia. W przypadku liczb zapisanych w postaci dziesietnej, operacja dodawania
dała wynik równy wi˛ekszemu czynnikowi, czyli wystapił typowy bład ˛ obci˛ecia, charakte-
rystyczny dla tego typu danych. Natomiast mnożenie zostało przeprowadzone precyzyjnie,
z niewielkim bł˛edem reprezentacji. Nasuwa si˛e tutaj wniosek, że typ mzRational wykazuje
zdecydowana˛ przewag˛e nad typem zmiennopozycyjnym, w operacjach dodawania (odej-
mowania) liczb skrajnie różnych.
Porównanie arytmetyki, dla dwóch innych liczb: c = 45e12 oraz d = 5e-8, wykazało,
że mnożenie wykonane zostało prawidłowo na liczbach mzRational, natomiast dodawanie
zakończyło si˛e bł˛edem spowodowanym przekroczeniem najwyższej wartości liczby typu
long long int. Podobnie, dla operacji mnożenia może wystapić
˛ overflow (underflow), czyli
tzw. bład˛ nadmiaru (niedomiaru), szczególnie wtedy, gdy redukcja ułamków zwykłych
jest niewykonalna. Zatem problem zachowania precyzji nie jest do końca rozwiazany. ˛
W tym konkretnym przypadku widoczna jest wyższość typów zmiennopozycyjnych typu
double(long double). Przy zastosowaniu liczb typu float sprawa przedstawia si˛e inaczej.
Porównanie zakresów możliwych prezentowanych wyników wypada na korzyść repre-
zentacji mzRational.
Dodatkowym atutem, reprezentacji liczb w postaci wymiernej, jest łatwość ich po-
równywania. Powszechnie wiadomo, że takie operacje na liczbach prezentowanych w
postaci ułamków dziesi˛etnych nie sa˛ możliwe. Można jedynie sprawdzić, czy dana liczba
zmiennopozycyjna mieści si˛e w pewnym jej zakresie, otoczeniu [3]. Typ mzRational
zapewnia nam operatory (<, >, ==, ! =) dla tego typu relacji, zwracajace ˛ odpowiednio
true, jeżeli została ona spełniona, w przeciwnym razie false. Poniżej znajduje si˛e fragment
implementacji operatora mniejszości:

template<typename Int>
bool mzRational<Int>::operator<( const mzRational<Int>& less){
mzRational<Int> l(*this);
mzRational<Int> r(less);

4
if(l.num < 0 && r.num >= 0) return true;
if(l.num >= 0 && r.num <= 0) return false;
if(l.den == r.den) return l.num < r.num;
l.normalize();
r.normalize();
Int gcd1 = gcd(l.num, r.num);
Int gcd2 = gcd(r.den, l.den);
return (l.num/gcd1) * (r.den/gcd2) < (l.den/gcd2) * (r.num/gcd1);
}
Funkcja normalize() służy do redukcji ułamków zwykłych, natomiast gcd(), jako re-
zultat zwraca najwi˛ekszy wspólny dzielnik. Zastosowanie operatora < wymaga zdefinio-
wania dwóch obiektów typu mzRational i porównaniu ich ze soba.˛ Ilustruje to poniższy
przykład:
int main(){
mzRational<long int> a(12, 78);
mzRational<long int> b(34, 13);
if(a < b){...}
return 0;
}
Inna˛ cecha˛ typu mzRational, jest reprezentacja wyników w postaci zredukowanych
ułamków zwykłych. Notacja dziesi˛etna jest zdecydowanie bardziej przyswajalna, od tego
rodzaju prezentacji danych. Na przykład, liczba a = 1223334444.5 zostanie przedsta-
wiona odpowiednio przez typ zmiennopozycyjny jako: 1223334444.500000000000000000
natomiast mzRational wyświetli si˛e jako: (2446668889 / 2) T˛e mała˛ niedogodność re-
kompensuje możliwość zamiany typu z mzRational na dowolny typ zmiennoprzecinkowy.
Trzeba si˛e liczyć z tym, że w niektórych przypadkach, konwersja może przyczynić si˛e, do
utraty dokładności prezentowanej liczby.
Porównanie typów: mzRational ze wszystkimi typami zmiennopozycyjnymi nie miało
na celu wykazania, który z nich jest lepszy. Zarówno jedna, jak i druga reprezentacja
niesie ze soba˛ wiele zalet i wad. Jednakże, wykazanie słabych i mocnych stron pomaga
w dobraniu odpowiedniego typu, w zależności od wykonywanych operacji.

4 Definicja arytmetyki mieszanej


Najważniejszym, a zarazem najtrudniejszym zagadnieniem jest arytmetyka liczb miesza-
nych, czyli określenie zasad działania na liczbach wymiernych typu mzRational, z licz-
bami zmiennopozycyjnymi w dowolnym formacie. Stosowanie zamiennie liczb zmienno-
pozycyjnych i wymiernych wymaga zdefiniowania operatorów rzutowania:
operator float( ){...}
operator double( ){...}
operator long double( ){...}
do zamiany typu mzRational, na jeden z powyższych typów zmiennopozycyjnych oraz zde-
finiowania konwersji odwrotnej, czyli liczby rzeczywistej w dowolnej reprezentacji zmien-
noprzecinkowej na liczb˛e mzRational:

5
template<typename Real> explicit mzRational(Real x){...}

Rzutowaniu ułamków, z postaci zwykłej na postać dziesietna,˛ towarzyszy cz˛esto utrata


precyzji. Jest to zwiazane
˛ przede wszystkim z bł˛edami w reprezentacji zmiennoprzecin-
kowej. Odwrotna zamiana typów również nie pozwala uniknać ˛ bł˛edów. Przyczyny tego
moga˛ być nast˛epujace.
˛ Po pierwsze, zamieniana liczba zmiennoprzecinkowa nie mieści si˛e
w granicach reprezentacji liczby wymiernej, wówczas konieczne jest obci˛ecie, badź ˛ za-
okraglenie
˛ liczby do n–cyfr (w jezyku C++, liczby typu long long int sa˛ zazwyczaj,
co najwyżej 18–cyfrowe). Drugi rodzaj bł˛edu, z jakim można si˛e spotkać przy konwersji
liczb rzeczywistych do typu mzRational, wynika z niedokładnej reprezentacji liczby zmien-
noprzecinkowej. Ostatnim powodem utraty precyzji jest zamiana liczb niewymiernych
na postać wymierna.˛ Tego typu dane nigdy nie zostana˛ poprawnie przedstawione, co wy-
nika z własności tych liczb [7].
Faktem jest, że nie każda zamiana typów spowoduje, że wartości liczbowe utraca˛
swoja˛ pierwotna˛ dokładność. Jednak świadomość tego, kiedy i gdzie sa˛ popełniane bł˛edy
ułatwia określenie zasad dodawania, odejmowania, mnożenia i dzielenia liczb mieszanych,
w taki sposób, by osiagn˛ ać
˛ jak najwyższa˛ prezyj˛e otrzymywanych wyników.

5 Podsumowanie
Artykuł przedstawia ogólna˛ charakterystyk˛e typu mzRational. Liczby prezentowane, ja-
ko ułamki zwykłe, poszerzaja˛ dotychczasowe możliwości, o wykonywanie precyzyjne-
go dodawania (a co za tym idzie, odejmowania) liczb, szczególnie o dużej rozbieżności
wykładników. Ponadto, łatwość wykonywania porównań, takich jak: która z liczb jest
wi˛eksza, badź:
˛ czy dwie liczby sa˛ równe, czy różne - to dodatkowy atut typu mzRational.
Niestety, każdy reprezentacja danych, w pami˛eci komputera posiada pewne wady. Tak też
jest w przypadku reprezentacji wymiernej implementacji. Świadcza˛ o tym wyżej przed-
stawione przykłady. Dzi˛eki poznaniu i zrozumieniu wszelkich ograniczeń, zastosowanie
w konkretnych aplikacjach arytmetyki liczb wymiernych, staje si˛e o wiele prostsze i bar-
dziej efektywne.

Literatura
[1] W. Hebish, A. Szustalewicz, K. Tabisz, Wst˛ep do informatyki, 2002.

[2] D. Goldberg, What Every Computer Scientist Should Know About Floating-Point
Arithmetic, 1991.

[3] K. Adamski, http://nr-k.namyslow.eu.org/, Liczby zmiennoprzecinkowe, 2003.

[4] Wikipedia, http://pl.wikipedia.org/wiki/Liczba_zmiennoprzecinkowa.

[5] P. Furmański, Ś. Sobieski, Wst˛ep do Informatyki, wer. RCI, 2004.

[6] Boost, http://www.boost.org/libs/rational.

[7] T. Trajdos, Matematyka, wyd. VI, 1993.

You might also like