Classe Fraction

di Antonio Bonifati <antonio.b@infinito.it>


 
Operazioni
Ovviamente adesso vogliamo implementare le operazioni, altrimenti la classe fraction non avrebbe alcuna utilità.
Iniziamo con quelle più semplici: il prodotto e la divisione:

 a     c     a*c
--- * --- = -----
 b     d     b*d

 a     c     a     d     a*d
--- / --- = --- * --- = -----
 b     d     b     c     b*c

In entrambi i casi occorre ridurre ai minimi termini prima di restituire il risultato.
Il codice è semplicissimo se si ignora il problema dell'overflow. Nei linguaggi ad alto livello l'overflow non si può di solito controllare in modo efficiente. Mi sono sempre chiesto come mai manchi una funzionalità per controllare se l'ultima operazione eseguita ha prodotto overflow: sarebbe perfettamente possibile standardizzare una operazione del genere ma nessun linguaggio che conosco ce l'ha. Il linguaggio KISS che sto progettando l'avrà. Per gestire efficientemente e semplicemente l'errore di overflow dovremmo scrivere queste nostre routine in linguaggio macchina. Per semplicità non gestiamo l'errore. E' facile invece gestire l'errore di divisione per zero: in pratica non dobbiamo fare niente per gestirlo: se ne occupa il costruttore!. Notare che le funzioni operatori sono friend, come si preferisce di solito, in modo che anche il primo operando possa essere convertito implicitamente in frazione (es. moltiplico 5*f dove f è una frazione):

friend fraction operator*(const fraction& f1, const fraction& f2)
{
   return fraction(f1._num*f2._num, f1._denom*f2._denom);
}

friend fraction operator/(const fraction& f1, const fraction& f2)
{
   return fraction(f1._num*f2._denom, f1._denom*f2._num);
}

L'accesso diretto in questo caso non è necessario, quindi non sarebbe stato necessario definirli come friend (avremmo potuto usare i metodi num( ) e denom( ) che sono efficienti come l'accesso diretto), però volevo sottolineare il fatto che questi operatori appartengono alla classe e li ho definiti friend lo stesso. Notare che il costruttore si occupa di ridurre ai minimi termini.

Prima di addizione e sottrazione, occupiamoci dei confronti che sono più facili: il metodo più semplice per implementare gli operatori <,>,<=,>=, consiste nell'usare la regola dei prodotti incrociati e nell'evitare la costosa operazione di riduzione ad un comune denominatore:

es. di regola dei prodotti incrociati

 a     c
--- > --- <=> a*d > b*c
 b     d

Ecco per esempio < e <= (> e >= sono analoghi):

friend bool operator<(const fraction& f1, const fraction& f2)
{
   return f1._num*f2._denom < f1._denom*f2._num;
}

friend bool operator<=(const fraction& f1, const fraction& f2)
{
   return f1._num*f2._denom <= f1._denom*f2._num;
}

La scelta tra metodi inline e non è una questione di gusti (maggiore velocità o programmi piccoli?).

Infine gli operatori == e !=, che sono semplici, ma occorre comunque fare attenzione. Si potrebbe pensare che siccome le frazioni sono memorizzate in forma ridotta ai minimi termini si abbia:

 a      c
--- == --- ridotte ai minimi termini <=> a==c && b==c
 b      d

 a      c
--- != --- ridotte ai minimi termini <=> a!=c || b!=c
 b      d

friend bool operator==(const fraction& f1, const fraction& f2)
{
   return f1._num==f2._num && f1._denom==f2._denom;
}

friend bool operator!=(const fraction& f1, const fraction& f2)
{
   return f1._num!=f2._num || f1._denom!=f2._denom;
}

Queste versioni sono palesemente sbagliate! Non vanno bene con la nostra convenzione di lasciare il segno - davanti a numeratore e denominatore. Infatti (-1,1) e (1,-1) sono considerate diverse, mentre sono uguali. Inoltre (0, 2) viene considerata diversa da (0,1), ecc...
La versione corretta discende dalla seguente implicazione (ancora in effetti la regola dei prodotti incrociati), sempre vera:

 a      b
--- == --- <=> a*d == b*c
 c      d

friend bool operator==(const fraction& f1, const fraction& f2)
{
   return f1._num*f2._denom == f1._denom*f2._num;
}

friend bool operator!=(const fraction& f1, const fraction& f2)
{
   return f1._num*f2._denom != f1._denom*f2._num;
}

