【每日算法】 NC151 最大公约数

342 阅读3分钟

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

描述

如果有一个自然数 a 能被自然数 b 整除,则称 a 为 b 的倍数, b 为 a 的约数。几个自然数公有的约数,叫做这几个自然数的公约数。公约数中最大的一个公约数,称为这几个自然数的最大公约数。

输入 a 和 b , 请返回 a 和 b 的最大公约数。

数据范围:1≤ a,b ≤10^9

进阶:空间复杂度 O(1),时间复杂度 O(logn)。

暴力 for 循环

最简单的一种做法了,直接循环i++去找出两个数能够同时整除的数,然后一直循环到 i 等于最小的那个数。

/**
* 暴力 for 循环
* @param a
* @param b
* @return
*/
public int gcd (int a, int b) {
   // write code here
   int min=a>b?a:b;
   int commonDivisor=1;
   for (int i=1;i<=min;i++){
       if (a%i==0&&b%i==0){
           commonDivisor=i;
       }
   }
   return commonDivisor;
}

上代码,十分简单粗暴是不?不过这可在牛客上不能通过。

当然我们今天不可能这么简单就结束了的。

改良

写完暴力 for 循环,我的第一个想法就是改良,在暴力循环的基础上改良一下,当然我也知道网上有很多非常好的方案,可能会用到各种公式,不过我还是想先看看自己能做到什么程度。

我觉得 1-100 中,最多的就是 2 的倍数,所以可以考虑先把 2 的倍数求出来,然后再去求两数的公约数,这样就能减少一定的运算时间。

public int gcd1 (int a, int b) {
    int doubleCommonDivisor=1;
    while (a%2==0&& b%2==0){
        doubleCommonDivisor*=2;
        a=a/2;
        b=b/2;
    }
    int min=a>b?a:b;
    int commonDivisor=1;

    for (int i=1;i<=min;i++){
        if (a%i==0&&b%i==0){
            commonDivisor=i;
        }
    }
    return commonDivisor*doubleCommonDivisor;
}

运行!

image.png

运行通过了,不过时间上比较长,内存上还不错。

除了 2 的数,基本上都不太好单独把倍数取出来,这种思路已经是穷途末路了。

辗转相除法

从大佬那里学了更加🐂的算法。

就是一直循环拿大的数去减小的数,然后拿差和小的数继续往下减,一直减到有一个数等于0,就说明最大公约数出来了。

比如说,10 和 30,第一遍 30-10=20;第二遍 20-10=10;第三遍 10-10=0;这时就得出了小的数 10 和差 0,这个小的数就是我们要的最大公约数。

如果是公约数为 1 的例子,17 和 30,第一遍 30-17=13;第二遍 17-13=4;第三遍 13-4=9;第四遍 9-4=5;第五遍 5-4=1;后面就是都减 1 了,知道有一个数等于 0,然后返回 1;

正反例都推理正确。

/**
 * 辗转相减法
 * @param a
 * @param b
 * @return
 */
public int gcd2 (int a, int b) {
    if (b==0) {
        //因为我们把小的数放在了后面,所以只需要看 b 是不是等于 0,当 b 等于 0 时,a
        return a ;
    }
    return a>b?gcd2(b,a-b):gcd2(a,b-a);
}

运行!

image.png

运行时间快了好几十倍了都,占用内存也没多多少,看来暴力 for 循环是真滴辣鸡。

辗转相除法

比辗转相减法更牛皮的是辗转相除法,效率更快,更加稳定。

辗转相减法如果在极端情况下(也就是有一个差等于 1 的情况),还是很容易达到跟 for 循环一样的运行时间。

辗转相除法可以说跟辗转相减法一毛一样,就是把减法改成了求余。

就是一直循环拿大的数去除小的数,然后拿余数和小的数继续往下除,一直除到有一个数等于0,就说明最大公约数出来了。

继续用 10 和 30 的例子,第一遍 30%10=0;一遍结果就出来了,10。非常快速。

如果是公约数为 1 的例子,17 和 30,第一遍 30%17=13;第二遍 17%13=4;第三遍 13%4=1;第四遍 4%1=0;第五遍,有一个数等于 0,返回最大公约数 1。

正反两例都比辗转相减法的效率都高。

敲码!

/**
 * 辗转相除法
 * @param a
 * @param b
 * @return
 */
public int gcd3 (int a, int b) {
    if (b==0) {
        //因为我们把小的数放在了后面,所以只需要看 b 是不是等于 0,当 b 等于 0 时,a
        return a ;
    }
    return a>b?gcd3(b,a%b):gcd3(a,b%a);
}

运行!

image.png

np!运行时间和占用内存没得说。

最后

感觉求最大公约数这道题对数学的要求比较高,仅靠自己的编码常识很难达到这道题的最优解。

总结出一个规律:

能不要遍历就不要遍历,能用乘除法就不要用加减法。

今天就到这里了。

这里是程序员徐小白,【每日算法】是我新开的一个专栏,在这里主要记录我学习算法的日常,也希望我能够坚持每日学习算法,不知道这样的文章风格您是否喜欢,不要吝啬您免费的赞,您的点赞、收藏以及评论都是我下班后坚持更文的动力。