快速找到两个数字的GCD

时间:2014-07-07 12:43:22

标签: performance perl greatest-common-divisor

有没有办法让这个程序更快?我正在考虑一些更快的用户输入工具等。

这是我的代码:

sub partia {
    my ( $u, $v ) = @_;
    if ( $u == $v ) { return $u }
    if ( $u == 0 )  { return $v }
    if ( $v == 0 )  { return $u }
    if ( ~$u & 1 ) {
        if ( $v & 1 ) {
            return partia( $u >> 1, $v );
        }
        else {
            return partia( $u >> 1, $v >> 1 ) << 1;
        }
    }

    if ( ~$v & 1 ) {
        return partia( $u, $v >> 1 );
    }

    if ( $u > $v ) {
        return partia( ( $u - $v ) >> 1, $v );
    }
    return partia( ( $v - $u ) >> 1, $u );
}

sub calosc {
    $t = <>;
    while ($t) {
        @tab = split( /\s+/, <> );
        print( partia( $tab[0], $tab[1] ), "\n" );
        $t--;
    }
}
calosc();

程序如何运作:
通常它返回用户输入的2个数字的最大公约数。这主要是斯坦因的算法。

INPUT
第一行:
用户想要检查多少对。[enter]
第二行:
第一个数字[空格]第二个数字[输入]

输出
GCD [输入]
在Python中我会使用类似的东西:

from sys import stdin
t=int(stdin.readline())

而不是

t=input() 

有什么办法吗?

2 个答案:

答案 0 :(得分:3)

您的解决方案 - 递归Stein算法

您似乎只是试图获取两个数字的GCD,并希望快速完成。

您显然正在使用Binary GCD Algorithm的递归版本。通常来说,使用迭代算法来提高速度和可扩展性要好得多。但是,我认为首先尝试更简单的Euclidean algorithm几乎肯定是值得的。

替代品 - 迭代斯坦因算法和基本欧几里得算法

我已经调整了你的脚本,将__DATA__块中的3个数字对作为输入。第一对只是两个小数字,然后我有两个来自Fibonacci Sequence的数字,最后是两个更大的数字,包括一些共享的两个数。

然后,我编写了两个新的子程序。其中一个使用迭代斯坦因算法(你使用的方法),另一个只是一个简单的欧几里德算法。 Benchmarking你的partia子程序与我的两个子程序进行了100万次迭代,报告迭代速度提高了50%,而且Euclid 快了3倍

use strict;
use warnings;

use Benchmark;
#use Math::Prime::Util::GMP qw(gcd);

# Original solution
# - Stein's Algorithm (recursive)
sub partia {
    my ( $u, $v ) = @_;
    if ( $u == $v ) { return $u }
    if ( $u == 0 )  { return $v }
    if ( $v == 0 )  { return $u }
    if ( ~$u & 1 ) {
        if ( $v & 1 ) {
            return partia( $u >> 1, $v );
        }
        else {
            return partia( $u >> 1, $v >> 1 ) << 1;
        }
    }

    if ( ~$v & 1 ) {
        return partia( $u, $v >> 1 );
    }

    if ( $u > $v ) {
        return partia( ( $u - $v ) >> 1, $v );
    }
    return partia( ( $v - $u ) >> 1, $u );
}

# Using Euclidian Algorithm
sub euclid {
    my ( $quotient, $divisor ) = @_;

    return $divisor if $quotient == 0;
    return $quotient if $divisor == 0;

    while () {
        my $remainder = $quotient % $divisor;
        return $divisor if $remainder == 0;
        $quotient = $divisor;
        $divisor = $remainder;
    }
}

# Stein's Algorithm (Iterative)
sub stein {
    my ($u, $v) = @_;

    # GCD(0,v) == v; GCD(u,0) == u, GCD(0,0) == 0
    return $v if $u == 0;
    return $u if $v == 0;

    # Remove all powers of 2 shared by U and V
    my $twos = 0;
    while ((($u | $v) & 1) == 0) {
        $u >>= 1;
        $v >>= 1;
        ++$twos;
    }

    # Remove Extra powers of 2 from U.  From here on, U is always odd.
    $u >>= 1 while ($u & 1) == 0;

    do {
        # Remove all factors of 2 in V -- they are not common 
        # Note: V is not zero, so while will terminate
        $v >>= 1 while ($v & 1) == 0;

        # Now U and V are both odd. Swap if necessary so U <= V,
        # then set V = V - U (which is even). For bignums, the
        # swapping is just pointer movement, and the subtraction
        # can be done in-place.
        ($u, $v) = ($v, $u) if $u > $v;

        $v -= $u;
    } while ($v != 0);

    return $u << $twos;
}

# Process 3 pairs of numbers
my @nums;
while (<DATA>) {
    my ($num1, $num2) = split;
#    print "Numbers = $num1, $num2\n";
#    print ' partia = ', partia($num1, $num2), "\n";
#    print ' euclid = ', euclid($num1, $num2), "\n";
#    print ' stein  = ', stein($num1, $num2), "\n";
#    print ' gcd    = ', gcd($num1, $num2), "\n\n";
    push @nums, [$num1, $num2];
}

# Benchmark!
timethese(1_000_000, {
    'Partia' => sub { partia(@$_) for @nums },
    'Euclid' => sub { euclid(@$_) for @nums },
    'Stein'  => sub { stein(@$_) for @nums },
#    'GCD'    => sub { gcd(@$_) for @nums },
});

__DATA__
20 25            # GCD of 5
89 144           # GCD of Fibonacci numbers = 1
4789084 957196   # GCD of 388 = 97 * 2 * 2

输出:

Benchmark: timing 1000000 iterations of Euclid, Partia, Stein...
    Euclid:  9 wallclock secs ( 8.31 usr +  0.00 sys =  8.31 CPU) @ 120279.05/s (n=1000000)
    Partia: 26 wallclock secs (26.00 usr +  0.00 sys = 26.00 CPU) @ 38454.14/s (n=1000000)
     Stein: 18 wallclock secs (17.36 usr +  0.01 sys = 17.38 CPU) @ 57544.02/s (n=1000000)

模块解决方案 - Math :: Prime :: Util :: GMP qw(gcd)

最快的解决方案可能是这些算法的C实现。因此,我建议找到Math::Prime::Util::GMP提供的已编码版本。

包括这个新函数在内的运行基准测试表明它的速度是我编程的基本欧几里德算法的两倍:

Benchmark: timing 1000000 iterations of Euclid, GCD, Partia, Stein...
    Euclid:  8 wallclock secs ( 8.32 usr +  0.00 sys =  8.32 CPU) @ 120264.58/s (n=1000000)
       GCD:  3 wallclock secs ( 3.93 usr +  0.00 sys =  3.93 CPU) @ 254388.20/s (n=1000000)
    Partia: 26 wallclock secs (25.94 usr +  0.00 sys = 25.94 CPU) @ 38546.04/s (n=1000000)
     Stein: 18 wallclock secs (17.55 usr +  0.00 sys = 17.55 CPU) @ 56976.81/s (n=1000000)

答案 1 :(得分:1)

除非我完全忘记了我正在做的事情(没有承诺) - 这个算法看起来在每个递归中保持将它的术语除以2,这意味着你的算法是O(log-base2-N)。除非你能找到一个恒定时间算法,否则你现在可能已经找到了最好的算法。

现在@ikegami已经提到了微优化...如果你想制作它们,我建议你查看Devel::NYTProf一个很棒的Perl剖析器应该可以告诉你你在哪里花时间在您的算法中,您可以针对您的微观定位。