LeetCode 412. Fizz Buzz
题目描述
题意分析
题目目标:输出 $1$ 到 $n$ 的字符串列表,其中 3 的倍数替换为
"Fizz",5 的倍数替换为"Buzz",同时是 3 和 5 的倍数替换为"FizzBuzz",其余为该数字本身的十进制表示。
核心约束:这题没有算法难度,它考的是条件覆盖的完整性与代码组织。四种情况必须互斥且穷尽,且「同时是 3 和 5 的倍数」这一类必须优先于单独的两类被处理,否则会被前面的分支截胡。另一个隐含要求是可扩展性——面试官经常紧跟着问「如果再加一个 7 对应
Whizz呢」,写法一旦是硬编码的四分支,就要推倒重来。
边界处理:$n \ge 1$,所以不存在空输出;下标从 1 开始而不是 0,循环边界要写成
i <= n;答案是字符串列表而非数字列表,数字必须显式转成字符串。
实现取舍:两种写法。一是拼接式——先看是不是 3 的倍数、追加
"Fizz",再看是不是 5 的倍数、追加"Buzz",最后如果还是空串就填数字;二是分支式——按 15、3、5、其他的顺序四选一。拼接式天然支持新增规则(再加一条if即可),分支式在规则少时更直观。本文两份代码分别演示了这两种写法。
解法:数学推导
核心思路
先把「同时是 3 和 5 的倍数」这件事说清楚:$i$ 同时被 3 和 5 整除,等价于被 $\text{lcm}(3, 5) = 15$ 整除。因为 3 与 5 互质,最小公倍数就是乘积,所以
i % 3 == 0 && i % 5 == 0与i % 15 == 0完全等价。分支式写法用后者,少一次取模。
拼接式的想法更本质:把「Fizz」和「Buzz」看成两条互不干扰的独立规则,各自决定是否往结果里追加自己的标签。$i$ 被 3 整除就追加
"Fizz",被 5 整除就追加"Buzz",两条规则都命中时自然拼成"FizzBuzz"——顺序由代码的书写顺序保证,恰好是题目要求的「Fizz 在前」。这样「同时命中」这一类根本不需要单独写分支,它是前两条规则的自然结果。
于是不变量是:处理第 $i$ 个数时,局部变量
s累积着当前已命中的所有标签;循环结束时若s仍为空串,说明一条规则都没命中,此时才回退到数字本身。「是否命中过」这个信息完全由s是否为空来承载,不需要额外的布尔标志。
分支式则依赖另一条不变量:四个分支按「约束由强到弱」排列。15 的倍数集合是 3 的倍数集合与 5 的倍数集合的交集,所以它必须排在最前;把它放到后面,
i = 15会先被i % 3 == 0捕获,输出"Fizz"而非"FizzBuzz"。「更特殊的条件写在前面」是所有多分支判定的通用规则。
两种写法都是单趟线性扫描,没有任何可优化的空间——本题的全部价值在于把条件写对、把结构写得能扩展。
解题步骤
第一步:准备一个容量为 $n$ 的结果容器。 为什么要预设容量:结果长度是已知的 $n$,预分配可以避免动态数组反复扩容与复制;这是个小细节,但在「简单题」里能体现工程习惯。
第二步:从
i = 1循环到i <= n。 为什么从 1 而不是 0:题目要求输出 $1$ 到 $n$,而且 0 被任何数整除,混进来会输出"FizzBuzz",属于凭空多出的错误项。
第三步(拼接式):
s初始化为空串,若i % 3 == 0追加"Fizz",若i % 5 == 0追加"Buzz"。 为什么两个都是独立的if而不是if-else:两条规则可以同时成立,用else会让 15 的倍数只拿到"Fizz"。为什么先判 3 后判 5:题目规定的顺序是"FizzBuzz",追加顺序直接决定了拼接结果的字符顺序。
第四步(拼接式):若
s仍为空串,则把i转成字符串填入。 为什么用「是否为空」判断:它等价于「3 和 5 都没命中」,把两个条件的否定合并成了一次检查,比再写一遍i % 3 != 0 && i % 5 != 0更短也更不易写反。
第五步(分支式):按 15 → 3 → 5 → 默认的顺序四选一。 为什么 15 必须最先:见上文,它的条件最强,放后面会被截胡。为什么最后要有默认分支:四种情况必须穷尽,缺了默认分支会漏掉所有普通数字。
第六步:把当前结果追加进列表,循环结束后返回。
以
n = 15走一遍(重点看关键位置):
$i = 1$:拼接式里
1 % 3 = 1不为 0、1 % 5 = 1不为 0,s保持空串,于是填入"1"。分支式里前三个条件全不成立,走默认分支得到"1"。两者一致。
$i = 3$:
3 % 3 == 0,s变成"Fizz";3 % 5 = 3不为 0,不追加;s非空,不填数字。结果"Fizz"。
$i = 5$:
5 % 3 = 2不追加;5 % 5 == 0,s变成"Buzz"。结果"Buzz"。注意这里s的第一段是空的,"Buzz"直接成为整个字符串,说明拼接式不需要为「只命中第二条规则」写任何特殊处理。
$i = 9$:
9 % 3 == 0得"Fizz",9 % 5 = 4不追加,结果"Fizz"。
$i = 10$:
10 % 3 = 1不追加,10 % 5 == 0得"Buzz",结果"Buzz"。
$i = 15$:这是全题唯一的关键点。拼接式里
15 % 3 == 0让s变成"Fizz",紧接着15 % 5 == 0让s变成"FizzBuzz"——两条规则各自独立生效,拼接顺序由代码顺序保证。分支式里第一个条件15 % 15 == 0直接命中,返回"FizzBuzz";如果把这个分支挪到后面,15 % 3 == 0会先成立并输出"Fizz",这就是分支顺序错误的直接后果。
完整输出为
["1","2","Fizz","4","Buzz","Fizz","7","8","Fizz","Buzz","11","Fizz","13","14","FizzBuzz"],与期望一致。
代码实现
class Solution {
public List<String> fizzBuzz(int n) {
List<String> answer = new ArrayList<>();
for (int i = 1; i <= n; ++i) {
String s = "";
if (i % 3 == 0) {
s += "Fizz";
}
if (i % 5 == 0) {
s += "Buzz";
}
if (s.length() == 0) {
s += i;
}
answer.add(s);
}
return answer;
}
}
func fizzBuzz(n int) []string {
answer := make([]string, 0, n)
for i := 1; i < n+1; i++ {
switch {
case i%15 == 0:
answer = append(answer, "FizzBuzz")
case i%3 == 0:
answer = append(answer, "Fizz")
case i%5 == 0:
answer = append(answer, "Buzz")
default:
answer = append(answer, strconv.Itoa(i))
}
}
return answer
}
复杂度分析
- 时间复杂度:$O(n)$。凭什么:一趟循环处理 $n$ 个数,每个数只做常数次取模、比较和字符串追加;数字转字符串的开销与位数成正比,位数是 $O(\log n)$,但由于是常数级小值($n \le 10^4$)通常按 $O(1)$ 计。
- 空间复杂度:$O(n)$。凭什么:输出列表本身要装 $n$ 个字符串,这部分是题目要求的返回值;除此之外只有一个局部字符串变量,额外空间是 $O(1)$。
关键点总结
- 多条件判定的第一原则是「更特殊的条件写在更前面」。 15 的倍数是 3 的倍数的子集,顺序写反就会被截胡——这条规则在权限判定、路由匹配、异常分类里同样成立。
- 能用「规则叠加」就别用「情况枚举」。 拼接式把 $2^k$ 种组合压成了 $k$ 条独立规则,新增一个「7 对应 Whizz」只要再加一个
if,而分支式要重写成八个分支。- 用「结果是否为空」承载「是否命中过任何规则」,可以省掉标志变量。 少一个变量就少一处忘记同步的风险。
- 互斥的条件用
else if,可共存的条件用连续的if。 这两者的混淆是本题唯一真正的坑。- 返回类型是字符串列表,数字必须显式转换。 Java 里
s += i依赖隐式转换,Go 里必须strconv.Itoa,跨语言时这类差异要留意。- 面试视角:这题真正被问的是「你的代码好不好改」。写完基础版后主动说出扩展方案——把规则抽成
[(3, "Fizz"), (5, "Buzz")]这样的表,循环遍历规则表拼接结果,新增规则只改数据不改代码。能主动讲到这一层,简单题也能留下好印象。
易错点总结
- 错误写法:分支式里把
i % 3 == 0放在i % 15 == 0前面 → 用例n = 15,i = 15先命中 3 的分支输出"Fizz",期望"FizzBuzz"。- 错误写法:拼接式里把两个
if写成if/else if→ 用例n = 15,i = 15只追加"Fizz"就跳过了第二条规则,输出"Fizz"。- 错误写法:先判 5 后判 3 → 用例
n = 15,i = 15拼出"BuzzFizz",顺序与期望的"FizzBuzz"相反。- 错误写法:循环从
i = 0开始 → 用例n = 3,输出多出一项且0 % 3 == 0、0 % 5 == 0使首项变成"FizzBuzz",列表长度 4 而期望 3。- 错误写法:循环条件写成
i < n→ 用例n = 15,最后一项 15 被漏掉,列表长度 14 而期望 15,恰好丢的就是唯一的"FizzBuzz"。- 错误写法:用
if (s == "")在 Java 里判断空串 → 用例任意n,s是运行期拼接产生的新对象,引用比较恒为false,所有数字项都不会被填入,输出全是空串;必须用isEmpty()或equals。- 错误写法:判断「非 3 非 5 的倍数」时写成
i % 3 != 0 || i % 5 != 0→ 用例i = 3,3 % 5 != 0成立,条件为真,会在已有的"Fizz"后面再追加"3",输出"Fizz3";正确的否定应是&&。- 错误写法:把
"FizzBuzz"写成"Fizzbuzz"或"fizzbuzz"→ 用例n = 15,大小写不符直接判错,这类拼写错误在本题占了相当比例的失败提交。- 错误写法:结果容器声明为
List<Integer>或直接返回int[]→ 编译期或判题期类型不匹配,题目要求的是字符串列表。- 错误写法:Go 里用
string(i)转换数字 → 用例i = 7,string(7)得到的是码点 7 对应的控制字符而非"7",输出乱码;必须用strconv.Itoa。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 66. 加一 | 简单 | 同为逐位模拟,但要处理进位链和「全 9 时结果长度增加」的边界 |
| 202. 快乐数 | 简单 | 迭代次数不确定,需要用快慢指针或哈希集合检测循环,而非固定区间遍历 |
| 258. 各位相加 | 简单 | 朴素模拟之外存在 $O(1)$ 的数根公式,考点是能否从模拟中提炼出数学规律 |