快速幂算法——带你从零开始一步一步优化


目录

快速幂算法——带你从零开始一步一步优化

什么是快速幂算法

再次思考

快速幂算法初步入门

压榨性能再优化

终极优化

参考资料

博客文章版权声明


什么是快速幂算法


首先,我们先来看一道ACM程序设计题,这道题是杭电OJ中序号为2035的题目,没做过这道题目的同学可以跟着一起做一下(点击此处传送),题目如下:

问题描述:

这道题目乍一看会觉得并不难啊,题目短短一行而已,而且思路也很容易,求幂这种算法一般在初学程序设计语言的时候应该都有联系过,只要写一个简单的循环就能够搞定。

/*** 普通的求幂函数* @param base 底数* @param power  指数* @return  求幂结果的最后3位数表示的整数*/
long long normalPower(long long base,long long power){long long result=1;for(int i=1;i<=power;i++){result=result*base;}return result%1000;
}

这道题不是分分钟解决吗?接下来,让我们来写一个主函数测试一下:

int main(){long long base,power;cin>>base>>power;cout<<"base="<<base<<" power="<<power<<" "<<normalPower(base,power)<<endl;return 0;}

然后,让我们愉快的来求一下2^100的结果的后三位数表示的整数是什么吧!输出结果如下:

为什么答案会是0呢?明明没有错误的啊!~

先不急,我们再来考虑一下,这道题其实出的很有意思,题目要求你输出结果的后三位,为什么不让你直接输出结果呢?难道仅仅只是为了增大题目的难度吗?当然不是,我们在初中就学过“指数爆炸”,下面我们在来回顾一下“指数”的概念:

指数:在乘方a中,其中的a叫做底数,n叫做指数,结果叫幂。

f(x)=a^x , 随着x单位长度的递增,f(x)会呈“爆炸性”增长。

一张纸对折一次,厚度变成原来的2倍。再对折第二次,变为原来的2的2次方倍即4倍。以此类推,假设纸的厚度为0.1mm,则对折24次以后,长度超过1千米;对折39次达55000千米,超过地球赤道长度;对折42次达44万千米,超过地球至月球的距离;对折51次达22亿千米,超过地球至太阳的距离;对折82次为51113光年,超过银河系半径的长度。

因此,如果题目让你求2的100次方,貌似我们程序设计语言中最大的long lnog类型也无法承载这么大的数值,所以题目才不会要求你输出结果,因为结果可能会非常的大,大到没有任何类型可以承载。所以我们会发现上面的结果为什么是0,因为已经发生溢出了。

那为什么题目要求输出结果的最后三位数表示的整数呢?有的同学可能会问:求一个数的最后三位数表示的整数好办,只要用这个结果进行“取模”运算,让其对1000取模,得到的数就是这个数最后三位数表示的整数。(例如:12345的最后三位数表示的整数是:12345%1000=345)。但是,你这结果都无法求出来,让我怎么进行“取模”运算呢?你这不是瞎闹吗?

别急,我们首先来了解一下“取模”运算的运算法则:(具体的证明感兴趣的同学可以问度娘)

  1. (a + b) % p = (a % p + b % p) % p (1)

  2. (a - b) % p = (a % p - b % p ) % p (2)

  3. (a * b) % p = (a % p * b % p) % p (3)

其中我们只要关注第“3”条法则即可:(a * b) % p = (a % p * b % p) % p ,我们仔细研究一下这个运算法则,会发现多个因子连续的乘积取模的结果等于每个因子取模后的乘积再取模的结果。也就是说,我们如果要求:

(a*b*c)%d=(a%d*b%d*c%d)%d;

因此,我们可以借助这个法则,只需要在循环乘积的每一步都提前进行“取模”运算,而不是等到最后直接对结果“取模”,也能达到同样的效果。

所以,我们的代码可以变成这个样子:

/*** 普通的求幂函数* @param base 底数* @param power  指数* @return  求幂结果的最后3位数表示的整数*/
long long normalPower(long long base,long long power){long long result=1;for(int i=1;i<=power;i++){result=result*base;result=result%1000;}return result%1000;
}

我们再来测试一下,这样又能不能输出结果呢?我们仍然来求一下2^100的后三位是什么:

这一次完美的得到了我们想要的结果。2^100的幂结果的后三位整数位376。

为了打消一些同学对这个运算法则的怀疑,我们再用一个结果比较小的式子来验证一下:我们知道2^10为1024,按理来说,最后输出的结果的后三位数表示的整数应该是24,那么是不是这样呢?我们来试一试:

最后的结果果然是24,所以这个法则是没有问题的。我们把下面的代码提交给OJ看一下是否能通过:

