用 Java 實作 FizzBuzz 謎題
1. 引言
在本教程中,我們將探索用 Java 解決 FizzBuzz 程式設計難題的多種方法。
2. 問題陳述
FizzBuzz 是一道經典的程式設計題,最初用於教導小學生除法。然而,2007 年,Imran Ghory 將其推廣為一道程式設計面試題。此後,程式設計界經常使用 FizzBuzz 來測試一些基本概念,例如條件邏輯、模運算和程式碼組織。
問題描述很簡單。給定一個整數n,我們按照以下規則從 1 迭代到n並列印對應的文字:
- 對於
3的倍數,我們印出“Fizz.” - 對於
5的倍數,我們印出“Buzz.” - 對於
3和5的倍數,我們列印“FizzBuzz.” - 否則我們就列印這個數字。
例如,當n = 15時,輸出應為:
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz 。
3. 簡單方法
預設的解決方案是使用取模運算子(%)來檢查是否可整除:
public List<String> fizzBuzzNaive(int n) {
List<String> result = new ArrayList<>();
for (int i = 1; i <= n; i++) {
if (i % 3 == 0 && i % 5 == 0) {
result.add("FizzBuzz");
} else if (i % 3 == 0) {
result.add("Fizz");
} else if (i % 5 == 0) {
result.add("Buzz");
} else {
result.add(String.valueOf(i));
}
}
return result;
}
條件的順序很重要。我們必須先檢查是否能被3和5整除;否則,根據各個檢查的順序,像 15 這樣的數字會錯誤地輸出“ Fizz ”或“Buzz”而不是“ FizzBuzz ”。
4. 連接法
這裡,我們使用 Java 中的StringBuilder來執行字串連接,從而避免明確檢查是否能被15整除。為了避免每次迭代都重複實例化一個新的StringBuilder所帶來的開銷,我們重複使用單一實例,並使用setLength(0)將其清除:
public List<String> fizzBuzzConcatenation(int n) {
List<String> result = new ArrayList<>();
StringBuilder output = new StringBuilder();
for (int i = 1; i <= n; i++) {
if (i % 3 == 0) {
output.append("Fizz");
}
if (i % 5 == 0) {
output.append("Buzz");
}
result.add(output.length() > 0 ? output.toString() : String.valueOf(i));
output.setLength(0);
}
return result;
}
這種方法預設處理“ FizzBuzz ”的情況。具體來說,當一個數字能同時被3和5整除時,我們會同時觸發這兩個條件。因此,我們會在數字後面附加「 Fizz 」和「 Buzz 」。
當需要新增條件時,這段程式碼更容易維護。新增條件只需編寫一個 if 語句即可。因此,與包含多個 if-then-else 程式碼區塊的簡單方法相比,此解決方案更易於閱讀。然而,如果條件過多,每個條件都放在單獨的 if 程式碼區塊中,也會使程式碼難以閱讀。
5. 基於反制措施的方法
對於較大的n值,取模運算的計算量非常大。我們可以使用計數器來消除它。和之前一樣,我們重複使用單一StringBuilder實例:
public List<String> fizzBuzzCounter(int n) {
List<String> result = new ArrayList<>();
StringBuilder output = new StringBuilder();
int fizz = 0;
int buzz = 0;
for (int i = 1; i <= n; i++) {
fizz++;
buzz++;
if (fizz == 3) {
output.append("Fizz");
fizz = 0;
}
if (buzz == 5) {
output.append("Buzz");
buzz = 0;
}
result.add(output.length() > 0 ? output.toString() : String.valueOf(i));
output.setLength(0);
}
return result;
}
在這種方法中,我們使用兩個計數器來追蹤與上一個3的倍數和上一個5的倍數之間的差異。當計數器達到目標值時,我們將對應的單字加到 StringBuilder 中,並將計數器重設為零。此方法的時間複雜度為O(n) ,空間複雜度為O(n) 。
6. 測試
首先,我們建立FizzBuzzUnitTest類別:
class FizzBuzzUnitTest {
private FizzBuzz fizzBuzz;
private static final List GROUND_TRUTH_SEQUENCE_LENGTH_5 = generateGroundTruth(5);
private static final List GROUND_TRUTH_SEQUENCE_LENGTH_100 = generateGroundTruth(100);
@BeforeEach
void setUp() {
fizzBuzz = new FizzBuzz();
}
private static List generateGroundTruth(int n) {
return IntStream.rangeClosed(1, n)
.mapToObj(i -> {
if (i % 15 == 0) return "FizzBuzz";
if (i % 3 == 0) return "Fizz";
if (i % 5 == 0) return "Buzz";
return String.valueOf(i);
})
.collect(Collectors.toList());
}
}
在這裡,我們將n = 5的真實輸出設為變數GROUND_TRUTH_SEQUENCE_LENGTH_5 ,將n = 100真實輸出設為變數GROUND_TRUTH_SEQUENCE_LENGTH_100 。
我們先針對n < 15的情況測試所有三種方法,取n = 5 :
@Test
void givenSequenceLength5_whenAllMethods_thenReturnCorrectSequence() {
List naiveResult = fizzBuzz.fizzBuzzNaive(5);
List concatResult = fizzBuzz.fizzBuzzConcatenation(5);
List counterResult = fizzBuzz.fizzBuzzCounter(5);
assertAll(
() -> assertEquals(GROUND_TRUTH_SEQUENCE_LENGTH_5, naiveResult,
"fizzBuzzNaive should return correct sequence for n=5"),
() -> assertEquals(GROUND_TRUTH_SEQUENCE_LENGTH_5, concatResult,
"fizzBuzzConcatenation should return correct sequence for n=5"),
() -> assertEquals(GROUND_TRUTH_SEQUENCE_LENGTH_5, counterResult,
"fizzBuzzCounter should return correct sequence for n=5")
);
}
另一個測試( n = 100)與之類似。
7. 結論
本文介紹了用 Java 解決 FizzBuzz 問題的多種方法。我們首先介紹了基於模運算的簡單方法,然後過渡到字串拼接方法以提高程式碼的簡潔性和可讀性,最後介紹了優化的基於計數器的解決方案,該方案無需進行模運算。字串拼接方法更易讀,但對於較大的n值(> 100),基於計數器的方法表現最佳。
像往常一樣,完整的源代碼可以在 GitHub 上找到。