两行代码实现快速幂取模算法

  咱们知道,对于ap(p>0)的求值,朴素的求幂算法采用递推的方式,即将底数a累乘指数p次做为结果。这种算法的时间复杂度为O(p),当指数p很大(>1e7)时,即使该算法拥有线性时间复杂度,在当前主流的机器上仍须要花大量的运算时间。算法

  所以须要对朴素幂算法进行改进,一种高效的方式是分治:当p为偶数时,直接将ap分为(ap/2)*(ap/2)两部分,而每一个ap/2又可继续作一样的划分,直到降为1次幂;当p为奇数时,咱们从式子中提出一个a,则剩下的ap-1就变为了偶指数形式,回到了前述状况;当指数为0时,能够很容易得出结果为1。最后再将分出的结果合并便可。由此咱们能够写出递归式的数学表达:函数

 

  分治解法的时间复杂度为O(log p),这个结果相比线性复杂度而言有了很大提高,例如当指数p=18,446,744,073,709,551,616时,只需计算64次乘法便可得出结果。性能

  如何实现这个算法呢?首先会想到递归地处理这个问题:注意对于较大的运算结果,应考虑高精度模拟运算,或者考虑本文末尾的模m算法。优化

typedef long long ll;

ll fspow(ll a, ll p) {
    if(p==0)
        return 1;
    if(p&1) {
        ll t = fspow(a, p-1);
        return t*a;
    } else {
        ll t = fspow(a, p/2);
        return t*t;
    }
}

  不少人对递归有一种直觉上的抵触,认为递归效率不高。实际并不老是如此。咱们知道函数调用的时间开销主要体如今两个方面:一、调用栈会保存函数的形参、局部变量、返回地址等信息,这须要由高延迟的访存指令来实现;二、分支指令会形成额外的开销,例如因为分支预测失效形成的流水线冲刷等等。加密

  但事实是,若数据规模很大时,相比于不当心把本来O(log p)的算法活生生地写成O(p)的算法形成的浪费而言,这些由系统底层形成的开销是微不足道的。除非你所面临的是执行性能很是有限的系统,或者你想经过极限优化来避免被卡常,不然大可没必要在乎函数调用引发的开销。固然必须考虑递归的空间复杂度。spa

 

两行代码的非递归实现3d

  递归算法的致命缺陷是它会增加地占用栈空间,这一点是咱们不但愿看到的。可否有一种既能保持简洁优雅,又能拥有常数空间复杂度的快速幂算法呢?code

  有的,它就是快速幂算法的非递归版本。下面这两行代码已经将核心浓缩到了极致:blog

1 for (s=1; p;p/=2,a=a*a)
2         if (p&1) s=s*a;

  其中a为底数,p为指数,算法结束后,s=ap递归

  再来看代码的原理。思路还是分治,这与咱们以前的讨论是一致的,不一样的是再也不递归地求解。

  “分”的本质是使指数折半,这里指数必须为偶数。当指数折半后,要使幂结果不变,底数必须平方。形式化描述。而指数p/2又可继续分为p/4 p/8……1,底数则不断平方。当指数降为1时,底数即为要求的结果。当指数不是偶数时,先拆出一个底数,则剩下的幂就是偶次幂,再按如上方法处理便可。

  代码中涉及一些小技巧:

    1. 利用了整除算符/的向下取整特性。当指数p为奇数时,p/2的结果实质上等同于p-1/2,这就免去了当咱们拆出一个底数时须要把p减一的麻烦。事实上,因为除数为2,也能够写成p>>=1,本质上利用了二进制除法。

    2. 咱们知道%运算符的开销是很大的,这里只需判断p的奇偶性,直接判断p的二进制最低位便可。

 

具体应用

  幂运算在许多场合都有着重要的运用。例如RSA非对称加密算法中,明文经过以下公式进行加密:

Ci = Mie mod n

    其中Ci为密文,Mi为与明文有关的数值,(e, n)为公钥对。

  而密文经过以下公式解密:

Mi = Cid mod n

  其中Mi为与明文有关的数值,(d, n)为私钥对。

 

  能够看到,不管是加密仍是解密,都要用到幂运算与模运算。这就与咱们上文所述的快速幂运算颇有关联。

 

  不止RSA,在其它运用中也经常出现类型的形式,它们归结下来以下面这个式子:

ap mod k

  因为对幂取模,最终结果并不会大于等于k。再看看咱们的快速幂算法,中间结果极可能远大于k,若是中间结果超出基本数据类型的范围,还须要经过高精度模拟运算。咱们很快发现,当k可经过基本数据类型表示时,彷佛没有必要采用高精度算法,上文的快速幂算法还能够改进。

  数论指出,(a*b) mod k = (a mod k)*(b mod k) mod k。即两个数的乘积再取模,等于两个数分别取模再相乘,最后取模。利用这个性质,咱们能够将ap mod k等价变形为(a mod k)p mod k,同时每次作乘法时,都先利用该性质缩小乘数,再相乘。

  上述代码修改成

1 a %= k;
2 for (s=1; p;p/=2,a=(a*a)%k)
3          if (p&1) s=(s*a)%k;

  其中a,p,k,s的含义与上文一致。

  这即是快速幂模k算法的非递归实现。

相关文章
相关标签/搜索