How to write a small graceful gcd function?

D

Dann Corbit

Tom St Denis said:
I don't see the problem here. The algorithm has a complexity of O(n^2)
whether that is space or time it's still that complex.

If an algorithm finishes in a fixed time and never takes more than a limited
number of clocks to complete, then it is a constant time algorithm. That's
the definition. I am talking about a hardware multiply of a hardware
integer on some C implementation.
Why do things get more efficient as you use more resources?

There is no claim on my part of increased efficiency with greater resources.
Different SIZED NUMBERS. Yes, in fact the size grows quadratically
with the size of the input if you want to keep O(1) time.

All the integers involved are 64 bit (or less). The algorithm I supplied
(and the one that you supplied) do not use variable sized registers or
numbers. It is an implementation that uses integers of a single, constant
size.
What? You think a 16-bit and 64-bit multiplier are the same size for
constant time?

In what way does the implementation that you supplied change the size if its
registers as it runs? The answer is, 'the integers in this algorithm are
all of the same size and they do not change width during the operation of
the algorithm'.

I think that a 64 bit hardware number uses an ALU and mutiplies in constant
time.
I have not made any claims about changing the size of the multiplier.

The algorithm that YOU supplied uses a constant sized hardware integer.

I think that you are smart enough to know that you are wrong. If not, then
I don't see any point in continuing the discussion.

It's OK to be wrong. I'm wrong a lot. But when you are wrong and you know
you are wrong and you insist that you are right it looks pretty strange.

[snip]
 
T

Tom St Denis

Dann said:
If an algorithm finishes in a fixed time and never takes more than a limited
number of clocks to complete, then it is a constant time algorithm. That's
the definition. I am talking about a hardware multiply of a hardware
integer on some C implementation.

I never said the algorithm was quadratic time. I said it's O(n^2)
complexity. It's linear because the processor trades space for
division time (even then though division algos usually have early outs
so they're not constant time anyways but let's disregard reality for
this conversation since "facts" have waved bye bye long ago).
All the integers involved are 64 bit (or less). The algorithm I supplied
(and the one that you supplied) do not use variable sized registers or
numbers. It is an implementation that uses integers of a single, constant
size.

Your implementation may be constant time (though I wouldn't say that in
a portable C group) but the algorithm is not constant complexity.
I think that you are smart enough to know that you are wrong. If not, then
I don't see any point in continuing the discussion.

Your asserting something I'm not trying to maintain. I said your
algorithm is O(n^2) complexity. I never said it takes that time [or if
I did originally that was in error, and I've been saying otherwise for
the last 10 replies or so...]
It's OK to be wrong. I'm wrong a lot. But when you are wrong and you know
you are wrong and you insist that you are right it looks pretty strange.

Well given that bignum math is one of my fortes, I'm comfortable in my
position. It's you who doesn't really seem to understand complexity
notation or discussion.

If we used your logic, sorting N numbers with bubble sort is N^2 steps,
but what if N is constant. Well now buble sort is constant time.
Therefore, bubble sort is the most efficient sort algorithm.

No, bubble sort is still N^2 but your instance is constant time because
the size doesn't change. Similarly, if you always multiply 64-bit
quantities you have a constant time, but the complexity is not constant
since multiplication is not constant complexity. Big-Oh notation is
about asymptotics and in our case about the complexity of something as
the size of the input changes.

Getting back to it. Division is a O(n^2) complexity problem when
implemented serially. There is no getting around that. You have n
bits and you have to perform n n-bit subtractions. That's n^2 bit
operations. You may implement various operations in parallel but that
doesn't change the complexity just the time.

Anyways enough ranting. You're too hung up on the specifics of the
problem and are ignoring the actual meaning of what I'm trying to say.
So think whatever the hell you want. Doesn't really matter I guess.

Tom
 
D

Dann Corbit

Tom St Denis said:
Dann said:
If an algorithm finishes in a fixed time and never takes more than a
limited
number of clocks to complete, then it is a constant time algorithm.
That's
the definition. I am talking about a hardware multiply of a hardware
integer on some C implementation.

I never said the algorithm was quadratic time. I said it's O(n^2)
complexity. It's linear because the processor trades space for
division time (even then though division algos usually have early outs
so they're not constant time anyways but let's disregard reality for
this conversation since "facts" have waved bye bye long ago).
All the integers involved are 64 bit (or less). The algorithm I supplied
(and the one that you supplied) do not use variable sized registers or
numbers. It is an implementation that uses integers of a single,
constant
size.

Your implementation may be constant time (though I wouldn't say that in
a portable C group) but the algorithm is not constant complexity.
I think that you are smart enough to know that you are wrong. If not,
then
I don't see any point in continuing the discussion.

