È tutta una questione di archiviazione e algoritmi adeguati per trattare i numeri come parti più piccole. Supponiamo che tu abbia un compilatore in cui an int
può essere solo da 0 a 99 e vuoi gestire numeri fino a 999999 (ci preoccuperemo solo dei numeri positivi qui per mantenerlo semplice).
Lo fai dando a ogni numero tre se int
usando le stesse regole che (dovresti) aver imparato alla scuola primaria per l'addizione, la sottrazione e le altre operazioni di base.
In una libreria di precisione arbitraria, non esiste un limite fisso al numero di tipi di base utilizzati per rappresentare i nostri numeri, qualunque sia la memoria che può contenere.
Aggiunta ad esempio 123456 + 78
:
12 34 56
78
-- -- --
12 35 34
Lavorando dall'estremità meno significativa:
- riporto iniziale = 0.
- 56 + 78 + 0 riporto = 134 = 34 con 1 riporto
- 34 + 00 + 1 riporto = 35 = 35 con 0 riporto
- 12 + 00 + 0 riporto = 12 = 12 con 0 riporto
Questo è, infatti, il modo in cui generalmente funziona l'addizione a livello di bit all'interno della CPU.
La sottrazione è simile (usando la sottrazione del tipo di base e prendere in prestito invece di riporto), la moltiplicazione può essere eseguita con addizioni ripetute (molto lente) o prodotti incrociati (più veloci) e la divisione è più complicata ma può essere eseguita spostando e sottrando i numeri coinvolti (la lunga divisione che avresti imparato da bambino).
In realtà ho scritto librerie per fare questo genere di cose usando le potenze massime di dieci che possono essere inserite in un numero intero quando è al quadrato (per evitare overflow quando si moltiplicano due int
s insieme, ad esempio un 16 bit int
limitato da 0 a 99 a generare 9.801 (<32.768) al quadrato o 32 bit int
utilizzando da 0 a 9.999 per generare 99.980.001 (<2.147.483.648)) che ha notevolmente facilitato gli algoritmi.
Alcuni trucchi a cui prestare attenzione.
1 / Quando si aggiungono o si moltiplicano numeri, pre-allocare lo spazio massimo necessario, quindi ridurlo in seguito se trovi che è troppo. Ad esempio, l'aggiunta di due numeri da 100 "cifre" (dove la cifra è un int
) non darà mai più di 101 cifre. Moltiplicare un numero di 12 cifre per un numero di 3 cifre non genererà mai più di 15 cifre (aggiungi i conteggi delle cifre).
2 / Per una maggiore velocità, normalizza (riduci lo spazio di archiviazione richiesto) i numeri solo se assolutamente necessario: la mia libreria aveva questa chiamata separata in modo che l'utente possa decidere tra velocità e problemi di archiviazione.
3 / L'addizione di un numero positivo e negativo è una sottrazione e la sottrazione di un numero negativo equivale all'aggiunta dell'equivalente positivo. È possibile salvare un bel po 'di codice facendo in modo che i metodi di aggiunta e sottrazione si chiamino l'un l'altro dopo aver regolato i segni.
4 / Evita di sottrarre numeri grandi da quelli piccoli poiché invariabilmente finisci con numeri come:
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
Invece, sottrai 10 da 11, quindi negalo:
11
10-
--
1 (then negate to get -1).
Ecco i commenti (trasformati in testo) da una delle librerie per cui ho dovuto farlo. Il codice stesso è, sfortunatamente, protetto da copyright, ma potresti essere in grado di raccogliere informazioni sufficienti per gestire le quattro operazioni di base. Assumiamo di seguito che -a
e -b
rappresentino numeri negativi e a
e b
siano zero o numeri positivi.
Per l' addizione , se i segni sono diversi, usa la sottrazione della negazione:
-a + b becomes b - a
a + -b becomes a - b
Per la sottrazione , se i segni sono diversi, utilizzare l'aggiunta della negazione:
a - -b becomes a + b
-a - b becomes -(a + b)
Anche una gestione speciale per assicurarci di sottrarre numeri piccoli da grandi:
small - big becomes -(big - small)
La moltiplicazione utilizza la matematica di livello base come segue:
475(a) x 32(b) = 475 x (30 + 2)
= 475 x 30 + 475 x 2
= 4750 x 3 + 475 x 2
= 4750 + 4750 + 4750 + 475 + 475
Il modo in cui si ottiene ciò comporta l'estrazione di ciascuna delle cifre di 32 una alla volta (all'indietro), quindi l'utilizzo di add per calcolare un valore da aggiungere al risultato (inizialmente zero).
ShiftLeft
e le ShiftRight
operazioni vengono utilizzate per moltiplicare o dividere rapidamente a LongInt
per il valore di avvolgimento (10 per la matematica "reale"). Nell'esempio sopra, aggiungiamo 475 a zero 2 volte (l'ultima cifra di 32) per ottenere 950 (risultato = 0 + 950 = 950).
Quindi abbiamo spostato a sinistra 475 per ottenere 4750 e lo shift a destra 32 per ottenere 3. Aggiungere 4750 a zero 3 volte per ottenere 14250, quindi aggiungere al risultato di 950 per ottenere 15200.
Spostamento a sinistra 4750 per ottenere 47500, spostamento a destra 3 per ottenere 0. Poiché lo spostamento a destra di 32 ora è zero, abbiamo finito e, in effetti, 475 x 32 è uguale a 15200.
Anche la divisione è complicata, ma basata sull'aritmetica iniziale (il metodo "gazinta" per "entra in"). Considera la seguente divisione lunga per 12345 / 27
:
457
+-------
27 | 12345 27 is larger than 1 or 12 so we first use 123.
108 27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
---
154 Bring down 4.
135 27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
---
195 Bring down 5.
189 27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
---
6 Nothing more to bring down, so stop.
Quindi 12345 / 27
è 457
con resto 6
. Verificare:
457 x 27 + 6
= 12339 + 6
= 12345
Ciò viene implementato utilizzando una variabile di abbassamento (inizialmente zero) per ridurre i segmenti di 12345 uno alla volta fino a quando non è maggiore o uguale a 27.
Quindi sottraiamo semplicemente 27 da quello fino a quando non scendiamo al di sotto di 27: il numero di sottrazioni è il segmento aggiunto alla riga superiore.
Quando non ci sono più segmenti da abbattere, abbiamo il nostro risultato.
Tieni presente che questi sono algoritmi piuttosto semplici. Ci sono modi molto migliori per eseguire operazioni aritmetiche complesse se i tuoi numeri saranno particolarmente grandi. Puoi esaminare qualcosa come GNU Multiple Precision Arithmetic Library : è sostanzialmente migliore e più veloce delle mie librerie.
Ha la sfortunata funzionalità sbagliata in quanto uscirà semplicemente se si esaurisce la memoria (un difetto piuttosto fatale per una libreria di uso generale secondo me) ma, se riesci a guardare oltre, è abbastanza bravo in quello che fa.
Se non puoi usarlo per motivi di licenza (o perché non vuoi che la tua applicazione esca semplicemente senza un motivo apparente), potresti almeno ottenere gli algoritmi da lì per l'integrazione nel tuo codice.
Ho anche scoperto che i corpi di MPIR (un fork di GMP) sono più suscettibili alle discussioni su potenziali cambiamenti - sembrano un gruppo più amichevole per gli sviluppatori.