题目是实现一个函数 pow(x, n),计算 xnx^nxn。约束里 n 可以到 ±231−1\pm 2^{31}-1±231−1,如果用最朴素的「循环 n 次相乘」,时间复杂度是 O(∣n∣)O(|n|)O(∣n∣),在这个范围明显不可接受。leetcode
这篇就从「最直观的数学思路」开始,一步步推到快速幂,让你真的明白为什么是 O(logn)O(\log n)O(logn)。
朴素做法:按定义来,O(n)
最直观的定义是:
xn=x⋅x⋅x⋯x⏟n 个x^n = \underbrace{x \cdot x \cdot x \cdots x}_{n\ \text{个}}xn=n 个x⋅x⋅x⋯x
实现思路:
-
准备一个 result,从 1 开始。
-
循环 n 次,每次乘上一个 x。
这个做法的特点:
-
思路简单,完全符合直觉。
-
时间复杂度是 O(∣n∣)O(|n|)O(∣n∣)。当 ∣n∣|n|∣n∣ 接近 10910^9109 时,完全跑不动。leetcode
所以我们需要换一个角度看「幂」,而不是一口一口「吃 x」。
换个视角:把 n 拆成若干个 2 的幂
关键想法不是「乘 n 个 x」,而是「用几块大积木拼出 xnx^nxn」。
积木的规格只有这些:
x1, x2, x4, x8, x16, x32, ...x^1,\ x^2,\ x^4,\ x^8,\ x^{16},\ x^{32},\ \dotsx1, x2, x4, x8, x16, x32, ...
也就是所有 x2kx^{2^k}x2k 这种形式。
数学上的事实:
任何正整数 n,都可以唯一写成若干个不同的 2 的幂之和。
例如:
-
13=8+4+113 = 8 + 4 + 113=8+4+1
-
10=8+210 = 8 + 210=8+2
-
7=4+2+17 = 4 + 2 + 17=4+2+1
于是:
-
x13=x8+4+1=x8⋅x4⋅x1x^{13} = x^{8+4+1} = x^8 \cdot x^4 \cdot x^1x13=x8+4+1=x8⋅x4⋅x1
-
x10=x8+2=x8⋅x2x^{10} = x^{8+2} = x^8 \cdot x^2x10=x8+2=x8⋅x2
-
x7=x4+2+1=x4⋅x2⋅x1x^7 = x^{4+2+1} = x^4 \cdot x^2 \cdot x^1x7=x4+2+1=x4⋅x2⋅x1
这就是快速幂的核心:只要我能高效拿到这些「规格为 2k2^k2k 的幂积木」,再选对几块乘起来,就能得到 xnx^nxn。
base 和 result:谁负责什么?
快速幂里有两个核心变量:
-
base:当前这一轮代表的「幂积木」。
-
result:到目前为止已经选中的那些积木的乘积,也就是部分结果。
职责分工:
-
base 从 x1x^1x1 开始,每一轮变为自己的平方:
-
第 1 轮:base = x1x^1x1
-
第 2 轮:base = x2x^2x2
-
第 3 轮:base = x4x^4x4
-
第 4 轮:base = x8x^8x8
-
...
第 k 轮的 base 是 x2k−1x^{2^{k-1}}x2k−1。
-
-
result 一开始是 1(什么都没选)。
在某一轮,如果当前这块积木是「需要的」,就做:
result∗=base\text{result} \mathrel{*}= \text{base}result∗=base
否则就跳过。
可以用一句话记住这点:
base 在「生产积木」,result 在「选择积木」。
base 可以每轮平方,result 不能每轮随便平方。
只要这句不忘,就不会写出错误的实现。
怎么知道当前这块积木要不要选?
引入一个变量 e 表示「还没处理完的指数」,一开始 e = |n|。
每一轮做三件事:
-
看 e 是否为奇数:
-
如果 e 是奇数,说明目前的指数里有一块「单独的 1 次幂」需要用,这一块恰好对应当前 base:
- result *= base
-
-
把能成对的部分合并:
- base *= base(利用 x2k=(xk)2x^{2k} = (x^k)^2x2k=(xk)2,把成对的幂打包成新的积木)
-
减少还没处理的指数:
- e /= 2(向下取整,相当于把已经处理完的这一位丢掉)
循环到 e 变成 0 为止,所有需要的积木都已经选完,result 就是 x∣n∣x^{|n|}x∣n∣。
用 x13x^{13}x13 走一遍
以 n = 13 为例,13 的分解是 13=8+4+113 = 8 + 4 + 113=8+4+1,我们应该选的积木是 x1,x4,x8x^1, x^4, x^8x1,x4,x8。
初始化:
-
base = x
-
result = 1
-
e = 13
按轮数走:
| 轮次 | e 进入时 | e 奇偶 | base 进入时 | 操作 | result 出来时 |
|---|---|---|---|---|---|
| 1 | 13 | 奇数 | x1x^1x1 | result = base | x1x^1x1 |
| base = base² = x2x^2x2 | |||||
| e = 13 / 2 = 6 | |||||
| 2 | 6 | 偶数 | x2x^2x2 | 跳过 result | x1x^1x1 |
| base = x4x^4x4 | |||||
| e = 6 / 2 = 3 | |||||
| 3 | 3 | 奇数 | x4x^4x4 | result = base | x5x^5x5 |
| base = x8x^8x8 | |||||
| e = 3 / 2 = 1 | |||||
| 4 | 1 | 奇数 | x8x^8x8 | result = base | x13x^{13}x13 |
| base = x16x^{16}x16 | |||||
| e = 1 / 2 = 0 |
可以看到:
-
用到的 base 是 x1,x4,x8x^1, x^4, x^8x1,x4,x8,刚好对应 13=1+4+813 = 1 + 4 + 813=1+4+8。
-
一共 4 轮,复杂度是 O(log13)O(\log 13)O(log13)。
为什么不能"每轮都乘一次 base"?
假设循环了 k 轮,base 依次是:
x1,x2,x4,x8,...,x2k−1x^1, x^2, x^4, x^8, \dots, x^{2^{k-1}}x1,x2,x4,x8,...,x2k−1
如果你每一轮都做 result *= base,那 result 会变成:
x1+2+4+⋯+2k−1=x2k−1x^{1+2+4+\dots+2^{k-1}} = x^{2^k - 1}x1+2+4+⋯+2k−1=x2k−1
问题:
-
指数固定是 2k−12^k - 12k−1,只能算出 1, 3, 7, 15, 31...,不能覆盖一般的 n。
-
完全没用上「n 的具体值」,只看了循环跑了几轮。
所以在快速幂的框架里:
"每轮都更新 result" 在数学上就是错的,等于把所有积木都拿了,而正确做法是像凑钱一样,只拿能刚好拼出 n 的那几张硬币。
负指数和边界情况
完整实现里还要处理两个细节。
1. 负指数
如果 n < 0,可以先计算 x−∣n∣x^{-|n|}x−∣n∣,最后取倒数:
xn=x−∣n∣=1x∣n∣x^n = x^{-|n|} = \frac{1}{x^{|n|}}xn=x−∣n∣=x∣n∣1
实现上的常见处理:
-
记录
isNegative = (n < 0)。 -
用 64 位整型保存 absN,避免最小 32 位整数取反溢出。
-
循环里只处理非负指数 absN,最后如果 isNegative 为真,返回 1 / result。leetcode
2. n = 0
任何非 0 数的 0 次幂是 1,直接返回 1 即可。leetcode
算法小结
快速幂可以用一句话总结:
用 base 依次生成 x1,x2,x4,x8,...x^1, x^2, x^4, x^8, ...x1,x2,x4,x8,... 这些「2 的幂次积木」,用 result 按照指数 n 的拆分结果,挑出其中若干块相乘;每一轮都把指数缩小一半,所以整体复杂度是 O(log∣n∣)O(\log |n|)O(log∣n∣)。leetcode
更程序化地描述就是:
-
处理 n = 0,直接返回 1。
-
如果 n < 0,先把 n 变为正数,记住最后要取倒数。
-
初始化:
-
result = 1
-
base = x
-
e = |n|
-
-
当 e > 0:
-
如果 e 是奇数,result *= base
-
base *= base
-
e /= 2
-
-
如果原来的 n 为负,返回 1 / result,否则返回 result。leetcode
理解了「积木」和「硬币凑钱」这两个类比之后,快速幂就不再是一个需要死记硬背的模板,而是一个非常自然的数学过程。