Fizz Buzz算法详解:从LeetCode 412题看循环与条件判断优化

📅 2026/8/25 20:42:36
Fizz Buzz算法详解:从LeetCode 412题看循环与条件判断优化
在实际编程面试和日常算法练习中Fizz Buzz 是一个绕不开的经典入门题。它看似简单却能在短短几行代码里考察开发者对循环、条件判断、字符串拼接以及边界情况处理的基本功。很多面试官喜欢用它作为开场快速过滤掉那些连基础语法都写不顺畅的候选人。力扣LeetCode上的第 412 题正是这道题要求你写一个函数根据特定规则生成从 1 到 n 的字符串列表。这篇文章将带你从零开始彻底理解 Fizz Buzz 问题。我们不仅会写出最直观的解法还会深入探讨如何优化代码的可读性和性能分析不同语言以 Python 和 Java 为例的实现差异并总结在面试中如何清晰地向面试官阐述你的解题思路。无论你是刚开始刷题的新手还是想巩固基础、优化代码的老手这篇文章都能提供清晰的路径和实用的建议。1. 理解 Fizz Buzz 问题规则与边界Fizz Buzz 是一个经典的编程游戏也常被用作编程面试题。它的核心规则非常简单从整数 1 开始按顺序报数。如果当前数字能被 3 整除则说 “Fizz”。如果当前数字能被 5 整除则说 “Buzz”。如果当前数字同时能被 3 和 5 整除则说 “FizzBuzz”。如果以上条件都不满足则直接说出这个数字本身。在力扣第 412 题中题目被形式化为一个函数给定一个整数n返回一个长度为n的字符串列表answer索引从 1 开始其中answer[i] FizzBuzz如果i同时是 3 和 5 的倍数。answer[i] Fizz如果i是 3 的倍数但不是 5 的倍数。answer[i] Buzz如果i是 5 的倍数但不是 3 的倍数。answer[i] i以字符串形式如果上述条件都不满足。1.1 为什么这道题值得深究很多初学者看到题目后可能会觉得“这太简单了一个循环加几个 if 就搞定了”。但正是这种“简单”的题目才能暴露出代码风格和思维严谨性的差异。面试官通过这道题可能会考察你以下几点边界条件处理输入的n是否可能为 0 或负数虽然题目通常约定n 1但思考一下边界情况能体现你的周全。条件判断的顺序判断“同时被 3 和 5 整除”的条件应该放在最前面还是最后面顺序错误会导致逻辑 bug。代码的清晰与效率是否有不必要的重复计算如多次求模运算代码是否易于阅读和维护扩展性思考如果规则变化比如增加“能被 7 整除输出 ‘Jazz’”你的代码是否容易修改理解这些潜在考点是写好这道题的第一步。2. 环境准备与解题框架在开始编码前我们需要明确解题环境。力扣支持多种编程语言这里我们主要使用 Python 和 Java 进行演示因为它们是算法面试中最常用的两种语言。你只需要一个能运行代码的本地环境或直接使用力扣的在线编辑器即可。2.1 核心解题思路无论使用哪种语言解决 Fizz Buzz 的核心算法流程都是一致的初始化创建一个空的列表Python或数组/列表Java用于存放结果。遍历使用一个循环从 1 迭代到给定的整数n包含。判断与填充在循环体内对当前的数字i应用 Fizz Buzz 规则先判断是否同时满足两个条件被3和5整除因为这是最特殊的 case。再判断是否只满足单个条件被3整除 或 被5整除。最后处理默认情况输出数字本身。返回结果循环结束后返回填充好的结果列表。这个流程的伪代码如下函数 fizzBuzz(n): 初始化结果列表 answer 对于 i 从 1 到 n: 如果 i 能被 15 整除: // 因为 15 是 3 和 5 的最小公倍数 answer 添加 “FizzBuzz” 否则如果 i 能被 3 整除: answer 添加 “Fizz” 否则如果 i 能被 5 整除: answer 添加 “Buzz” 否则: answer 添加 i 的字符串形式 返回 answer2.2 为什么使用 15 而不是i%30 and i%50在判断“FizzBuzz”时我们可以直接判断i % 15 0。因为 3 和 5 互质它们的最小公倍数就是 15。这样做有两点好处逻辑清晰直接表达了“同时是 3 和 5 的倍数”这一概念。微小的性能优化一次求模运算代替了两次求模运算和一次逻辑与操作。虽然在n很小的时候差别可以忽略不计但体现了对代码效率的考虑。3. 代码实现从基础到优化现在我们分别用 Python 和 Java 来实现上述算法并逐步探讨优化方案。3.1 Python 实现Python 以其简洁的语法而闻名实现 Fizz Buzz 非常直观。版本一最直接的实现class Solution: def fizzBuzz(self, n: int) - List[str]: answer [] for i in range(1, n 1): if i % 15 0: answer.append(FizzBuzz) elif i % 3 0: answer.append(Fizz) elif i % 5 0: answer.append(Buzz) else: answer.append(str(i)) return answer代码解释range(1, n 1)生成从 1 到 n包含的整数序列。使用if-elif-else链确保每个数字只匹配一个条件。注意else分支中需要使用str(i)将整数转换为字符串。版本二使用列表推导式更Pythonic对于熟悉 Python 的开发者可以使用列表推导式让代码更紧凑。但请注意复杂的条件逻辑可能会降低推导式的可读性。class Solution: def fizzBuzz(self, n: int) - List[str]: return [ “FizzBuzz” if i % 15 0 else “Fizz” if i % 3 0 else “Buzz” if i % 5 0 else str(i) for i in range(1, n 1) ]3.2 Java 实现Java 版本需要更多样板代码但逻辑完全相同。版本一标准循环与条件判断import java.util.ArrayList; import java.util.List; class Solution { public ListString fizzBuzz(int n) { ListString answer new ArrayList(n); // 指定初始容量避免扩容开销 for (int i 1; i n; i) { if (i % 15 0) { answer.add(FizzBuzz); } else if (i % 3 0) { answer.add(Fizz); } else if (i % 5 0) { answer.add(Buzz); } else { answer.add(Integer.toString(i)); // 或 String.valueOf(i) } } return answer; } }代码解释ListString answer new ArrayList(n);这里指定了列表的初始容量为n。因为结果列表的大小是已知的提前指定容量可以避免ArrayList在添加元素时多次进行耗时的内部数组扩容操作。这是一个针对性能的小优化。Integer.toString(i)将整数i转换为字符串。版本二使用 StringBuilder 预判进阶优化在极致的性能优化场景下虽然对本题不必要我们可以减少字符串拼接的中间对象。标准解法中“Fizz”、“Buzz”和数字都是独立字符串。我们可以尝试用StringBuilder来构建结果。class Solution { public ListString fizzBuzz(int n) { ListString answer new ArrayList(n); StringBuilder sb new StringBuilder(); for (int i 1; i n; i) { boolean divisibleBy3 (i % 3 0); boolean divisibleBy5 (i % 5 0); if (divisibleBy3) { sb.append(Fizz); } if (divisibleBy5) { sb.append(Buzz); } if (sb.length() 0) { // 既不是3的倍数也不是5的倍数 sb.append(i); } answer.add(sb.toString()); sb.setLength(0); // 清空StringBuilder供下一次循环使用 } return answer; } }代码解释这个方法避免了if-else链而是通过两个布尔变量记录状态。它分别检查 3 和 5 的整除性并相应地追加字符串。如果两者都满足自然就拼接成了 “FizzBuzz”。如果StringBuilder最终为空说明当前数不被 3 或 5 整除则追加数字。每次循环后需要清空StringBuilder。这个方法减少了条件判断的嵌套但代码行数增多在n不大时优势不明显主要展示一种不同的思路。3.3 关键参数与逻辑选择在实现中我们主要处理一个输入参数n并做出了一些逻辑选择选择项方案A方案B理由与影响判断顺序先判断i%150先判断i%30 and i%50推荐A。i%15一次计算等价于两次求模加一次与运算更简洁高效。数字转换str(i)(Python) /Integer.toString(i)(Java)“” i(Java)推荐显式转换。显式调用转换方法意图更清晰。“”i在Java中会产生临时StringBuilder对象但在简单场景下可读性尚可。结果容器初始化指定初始容量Java不指定容量推荐指定容量。当n很大时能避免列表/动态数组多次扩容带来的性能损耗。循环变量从1开始包含n从0开始输出时i1推荐从1开始。更符合题目语义报数从1开始代码更直观。4. 运行验证与结果分析编写完代码后必须进行验证。我们以n 15为例因为 15 包含了所有情况Fizz, Buzz, FizzBuzz 普通数字。测试用例n 15预期输出[“1”, “2”, “Fizz”, “4”, “Buzz”, “Fizz”, “7”, “8”, “Fizz”, “Buzz”, “11”, “Fizz”, “13”, “14”, “FizzBuzz”]你可以在力扣的题目页面直接运行你的代码或者在本地的 IDE 中编写一个简单的main函数进行测试。Python 测试示例sol Solution() print(sol.fizzBuzz(15)) # 输出: [‘1‘, ’2‘, ’Fizz‘, ’4‘, ’Buzz‘, ’Fizz‘, ’7‘, ’8‘, ’Fizz‘, ’Buzz‘, ’11‘, ’Fizz‘, ’13‘, ’14‘, ’FizzBuzz‘]Java 测试示例public static void main(String[] args) { Solution sol new Solution(); System.out.println(sol.fizzBuzz(15)); // 输出: [1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz] }验证时重点检查几个关键点索引对应输出列表的第一个元素索引0是否对应数字1规则正确性3的倍数、5的倍数、15的倍数是否正确输出字符串类型普通数字是否已转换为字符串边界n1时是否返回[“1”]5. 常见问题与排查路径即使是这样简单的题目在实现或面试中也可能遇到问题。下面是一些常见坑点及其解决方法。5.1 问题一输出全部是数字没有 “Fizz” 或 “Buzz”现象运行程序结果列表里全是数字字符串规则似乎没生效。可能原因与排查条件判断顺序错误最常见的原因是把i % 3 0或i % 5 0的判断放在了i % 15 0之前。例如数字15既满足i%30也满足i%150但如果先判断i%30程序就会进入“Fizz”分支并跳过后续判断。检查确认你的if-elif-else链中i % 15 0的条件是否排在第一位。求模运算符错误误写成了除法/而不是取余%。i / 15 0在整数运算中只有i0时才成立。检查确认所有条件判断中使用的都是%。逻辑运算符错误在判断“同时整除”时错误地使用了||(或) 而不是(且)。检查如果使用了i%30 i%50这种写法请确认是。5.2 问题二输出结果中混入了整数类型现象返回的列表里有些元素是字符串如“Fizz”有些却是整数如4。可能原因与排查忘记类型转换在else分支中直接answer.append(i)而不是answer.append(str(i))。检查在输出数字的分支确保使用了str(i)(Python) 或Integer.toString(i)(Java)。5.3 问题三列表索引从0开始但题目要求从1开始对应现象理解错题意认为answer[0]应该对应n0的情况导致整个逻辑错位。可能原因与排查错误理解题意题目描述中的answer[i]指的是第i个元素1-based index即answer中存储的第一个字符串对应数字1。检查你的循环是否从1开始到n结束包含正确写法是for i in range(1, n1)或for (int i1; in; i)。5.4 通用排查清单当你的 Fizz Buzz 代码没有通过测试或行为异常时可以按以下顺序检查输入验证确认函数接收到的n是正数。虽然题目保证n1但思考边界是加分项。循环范围确认循环变量i的起始值1和终止值n包含。条件顺序确认判断“FizzBuzz”或i%150的条件是否在最前面。运算符核对确认所有条件中使用的都是取余运算符%。类型一致性确认所有分支添加到结果列表中的都是字符串类型。输出验证用一个小例子如n5手动模拟或打印中间结果看是否符合预期。6. 最佳实践与扩展思考掌握了基础解法后我们可以思考如何写出更健壮、更易维护的代码以及问题可能的变体。6.1 编码最佳实践可读性优先对于如此简单的问题最直接的if-elif-else链通常就是可读性最好的。不要为了“炫技”而使用过于晦涩的写法如复杂的字典映射或嵌套三元运算符除非你能保证其清晰性。避免魔法数字在真实项目中可以将 3、5、15 等数字定义为常量如FIZZ_DIVISOR 3提高代码的可维护性。如果规则需要改变例如改成 4 和 7只需修改常量即可。class Solution: FIZZ_DIVISOR 3 BUZZ_DIVISOR 5 def fizzBuzz(self, n: int): answer [] for i in range(1, n1): if i % (self.FIZZ_DIVISOR * self.BUZZ_DIVISOR) 0: answer.append(“FizzBuzz”) elif i % self.FIZZ_DIVISOR 0: answer.append(“Fizz”) elif i % self.BUZZ_DIVISOR 0: answer.append(“Buzz”) else: answer.append(str(i)) return answer考虑扩展性面试加分项如果面试官问“规则增加到3个、4个除数怎么办”你可以提到“查表法”或“字符串拼接法”。字符串拼接法就像我们之前 Java 的StringBuilder版本为每个除数准备一个映射{3: “Fizz“, 5: “Buzz“, 7: “Jazz“}遍历映射如果整除就拼接对应的字符串。最后如果字符串为空则输出数字。优点添加新规则只需修改映射表符合“开闭原则”。6.2 性能与内存考量对于本题n通常不会极大力扣测试用例一般不超过 10^4所以时间复杂度 O(n) 和空间复杂度 O(n) 都是完全可以接受的。任何试图优化到低于 O(n) 的尝试都是没有意义的因为你必须遍历每个数并产生输出。微优化点在于减少求模运算使用i % 15代替i % 3 0 and i % 5 0。预分配列表/数组空间在 Java 中new ArrayList(n)在 Python 中虽然不能直接预分配列表大小但可以预先创建固定大小的列表answer [None] * n然后通过索引赋值但这会牺牲一些代码简洁性。注意在绝大多数情况下代码的清晰性和正确性远比这些微优化重要。不要过早优化。6.3 问题变体与扩展Fizz Buzz 有很多变体理解基础解法后你可以轻松应对自定义映射不一定是 3 和 5可能是任意数字和对应的字符串。范围变化不从 1 开始或者到 n 结束但不包含 n。输出格式变化不返回列表而是直接打印或者用特定分隔符连接成一个字符串。并发FizzBuzz一个经典的并发编程面试题多个线程协作按顺序打印数字或单词。这完全上升到了另一个难度涉及线程同步机制。解决这些变体的核心依然是清晰定义规则正确处理条件判断的顺序和边界保证输出格式。6.4 在面试中如何阐述如果你在面试中被问到这道题在写出代码后可以主动进行以下阐述展示你的思维深度陈述基础解法“这是一个简单的循环和条件判断问题。我从1遍历到n优先检查是否能被15整除然后依次检查3和5。”解释关键选择“我选择先检查15因为它等价于同时被3和5整除且一次求模运算效率稍高。”提及边界和细节“我注意了循环的起止范围1到n以及最后将数字转换为字符串。”讨论扩展性“如果规则需要扩展例如增加更多除数我可能会采用一种更通用的方法比如使用一个除数单词的映射表遍历每个数时根据映射表拼接字符串。”分析复杂度“时间和空间复杂度都是 O(n)这是最优的因为我们必须生成 n 个输出。”通过这样的解答你展示的不仅仅是编码能力还有沟通能力、对细节的关注以及面向未来的设计思维。Fizz Buzz 作为起点其价值正在于此。