幂运算是我们平时写代码的时候最常用的运算之一。根据幂运算的定义我们可以知道,如果我们要求 x
的 N
次幂,那么想当然的就会写出一个 N
次的循环,然后累乘得到结果。所以我们要求幂运算的复杂度仍旧是 O(N)
?
那么有没有一种更快的方法呢?
这里给出一种在计算机领域常用的快速幂算法,将 O(N)
降为 O(logN)
。
我通过例子来讲解这个优化过程:
假设我们要算 x
的 n
次幂,使用累乘来编写代码:
1res = 1
2for i in range(n):
3 res *= x
好的,我们已经完成了 O(N)
的解法。
二进制拆分
为了优化这个算法,我们接下来进行数学推导:
我们继续思考当 N = 10
这个具体场景,我们可以把 10 写成二进制来表示 1010(BIN)
,然后我们模拟一次二进制转十进制的过程(复习一下大学知识):
我用下划线把二进制的 1010
标识出来,这样大家就可以发现二进制和十进制转换时的代数式规律。
继续回想刚才的场景,那么我们求 x
的 10
次幂,则式子我们可以写成这样:
我们按照二进制低位到高位从左往右交换一下位置:
我们关注相邻的两项,如果我们不考虑幂指数的 *0
和 *1
,我们只看前半部分,会发现有这么一个规律:
也就是说,不考虑幂指数的 *0
和 *1
右式,左式每次只要每次乘以自身,就是下一项的左式。在我们的例子中其实就是:
用编程思维来考虑这个问题,只要我们从 x
开始维护这么一个左式,每一次迭代都执行 x *= x
,然后每次遇到右边是 *1
的情况,就记录一下 res *= x
是不是就能模拟咱们二进制拆分的计算思路了呢?
编程实现一下 x 的 10 次方
我们用上面的思路,通过代码来计算一下 2 的 10 次方,答案应该是 1024。
1#include <iostream>
2using namespace std;
3
4int main() {
5 int n = 10; // 幂指数,下面通过二进制拆分成 1010
6 int x = 2; // 底数
7 int res = 1; // 累乘的答案
8 while (n) {
9 // 去除二进制的最低位,也就是上面推导中的右式,如果 n & 1 == 1,说明是 *1
10 if (n & 1) {
11 // 如果是 *1,则根据我们观察出来的规律,对维护的结果做累乘
12 res *= x;
13 }
14 // 转换到下一位
15 x *= x;
16 // 二进制右移一位,目的是取到下一个低位二进制
17 n >>= 1;
18 }
19 cout << res << endl; // 1024
20 return 0;
21}
是不是发现非常的简单!我们至此已经实现了快速幂算法。我们将 n
, x
做成参数,编写一个快速幂的方法:
1#include <iostream>
2using namespace std;
3
4int qpow(int x, int n) {
5 int res = 1;
6 while (n) {
7 if (n & 1) res *= x;
8 x *= x;
9 n >>= 1;
10 }
11 return res;
12}
13
14int main() {
15 cout << qpow(2, 10) << endl; // 1024
16 cout << qpow(4, 2) << endl; // 16
17 cout << qpow(5, 3) << endl; // 125
18 cout << qpow(10, 6) << endl; // 1000000
19 return 0;
20}
复杂度
通过上面对幂指数的拆分,发现快速幂只需要循环拆分的项数就可以完成整个幂运算。
我们不妨设求 x 的 N 次方,并且令 x 的所有二进制位都为 1,就可以得到下面这个等式:
那么其实,k 就是计算机需要计算的次数,也就是时间复杂度。套入公比是 1 的等比数列前 k 项和来反推 k 的大小:
好了,这就是快速幂的全部内容了。你可以使用这道题的知识来求解 LeetCode 372. Super Pow(点击原文直达) 。下一篇文我们来手把手 AC 这题