🎉 问题背景
在非对称加密中,很多时候需要计算一个数的高阶次幂(比如说99次幂),但是常规的求n次幂算法在n很大的时候耗时比较长,有没有什么方法可以加快求n次幂的速度呢?此外,当幂很大的时候,一个数的n次幂很容易就会超出数的最大表示范围,这个问题又该怎么解决呢?
🎉 快速指数算法
要解决第一个问题,可以使用快速指数算法。这里我们先看看常规情况下求n次幂应该怎么求。
/* 一般算法,求g的n次幂 */
static int fun1(int g, int n) {
int res = 1;
for(int i=0;i<n;i++){
res *= g;
}
return res;
}
快速指数算法的实现如下:
/* 快速幂算法,求g的n次幂 */
static int fun2(int g, int n) {
int res = 1;
while (n > 0) {
if ((n & 1) == 1) {
res = res * g;
}
g = g * g;
n >>= 1;
}
return res;
}
其算法原理如下:
算法的核心思想就是将指数拆分成2的倍数的累加,以计算56为例,将指数部分转换成二进制,则56 = 5110= 5100+10+0 = 5100 * 510 * 50 = 54 * 52 * 50。这里要依次获得0、1、1这三个数非常容易实现,只需要让6和1作&即可获得最低位(实际上是二者的二进制数作&),然后再循环右移。在每次循环时做两件事:
- 判断当前位是0还是1 ,如果是1,则将res*gi
- 对gi求平方(依次获得g2、g4)
显然,对于求n次幂,普通算法需要执行n次,而快速指数算法,只需要⌊ log2n⌋次。(⌊x⌋
表示对x向上取整)。
下面以一个具体的例子比较一下普通算法和快速指数算法计算5的6次方的次数:
普通算法
51 : 1*5
52 : 5*5
53 : (5*5) * 5
54 : ((5*5) * 5) * 5
55 : (((5*5) * 5) * 5) * 5
56 : ((((5*5) * 5) * 5) * 5) * 5
共需执行6次
快速指数算法
50
52 * 50
54 * 52 * 5^0
共需执行3次
当n很大的时候,快速指数算法可以节省不少时间。
🎉 高阶求模溢出问题
在上述求n次幂问题的基础,还能再引申出一个问题,那就是很多加密算法中,都需要对一个数的n次幂求模,但是这个n往往很大,超出了最大表数范围,这时求模运算得到的结果实际上是一个溢出的数求模的结果,这显然不是我们想得到的。一个解决方法是在求n次幂的计算过程中求模。
下面先给出具体代码,让我们结合具体代码分析一下:
对于一般算法,有如下代码:
/* 快速幂算法,求g的n次幂模p */
static int fun2(int g, int n, int p) {
int res = 1;
g %= p;
while (n > 0) {
if ((n & 1) == 1) {
res = res * g;
}
g = g * g;
n >>= 1;
}
return res % p;
}
这是在求出最终n次幂的结果后来一次对p求模
/* 快速幂算法,求g的n次幂模p */
static int fun2(int g, int n, int p) {
int res = 1;
g %= p;
while (n > 0) {
if ((n & 1) == 1) {
res = (res * g) % p;
}
g = (g * g) % p;
n >>= 1;
}
return res;
}
这是在每次循环中对p求模。
为了便于说明,我们假设指数n的二进制表示为全1,即g11··1
对于一次性求模,需先算出g11··1 的结果,即 g2n−1g^{2^n - 1}g2n−1 的值,然后再计算g2n−1g^{2^n - 1}g2n−1 % p
下面证明每次求模等价于最后一次性求模:
首先,我们需要知道一个很重要的定理:
(a * b)mod n = [(a mod n) * (b mod n)]mod n
初始化:res = 1,g = g % p
第一次:
res1 = (res * g) % p ;
g1 = (g * g) % p;
第二次:
res2 = (res1 * g1) % p = [(res * g)%p * (g * g)%p] % p = (res * g * g * g) % p;
g2 = (g1 * g1) % p = [(g*g) % p * (g *g) % p] % p = (g * g * g * g) % p;
第三次:
res3 = (res2 * g2) % p = [(res * g * g * g) % p * (g * g * g * g) % p] = (res * g * g * g * g * g * g * g);
g3 = (g2 * g2)= [ (g * g * g * g) % p * (g * g * g * g) % p] = (g * g * g * g * g * g * g * g) % p;
……
从中可以归纳出,第n次:
resn = [resn-1 * g n-1] % p = (res * g * g ··· * g) % p,其中共有2n-1个g,而res = 1,所以resn = g2n−1g^{2^n - 1}g2n−1 % p,至此证明了在每一次循环中求模其实本质上和最后一次性求模是等价,但是在计算机系统中,由于计算部件是有限的,所能表示的数也是有限的,所以在每次循环中求模能解决表数溢出问题。