Your asserting something I'm not trying to maintain. I said your
algorithm is O(n^2) complexity. I never said it takes that time [or if
I did originally that was in error, and I've been saying otherwise for
the last 10 replies or so...]
It's OK to be wrong. I'm wrong a lot. But when you are wrong and you
know
you are wrong and you insist that you are right it looks pretty strange.

Well given that bignum math is one of my fortes, I'm comfortable in my
position. It's you who doesn't really seem to understand complexity
notation or discussion.

If we used your logic, sorting N numbers with bubble sort is N^2 steps,
but what if N is constant. Well now buble sort is constant time.
Therefore, bubble sort is the most efficient sort algorithm.

No, bubble sort is still N^2 but your instance is constant time because
the size doesn't change. Similarly, if you always multiply 64-bit
quantities you have a constant time, but the complexity is not constant
since multiplication is not constant complexity. Big-Oh notation is
about asymptotics and in our case about the complexity of something as
the size of the input changes.

Getting back to it. Division is a O(n^2) complexity problem when
implemented serially. There is no getting around that. You have n
bits and you have to perform n n-bit subtractions. That's n^2 bit
operations. You may implement various operations in parallel but that
doesn't change the complexity just the time.

Anyways enough ranting. You're too hung up on the specifics of the
problem and are ignoring the actual meaning of what I'm trying to say.
So think whatever the hell you want. Doesn't really matter I guess.

According to your definition this algorithm:

int multiply(int a, int b)
{
return a*b;
}
is O(n^2)

and this algorithm:

int subtract(int a, int b)
{
return a-b;
}
is O(log(n))

It just shows that you have no idea what the terms mean.

Both of those algorithms are O(1).

Now (on the other hand) if we were implementing multiple precision
algorithms to perform the fundamental operations, then (depending on the
implementation) your argument might have some validity.

As it is, you have just demonstrated you preposterous ignorance for all time
in USENET, to be salted away into the archives for posterity.
Congratulations.
At least some future denizens of earth may get a good chuckle from it.
 
T

Tom St Denis

Dann said:
According to your definition this algorithm:

int multiply(int a, int b)
{
return a*b;
}
is O(n^2)

*complexity*. Yes, that's true.

You can't use O() notation for constant sized data sets. You're
basically saying "as 64 => oo this function takes constant time".
That's nonsense.

However, for the general term "int" without upper bounds the algorithm
has a O(n^2) complexity.
and this algorithm:

int subtract(int a, int b)
{
return a-b;
}
is O(log(n))

Actually, addition and subtraction have O(n) complexity.
It just shows that you have no idea what the terms mean.

Funny, I was just going to say the same thing.
Both of those algorithms are O(1).

No. They're not. And for the very reason you think, O() notation does
not apply.
Now (on the other hand) if we were implementing multiple precision
algorithms to perform the fundamental operations, then (depending on the
implementation) your argument might have some validity.

Might ... does ... whatever.
As it is, you have just demonstrated you preposterous ignorance for all time
in USENET, to be salted away into the archives for posterity.
Congratulations.
At least some future denizens of earth may get a good chuckle from it.

