快速幂
快速幂是一种能够对m的n次幂
快速进行计算的一种算法。能在log(n)
的时间内求出结果。
传统算法
一般情况下我们会想到直接对m
乘n
次不就得到了m的n次幂
了吗?也就是下面的这种算法,我们可以看到它的时间复杂度是O(n)
。
private static double power1(int a,int b){
double res = 1;
for (int i = 0; i < b ; i++){
res *= a;
}
return a == 0 ? 0 : res;
}
如果b足够大,很容易超时。
递归快速幂
递归快速幂的思想大致如下:
2^15
可以分解成求2^7 * 2^7 * 2
,2^7
分解为2^3 * 2^3 * 2
2^3
分解为2^1 * 2^1 * 2
,当幂次为1的时候就是递归的边界条件,我们直接return
。我们可以看到2^15
依赖2^7
的计算,2^7
依赖2^3
,
所以我们可以用递归来实现它
private static double powers(int a, int b) {
if(b == 1) {
return a;
}
if((b & 1) == 1) {
double temp = powers(a, b/2);
return temp * a * temp;
} else {
double temp = powers(a,b/2);
return temp * temp;
}
}
总结:
递归快速幂就是指将b不停的分割,从而进行简化操作
- 如果是奇数就变成
(a ^ (b/2)) * (a ^ (b/2)) * a
- 如果是偶数的话就变成了
(a ^ (b/2)) * (a ^ (b/2))
- 一直往后进行划分,直到b变成1为止。
非递归快速幂
非递归快速幂的思想大致如下:
我们通过递归快速幂可以看到不断除以2
,是不是跟右移动一位是一样的?所以我们换个角度,从n这个幂次
的二进制出发,例如2^42
,我们可以将42
化成101010
的二进制。
上述式子我们是很容易知晓的,指数相加。可以看到我们把101010
拆成了100000
和1010
,这是因为我们在计算的过程中,会先计算m * m
== m^2
=m^10(2)
,然后再计算(m * m) * (m * m)
== m^4
m^100(2)
,m^8
m^1000(2)
不就是对指数在移位吗?有多少位二进制就计算几次。
所以像计算2^100000(2)
这种我们只需要移动100000(2)的长度6
就行了,每次将上次的结果*结果
得到新结果
,幂次就会 * 2
啦,我们知道* 2
是会后面补0
的,所以自然就慢慢得到100000
,那么新结果这个时候就是我们2^100000(2)
,首先2^1
* 2^1
变成了2^10(2)
,然后2^2
*2^2
变成了2^100(2)
,最后通过5次这样的计算
我们就可以算到2^100000(2)
。
回归正题,我们还有2^1010(2)
没求呢?2^1010(2)
分解成2^1000(2)
* 2^10(2)
,2^1000(2)
和2^10(2)
我们是不是在上述求2^100000(2)
的过程中会有计算到2^1000(2)
和2^10(2)
的步骤呢?所以我们只需要在计算到这的时候,记录一下当前的值就行了。根据我们的拆分规则,以为1
为分界点的,101010(2)
即100000(2) + 1000(2) + 10(2)
具体可以回归到上图观察下。每次我们遇到位数为1
的时候就需要记录一下,以便于贡献给下次为1
的时候计算。具体代码如下:
private static double power3(int a, int b) {
double res = 1;
double base = a;
whlie(b > 0) {
if((b & 1) == 1) {
//res是保存的数据,就比如上面说的,1010分为1000和10,我们最后要合并的,也就
//是1000 + 10,当我们第一次调用这个方法的时候就是在res中保存了一个10的结果
//当我再次调用的时候res = res * base 此时base是1000的结果,res是10的结果,*就是相加
res *= base;
}
base *= base;
b >>= 1;
}
}
当我们进行运算速度测试时,我们可以看到传统算法为11
,而其他两种为0
,在数据量特别大的时候,这种对比是特别明显的。传统的为O(n)
,快速幂为O(logn)
,所以为什么有这么大差别很明显了。
实战
正片开始
这道题说了我们的n可能会存在负数,所以为了防止溢出,我们要给n的类型变为long类型,其余的和上面的一致。
public double myPow2(double x, int n) {
boolean is_minus = n < 0;
//这里需要注意越界问题
long i = n;
long m = Math.abs(i);
double res = 1;
while(m > 0) {
if ((m & 1) == 1) {
res *= x;
}
x *= x;
m >>= 1;
}
if (is_minus) {
res = 1 / res;
}
return res;
}