介绍快速幂之前我们先来看一道例题
例题:
比如说要求解2的16次方等于多少?
常规方法:
先定义一个变量 mul,一开始 mul 为 2的0次方 = 1,然后将 mul * 2 循环 16 次。
/**
* 快速幂 - 分治
*/
public class QuickPowLeetcode50 {
public static double myPow(int x, int n){
double mul = 1;
for (int i = 0; i < n; i++) {
mul *= x;
}
return mul;
}
public static void main(String[] args) {
System.out.println(myPow(2, 10)); // 1024.0
}
}
但是这种方法效率比较低,要做很多次乘法,时间复杂度高。
优化:
这题我们可以采用分治的思想:
首先, 2的16次方可以拆成两个2的8次方, 2的8次方又可以拆成两个2的4次方, 2的4次方可以拆成两个2的2次方,最后2的2次方可以拆成 2 x 2。
我们可以把它拆成左右两边都对称的二叉树。
从最底层开始看(右子树开始),2 x 2 = 4(第一次乘),当右子树计算出来后,因为左右子树是对称的,对应的左子树就不用重复计算了, 4 x 4 = 16 (第二次乘),然后 16 x 16 = 256(第三次乘),256 x 256 = 65536(第四次乘)。
我们运用分治这种思想只进行了4次乘法运算,相比之前用常规方法循环乘16次效率提高了不少。所以它叫快速幂。
使用快速幂我们还需要考虑一种情况,前面的幂都是偶数,所以可以直接把左右两边的值相乘。
幂如果是奇数的话,就是另外一种情况了,如下图:
比如上图中的 2^10 拆成 两个2^5,然后2^5可以拆成 2 * 两个2^2, 接着 2^2又可以拆成2 x 2。
代码实现:
/**
* 快速幂 - 分治
*/
public class QuickPowLeetcode50 {
public static double myPow2(int x, int n) {
double mul = 1;
for (int i = 0; i < n; i++) {
mul *= x;
}
return mul;
}
public static double myPow(int x, int n) {
if (n == 0) {
return 1.0;
}
//首先找到递归出口
if (n == 1) {
return x;
}
double y = myPow(x, n / 2);
if (n % 2 == 1) {//奇数
return y * y * x;
} else {//偶数
return y * y;
}
}
public static void main(String[] args) {
System.out.println(myPow(2, 10)); // 1024.0
//System.out.println(myPow(2.1, 3)); // 9.261
System.out.println(myPow(2, -2)); // 0.25
System.out.println(myPow(2, 0)); // 1.0
System.out.println(myPow(2, -2147483648)); // 1.0
}
}
当然,这样的代码拿去leetcode上去跑,有几个测试用例不能通过。
首先,幂如果为负数的情况我们没有考虑到,其次,幂为整数型最小负数的情况也跑不出来。
如果幂为负数,我们可以把它当作正数来求,最后对计算结果求倒数就可以了。
因为整型int 的最小值为 -2147483648,但是最大值为 2147483647,这就比较恶心了,负负得正会导致溢出,在这里leetcode官方也没给出什么好的解决办法,把数据类型int 改成 long型就可以了。
优化后:
/**
* 快速幂 - 分治
*/
public class QuickPowLeetcode50 {
public static double myPow(int x, int n){
long p = n;
if(p < 0){
p = -p; //负负得正
}
double r = myPowPositive(x, p);
return n < 0 ? 1 / r : r;
}
public static double myPowPositive(int x, long n) {
if (n == 0) {
return 1.0;
}
//首先找到递归出口
if (n == 1) {
return x;
}
double y = myPowPositive(x, n / 2);
if (n % 2 == 1) {//奇数
return y * y * x;
} else {//偶数
return y * y;
}
}
public static void main(String[] args) {
System.out.println(myPow(2, 10)); // 1024.0
//System.out.println(myPow(2.1, 3)); // 9.261
System.out.println(myPow(2, -2)); // 0.25
System.out.println(myPow(2, 0)); // 1.0
System.out.println(myPow(2, -2147483648)); // 1.0
}
}
接下来再介绍一种判断一个数是奇数还是偶数的方法:
我们可以用位运算,看一下 1,2,3,4,5,6,7 对应的二进制数。
可以看到,奇数的二进制的最后一位永远是1,而偶数的最后一位是0,我们只要拿到它们的最后一位,判断是0还是1即可。
我们可以把它和1做按位与,就能拿到二进制数的最后一位。
完整代码:
/**
* 快速幂 - 分治
*/
public class QuickPowLeetcode50 {
public static double myPow(double x, int n){
long p = n;
if(p < 0){
p = -p; //负负得正
}
double r = myPowPositive(x, p);
return n < 0 ? 1 / r : r;
}
public static double myPowPositive(double x, long n) {
if (n == 0) {
return 1.0;
}
//首先找到递归出口
if (n == 1) {
return x;
}
double y = myPowPositive(x, n / 2);
if ((n & 1) == 1) {//奇数
return y * y * x;
} else {//偶数
return y * y;
}
}
public static void main(String[] args) {
System.out.println(myPow(2, 10)); // 1024.0
System.out.println(myPow(2.1, 3)); // 9.261
System.out.println(myPow(2, -2)); // 0.25
System.out.println(myPow(2, 0)); // 1.0
System.out.println(myPow(2, -2147483648)); // 1.0
}
}
感谢阅读!