扩展算法:

快速幂算法:

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

算法讲解:

比如说一道ACM算法题: 求 A^B的最后三位数表示的整数

  • 解法一: 很简单啊,先求出该数然后对1000求余就得到了结果
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 普通的求幂函数
* @param base 底数
* @param power 指数
* @return 求幂结果的最后3位数表示的整数
*/
public long normalPower(long base,long power){
long result = 1;
for(int i=1;i<=power;i++){
result = result*base;
}
return result%1000;
}

看起来没有问题,但是输入测试用例(2 ,100) ,发现结果为0。为什么呢?

首先Java 中的数据表示是有一定的范围的,比如说int(-2147483648,+2147483648),long 的范围相应更大。但是题目中2^100它的数值已经远远大于了long表示的范围。如果一个数已经无法表示,那么相应它与1000的余数就更不用说求出来了。

  • 解法二: 在计算过程中进行求余运算

“取模”运算的规则:

1
2
3
4
5
(a + b) % p = (a % p + b % p) % p (1)

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

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

第三条,两个数乘积的取模 = 各个因子分别取模然后乘起来综合取模。 相应的代码变换成这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 普通的求幂函数
* @param base 底数
* @param power 指数
* @return 求幂结果的最后3位数表示的整数
*/
public long normalPower(long base,long power){
long result = 1;
for(int i=1;i<=power;i++){
result = result*base;
result = result%1000;
}
return result%1000;
}
  • 解法三:解法二已经能够解决问题,但是速度太慢,如果我要求 2^10000000000,那么循环就需要执行10000000000次,这显然不合理。因此考虑降低计算循环的次数。

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

1
2
3
4
5
3^10=(3*3)*(3*3)*(3*3)*(3*3)*(3*3)   //循环10次

3^10=(3*3)^5 //循环5次

3^10=9^5 //循环1次

可以看出,通过对指数进行折半,底数翻倍 的操作能够很大程度上降低循环的次数。如果说指数不是一个偶数,那么可以将它转化为偶数次幂乘以本身,将单独的一个提取出来先进行和结果的乘法操作。代码可以修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 普通的求幂函数
* @param base 底数
* @param power 指数
* @return 求幂结果的最后3位数表示的整数
*/
public long normalPower(long base,long power){
long result = 1;
while(power>0){
if(power%2==0){
//指数为偶数
power=power/2;
base=base*base%1000;
}
else{
//指数为奇数
result=result*base%1000;
power=power-1;
power=power/2;
base=base*base%1000;
}
}
return result%1000;
}
  • 解法四: 上面的解法三已经有了快速幂算法的味道了,接下来我们需要对代码中的重复部分进行提取优化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 普通的求幂函数
* @param base 底数
* @param power 指数
* @return 求幂结果的最后3位数表示的整数
*/
public long normalPower(long base,long power){
long result = 1;
while(power>0){
if(power%2==1){
//指数为奇数,先进行乘法操作将其转化为偶数
result=result*base%1000;
}
power=power/2;
base=base*base%1000;
}
return result%1000;
}
  • 解法五:终极优化 power%2==1 的求余运算可以用位运算来代替。例如:power&1。因为如果power为偶数,则其二进制表示的最后一位一定是0;如果power是奇数,则其二进制表示的最后一位一定是1。将他们分别与1的二进制做“与”运算,得到的就是power二进制最后一位的数字了,是0则为偶数,是1则为奇数。例如5是奇数,则5&1=1;而6是偶数,则6&1=0;因此奇偶数的判断就可以用“位运算”来替换了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 普通的求幂函数
* @param base 底数
* @param power 指数
* @return 求幂结果的最后3位数表示的整数
*/
public long normalPower(long base,long power){
long result = 1;
while(power>0){
if((power&1)==0){
//指数为奇数,先进行乘法操作将其转化为偶数
result=result*base%1000;
}
power=power/2;
base=base*base%1000;
}
return result%1000;
}

回到这一题,直接通过快速幂算法进行求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public double myPow(double x, int n) {
if(x==0)
return 0;
long b = n;
if(b<0){
x=1/x;
b=-b;
}
double res = 1;
while(b>0){
if((b&1)==1){
res = res*x;
}
x= x*x;
b>>=1;
}
return res;
}
}