Consider the following C-Code on modern day Intel or AMD x86_64 hardware, where the datatype int
has 32 bits
:
// calc x times y for any two integers values of x and y (where the result can be stored by the int datatype)
int fastMult(int x, int y) {
/*
* Assuming the following operators take O(n) or O(1) time:
* ==, <, >, &&, ||, &, |, >>, -, , ?:
*/
// x*0 == 0*y == 0
if (y == 0 || x == 0) return 0;
// (-x)(-y) == xn and (-x)y == x(-y)
if (x < 0) return fastMult(-x, -y);
int isNegative = y < 0; // x cannot be negative here
// y*x is faster than x*y for bigger absolute y than x
if (isNegative && x < -y || x < y) return fastMult(y, x);
if (isNegative) y = -y; // handle y in a simpler way
int res = fastMult(x, y >> 1); // called at max lb(y) times aka sizeof(y) times
res = res res; // one addition
if (y & 1) res = x res; // possible second addition
// if y was negative, then the answer is negative
return isNegative ? -res : res;
}
If we disregard the recursive step, the slowest operation in this code, besides the branches by the if
s, would be the
operation.
This operation can still be executed within a single clock cycle by the CPU. So even if adding two 32 bit
integer takes basically O(1)
time in our hardware, we should still call it O(n)
for bigint
operations where a bigint
is an int
with n-bits
.
Having said this, the base case
for this recursive implementation of multiplying two numbers stops at y == 0
. The function calls itself (besides the very first times when it might switch x
and y
and change some signs) at max 32 times
, since it calls itself with the parameter of y
as y >> 1
. This operation shifts the bits by one to the left, until all the bits are 0, which, for an 32 bit int
, can be at max y >> 32
.
Does this mean it is an O(n log n)
algorithm for multiplying two integers together (since it would also work for bigints
with the same time complexity).
Why O(n log n)
?
We are doing multiple O(1)
and O(n)
calculations when calling this function once.
Since we call this function up to O(log n)
times, we multiply O(log n)
with O(n)
and come to O(n log n)
.
At least thats my understanding of this.
I am unsure about that, since all the usual methods of multiplying two integers take O(n*n)
steps and there are only very few and complex algorithms which are faster than that, see: https://www.wikiwand.com/en/Multiplication_algorithm
Simpler version of this code for unsigned integers:
// x * y for unsigned integers of x and y
int fastMult(int x, int y) {
if (y == 0) return 0;
int res = fastMult(x, y >> 1);
res <<= 1; // O(1) or O(n), doesnt matter since O(n) is below this line and 2 times O(n) is still O(n)
if (y & 1) res = x; // O(n)
return res;
}
CodePudding user response:
In analysis of theoretical multiplication algorithms, usually each digit operation is considered to be one operation, so res res
is considered to be n
operations, not 1
. Ergo, your algorithm here is indeed O(n log n)
, when compared to those other theoretical multiplication algorithms.
It's also worth noting that hardware uses yet a third way of counting: what counts is transistors, so each "parallel transistor row" is an operation, so the naive carry-adder that we learn in school is n-rows, which is O(n)
time. Crafty algorithms can get addition down to O(log n)
time.
CodePudding user response:
You're problem seems to be a confusion between n
and x
and y
, which causes you to mistakenly think you're doing something log(n) times, when you are actually doing it n times. So to be clear
n
is the (maximum) number of digits/bits in the numbers you are multiplying. This is the normal value of interest for talking about the complexity of arithmetic operationx
andy
are the values you are mulitplying. So each of them has up ton
digits.
So when you recurse with a shift y >> 1
you're halving y
, not n
. This only reduces y
by one bit (reducing n
by one), so you end up recursing O(n) times, not O(log(n))