First off, supposing I was wrong [which in this regard I'm not], this
is far from the worst post I've ever made. I've been on usenet since I
was ~16 or so and have enough good and bad posts to be rather "famous",
at least in the sci.crypt world.

I don't get why you're arguing this. You can't seem to discuss
complexity with any sense of proficiency.

Tom
 
O

Old Wolf

Dann said:
I'm sorry, but I'm not wrong. You fundamentally misunderstand what O(f(n))
means.

Subtraction, division, multiplication, modulus, etc. are ALL O(1) on modern
64 bit hardware if the integer values are also 64 bit.

I'll add my voice to the chorus: you fundamentally misunderstand
what it means.

To say that an algorithm is O(n^2) means that as n approaches
infinity, the complexity of the algorithm approaches n^2 (perhaps
multiplied by some constant).

It is meaningless to assert "Operation X is O(1) on two 64-bit
numbers". Big-O notation is for describing how the metrics of
an algorithm grow as the size of the parameters grows. It
cannot be used to assess one specific instance.

To say that multiplication is O(n^2) is to say that a 128-bit
multiplication takes roughly 4 times as many bit operations
as a 64-bit multiplication. It says nothing about how long a 64-bit
multiplication takes in real time, nor about how a 64-bit
multiplication compares to a 64-bit addition.

A further note: in this case, "n" is the size of the number (eg.
the number of bits) and complexity is taken as the number of
bit-operations needed.

Other types of complexity can also be measured by this notation,
eg. time-complexity and space-complexity. Also, complexity can
be measured against other parameters besides the size of the inputs.
 
E

ena8t8si

lovecreatesbeauty said:
Keith said:
lovecreatesbeauty said:
Keith Thompson wrote:
[...]
int gcd( int m, int n ) // recursive
{
if(n==0) { return m; }
else
{ int answer = gcd(n,m%n);
return answer; }
}

The temporary is unnecessary, and the formatting is just odd. Here's
how I'd write it:

int gcd(int m, int n)
{
if (n == 0) {
return m;
}
else {
return gcd(n, m % n);
}
}

Thanks.

The function accepts zeros and negative integers as their arguments.
How are users without much mathematical knowledge like me supposed to
provide correct input to use it?

The obvious solution to that is to change the arguments and result
from int to unsigned.

Different mathematical functions have different domains and ranges
(values that make sense for arguments and results). C, unlike some
other languages, doesn't let you declare a type with a specified range
of values -- but in this case, unsigned happens to be just what you
want.

The recursive way becomes the worst one and can not be improved to add
more parameter validation to it. If the function accepts input from
other automatic software systems, then someone should still keep an eye
on it, because the result may be wrong without warning.

int gcd(int x, int y){
if (x > 0 && y > 0){
while (x % y){
y = x + y;
x = y - x;
y = (y - x) % x;
}
} else {
y = -1; /*input invalid*/
}
return y;
}

int gcd3(int m, int n){
if (n == 0){
return m;
} else {
return gcd3(n, m % n);
}
}
...

The greatest common divisor is perfectly well
defined for negative integers: gcd(-4,-6) == 2.

int gcd(int a, int b)
{ return b ? gcd(b,a%b) : a<0 ? -a : a ? a : 1; }

Change the 1 to a -1 if you want gcd(0,0) to
return an "error indicator".

By the way, any halfway decent compiler will turn
a recursive version like the one shown here into
an iterative one. No need to micro-optimize.
 
M

Mark McIntyre

You can't use O() notation for constant sized data sets. You're
basically saying "as 64 => oo this function takes constant time".
That's nonsense.

You apparently have absolutely no idea what this notation means.
Perhaps you should read some books, and then creep quietly back.
I don't get why you're arguing this. You can't seem to discuss
complexity with any sense of proficiency.

Most amusing.
--
Mark McIntyre

"Debugging is twice as hard as writing the code in the first place.
Therefore, if you write the code as cleverly as possible, you are,
by definition, not smart enough to debug it."
--Brian Kernighan
 
T

Tom St Denis

Mark said:
You apparently have absolutely no idea what this notation means.
Perhaps you should read some books, and then creep quietly back.

Um ok. Show me where O() notation applies to a constant sized input.

I realize this is clc and not some math group but computer scientists
should understand how to speak about complexity. Saying multiplication
is O(1) a surefire sign that you don't know what the heck you're
talking about.

Tom
 
C

Chris Dollin

Tom said:
Um ok. Show me where O() notation applies to a constant sized input.

I realize this is clc and not some math group but computer scientists
should understand how to speak about complexity. Saying multiplication
is O(1) a surefire sign that you don't know what the heck you're
talking about.

Isn't saying "multiplication is O(1)" saying "multiplication takes
constant time", independent of the operands?

Which - for fixed-size arguments, eg 32-bit int(egers) such as are
found on many machines & in many programming languages - is, I thought,
true.

Of course (as someone alluded to) if the operands are of variable
size, not fixed in advance, as might be found in an unbounded-precision
library or (sort of equivalently) in Lisp/Pop11/Smalltalk general arithmetic,
then multiplication is ... unlikely ... to be O(1).

[It /could/ be O(1). But I'd prefer not to use that implementation.]
 
T

Tom St Denis

Chris said:
Isn't saying "multiplication is O(1)" saying "multiplication takes
constant time", independent of the operands?

No, it means independent of operand SIZE [or complexity if you will].

In the computer science world O() is typically a function of input
size, e.g. sort set size, input width, etc...
Which - for fixed-size arguments, eg 32-bit int(egers) such as are
found on many machines & in many programming languages - is, I thought,
true.

For constant sized implementations of an algorithm O() doesn't apply.
O() applies to the algorithm itself when not restrained to given
operand sizes.
Of course (as someone alluded to) if the operands are of variable
size, not fixed in advance, as might be found in an unbounded-precision
library or (sort of equivalently) in Lisp/Pop11/Smalltalk general arithmetic,
then multiplication is ... unlikely ... to be O(1).

Multiplication can never be O(1) complexity. At best you can
"linearize" it with O(n^k) for some ridiculously small k. However, as
k gets smaller the runtime of the operation goes up and only proves
useful for very large inputs.

Old Wolf summed up the argument well. O() notation only applies to
functions that are unbounded.

Tom
 
R

Richard Heathfield

Dann Corbit said:

It just shows that you have no idea what the terms mean.

No, I think Tom St D has a pretty good handle on what the terms mean - and I
think you do too. But you're talking at cross-purposes.

Tom is talking about the true complexity of multiplication, which is
actually O(m*n) when multiplying an m-digit number by an n-digit number.
You appear to be talking about the special case where m and n are both 1,
calculating in base (INTEGERTYPE_MAX + 1).
Both of those algorithms are O(1).

Now (on the other hand) if we were implementing multiple precision
algorithms to perform the fundamental operations, then (depending on the
implementation) your argument might have some validity.

That's basically what he's talking about.
As it is, you have just demonstrated you preposterous ignorance for all
time in USENET, to be salted away into the archives for posterity.

Tom St Denis has little reason to count me among his greatest admirers, but
even I think you're over-egging it here. He certainly knows his bignums.
 
C

Chris Dollin

Tom said:
Chris Dollin wrote:

Multiplication can never be O(1) complexity. At best you can
"linearize" it with O(n^k) for some ridiculously small k. However, as
k gets smaller the runtime of the operation goes up and only proves
useful for very large inputs.

I was thinking of something a little more drastic, for the ordinary
sort of bignum implementations on machines with known limits on their
(virtual) address space.

(More drastic and less useful.)
Old Wolf summed up the argument well. O() notation only applies to
functions that are unbounded.

So not bignums then?

(fx:innocentLook)
 
T

Tom St Denis

Chris said:
I was thinking of something a little more drastic, for the ordinary
sort of bignum implementations on machines with known limits on their
(virtual) address space.

(More drastic and less useful.)

Even if you're thinking about a huge lookup table that's still an
O(n^2) algorithm. Again you just traded space for time. You didn't
change the complexity of the algorithm.

You can linearize multiplication by CHANGING THE ALGORITHM not the
implementation.
So not bignums then?

You're impossibly stupid, how do you even function in society?

Tom
 
C

Chris Dollin

Tom said:
Even if you're thinking about a huge lookup table
No.

that's still an
O(n^2) algorithm. Again you just traded space for time. You didn't
change the complexity of the algorithm.

You can linearize multiplication by CHANGING THE ALGORITHM not the
implementation.

I don't think you're looking carefully enough at what I wrote -- including
the giveaway signatures -- and considering how deliberately stupid an algorithm
I'm implying. (I also think your deadpan humour detector is broken.)
You're impossibly stupid, how do you even function in society?

How on Earth can I be /impossibly/ stupid? If I'm that stupid, it must
be /possible/ to be that stupid. Duh.

I should of course have written "So not functions over bignums then?".
how do you even function in society?

Is that bounded or unbounded functions you're thinking of?
 
M

Mark McIntyre

Um ok. Show me where O() notation applies to a constant sized input.

I realize this is clc and not some math group but computer scientists
should understand how to speak about complexity.

I strongly suspect we do. However bear in mind that computing is not
abstract maths, its applied maths working with finite and discrete
data sets. This has an appreciable effect.
Saying multiplication
is O(1) a surefire sign that you don't know what the heck you're
talking about.

Well, O(1) means "takes the same time irrespective of the magnitude of
the argument". I strongly suspect this is true for actual real
computer systems.
--
Mark McIntyre

"Debugging is twice as hard as writing the code in the first place.
Therefore, if you write the code as cleverly as possible, you are,
by definition, not smart enough to debug it."
--Brian Kernighan
 
T

Tom St Denis

Mark said:
Well, O(1) means "takes the same time irrespective of the magnitude of
the argument". I strongly suspect this is true for actual real
computer systems.

Except that O() notation doesn't apply to fixed implementations of an
algorithm because the size doesn't change [nor approach oo].

What it does apply to is the algorithm in general. For example, as you
scale the multiplier you're placing in silicon the size scales with the
square of the operand length. That, or the time it takes to complete
squares with the operand size. This is how CPU designers can model the
tradeoff between multiplier size and opcode timing.

For instance, the AMD multiplier does 64-bits in 5 cycles [after a 2
cycle delay it's pipelined] which suggests [*] that they use a
Karatsuba trick. The ARM7 multiplier takes 1 cycle per non-trivial
byte in the right operand which suggests they implemented it as a 8x32
with the appropriate adder. etc, etc. They can make these decisions
because they can correctly model the algorithms space/time trade offs.

Totally OT for this group and I suggest we just leave it at that. If
you really want to understand O() better read the wikipedia page on it.

http://en.wikipedia.org/wiki/Big_oh

Tom
 
D

Dann Corbit

Richard Heathfield said:
Dann Corbit said:



No, I think Tom St D has a pretty good handle on what the terms mean - and
I
think you do too. But you're talking at cross-purposes.

Tom is talking about the true complexity of multiplication, which is
actually O(m*n) when multiplying an m-digit number by an n-digit number.
You appear to be talking about the special case where m and n are both 1,
calculating in base (INTEGERTYPE_MAX + 1).

I have no argument with bignum multiplication complexity.
If his analysis were correct, then any algorithm that contained a division
would be O(n^2).
That's basically what he's talking about.


Tom St Denis has little reason to count me among his greatest admirers,
but
even I think you're over-egging it here. He certainly knows his bignums.

I have downloaded some of his work, a long time ago. I know that he knows
what he is talking about when it comes to bignums. I know that he is
intelligent and good at mathematics. Be that as it may, none of the GCD
algorithms presented were O(n^2).

I think his analysis is probably correct if we are finding the GCD of two
billion digit numbers with arbitrary precision number classes. But that is
not the algorithm that was up for discussion.

I am through arguing about it. In fact, this thread was a reminder to me of
why I stopped posting on USENET for a couple years.
 
B

Ben Pfaff

Dann Corbit said:
I am through arguing about it. In fact, this thread was a reminder to me of
why I stopped posting on USENET for a couple years.

It'd be a shame if you stopped again. I learn so much from the
articles you post about sorting (and other algorithms).
 
C

Chris Torek

I'll add my voice to the chorus: you fundamentally misunderstand
what it means.

To say that an algorithm is O(n^2) means that as n approaches
infinity, the complexity of the algorithm approaches n^2 (perhaps
multiplied by some constant).

This is true -- or more precisely, g(n) = O(f(n)) means g(n) is
bounded above by f(n). That is, there is some constant c and value
x-sub-0 such that for all x > x-sub-0, g(x) <= c f(x). (There are
also Omega and Theta notations for bounded below, and bounded on
both sides.)

But:
It is meaningless to assert "Operation X is O(1) on two 64-bit
numbers".

This is *not* true. If k is any constant, O(k) and O(1) mean
the same thing, since the constant can be "moved out": if k is
a constant, then g(k) = k g(1), so we can replace c f(x) with
(k*c) f(x) above.

As an aside, if some algorithm is O(1), it is also automatically
O(n) and O(n^2) and O(exp(n)), since big-O notation merely gives
you an upper bound. (One should use Theta(n) instead of O(n) if
one wishes to disallow this.)
Big-O notation is for describing how the metrics of
an algorithm grow as the size of the parameters grows. It
cannot be used to assess one specific instance.

Again, it *can*. It is just not particularly *useful* to do so.
But we do it all the time. :)
To say that multiplication is O(n^2) is to say that a 128-bit
multiplication takes roughly 4 times as many bit operations
as a 64-bit multiplication.

(Again, this is only strictly true if you say it is Theta(n^2).)

In any case, the real problems here (there are two) are:

(a) In C, we do not compute a 2n-bit product when multiplying
two n-bit numbers. Instead, we prefer to get the wrong answer
as fast as possible, so we retain only the lower n bits of
the result (with -- for signed integers -- undefined behavior
if the result does not fit in those n bits, although typically
overflow is ignored; for unsigned integers, overflow is always
deliberately ignored.)

(b) Again, in order to get the wrong answer as fast as possible,
we set "n" to a constant. And when n is a constant, O(n)
*is* O(1) (because O(32) is just O(1), as is O(64), or even
O(1048976)).

To avoid both of those problems, we have to write functions to
operate on "bignums", and replace the simple:

result = n1 * n2;

syntax with, e.g.:

struct bignum *n1, *n2, *result;
...
result = bignum_multiply(n1, n2);

or similar. Now we can calculate the correct answer slowly, and
use an algorithm in which the number of bits is not a constant,
and therefore get multiplication that is indeed O(mn) (where m
and n are the number of bits needed for n1 and n2 respectively).

If C were C++, we could use operator overloading to make:

result = n1 * n2;

into syntactic sugar for a call to bignum_multiply(), and therefore
compute the right answer slowly, with an O(n^2) algorithm; but C
is not C++ (for which, for some bizarre reason, some of us are
actually grateful :) ).
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,065
Latest member
OrderGreenAcreCBD

Latest Threads

Top