#include <iostream>
#include <cmath>using namespace std;/*** 普通的求幂函数* @param base 底数* @param power  指数* @return  求幂结果的最后3位数表示的整数*/
long long normalPower(long long base, long long power) {long long result = 1;for (int i = 1; i <= power; i++) {result = result * base;result = result % 1000;}return result % 1000;
}int main() {long long base, power;while (true) {cin >> base >> power;if (base == 0 && power == 0) break;cout << normalPower(base, power) << endl;}return 0;}

最后的结果是成功Accept了。

再次思考


虽然这个求幂的方法很有用,并且提交给OJ也直接Accept了,但是我们来考虑一下这个算法的时间复杂度,假设我们求2的100次方,那么将会执行100次循环。如果我们分析一下这个算法,就会发现这个算法的时间复杂度为O(N),其中N为指数。求一下小的结果还好,那如果我们要求2的1000000000次方呢?这个程序可能会运行很久很久,具体会多久呢,让我们来测试一下,测试代码如下:

#include <iostream>
#include <cmath>
#include <time.h>using namespace std;/*** 普通的求幂函数* @param base 底数* @param power  指数* @return  求幂结果的最后3位数表示的整数*/
long long normalPower(long long base, long long power) {long long result = 1;for (int i = 1; i <= power; i++) {result = result * base;result = result % 1000;}return result % 1000;
}int main() {clock_t start, finish;//clock_t为CPU时钟计时单元数long long base, power;cin >> base >> power;start = clock();//clock()函数返回此时CPU时钟计时单元数cout << normalPower(base, power) << endl;finish = clock();//clock()函数返回此时CPU时钟计时单元数cout << "the time cost is" << double(finish - start) / CLOCKS_PER_SEC;//finish与start的差值即为程序运行花费的CPU时钟单元数量,再除每秒CPU有多少个时钟单元,即为程序耗时return 0;}

结果如图所示:

我们发现,虽然结果是成功求出来了,但是用了将近18秒的时间才求出最后的答案。这效率当然是非常的低下的,更谈不上实际的生产应用了。那么有没有什么好的办法能够对其进行优化呢?接下来就是我们本次的主题了:快速幂算法。

快速幂算法初步入门


快速幂算法能帮我们算出指数非常大的幂,传统的求幂算法之所以时间复杂度非常高(为O(指数n)),就是因为当指数n非常大的时候,需要执行的循环操作次数也非常大。所以我们快速幂算法的核心思想就是每一步都把指数分成两半,而相应的底数做平方运算。这样不仅能把非常大的指数给不断变小,所需要执行的循环次数也变小,而最后表示的结果却一直不会变。让我们先来看一个简单的例子:

3^10=3*3*3*3*3*3*3*3*3*3

//尽量想办法把指数变小来,这里的指数为10

3^10=(3*3)*(3*3)*(3*3)*(3*3)*(3*3)

3^10=(3*3)^5

3^10=9^5

//此时指数由10缩减一半变成了5,而底数变成了原来的平方,求3^10原本需要执行10次循环操作,求9^5却只需要执行5次循环操作,但是3^10却等于9^5,我们用一次(底数做平方操作)的操作减少了原本一半的循环量,特别是在幂特别大的时候效果非常好,例如2^10000=4^5000,底数只是做了一个小小的平方操作,而指数就从10000变成了5000,减少了5000次的循环操作。

//现在我们的问题是如何把指数5变成原来的一半,5是一个奇数,5的一半是2.5,但是我们知道,指数不能为小数,因此我们不能这么简单粗暴的直接执行5/2,然而,这里还有另一种方法能表示9^5

9^5=(9^4)*(9^1)

//此时我们抽出了一个底数的一次方,这里即为9^1,这个9^1我们先单独移出来,剩下的9^4又能够在执行“缩指数”操作了,把指数缩小一半,底数执行平方操作

9^5=(81^2)*(9^1)

//把指数缩小一半,底数执行平方操作

9^5=(6561^1)*(9^1)

//此时,我们发现指数又变成了一个奇数1,按照上面对指数为奇数的操作方法,应该抽出了一个底数的一次方,这里即为6561^1,这个6561^1我们先单独移出来,但是此时指数却变成了0,也就意味着我们无法再进行“缩指数”操作了。

9^5=(6561^0)*(9^1)*(6561^1)=1*(9^1)*(6561^1)=(9^1)*(6561^1)=9*6561=59049

我们能够发现,最后的结果是9*6561,而9是怎么产生的?是不是当指数为奇数5时,此时底数为9。那6561又是怎么产生的呢?是不是当指数为奇数1时,此时的底数为6561。所以我们能发现一个规律:最后求出的幂结果实际上就是在变化过程中所有当指数为奇数时底数的乘积。

让我们来看一段简单的动画演示(点击放大):

接下来,再让我们用代码来演示一下上面的算法:

long long fastPower(long long base, long long power) {long long result = 1;while (power > 0) {if (power % 2 == 0) {//如果指数为偶数power = power / 2;//把指数缩小为一半base = base * base % 1000;//底数变大成原来的平方} else {//如果指数为奇数power = power - 1;//把指数减去1,使其变成一个偶数result = result * base % 1000;//此时记得要把指数为奇数时分离出来的底数的一次方收集好power = power / 2;//此时指数为偶数,可以继续执行操作base = base * base % 1000;}}return result;
}

我们再来测试一下此时的快速幂算法和普通的求幂算法的效率,我们仍然来求2的1000000000次方,看一看用时又会是多少:

真让人简直不可思议,竟然只花了0.002秒就求出了结果,而且结果也是376,然而普通的算法却用了将近18秒的时间才求出最后的结果。

压榨性能再优化


虽然上面的快速幂算法效率已经很高了,但是我们仍然能够再一次的对其进行“压榨级别”的优化。我们上面的代码看起来仍然有些地方可以再进一步地进行简化,例如在if和else代码块中仍然有重复性的代码:

            power = power / 2;base = base * base % 1000;

            power = power - 1;//把指数减去1,使其变成一个偶数power = power / 2;
可以压缩成一句:
            power = power / 2;

因为power是一个整数,例如当power是奇数5时,power-1=4,power/2=2;而如果我们直接用power/2=5/2=2。在整型运算中得到的结果是一样的,因此,我们的代码可以压缩成下面这样:

long long fastPower(long long base, long long power) {long long result = 1;while (power > 0) {if (power % 2 == 1) {result = result * base % 1000;}power = power / 2;base = (base * base) % 1000;}return result;
}

接下来,我们来测试一下优化后的性能如何,仍然是求2的1000000000次方:

结果仍然是正确的376,但时间上的花费从0.002减少成了0.001。

终极优化


在C语言中,power%2==1可以用更快的“位运算”来代替,例如:power&1。因为如果power为偶数,则其二进制表示的最后一位一定是0;如果power是奇数,则其二进制表示的最后一位一定是1。将他们分别与1的二进制做“与”运算,得到的就是power二进制最后一位的数字了,是0则为偶数,是1则为奇数。例如5是奇数,则5&1=1;而6是偶数,则6&1=0;因此奇偶数的判断就可以用“位运算”来替换了。

同样,对于power=power/2来说,也可以用更快的“位运算”进行替代,我们只要把power的二进制表示向右移动1位就能变成原来的一半了。

最后,我们的代码就能优化成下面这样:
long long fastPower(long long base, long long power) {long long result = 1;while (power > 0) {if (power & 1) {//此处等价于if(power%2==1)result = result * base % 1000;}power >>= 1;//此处等价于power=power/2base = (base * base) % 1000;}return result;
}

我们仍然测试一下求2的1000000000次方,看看终极优化后的代码的性能是怎样的:

简直可怕,时间花费竟然接近于0秒,我们从最开始的18秒最后压缩到接近0秒,真的是感慨算法的威力!如果同样两家公司,采用不同的算法,给用户带来的体验区别是非常大的,这无不让我们感受到算法的威力。

基础不牢?新手不友好?无人带路?关注《扬俊的小屋》公众号吧!


参考资料


【1】https://www.rookieslab.com/posts/fast-power-algorithm-exponentiation-by-squaring-cpp-python-implementation#brute-force-python-implementation  作者:Ravi Ojha 翻译:刘扬俊

【2】百度百科——指数爆炸

https://baike.baidu.com/item/%E6%8C%87%E6%95%B0%E7%88%86%E7%82%B8/8440078?fr=aladdin

博客文章版权声明


快速幂算法(全网最详细地带你从零开始一步一步优化)相关推荐

  1. 快速幂算法及其在动态规划中的应用(矩阵幂)

    文章目录 一.快速幂算法引入 二.快速幂算法 三.快速幂算法在矩阵幂中的运算 四.LeetCode1220的快速幂做法 五.参考资料 一.快速幂算法引入   假如我们有一个需要求2^100的后三位的问 ...

  2. 快速幂算法 超详细教程

    快速幂 求幂运算 快速幂引入 快速幂 二进制 快速幂 指数折半 快速幂的应用 求幂运算 求幂运算大家都不陌生,幂是指数运算的结果,当m是正整数时nᵐ的意义为m个n相乘,n的m次幂也就是n的m次方.用代 ...

  3. mysql 幂运算_算法—史上最好快速幂算法讲解

    前言 快速幂是什么?顾名思义,快速幂就是快速算底数的n次幂. 有多快?其时间复杂度为 O(log₂n), 与朴素的O(n)相比效率有了极大的提高. 用的多么?快速幂属于数论的范畴,本是ACM经典算法, ...

  4. 快速幂算法(C++)

    基本概念 什么是快速幂呢?个人理解,就是更快速的计算幂运算. 比如计算a^b 刚学这个算法的时候我也很疑惑,幂运算不是有现成的公式么,直接pow(a,b)不就好了吗? 后来才明白,pow(a,b)的时 ...

  5. 六十八、快速幂算法、牛顿迭代法、累加数组+二分查找的变形

    @Author:Runsen 编程的本质来源于算法,而算法的本质来源于数学,编程只不过将数学题进行代码化. ---- Runsen 上次介绍了二分查找算法及其四个变形问题,下面介绍二分法常用的场景和典 ...

  6. 快速幂算法相关题目(Leetcode题解-Python语言)

    50. Pow(x, n) 快速幂算法的目的,就是快速计算 x 的 n 次方.基本思路是把 n 视作二进制数,则 n 可以被分解为多个 2 的幂次方之和,如 12 对应 1100 等于 0∗20+0∗ ...

  7. 【复习】快速幂算法详解

    快速幂算法 就是求一个高精度幂次取余时的一个快速算法 比如我们要求aba^bab%m的时候 应用于快速幂算法 也就是将O(n)转化为O(logn)的算法 他的原理就是: 252^525 = 2∗2∗2 ...

  8. c语言库快速幂函数,C语言 - 快速幂 - 迭代法+递归法 - 详细讲解

    快速幂的作用: 解决 求 a ^ n 的问题 (n可以大于1e18), 如果用for循环的话,毫无疑问直接炸掉 -- 所以也就用了算法复杂度在 o(log n)的快速幂算法来解决此类问题. 快速幂递归 ...

  9. java位运算求幂,程序员必学:快速幂算法

    前阵子,有小伙伴在我B站的算法教程底下留言 小伙伴们有任何疑问或者希望我解说任何内容,都可以在我的小我私家B站或民众号(xmg_mj)留言哦,我会尽我最大能力.只管抽时间去写文章\录视频来回应人人. ...

  10. 【算法分析与设计】快速幂算法与快速幂取模算法

    文章目录 快速幂算法 算法分析 算法实现 位运算优化 BigInteger支持 快速幂取模算法 算法优点 算法推导 算法实现 BigInteger支持 本文完整代码实现(Java语言描述) 快速幂算法 ...

最新文章

  1. B/S,C/S简单介绍
  2. Base64加密解密算法的C/C++代码实现
  3. list set map 各自实现类的区别以及它们的底层原理和实现(部分)
  4. java高分面试指南:javamvc模式简单案例
  5. Spring Boot JWT 快速入门
  6. gan怎么输入一维数据_时空序列预测模型GAN+LSTM
  7. 【Kibana】es 报错 all shards failed: [search_phase_execution_exception] all shards failed
  8. Leetcode18.四数之和
  9. c#中Split用法总结
  10. 推荐一款专为新手用的Python开发工具
  11. 知识表示与计算机,两分钟了解人工智能中的“知识与知识表示”
  12. MAC 安装python3 菜鸟教程
  13. Talib技术因子详解(五)
  14. ansys软件linux安装教程,ansys 15 for linux 安装纪录
  15. Python数学建模入门【3】
  16. 服务器SN信息,查询服务器sn号
  17. python如何设置画布开始位置_Python用Turtle绘图,原来如此简单
  18. Linux系统磁盘分区格式MBR格式转换GPT
  19. 数据结构c语言版第三版实验四答案,数据结构(C语言版)第三四章习题答案
  20. python bins分箱,划分数值区间

热门文章

  1. Normalize.css 支持 HTML5 的CSS Reset
  2. mysql建数据库实例_MySQL数据库基础(四)——MySQL数据库创建实例
  3. 台式计算机怎么开声音,台式机如何设置声音
  4. JavaScript JQuery 交互式Web前端开发
  5. 那些烦人的VC++库、win10中的VC++库(全部)
  6. 【软件工程师学硬件】之 接口
  7. wpf制作旋转小方块
  8. 用云原生的思维践行云原生,一切皆服务
  9. 解决IE11兼容HTML5 设置
  10. el-upload 上传 照片墙上传照片,上传一张之后,上传框就消失