Wszystko zależy od odpowiedniego przechowywania i algorytmów traktowania liczb jako mniejszych części. Załóżmy, że masz kompilator, w którym a int
może wynosić tylko od 0 do 99 i chcesz obsługiwać liczby do 999999 (będziemy się tutaj martwić tylko o liczby dodatnie, aby było to proste).
Robisz to, dając każdej cyfrze trzy int
si używając tych samych zasad, których (powinieneś) nauczyć się w szkole podstawowej, dotyczących dodawania, odejmowania i innych podstawowych operacji.
W bibliotece o dowolnej precyzji nie ma stałego limitu liczby typów podstawowych używanych do reprezentowania naszych liczb, tylko tyle, ile może pomieścić pamięć.
Dodatek na przykład 123456 + 78
:
12 34 56
78
-- -- --
12 35 34
Praca od najmniej znaczącego końca:
- początkowe przeniesienie = 0.
- 56 + 78 + 0 noszenia = 134 = 34 z 1 noszeniem
- 34 + 00 + 1 przeniesienie = 35 = 35 z 0 przeniesieniem
- 12 + 00 + 0 przeniesienia = 12 = 12 z 0 przeniesieniem
Tak właśnie działa dodawanie na poziomie bitów wewnątrz procesora.
Odejmowanie jest podobne (używając odejmowania typu podstawowego i pożyczania zamiast przenoszenia), mnożenie można wykonać za pomocą powtarzających się dodawań (bardzo wolno) lub iloczynów krzyżowych (szybciej), a dzielenie jest trudniejsze, ale można to zrobić poprzez przesuwanie i odejmowanie liczb zaangażowany (długi podział, którego nauczyłeś się jako dziecko).
Właściwie napisałem biblioteki, aby robić tego rodzaju rzeczy, używając maksymalnych potęg dziesięciu, które można dopasować do liczby całkowitej do kwadratu (aby zapobiec przepełnieniu podczas mnożenia dwóch int
s, takich jak 16-bitowe int
ograniczenie od 0 do 99 do generuje 9,801 (<32768) po int
podniesieniu do kwadratu lub 32-bitowe, używając od 0 do 9 999, aby wygenerować 99 980 001 (<2 147 483 648)), co znacznie uprościło algorytmy.
Kilka sztuczek, na które trzeba uważać.
1 / Podczas dodawania lub mnożenia liczb przydziel wstępnie maksymalną potrzebną przestrzeń, a następnie zmniejsz ją później, jeśli uznasz, że jest za dużo. Na przykład dodanie dwóch 100-cyfrowych int
liczb (gdzie cyfra jest ) nigdy nie da więcej niż 101 cyfr. Pomnożenie 12-cyfrowej liczby przez 3-cyfrową liczbę nigdy nie wygeneruje więcej niż 15 cyfr (dodaj liczbę cyfr).
2 / Aby zwiększyć szybkość, normalizuj (zmniejsz ilość wymaganej pamięci) tylko wtedy, gdy jest to absolutnie konieczne - moja biblioteka miała to jako oddzielne wezwanie, aby użytkownik mógł zdecydować między szybkością a kwestiami dotyczącymi pamięci.
3 / Dodawanie liczby dodatniej i ujemnej jest odejmowaniem, a odejmowanie liczby ujemnej jest tym samym, co dodawanie równoważnej liczby dodatniej. Możesz zaoszczędzić sporo kodu, wywołując nawzajem metody add i subtract po dostosowaniu znaków.
4 / Unikaj odejmowania dużych liczb od małych, ponieważ niezmiennie otrzymujesz liczby takie jak:
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
Zamiast tego odejmij 10 od 11, a następnie zaneguj to:
11
10-
--
1 (then negate to get -1).
Oto komentarze (zamienione na tekst) z jednej z bibliotek, dla których musiałem to zrobić. Sam kod jest niestety objęty prawem autorskim, ale możesz wybrać wystarczającą ilość informacji, aby obsłużyć cztery podstawowe operacje. Zakładamy, w następstwie tego -a
i -b
reprezentują liczby ujemne, a a
i b
to zero lub liczby dodatnie.
Dla Dodatkowo , jeśli objawy są różne, zastosowanie odejmowanie negacji:
-a + b becomes b - a
a + -b becomes a - b
Do odejmowania , jeśli znaki są różne, użyj dodania negacji:
a - -b becomes a + b
-a - b becomes -(a + b)
Również specjalna obsługa zapewniająca odejmowanie małych liczb od dużych:
small - big becomes -(big - small)
Mnożenie wykorzystuje matematykę dla początkujących w następujący sposób:
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
Sposób, w jaki to jest osiągane, polega na wyodrębnieniu każdej z cyfr 32 po jednej naraz (wstecz), a następnie użyciu funkcji add do obliczenia wartości, która ma zostać dodana do wyniku (początkowo zero).
ShiftLeft
a ShiftRight
operacje są używane do szybkiego mnożenia lub dzielenia a LongInt
przez wartość zawijania (10 dla "prawdziwej" matematyki). W powyższym przykładzie 2 razy dodajemy 475 do zera (ostatnia cyfra 32), aby uzyskać 950 (wynik = 0 + 950 = 950).
Następnie przesuwamy w lewo 475, aby uzyskać 4750, i prawe przesunięcie 32, aby uzyskać 3. Dodaj 4750 do zera 3 razy, aby uzyskać 14250, a następnie dodaj do wyniku 950, aby uzyskać 15200.
Przesunięcie w lewo 4750, aby uzyskać 47500, przesunięcie w prawo 3, aby uzyskać 0. Ponieważ 32 przesunięte w prawo wynosi teraz zero, skończyliśmy i faktycznie 475 x 32 równa się 15200.
Dzielenie jest również trudne, ale opiera się na wczesnej arytmetyce (metoda „gazinty” dla „idzie w”). Rozważ następujący długi podział dla 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.
Dlatego 12345 / 27
jest 457
z resztą 6
. Zweryfikować:
457 x 27 + 6
= 12339 + 6
= 12345
Jest to realizowane za pomocą zmiennej draw-down (początkowo zero), aby obniżyć segmenty 12345 pojedynczo, aż będzie większa lub równa 27.
Następnie po prostu odejmujemy od tego 27, aż uzyskamy poniżej 27 - liczba odejmowań to odcinek dodany do górnej linii.
Kiedy nie ma już segmentów do obalenia, mamy swój wynik.
Pamiętaj, że są to dość podstawowe algorytmy. Jeśli twoje liczby będą szczególnie duże, są znacznie lepsze sposoby wykonywania skomplikowanych działań arytmetycznych. Możesz zajrzeć do czegoś w rodzaju biblioteki arytmetycznej GNU Multiple Precision - jest znacznie lepsza i szybsza niż moje własne biblioteki.
Ma raczej niefortunną wadę, ponieważ po prostu wyjdzie, jeśli zabraknie jej pamięci (moim zdaniem, raczej fatalna wada dla biblioteki ogólnego przeznaczenia), ale jeśli możesz spojrzeć poza to, jest całkiem niezły w tym, co robi.
Jeśli nie możesz go użyć ze względów licencyjnych (lub ponieważ nie chcesz, aby Twoja aplikacja została zamknięta bez wyraźnego powodu), możesz przynajmniej pobrać stamtąd algorytmy do integracji z własnym kodem.
Odkryłem również, że osoby z MPIR (rozwidlenia GMP) są bardziej podatne na dyskusje na temat potencjalnych zmian - wydają się bardziej przyjazne dla programistów.