目录

题目描述

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 == 0i % 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 == 0s 变成 "Fizz"3 % 5 = 3 不为 0,不追加;s 非空,不填数字。结果 "Fizz"

$i = 5$:5 % 3 = 2 不追加;5 % 5 == 0s 变成 "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 == 0s 变成 "Fizz",紧接着 15 % 5 == 0s 变成 "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 = 15i = 15 先命中 3 的分支输出 "Fizz",期望 "FizzBuzz"
  • 错误写法:拼接式里把两个 if 写成 if / else if → 用例 n = 15i = 15 只追加 "Fizz" 就跳过了第二条规则,输出 "Fizz"
  • 错误写法:先判 5 后判 3 → 用例 n = 15i = 15 拼出 "BuzzFizz",顺序与期望的 "FizzBuzz" 相反。
  • 错误写法:循环从 i = 0 开始 → 用例 n = 3,输出多出一项且 0 % 3 == 00 % 5 == 0 使首项变成 "FizzBuzz",列表长度 4 而期望 3。
  • 错误写法:循环条件写成 i < n → 用例 n = 15,最后一项 15 被漏掉,列表长度 14 而期望 15,恰好丢的就是唯一的 "FizzBuzz"
  • 错误写法:用 if (s == "") 在 Java 里判断空串 → 用例任意 ns 是运行期拼接产生的新对象,引用比较恒为 false,所有数字项都不会被填入,输出全是空串;必须用 isEmpty()equals
  • 错误写法:判断「非 3 非 5 的倍数」时写成 i % 3 != 0 || i % 5 != 0 → 用例 i = 33 % 5 != 0 成立,条件为真,会在已有的 "Fizz" 后面再追加 "3",输出 "Fizz3";正确的否定应是 &&
  • 错误写法:把 "FizzBuzz" 写成 "Fizzbuzz""fizzbuzz" → 用例 n = 15,大小写不符直接判错,这类拼写错误在本题占了相当比例的失败提交。
  • 错误写法:结果容器声明为 List<Integer> 或直接返回 int[] → 编译期或判题期类型不匹配,题目要求的是字符串列表。
  • 错误写法:Go 里用 string(i) 转换数字 → 用例 i = 7string(7) 得到的是码点 7 对应的控制字符而非 "7",输出乱码;必须用 strconv.Itoa

相似题目

题目 难度 考察点
66. 加一 简单 同为逐位模拟,但要处理进位链和「全 9 时结果长度增加」的边界
202. 快乐数 简单 迭代次数不确定,需要用快慢指针或哈希集合检测循环,而非固定区间遍历
258. 各位相加 简单 朴素模拟之外存在 $O(1)$ 的数根公式,考点是能否从模拟中提炼出数学规律