多角度聊聊最大公约数:从粗糙到优雅

引言

前几天看了《小灰的算法之旅》的最大公约数部分,看完只晓得说“妙啊!”,然后就没然后了

今天汇编老师突然在群里发题目,让我们用x86汇编写个最大公约数,我突然一楞

意识到问题严重性的我打算好好深入了解一下,吓得我找了波李永乐老师

(此文代码演示使用C++)

引入——图解法

假设有一个长为104,宽为40的长方形,我们要找一个长度为a(a为整数)的正方形,将这个长方形铺满,那么a=?

(其实本质就是在求104和40的最大公约数)

在这里插入图片描述

按宽(即小的一边)来切:40、40、24、16、8、8,刚好剩下2个边长相等的正方形就是了

熟悉吗xdm?这不就是大名鼎鼎的《九章算术》中的更相减损术吗?

更相减损术

还是上面那道题,演算一下好吧:

104 - 40 = 64
64 - 40 = 24
40 - 24 = 16
24 - 16 = 8
16 - 8 = 8
8 - 8 = 0

剩了两个8,得出结果8,easy的,我们看看代码

#include <iostream>
using namespace std; 

int gcd(int a,int b){
	if(a == b){
		return a;
	}else{
		int gmax = a > b ? a : b;
		int gmin = a < b ? a : b;
		return gcd(gmin,gmax-gmin);
	}
} 

int main(){
	
	int a=104,b=40;
	cout << gcd(a,b) << endl;
	
	return 0;
}

假如说我要减少一下运行次数呢,比如我不想减两次40,万一我是4020和40,那我搁儿那减100下?

这时候就不得不亮求最大公约数的升级版算法了——辗转相除法

辗转相除法

也叫欧几里得算法,听着就贼吊对吧,其实就是把“-”升级成“%”,得到了余数为0的那个除数就是了

还是那题,演算一遍:

104 % 40 = 24
40 % 24 = 16
24 % 16 = 8
16 % 8 = 0

不多bb,直接看代码

#include <iostream>
using namespace std; 

int gcd(int a,int b){
	int gmax = a > b ? a : b;
	int gmin = a < b ? a : b;
	if(a % b == 0){
		return gmin;
	}	
	return gcd(gmin,gmax%gmin);
} 

int main(){
	
	int a=104,b=40;
	cout << gcd(a,b) << endl;
	
	return 0;
}

讲道理,其实差不多,微改一下而已

虽然我们可能减少了运行的次数,但是取模的性能是比较抱歉的,所以反而更相减损在计算机里面用会更厉害一点,只是我们人类手算,用辗转相除会友好一点而已

所以在写代码时候要二选一,还是建议更相减损

但这还没完,如果你要用更相减损的话,你有没有发现这其中存在一个问题?

不够优雅?算死!

对于更相减损来说,如果说我是求1000000000和1的最大公约数呢?

减到你xxx,算s你吧xd

发现它的不稳定了没?

那咋办?还能优化吗???可以!

我们的思路就是:

  • 避免大整数取模
  • 尽可能减少运算次数

所以,我们将会在更相减损的基础上增加移位运算,就是不要楞头减

更相减损 + 位运算 = 优雅解出gcd

取模看起来是快,但是实则性能不佳,什么佳?位运算。

方法论:(假设求a和b的gcd)

(1)a、b均为偶:
gcd(a,b) = 2 * gcd(a/2,b/2) 
(2)a为偶,b为奇:
gcd(a,b) = gcd(a/2,b) 
(3)a为奇,b为偶:
gcd(a,b) = gcd(a,b/2) 
(4)a、b均为奇:(如果b>a-b)
gcd(a,b) = gcd(b,a-b) 
这时a-b肯定是偶数,就可以继续位运算了
----------------------------------------
以上所有的“/2”或者“*2”操作,均改为“>>1”或者“<<1”位运算模式,上面这么写方便表达而已
判断奇偶就直接和1相与就好,别忘了位运算都加上括号(位运算优先级淦低)

来演算一遍嘛

gcd(104,40) = 2 * gcd(52,20)
			= 4 * gcd(26,10)
			= 8 * gcd(13,5)
			= 8 * gcd(8,5)
			= 8 * gcd(5,4)
			= 8 * gcd(5,2)
			=...
			= 8 

get了吗?代码写起来也很快的

#include <iostream>
using namespace std; 

int gcd(int a,int b){
	if(a == b){
		return a;
	}
	if((a&1)==0 && (b&1)==0){
		return gcd((a >> 1),(b >> 1)) << 1;
	}
	else if((a&1)==1 && (b&1)==0){
		return gcd(a,(b >> 1));
	}
	else if((a&1)==0 && (b&1)==1){
		return gcd((a >> 1),b);
	}
	else{
		int gmax = a > b ? a : b;
		int gmin = a < b ? a : b;
		return gcd(gmax-gmin,gmin);
	}
} 

int main(){
	
	int a=104,b=40;
	cout << gcd(a,b) << endl;
	
	return 0;
}

xdm,把公屏打在优雅上!!!!

比较时间复杂度,看清谁是真的菜

  • 暴力枚举法——O(min(a,b))
  • 辗转相除法——O(log(max(a,b))),其实还行,输在性能上
  • 更相减损术——O(max(a,b)),不稳定的时候是比暴力还菜
  • 更相减损 + 位运算 —— O(log(max(a,b)))

懂自懂!

深入数论,探究本质

别跑啊,研究一下本质啊!

什么?图解法不就是吗???

不不不,要用数学方法证明比较好!

证明

(如果 有a | b ,说明a是b的一个约数)

假设:d | a ,且 d | b ,即d为a、b的公约数,证明辗转相除法的依据

∵d | a ,且 d | b

∴余数r = a - k * b(k为整数) = x1 * d - k * x2 * d = (x1 - k * x2) * d ,且(x1 - k * x2)为整数

∴d | r

∴gcd(a,b) = gcd(b,r),故而得证

最小公倍数

很简单,前面算出来最大公约数了,最小公倍数就是两个数的乘积除以最大公约数。比如:

  • 假设求10,8的最小公倍数。
gcd(10,8) = 2
lcm(10,8) * 2 = 10 * 8
lcm(10,8) = 40

你自己验算一波,看看是不是

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值