快速幂算法

快速幂Exponentiation by squaring,平方求幂)是一种简单而有效的小算法,它可以以**O(log N)**的时间复杂度计算乘方。快速幂不仅本身非常常见,而且后续很多算法也都会用到快速幂。

思考一个问题, 2的10次方,怎么做?

方法一:最朴素的做法,就是 2 * 2 = 4, 4 * 2 = 8, 8 * 2 = 16… 共进行了 9次乘法运算。

**方法二 ** : 快速幂算法

2 的10次方 = 2的5次方 * 2的5次方;

2的5次方 = 2的2次方 * 2的2次方 * 2

使用这种方式, 我们计算 2的10次方进行了 5 次乘法运算

一,快速幂 + 递归

快速幂算法的本质是分治算法。 举个例子, 如果要计算 x ^ 64

xx^2 → x^4 → x^8 → x^16 → x^32 → x^64

从x 开始,依次对上一次的结果进行平方运算, 我们只需要进行 6 次运算就可以得到 x ^ 64的值。

而不需要线性的计算63次

直接从左到右进行推导看上去很困难,因为在每一步中,我们不知道在将上一次的结果平方之后,还需不需要额外乘 x 但如果我们从右往左看,分治的思想就十分明显了:

  • 当我们计算 x ^ n时,我们先计算 x ^ (n/2)
    • 如果 n 为偶数, 则 x = x ^ (n/2) * x ^ (n/2)
    • 如果 n 为奇数, 则 x = x ^ (n/2) * x ^ (n/2) * x
  • 递归边界条件, n == 0, 任何数(除0以外) 的0次方都是1.

代码如下 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 快速幂
double quickMul(double x, long N) {
    // 任何数(除0外) 的 0次幂都是1
    if (N == 0) return 1.0;
  	// 先计算 x 的 N / 2幂
    double y = quickMul(x, N / 2);
    // 如果 N 为偶数, 则 x 的 N次幂等于 y * y
    // 如果 N 为奇数, 则 x 的 N次幂等于 y * y * x
    return N % 2 == 0 ? y * y : y * y * x;
}

double myPow(double x, int n) {
    long N = n;
    // 如果 n < 0,结果为 1/快速幂
    // 如果 n >= 0, 结果为 快速幂
    return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}

复杂度分析 :

时间复杂度 : O(log N) 分治计算,每次计算均折半,复杂度 O(log N)

空间复杂度 : O(log N) 递归使用栈空间, O(log N)

二,快速幂 + 迭代

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
double myPow(double x, int n) {
    long N = n;
    // 如果 n < 0,结果为 1/快速幂
    // 如果 n >= 0, 结果为 快速幂
    return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}

double quickMul(double x, long N) {

    double ans = 1.0;
    //初始值为 x
    double current_product = x;
    // 在对 N 进行二进制拆分的同时计算答案
    while (N > 0) {
        if (N % 2 == 1) {
            // 如果 N 二进制表示的最低位为 1,那么需要计入贡献
            ans *= current_product;
        }
        // 将上次结果不断地平方
        current_product *= current_product;
        // 舍弃 N 二进制表示的最低位,这样我们每次只要判断最低位即可
        N /= 2;
    }
    return ans;
}

复杂度分析 :

时间复杂度 : O(log N) 每次均折半查找

空间复杂度 : O(1) 没有使用额外的内存空间