五十六、从高中碾转相除法、更相减损术算法谈起

1,033 阅读6分钟

@Author:Runsen

编程的本质来源于算法,而算法的本质来源于数学,编程只不过将数学题代码化。 ---- Runsen

先问你们一个小学问题:如何求两个整数的最大公约数?

曾经见过不少的算法题,发现有的并不在数据结构和算法大纲中,而是来源于高中数学。

高中数学在必修三中,有一个非常重要的知识点,叫做碾转相除法、更相减损术

辗转相除法, 又名欧几里德算法(Euclidean algorithm)乃求两个正整数之最大公因子的算法。它是已知最古老的算法, 其可追溯至公元前300年前。

在古代,有一个比较出名的数学家,叫做刘徽。而更相减损术是我国数学家刘徽的专著《九章算术》中记载的.

碾转相除法

辗转相除是求最大公约数的一种算法。给两个数,我们可以把它组成数对(a,b)

辗转相除法基于如下原理:两个整数的最大公约数等于其中较小的数和两数的相除余数的最大公约数。

求a和b的最大公约数,就用ab中较小的数去除另一个数,这个时候会有一个余数,当余数是0的时候,那个较小的数就是最大公约数。

若余数不是0,那么我们用这个余数来替换那个比较大的数,然后以此类推,直到算出最大公约数。

比如,下面我用碾转相除法求100和24的最大公约数,很明显最大公约数就是25。

100 = 24 * 4  + 4
24  =  4 * 6  + 0 

很显然46中,那个较小的数4就是10024最大公约数。

下面用碾转相除法求55和120的最大公约数,很明显最大公约数就是5。

55 = 120 * 0  + 55
120  =  55 * 2  + 10
55 = 10 * 5 + 5

很显然105中,那个较小的数5就是55120最大公约数。

算法的流程图(摘自百度百科)

因此得到设两数为m,n,这里不需要判断两数中谁最大。

求m,n两数的最大公约数的步骤为:

用m除以n,m%n=r(r>=0)。如果r=0,则min(m,n)

如果r≠0,用n除以r,依此循环,直到r=0结束

下面,我们将使用对碾转相除法进行代码化。

def gcd(a, b):
    # 如果b是0,退出循环
    while b:
        # 循环赋值
        a, b = b, a%b
    return a
print(gcd(100,25)) #25

辗转相除法本质上是一种递归的代码,把求两个大数的公约数gcd(a,b)转化为 求其中较小的数和两数的相除余数的最大公约数gcd(b,a%b),直至b为0,则返回a为求得的最大公约数gcd(gcd(a,b), 0)

因此可以得到:gcd(a,b) = gcd(b,a%b) = gcd(gcd(a,b), 0)

def gcd(a, b): 
    return gcd(b, a % b) if b != 0 else a
    
print(gcd(55,120)) #5

下面对Python代码进行Java的代码转化

/**
 * @author Runsen
 * @date 2020/12/9 13:18
 */
public class Gcd {
    public static void main(String[] args) {
        int gcd = gcd(91, 49);
        System.out.println(gcd);
    }

    private static int gcd(int a, int b) {
        while(b != 0) {
            int temp = a % b;
            a = b;
            b = temp;
        }
        return a;
    }
}

下面对Python代码进行JavaScript的代码转化。

function gcd(a, b){
    while(b != 0){
       temp = a % b;
       a = b;
       b = temp;
    };
    return a;
}
     
console.log((gcd(55,120))) #5

更相减损术

我国早期也有求最大公约数问题的算法,就是更相减损术。

在《九章算术》中有更相减损术求最大公约数的步骤:可半者半之,不可半者,副置分母子之数,以少减多,更相减损,求其等也,以等数约之。

更相减损术来源于数的整除性质:即如果两个整数a、b都能被c整除,那么a与b的差也能被C整除。

比如求98和63的最大公约数。

先看98和63这两个数,因为63不是偶数,所以用大数减去小数,得到98-63=35 , 63-35=28 35-28=7 , 28-7=21 , 21-7=14 , 14-7=7

此时,减数和差相等7,所以98和63的最大公约数是7。

再比如求260和104的最大公约数。

先看260和104两个数,这两个数都是偶数,所以用2约简得130和52。

约简之后的130和52也都是偶数,继续用2约简得65和26,此时65不是偶数,所以用大数减去小数 65-26=39 , 39-26=13 , 26-13=13

此时,减数和差相等,再上面约去2个2, 得到的数是13,所以260和104的最大公约数是2×2×13=52。

因此更相减损术不在如下:

  • 如果两个整数都是偶数,就使用2约简,直到两个整数不再都是偶数,然后执行第2步。如果两个整数不都是偶数,则直接执行第2步。
  • 用较大的数减去较小的数,如果得到的差恰好等于较小的数,则停止。否则,对较小的数和差值重复这个过程。
  • 第1步中约掉的若干个2和第2步中得到的差的乘积为原来两个整数的最大公约数。

下面,我们将使用对更相减损术进行代码化。

'''
@Author: Runsen
@WeChat:RunsenLiu 
@微信公众号: Python之王
@CSDN: https://blog.csdn.net/weixin_44510615
@Github: https://github.com/MaoliRUNsen
@Date: 2020/12/9
'''
def MaxCommDivisor(m, n):
    # 如果两个整数都是偶数,就使用2约简,需要记录约简次数
    index = 1
    while m % 2 == 0 and n % 2 == 0:
        m = m / 2
        n = n / 2
        index = index * 2
    # 用较大的数减去较小的数,因此需要判断m和n的大小,确保m是最大的。
    if m < n:
        m, n = n, m
    # 用较大的数减去较小的数,如果得到的差恰好等于较小的数,则停止。否则,对较小的数和差值重复这个过程。
    while m - n != n:
        diff = m - n
        if diff > n:
            m = diff
        else:
            m = n
            n = diff
    return n * index

print(MaxCommDivisor(24, 12)) #12

更相减损术和辗转相除法在一千多年前的东方和西方同时被提出,这说明天才的想法总是惊人的相似,人类科技文明的进程也是同步的,这就是算法之美。

本文已收录 GitHub,传送门~ ,里面更有大厂面试完整考点,欢迎 Star。