Step 1: Decide how you're going to represent bignums
There exist libraries out there already for this. The GNU Multiple Precision Integer library is a commonly used option. (But according to your edit, that isn't an option. You might still glance at some of them to see how they do things, but it isn't necessary.)
If you want to roll your own, I do not recommend storing the decimal digits. If you do that, you'll need to convert to and from a binary representation every time you want to do arithmetic on the components. Better to have something like a linked list of uint32_t
s, along with a sign bit. You can convert from/to decimal when you want to read and write, but do your math in binary.
Step 2: Implement exponentiation
I'll be assuming the linked list bignum implementation here; you can adapt the algorithms as needed.
If you're just calculating a power of 2, it's easy. It's a 1 followed by N 0s, so if each block stores M bits and you want to represent 2^N
, then just have floor(N/M)
blocks of all 0s, and store 1 << (N % M)
in the most significant block.
If you want to be able to do exponentiation with arbitrary bases in an efficient manner, you should use exponentiation by squaring. The idea behind this is that if you want to compute 3^20, you don't multiply 3 * 3 * 3 * ... * 3. Rather, you compute 3^2 = 3 * 3
. Then 3^4 = 3^2 * 3^2. 3^8 = 3^4 * 3^4. 3^16 = 3^8 * 3^8
. And you store each of these intermediate results as you go. Then once you reach the point where squaring it again would result in a larger number than the one you want, you stop squaring and assemble the final result from the pieces you have. In this case, 3^20 = 3^16 * 3^4
.
This approach computes the final result in 5 steps instead of 20, and since the time is logarithmic in terms of the exponent, the speed gain gets more pronounced the larger the exponent. Even computing 3^100000 only takes 21 multiplications.
There isn't a clever approach to the multiplication that I know of; you can probably just do something along the lines of the basic long multiplication algorithm you learned in elementary school, but at the level of blocks: the reason we used uint32_t
s earlier instead of uint64_t`s is so we can cast the operands to the larger type and multiply them without risk of losing the carry bits to overflow.
Convert from binary to decimal for printing
First, find the largest multiple of 10 less than your number.
I leave doing this efficiently as an exercise for the reader, but you can probably manage it by doing exponentiation by squaring to find an upper bound, then subtracting various stored intermediate values to get down to the actual value faster than you would by dividing by 10 repeatedly.
Or you can just find the number by repeatedly multiplying by 10; the rest of this is going to be linear no matter how the first part is handled.
But however you get it, you have a q
such that q = k * 10, 10 * q > n, q <= n
, you can just cycle through one decimal digit at a time:
for (; q; q /= 10) {
int digit = n / q; //truncated down to floor(n/q)
printf("%d", digit);
n -= digit * q;
}
It's possible that there's a more efficient method in the literature somewhere, but I'm not familiar with one offhand. But it's not a major deal so long as we only need to do the inefficient part when writing output; that's slow no matter the algorithm. By which I mean, it might take a millisecond or two to print all 100,000 digits. That doesn't matter when we're displaying the number for human consumption, but if we had to wait a millisecond as part of a calculation in a loop somewhere, it'd add up and become terribly inefficient. That's why we never store numbers in a decimal representation: by representing it as binary internally, we do the inefficient parts once on input and once on output, but everything in between is fast.