现在的位置: 首页 > 综合 > 正文

《编程之美》学习笔记——2.7最大公约数问题

2018年03月30日 ⁄ 综合 ⁄ 共 5847字 ⁄ 字号 评论关闭

一、问题

  求两个正整数的最大公约数。如果两个正整数都很大,有什么简单的算法吗?

问题分析:

什么是最大公约数? (参考维基百科:http://en.wikipedia.org/wiki/Greatest_common_divisor)

最大公约数Greatest Common Divisor (GCD); greatest common factor (gcf);highest common factor (hcf);greatest common measure (gcm);highest common divisor (hcd)

In mathematics, the greatest common divisor (gcd) of two or more integers, when at least one of them is not zero, is the largest positive integer that divides the numbers without a remainder.

符号:gcd(a,b)

例子:

The number 54 can be expressed as a product of two integers in several different ways:
54 * 1 = 27 * 2 = 18 * 3 = 9 * 6
Thus the divisors of 54 are:
1, 2, 3, 6, 9, 18, 27, 54.
Similarly the divisors of 24 are:
1, 2, 3, 4, 6, 8, 12, 24.
The numbers that these two lists share in common are the common divisors of 54 and 24:
1, 2, 3, 6.
The greatest of these is 6. That is the greatest common divisor of 54 and 24. One writes:
gcd(54,24) = 6.

拓展:什么是最小公倍数?(参考维基百科:http://en.wikipedia.org/wiki/Least_common_multiple)

最小公倍数lowest common multiple(LCM);least common multiple;smallest common multiple

In arithmetic and number theory, the least common multiple of two integers a and b, usually denoted by LCM(a, b), is the smallest positive integer that is divisible by both a and b.Since division of integers by zero is undefined,
this definition has meaning only if a and b are both different from zero.

符号:lcm(a,b)

例子:

What is the LCM of 4 and 6?
Multiples of 4 are:
4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, ...
and the multiples of 6 are:
6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, ...
Common multiples of 4 and 6 are simply the numbers that are in both lists:
12, 24, 36, 48, 60, 72, ....
So, from this list of the first few common multiples of the numbers 4 and 6, their least common multiple is 12.

最大公约数和最小公倍数的关系?

If a and b are both nonzero, the greatest common divisor of a and b can be computed by using least common multiple (lcm) of a and b:

通过上面这个公式,我们在求两个数的最小公倍数时,可以先求出这两个数的最大公约数,再利用上面的公式求出它们的最小公倍数。

二、解法

版本一:辗转相除法 / 欧几里德算法 —— 求余

算法原理:两个整数的最大公约数等于其中较小的数和两数相除余数的最大公约数。(gcd(a, b) = gcd(b, a mod b),可自行证明)通过这个方法,把两个整数不断缩小至其中一个为零,剩下没变成零的数就是两个数的最大公约数。

  算法实现我们考虑用较小的数和两数相除余数作为两个新的数求最大公约数(主要利用了求模运算)。

分析:当两个整数比较大时,采用求模运算(调用除法),是非常昂贵的开销。

算法C实现如下:(需要考虑的有:第一步循环中算法会从两个数中获取较小的数赋值给n,因此不需要提前判断m和n的大小,这部分原来是有加的,后面进行分析并把它优化掉了;while语句循环直到两个数其中一个为零,另一个非零数即为解;除数不能为零)

/**
 * @brief get greatest common divisor(gcd) for value m and n.
 *
 * @param[in]      m  first value
 * @param[in]      n  second value
 *
 * @return gcd value
 */
TYPE gcd(TYPE m, TYPE n)
{
    /// m and n range limits
    assert(m >= 0 && n >= 0);
    /// euclidean algorithm
    TYPE temp;
    while (n != 0) {
        temp = m % n;
        m = n;
        n = temp;
    }
    return m;
}

版本二:更相减损法(术)  —— 做差

  算法原理:两个整数的最大公约数等于其中较小的数和两数相差的最大公约数。这里利用的是两数相差,而不是辗转相除法的两数求余,但两者实际非常相似。

  我们考虑用较小的数和两数相差作为两个新的数求最大公约数(主要利用了减法运算)。

更相减损法与辗转相除法对比:对于大整数而言,相比辗转相除法的两数求余运算,单次时减法运算效率会更高。但两个正整数相差较大时,减法运算的执行次数要必求余运算执行次数多很多。

算法C实现如下(这里做了下优化,内嵌一个while循环用求两数相差时,多次做差,使尽可能减少大while循环执行次数):

/**
 * @brief get greatest common divisor(gcd) for value m and n.
 *
 * @param[in]      m  first value
 * @param[in]      n  second value
 *
 * @return gcd value
 */
TYPE gcd(TYPE m, TYPE n)
{
    /// m and n range limits
    assert(m >= 0 && n >= 0);
    /// subtraction
    while (m != n) {
        while (m > n)
            m -= n;
        temp = m;
        m = n;
        n = temp;
    }
    TYPE temp;
    return m;
}


版本三:辗转相除法 / 欧几里德算法 ——递归求余 (来自《编程之美》)

  这个版本与版本一都是用辗转相除法,原理中含有递归的思想,所以可以通过递归算法实现,这时算法实现必版本一算法实现简单得多。

算法C实现:

/**
 * @brief get greatest common divisor(gcd) for value m and n.
 *
 * @param[in]      m  first value
 * @param[in]      n  second value
 *
 * @return gcd value
 */
TYPE gcd(TYPE m, TYPE n)
{
    /// m and n range limits
    assert(m >= 0 && n >= 0);
    /// euclidean algorithm by recursion
    return (!n) ? m : gcd(n, m % n);
}

版本四:更相减损法(术) ——递归做差 (来自《编程之美》)

  这个版本与版本二都是用更相减损法(术),原理中含有递归的思想,所以可以通过递归算法实现,这时算法实现必版本二算法实现简单得多。

算法C实现:

/**
 * @brief get greatest common divisor(gcd) for value m and n.
 *
 * @param[in]      m  first value
 * @param[in]      n  second value
 *
 * @return gcd value
 */
TYPE gcd(TYPE m, TYPE n)
{
    /// m and n range limits
    assert(m >= 0 && n >= 0);
    if (m < n)
        return gcd(n, m);
    else if (n == 0)
        return m;
    else
        return gcd(m - n, n);
}

版本五: Stein's algorithm(参考维基百科及《编程之美》)

原理:

1.如果y=k*y1,x=k*x1。那么有f(x,y) = k*f(x1,y1);

2.如果x = p*x1,假设p是素数,并且y%p != 0,那么f(x,y)=f(p*x1,y)=f(x1,y)。

  由于除2和乘2运算都可以采用移位运算,同时我们已知2是质数,结合上面的两个结论,我们可以引出这种新的方法:

对于gcd(a,b)来说:

Both a and b are even.
In this case 2 is a common factor. Divide both a and b by 2, double d, and continue.
a is even and b is odd.
In this case 2 is not a common factor. Divide a by 2 and continue.
a is odd and b is even.
Like the previous case 2 is not a common factor. Divide b by 2 and continue.
Both a and b are odd.
Without loss of generality, assume that for a and b as they are now, a ≥ b. In this case let c = (a − b)/2. Then gcd(a,b) = gcd(a,c) = gcd(b,c). Because b ≤ a it is usually easier (and computationally faster) to determine gcd(b,c). If computing this algorithm
by hand, gcd(b,c) may be apparent. Otherwise continue the algorithm until c = 0. Note that the gcd of the original a and b is still d times larger than the gcd of the odd a and odd b above.

分析:这个算法既避免了辗转相除法中求余运算开销,也避免了更相减损法中较多的减法执行次数,增加了判断次数,但是使用的与运算和移位运算效率都比较高。算法通过移位运算来代替乘2和除2运算(拓展的,可利用与运算来实现除2求余运算)。

算法C实现如下:(这里判断数字奇偶性用与操作,通过判断数字二进制表示时最低位时1还是0来判断数字是奇数还是偶数;同时使用移位运算来代替乘2和除2运算;算法以递归的方式实现)

/**
 * @brief get greatest common divisor(gcd) for value m and n.
 *
 * @param[in]      m  first value
 * @param[in]      n  second value
 *
 * @return gcd value
 */
TYPE gcd(TYPE m, TYPE n)
{
    /// m and n range limits
    assert(m >= 0 && n >= 0);
    if (m < n)
        return gcd(n, m);
    else if (n == 0)
        return m;
    if (m & 1) {
        if (n & 1)
            return gcd(n, (m - n) >> 1);
        else
            return gcd(m, n >> 1);
    } else {
        if (n & 1)
            return gcd(m >> 1, n);
        else
            return gcd(m >> 1, n >> 1) << 1;
    }
}

版本六: Stein's algorithm(非递归)

算法原理与版本五算法一致,版本六采用了非递归方式的实现,减少了递归调用的开销。

算法C实现:

/**
 * @brief get greatest common divisor(gcd) for value m and n.
 *
 * @param[in]      m  first value
 * @param[in]      n  second value
 *
 * @return gcd value
 */
TYPE gcd(TYPE m, TYPE n)
{
    /// m and n range limits
    assert(m >= 0 && n >= 0);
    /// binary method
    TYPE temp;
    TYPE shift_value = 0;
    while (n != 0) {
        if (m < n) {
            temp = m;
            m = n;
            n = temp;
        }
        if (m & 1) {
            if (n & 1) {
                temp = (m - n) >> 1;
                m = n;
                n = temp;
            } else {
                n >>= 1;
            }
        } else {
            if (n & 1) {
                m >>= 1;
            } else {
                shift_value++;
                m >>= 1;
                n >>= 1;
            }
        }
    }
    return m << shift_value;
}

其他方法:

   穷举法(暴力破解法):可以利用最大公约数的定义,初始化一个和这两个数中较小的数相同的数,不断对它进行减操作并判断这两个数能否都被它整除(求余为零)来找到最大公约数。这个方法也适用于最小公倍数。

   因质分解法:分解并记录两个数共有的因数,最后这些因数的乘积便是最大公约数。

   扩展欧几里德算法(见于维基百科)。

拓展:

  求解多个数的最大公约数及最小公倍数时,可把上面的算法拓展到多个数的实现。

总结:

  最小公倍数可以利用公式和最大公约数来求得,但需注意对于两个同样为零的数,最小公倍数是没有定义的。

  递归算法需要考虑递归调用的开销,但是实现代码相对简洁。

  Stein算法巧妙利用了求解最小公约数过程中的一些性质,结合移位运算高效实现。

  这些算法都指定了输入为正整数或零,实际上要考虑下负整数的情况,要注意的是负数求余在不同的编译器上实现很有可能不一样(可移植性问题)。

备注:

1.转载请注明出处:http://blog.csdn.net/chensilly8888/

2.全文源码均开源(在UBUNTU + GCC4.8.2下编译并测试通过),可下载或查看:https://github.com/chenxilinsidney/funnycprogram/tree/master/beauty_of_programming/gcd_lcm

3.有写错或写漏之处请指正,谢谢!

抱歉!评论已关闭.