L'addizione e la sottrazione sono più difficili: ci occorre un algoritmo efficiente per calcolare il minimo comune multiplo. Infatti occorre ridurre le frazioni al loro minimo comune denominatore. Il minimo comune denominatore non è altro che il minimo comune multiplo dei denominatori:

Es.

 2     5      6/3*2 + 6/6*5     9     3
--- + ---  = --------------- = --- = ---
 3     6            6           6     2

come evidente occorre infine in generale ridurre ai minimi termini. L'operazione è piuttosto costosa come vedete, ma non c'è scampo. La sottrazione è analoga, basta mettere un - nella formula anzichè il +:

 a       c     mcm(b,d)/b*a +/- mcm(b,d)/d*c
--- +/- --- = -------------------------------
 b       d                mcm(b,d)

L'algoritmo del mcm è molto semplice. Nella scorsa lezione avete visto come la matematica e l'informatica non sono in realtà due scienze separate (in realtà nessuna scienza è separata dall'altra e personalmente ritengo sia riduttivo o addirittura ridicolo ridurre la scienza in discipline separate).
Non vi preoccupate, non ho intenzione di ripetere una dimostrazione formale simile a quella fatta per l'algoritmo del mcd, anche se è consigliata ai lettori più volenterosi:

                                // mcm.c
#include <stdio.h>

long mcm(long a, long b)
{
   long o=1;

   while (o*a % b)
      ++o;
   return o*a;
}

int main()
{
   long a,b;

   scanf("%ld%ld", &a, &b);

   printf("%ld", mcm(a,b));

   exit(0);
}

Come potete vedere l'algoritmo agisce a forza bruta, provando tutti i multipli di a fino a che ne trova il primo divisibile per b. Questo è il mcm. Questa era la mia prima versione, funziona, ma ci sono dei miglioramenti che possiamo fare al fine di renderlo più veloce.

Un primo miglioramento consiste nel sostituire nel ciclo una moltiplicazione con un'addizione, che risulta un po' più veloce:

long mcm(long a, long b)
{
   long o=a;

   while (a % b)
      a+=o;
   return a;
}

oppure, equivalentemente (e lo preferisco perchè più leggibile):

long mcm(long a, long b)
{
   long mcm=a;

   while (mcm % b)
      mcm+=a;
   return mcm;
}

Notare che assegnare lo stesso nome ad una variabile locale della funzione è lecito (l'unico inconveniente è che non si potrà chiamare ricorsivamente la funzione, ma qui vogliamo fare una funzione ottimizzata, mica ricorsiva!)

Un altro miglioramento possibile: considerate il caso in cui a=2 e b=100,000. Occorre fare 49999
addizioni per rendersi conto semplicemente che il mcm è 100,000. Se poi b=1,000,000,000 vedrete che il computer si impalla per un bel po. Infatti 499999999 sono un bel lavoro anche per Pentium. Un vero spreco. Come possiamo porre rimedio?

nota: come calcolo quante addizioni occorrono? faccio questo ragionamento: se b fosse 4, occorrerebbe incrementare a di 2 una volta. Se b fosse 6, 2 volte. Se b fosse 8, 3 volte. Vedi una qualche relazione? Quando b è 100,000 occorre incrementarlo (100,000-2)/2=49999.

La soluzione è semplice: osservate ad es. che se fosse a=100,000 e b=2, il while non verrebbe eseguito nemmeno una volta. Quindi occorre fare in modo che prima che l'algoritmo inizi, a sia maggiore di b. Se già non lo è occorre scambiarli

// versione migliorata
long mcm(long a, long b)
{
   long mcm;

   if (a<b)
   {
      long tmp=a;
      a=b;
      b=tmp;
   }

   mcm=a;
   while (mcm % b)
      mcm+=a;
   return mcm;
}

Bene, questo non vi "impalla" più il computer. Più ottimizzato di così non riesco a concepirlo. Se avete idee migliori, fatemelo sapere. Tenete conto che deve comportarsi bene coi numeri negativi.

Bene, finalmente possiamo scrivere gli operatori + e - (ricordate l'identità di prima):

friend fraction operator+(const fraction& f1, const fraction& f2)
{
   long m=mcm(f1._denom, f2._denom);
   return fraction(m/f1._denom*f1._num + m/f2._denom*f2._num, m);
}

friend fraction operator-(const fraction& f1, const fraction& f2)
{
   long m=mcm(f1._denom, f2._denom);
   return fraction(m/f1._denom*f1._num - m/f2._denom*f2._num, m);
}


pag. precedente pag. iniziale pag. seguente