LeetCode 326. 3 的幂
题目描述
题意分析
给定一个 32 位有符号整数
n,判断它是否等于 3 的某个非负整数次幂,也就是是否存在整数 $k \ge 0$ 使得 $n = 3^k$。最容易被忽略的约束信号是输入范围:
n覆盖 $[-2^{31},\ 2^{31}-1]$,包含 0 和全部负数。而 3 的非负次幂全都是正整数,所以非正数一律为假,这个前置判断不是可选项——它同时挡住了后续除法逻辑里的死循环风险。指数从 0 起算,意味着 $3^0 = 1$ 也算数,
n = 1必须返回true。这一点常被误当成特例排除。值域的上界也值得留意:
int范围内最大的 3 的幂是 $3^{19} = 1162261467$,再乘一次就会溢出。任何「从 1 开始不断乘 3 去逼近」的写法都必须处理这个溢出点。题目还附带一个进阶要求:不使用循环或递归完成判断。这提示存在一个基于数论性质的常数时间解法。
解法:循环除法
核心思路
朴素思路有两条方向。一是从 1 开始不断乘 3,看能不能正好撞上
n;二是从n开始不断除以 3,看能不能正好落到 1。两者逻辑对称,但风险完全不同——乘法会在超过 $3^{19}$ 后溢出,需要额外用长整型或提前判界来兜底,而除法的中间值只会越来越小,永远不会溢出。所以瓶颈在于「乘法方向天然带溢出隐患」,把方向反过来就消失了。
从数论角度看,$n = 3^k$ 等价于「
n的质因数分解里只含因子 3」。要验证这件事,只需要把所有的 3 反复除掉,再看剩下什么:如果一个 3 都不剩且商为 1,说明原数完全由 3 相乘构成;如果商是别的数,说明存在 3 以外的质因子。循环维持的不变量是:设已经成功整除了 $t$ 次,则始终有「原始值 $= n \times 3^{t}$」,且当前的
n是一个正整数。每一步除法都是精确整除,不会丢失信息,所以这个等式一直成立。循环因为「
n不再能被 3 整除」而退出,此时原始值 $= n \times 3^{t}$ 且n与 3 互质。于是原始值是 3 的幂,当且仅当此时n恰好等于 1。判断必须放在循环结束之后——在循环内部提前下结论会漏掉指数为 0 的情况。
解题步骤
- 先判断
n <= 0,是则直接返回false。这既符合「3 的非负次幂必为正」的数学事实,也挡住了n == 0时0 % 3 == 0恒成立而0 / 3仍为 0 造成的死循环。- 只要
n % 3 == 0就执行n /= 3。用整除作为循环条件,保证每一步都是无损的,商乘回去能精确还原原值。- 循环退出后判断
n == 1。退出意味着当前值与 3 互质:等于 1 说明原数只由若干个 3 相乘而来;大于 1 说明还残留着 3 以外的质因子,答案为假。- 把判断放在循环之外而不是循环内部,这样
n = 1这种一次除法都不执行的情况也能被正确覆盖。以
n = 45走一遍:45 为正,进入循环。第一轮45 % 3 == 0,n变成 15;第二轮15 % 3 == 0,n变成 5;第三轮5 % 3 == 2不为 0,循环退出。此时n = 5,验证不变量:$5 \times 3^2 = 45$ 成立,而 5 显然不是 1,说明 45 除了两个 3 之外还含有质因子 5,返回false。换成n = 27:三轮除法依次把n变成 9、3、1,第四轮1 % 3 == 1退出,此时 $1 \times 3^3 = 27$,且n == 1,返回true。再换成n = 1:循环条件1 % 3 == 1一次都不成立,直接跳到判断,n == 1成立,返回true,恰好对应 $3^0$。
代码实现
class Solution {
// 如果 n 是 3 的幂,反复除以 3 的过程中每一步都应该整除。
public boolean isPowerOfThree(int n) {
if (n <= 0) {
return false;
}
while (n % 3 == 0) {
n /= 3;
}
return n == 1;
}
}
func isPowerOfThree(n int) bool {
// 如果 n 是 3 的幂,反复除以 3 的过程中每一步都应该整除。
if n <= 0 {
return false
}
for n%3 == 0 {
n /= 3
}
return n == 1
}
复杂度分析
- 时间复杂度:$O(\log_3 n)$,每轮循环把
n缩小为原来的三分之一,因此循环次数不超过 $\log_3 n$;在int范围内这个上界是 19 次,实际是常数级。- 空间复杂度:$O(1)$,全程只在原变量上做原地除法,没有递归也没有任何辅助结构。
关键点总结
- 「判断是否为某个底数的幂」可以统一转化为「质因数分解里是否只含该底数」,除净因子后看剩不剩 1,这个判据对 2、3、4、5 乃至任意底数都适用。
- 在乘法和除法两个等价方向之间,优先选让中间值变小的那个方向。除法天然免疫溢出,而乘法必须额外处理越界。
- 非正数要在最前面挡掉。
n == 0会让整除条件永远成立而商恒为 0,直接死循环;负数在不同语言里取模的符号规则还不一致,放进循环是纯粹的隐患。- 终止判断要放在循环之外。放进循环体内提前返回,会让指数为 0(即
n = 1)这种连一次除法都不做的输入被误判。- 面试视角:进阶追问几乎必然出现。因为 3 是质数,
int范围内最大的 3 的幂 $3^{19} = 1162261467$ 的所有正因数恰好就是 $3^0$ 到 $3^{19}$,所以n > 0 && 1162261467 % n == 0是常数时间判定。要能主动说明这一步依赖「底数是质数」,同样的技巧搬到 4 的幂上就会失效。- 面试视角:如果被问到 2 的幂,要能立刻给出
n > 0 && (n & (n - 1)) == 0,同时指出这是二进制表示带来的专属捷径,3 没有对应写法。能讲清方法的前提,比会用方法更能体现水平。
易错点总结
- 错误写法:省略非正数判断直接进入循环。用例
n = 0→0 % 3 == 0永远成立且0 / 3仍是 0,循环永不退出,直接超时。- 错误写法:在循环体内提前下结论,写成
if (n % 3 != 0) return false;。用例n = 1→ 第一次检查1 % 3 = 1就返回false,而 $1 = 3^0$,正确答案是true。- 错误写法:额外特判
n == 1返回false,以为指数必须从 1 起算。用例n = 1→ 返回false,正确答案是true。- 错误写法:循环条件写成
while (n > 1)并在里面无条件执行n /= 3。用例n = 5→ 整数除法把 5 变成 1,循环退出后判为true,正确答案是false。- 错误写法:用浮点对数判定,
Math.log(n) / Math.log(3) % 1 == 0。用例n = 243→ 两次浮点对数相除的结果并非精确的 5,取余不为 0 而被判为false;反向的舍入误差也可能让非幂次被误判为true。- 错误写法:改用乘法逼近但把累乘变量声明成
int。用例n = 2147483647→ 累乘到 1162261467 后再乘 3 溢出成负数,循环条件失效,要么提前退出误判,要么陷入死循环。- 错误写法:套用 2 的幂的位运算写法
n > 0 && (n & (n - 1)) == 0。用例n = 3→3 & 2等于 2 不为 0,返回false,正确答案是true;这个技巧只在底数为 2 时成立。- 错误写法:用常数整除法但漏掉正数判断,直接写
1162261467 % n == 0。用例n = -3→ 在 Java 中该取余结果为 0,返回true,正确答案是false;n = 0时更会直接抛出除零异常。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 231. 2 的幂 | 简单 | 底数为 2 时可用 n & (n - 1) 一步判定,是二进制专属捷径 |
| 342. 4的幂 | 简单 | 先确认是 2 的幂,再要求唯一的 1 落在偶数位,多一层掩码约束 |
| 263. 丑数 | 简单 | 需要依次除净 2、3、5 三种因子,判据同样是剩余值为 1 |
| 264. 丑数 II | 中等 | 从判定问题变成构造问题,要用多路指针按序生成第 n 个丑数 |