什么是GCD最有效的?


26

我知道Euclid算法是获取正整数列表的GCD(最大公约数)的最佳算法。但是实际上,您可以通过多种方式对该算法进行编码。(就我而言,我决定使用Java,但C / C ++可能是另一种选择)。

我需要在程序中使用最高效的代码。

在递归模式下,您可以编写:

static long gcd (long a, long b){
    a = Math.abs(a); b = Math.abs(b);
    return (b==0) ? a : gcd(b, a%b);
  }

在迭代模式下,它看起来像这样:

static long gcd (long a, long b) {
  long r, i;
  while(b!=0){
    r = a % b;
    a = b;
    b = r;
  }
  return a;
}

GCD还有二进制算法,可以这样简单地编码:

int gcd (int a, int b)
{
    while(b) b ^= a ^= b ^= a %= b;
    return a;
}

3
我认为这过于主观,甚至更适合StackOverflow。“最有效的在实践”取决于许多(即使不可预知的)因素,如底层architechture,存储器层级,尺寸和输入的形式等
的Juho

5
这与以递归和迭代方式表达的算法相同。我认为它们的差异可以忽略不计,因为Euclid算法收敛得很快。选择一个适合您的偏好。
2012年

6
您可能要尝试对这两个进行概要分析。由于递归版本是尾调用,因此编译器实际上发出几乎相同的代码是不太可能的。
路易(Louis)

1
这是错误的。应该在b!= 0时,然后返回a。否则,它会导致被零除的错误。如果您有很大的gcd,也不要使用递归。...您会得到一堆堆栈和函数状态...为什么不只是进行迭代呢?
Cris Stringfellow 2012年

4
请注意,有渐近更快的GCD算法。例如en.wikipedia.org/wiki/Binary_GCD_algorithm
Neal Young

Answers:


21

您的两种算法是等效的(至少对于正整数而言,命令式版本中的负整数会发生什么情况取决于%我不了解的Java语义)。在递归版本中,令和为第个递归调用的参数: b i i a i + 1 = b i b i + 1 = a i m o d b iaibii

ai+1=bibi+1=aimodbi

在命令式版本中,令和为变量的值,并在循环的第次迭代开始时。 b ' 一个' + 1 = b ' b ' + 1 = 一个' 中号ö d b ' aibiabi

ai+1=bibi+1=aimodbi

注意到类似吗?您的命令式版本和递归版本正在计算完全相同的值。此外,当(分别为)时,它们都同时结束,因此它们执行相同数量的迭代。因此,从算法上讲,两者之间没有区别。任何差异都将取决于实现方式,高度依赖于编译器,其运行的硬件以及操作系统和同时运行的其他程序的可能性。' = 0ai=0ai=0

递归版本仅进行尾递归调用。大多数用于命令式语言的编译器没有对它们进行优化,因此它们生成的代码可能会浪费一点时间和内存,从而在每次迭代时都构建一个堆栈框架。使用优化尾部调用的编译器(功能语言的编译器几乎总是这样做),生成的机器代码对于两者而言可能是完全相同的(假设您协调对的调用abs)。


8

对于较小的数字,二进制GCD算法就足够了。

GMP是一个维护良好且经过实际测试的库,在通过特殊的阈值后(Lehmer算法的推广),它将转换为特殊的一半GCD算法。Lehmer使用矩阵乘法来改进标准的Euclidian算法。根据文档,HGCD和GCD的渐近运行时间均为O(M(N)*log(N)),其中M(N)是两个N肢数相乘的时间。

有关其算法的完整详细信息,请参见此处


该链接确实没有提供完整的细节,甚至都没有定义“肢体”是什么……
einpoklum-恢复莫妮卡


2

据我所知,Java通常不支持尾递归优化,但是您可以为此测试Java实现。如果它不支持,则简单的for-loop应该更快,否则递归也应该一样快。另一方面,这些是位优化,请选择您认为更容易理解的代码。

我还要注意,最快的GCD算法不是Euclid算法,Lehmer算法更快。


我所知,你的意思?您是说语言规范没有强制执行此优化(如果这样做会令人惊讶),或者大多数实现未实现该优化?
PJTraill '18

1

首先,不要使用递归来替代紧密的循环。太慢了 不要依靠编译器对其进行优化。其次,在代码中,您在每次递归调用中都调用Math.abs(),这是没有用的。

在循环中,您可以轻松地避免使用临时变量,并且始终可以交换a和b。

int gcd(int a, int b){
    if( a<0 ) a = -a;
    if( b<0 ) b = -b;
    while( b!=0 ){
        a %= b;
        if( a==0 ) return b;
        b %= a;
    }
    return a;
}

使用a ^ = b ^ = a ^ = b交换会使源代码更短,但需要执行许多指令。它会比无聊的临时变量慢。


3
“避免递归。这太慢了”-作为一般建议,这是虚假的。这取决于编译器。通常,即使没有优化递归的编译器,它也不会很慢,只会消耗堆栈。
吉尔斯(Gilles)'所以

3
但是对于像这样的短代码,区别是巨大的。堆栈消耗意味着写入内存和从内存读取。太慢了 上面的代码在2个寄存器上运行。递归还意味着进行调用,这比条件跳转要长。对于分支预测而言,递归调用要困难得多,而内联则要困难得多。
Florian F

-2

对于较小的数字,%是一个非常昂贵的运算,也许更简单的递归

GCD[a,b] := Which[ 
   a==b , Return[a],
   b > a, Return[ GCD[a, b-a]],
   a > b, Return[ GCD[b, a-b]]
];

更快?(对不起,Mathematica代码而不是C ++)


看起来不对。对于b == 1,它应返回1。GCD[2,1000000000]将变慢。
Florian F

啊,是的,我弄错了。固定(我认为),并澄清。
Per Alexandersson

通常,GCD [a,0]也应返回a。你的永远循环。
Florian F

我很遗憾,因为您的答案仅包含代码。我们希望重点关注此站点上的想法。例如,为什么%操作昂贵?我认为,对一段代码的猜测并不是该站点的真正好答案。
Juho 2015年

1
我认为取模比减法慢的想法可以认为是民间文学艺术。它既适用于小整数(减法通常需要一个周期,很少执行模运算),也适用于大型整数(减法是线性的,我不确定模的最佳复杂度是多少,但是肯定比这差)。当然,您还需要考虑必要的迭代次数。
吉尔(Gilles)'所以

-2

Euclid算法对于计算GCD最有效:

静态long gcd(long a,long b)
{
如果(b == 0)
返回
其他
返回gcd(,a%b);
}

例:-

设A = 16,B = 10。
GCD(16,10)= GCD(10,16%10)= GCD(10,6)
GCD(10,6)= GCD(6,10%6)= GCD(6,4)
GCD(6,4)= GCD(4,6%4)= GCD(4,2)
GCD(4,2)= GCD(2,4%2)= GCD(2,0)


由于B = 0,所以GCD(2,0)将返回2。 

4
这不能回答问题。询问者提出了两个版本的Euclid,并询问哪个版本更快。您似乎没有注意到这一点,只是将递归版本声明为唯一的Euclid算法,并且没有任何证据断言它比其他任何东西都快。
David Richerby '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.