Fizz Buzz in C: Naive, Concatenation & Table Approaches
Fizz Buzz in C: A Comprehensive Tutorial
Fizz Buzz is one of the most famous beginner programming problems, widely used in coding interviews to test basic control flow, loops, and modular arithmetic. Despite its simplicity, it can be extended in interesting ways that reveal software design principles such as scalability, maintainability, and separation of concerns.
This tutorial provides an in-depth exploration of Fizz Buzz in C: fundamental concepts, three progressive approaches (naive, string concatenation, hash map), extensions for additional rules, practical implementation, common pitfalls, and best practices.
Conceptual Introduction & Fundamentals
What is Fizz Buzz?
Given an integer n, for every positive integer i ≤ n, print:
"FizzBuzz"ifiis divisible by both 3 and 5."Fizz"ifiis divisible by 3."Buzz"ifiis divisible by 5.iitself otherwise.
Examples:
text
Input: n = 3 Output: 1 2 Fizz Input: n = 10 Output: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz Input: n = 20 Output: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz
Why Fizz Buzz Matters
- Control flow mastery: Tests loops, conditionals, and modulo.
- Scalability insight: Reveals how naive branching doesn’t scale.
- Design patterns: Hash maps and concatenation show extensibility.
- Interview preparation: Extremely common warm-up problem.
- String handling: Illustrates safe string concatenation in C.
The Three Approaches
| Approach | Time Complexity | Space Complexity | Extensibility |
|---|---|---|---|
| Naive (individual checks) | O(n) | O(1) | Poor |
| String concatenation | O(n) | O(1) | Good |
| Hash map / table | O(n × k) | O(k) | Excellent |
Where k = number of divisor-word pairs.
The Scalability Problem
The naive approach checks every combination of divisors. For 3 rules, we need 2³ = 8 branches. For 10 rules, we’d need 1024 branches — clearly unsustainable.
Solution: Separate the divisor-word mappings from the logic. Concatenate matching words, then fall back to the number if no word matched.
Syntax & Core Mechanics
Approach 1: Naive Branching
c
#include <stdio.h> void fizzBuzzNaive(int n) { for (int i = 1; i <= n; i++) { if (i % 3 == 0 && i % 5 == 0) { printf("FizzBuzz "); } else if (i % 3 == 0) { printf("Fizz "); } else if (i % 5 == 0) { printf("Buzz "); } else { printf("%d ", i); } } printf("\n"); } int main(void) { fizzBuzzNaive(20); return 0; } Expected output:
text
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz
Time: O(n), Space: O(1)
Approach 2: String Concatenation
Build the string by appending each matching word.
c
#include <stdio.h> #include <string.h> void fizzBuzzConcat(int n) { for (int i = 1; i <= n; i++) { char s[20] = ""; /* Enough for "FizzBuzz" + null */ if (i % 3 == 0) strcat(s, "Fizz"); if (i % 5 == 0) strcat(s, "Buzz"); if (strlen(s) == 0) { snprintf(s, sizeof(s), "%d", i); } printf("%s ", s); } printf("\n"); } int main(void) { fizzBuzzConcat(20); return 0; } Expected output:
text
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz
Time: O(n × k), Space: O(1)
Advantage: Adding new rules requires only a new if — no combinatorial explosion.
Approach 3: Hash Map / Rule Table
Store divisor-word pairs in a table; iterate rules.
c
#include <stdio.h> #include <string.h> typedef struct { int divisor; const char *word; } Rule; void fizzBuzzTable(int n, const Rule rules[], int ruleCount) { for (int i = 1; i <= n; i++) { char s[64] = ""; for (int r = 0; r < ruleCount; r++) { if (i % rules[r].divisor == 0) { strcat(s, rules[r].word); } } if (strlen(s) == 0) { snprintf(s, sizeof(s), "%d", i); } printf("%s ", s); } printf("\n"); } int main(void) { Rule rules[] = { {3, "Fizz"}, {5, "Buzz"}, {7, "Jazz"} /* Extension: multiples of 7 */ }; fizzBuzzTable(25, rules, 3); return 0; } Expected output (partial):
text
1 2 Fizz 4 Buzz Fizz Jazz 8 Fizz Buzz 11 Fizz 13 Jazz FizzBuzz 16 17 Fizz 19 Buzz FizzJazz 22 23 Fizz Buzz
Time: O(n × k), Space: O(k)
Full Practical Code Example with Explanation
This program demonstrates all three approaches plus an extended rule set.
c
/** * fizzbuzz.c * Demonstrates Fizz Buzz implementations: * 1. Naive branching * 2. String concatenation * 3. Table-driven rules * 4. Extended rules (Fizz, Buzz, Jazz) * 5. Safe string building and memory */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MAX_LINE_LEN 100 /* ---------- Approach 1: Naive ---------- */ void fizzBuzzNaive(int n) { for (int i = 1; i <= n; i++) { if (i % 15 == 0) printf("FizzBuzz "); else if (i % 3 == 0) printf("Fizz "); else if (i % 5 == 0) printf("Buzz "); else printf("%d ", i); } printf("\n"); } /* ---------- Approach 2: String concatenation ---------- */ void fizzBuzzConcat(int n) { char s[32]; for (int i = 1; i <= n; i++) { s[0] = '\0'; if (i % 3 == 0) strncat(s, "Fizz", sizeof(s) - strlen(s) - 1); if (i % 5 == 0) strncat(s, "Buzz", sizeof(s) - strlen(s) - 1); if (s[0] == '\0') snprintf(s, sizeof(s), "%d", i); printf("%s ", s); } printf("\n"); } /* ---------- Approach 3: Table-driven ---------- */ typedef struct { int divisor; const char *word; } Rule; void fizzBuzzTable(int n, const Rule rules[], int ruleCount) { char s[64]; for (int i = 1; i <= n; i++) { s[0] = '\0'; for (int r = 0; r < ruleCount; r++) { if (i % rules[r].divisor == 0) { strncat(s, rules[r].word, sizeof(s) - strlen(s) - 1); } } if (s[0] == '\0') snprintf(s, sizeof(s), "%d", i); printf("%s ", s); } printf("\n"); } /* ---------- Collector (returns strings) ---------- */ char *fizzBuzzCollector(int n, const Rule rules[], int ruleCount, size_t *outCount) { /* Worst-case: each entry ≤ 63 chars + space */ size_t cap = (size_t)n * 64 + 1; char *result = malloc(cap); if (result == NULL) return NULL; result[0] = '\0'; size_t used = 0; for (int i = 1; i <= n; i++) { char s[64]; s[0] = '\0'; for (int r = 0; r < ruleCount; r++) { if (i % rules[r].divisor == 0) { strncat(s, rules[r].word, sizeof(s) - strlen(s) - 1); } } if (s[0] == '\0') snprintf(s, sizeof(s), "%d", i); size_t len = strlen(s); if (used + len + 2 > cap) break; /* Safety */ memcpy(result + used, s, len); used += len; result[used++] = ' '; result[used] = '\0'; } *outCount = used; return result; } /* ---------- Utility ---------- */ static void printHeader(const char *title) { printf("\n=== %s ===\n", title); } static void demoNaive(void) { printHeader("Naive FizzBuzz (n=20)"); fizzBuzzNaive(20); } static void demoConcat(void) { printHeader("String Concatenation (n=20)"); fizzBuzzConcat(20); } static void demoTable(void) { printHeader("Table-Driven (Fizz/Buzz, n=20)"); Rule rules[] = {{3, "Fizz"}, {5, "Buzz"}}; fizzBuzzTable(20, rules, 2); } static void demoExtended(void) { printHeader("Extended Rules (Fizz/Buzz/Jazz, n=25)"); Rule rules[] = {{3, "Fizz"}, {5, "Buzz"}, {7, "Jazz"}}; fizzBuzzTable(25, rules, 3); } static void demoCollector(void) { printHeader("Collector (returns string)"); Rule rules[] = {{3, "Fizz"}, {5, "Buzz"}}; size_t count = 0; char *result = fizzBuzzCollector(15, rules, 2, &count); if (result == NULL) { printf("Allocation failed\n"); return; } printf("Result: %s\n", result); printf("Length: %zu bytes\n", count); free(result); } static void demoEdgeCases(void) { printHeader("Edge Cases"); printf("n = 0:\n "); fizzBuzzConcat(0); printf("(empty)\n"); printf("\nn = 1:\n "); fizzBuzzConcat(1); printf("\nn = 3:\n "); fizzBuzzConcat(3); printf("\nn = 5:\n "); fizzBuzzConcat(5); printf("\nn = 15 (FizzBuzz):\n "); fizzBuzzConcat(15); printf("\nNegative n = -5:\n "); fizzBuzzConcat(-5); printf("(empty)\n"); } static int readInteger(const char *prompt, int *outValue) { char buffer[MAX_LINE_LEN]; char *endPtr; long val; printf("%s", prompt); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) return -1; errno = 0; val = strtol(buffer, &endPtr, 10); if (endPtr == buffer || (*endPtr != '\n' && *endPtr != '\0')) return -1; if (errno == ERANGE || val < -2147483647 - 1 || val > 2147483647) return -1; *outValue = (int)val; return 0; } static void demoInteractive(void) { int n; if (readInteger("Enter n: ", &n) != 0) { printf("Invalid input.\n"); return; } if (n < 0) { printf("n must be non-negative.\n"); return; } printf("\nTable-driven output:\n "); Rule rules[] = {{3, "Fizz"}, {5, "Buzz"}}; fizzBuzzTable(n, rules, 2); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Fizz Buzz Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Naive (n=20)\n"); printf("2. String Concatenation (n=20)\n"); printf("3. Table-Driven (n=20)\n"); printf("4. Extended Rules Fizz/Buzz/Jazz (n=25)\n"); printf("5. Collector (returns string)\n"); printf("6. Edge Cases\n"); printf("7. Interactive\n"); printf("8. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: demoNaive(); break; case 2: demoConcat(); break; case 3: demoTable(); break; case 4: demoExtended(); break; case 5: demoCollector(); break; case 6: demoEdgeCases(); break; case 7: demoInteractive(); break; case 8: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; } Explanation of Approaches
- Naive: Uses explicit branches. Fast and simple; doesn’t scale with more rules.
- Concatenation: Appends matching words; scales linearly with rule count.
- Table-Driven: Stores rules in an array; adding rules requires no code changes.
- Collector: Builds an output string with dynamic memory.
- Edge cases: Zero, one, single rules, negatives.
Expected Output (sample run)
text
=== Naive FizzBuzz (n=20) === 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz === String Concatenation (n=20) === 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz === Table-Driven (Fizz/Buzz, n=20) === 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz === Extended Rules (Fizz/Buzz/Jazz, n=25) === 1 2 Fizz 4 Buzz Fizz Jazz 8 Fizz Buzz 11 Fizz 13 Jazz FizzBuzz 16 17 Fizz 19 Buzz FizzJazz 22 23 Fizz Buzz === Collector (returns string) === Result: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz Length: 51 bytes === Edge Cases === n = 0: (empty) n = 1: 1 n = 3: 1 2 Fizz n = 5: 1 2 Fizz 4 Buzz n = 15 (FizzBuzz): 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz Negative n = -5: (empty)
Common Pitfalls, Edge Cases & Memory Considerations
1. Checking Both Conditions Separately
Using i % 3 == 0 && i % 5 == 0 in the naive approach works, but ordering is crucial — the combined case must come first.
c
if (i % 15 == 0) /* FizzBuzz first */ else if (i % 3 == 0) /* Fizz */ else if (i % 5 == 0) /* Buzz */
2. Buffer Overflow in Concatenation
Concatenating multiple words can overflow small buffers.
Solution: Size buffers generously (char s[64]) and use strncat with proper bounds.
c
strncat(s, "Fizz", sizeof(s) - strlen(s) - 1);
3. Using strcat Without Initializing
strcat on uninitialized buffer is undefined behavior.
c
char s[20]; /* BUG: not initialized */ strcat(s, "Fizz"); char s[20] = ""; /* Correct */
4. sprintf Deprecation
sprintf lacks bounds checking.
Solution: Use snprintf.
c
snprintf(s, sizeof(s), "%d", i);
5. Negative n
Loop for (int i = 1; i <= n; i++) naturally skips when n < 0. Document this behavior.
6. Zero n
Empty output is correct.
7. Duplicate Divisors
If two rules share the same divisor, both words are appended (correct). But if divisors are non-coprime, results may surprise.
8. Case Sensitivity
Rule words should be consistent. "Fizz" vs "fizz" matters if compared later.
9. Unicode / Multibyte
strlen, strcat operate on bytes. For UTF-8, still works, but lengths differ from display width.
10. Memory Considerations
- Naive/concatenation: O(1) extra space.
- Collector: O(n × word length) — must
free. - Table: O(k) rule storage.
Summary & Best Practices
Fizz Buzz is a deceptively simple problem that teaches fundamental control flow and scalable design. The table-driven approach is the most extensible and is preferred when rules may grow.
Key Takeaways
- Naive: simple but doesn’t scale with more rules.
- Concatenation: appends matching words — linear in rules.
- Table-driven: clean, extensible, easy to test.
- Order matters: check combined divisors before individual ones in naive approach.
- Use
snprintfandstrncatfor safe string operations. - Negative
nyields empty output.
Best Practices
- Prefer the table-driven approach for extensibility.
- Use
snprintfandstrncatfor safe string handling. - Initialize string buffers to
"". - Size buffers generously (e.g., 64 bytes).
- Free dynamically allocated result strings.
- Guard against negative or zero
n. - Keep rule arrays
constwhen possible. - Separate logic from presentation — collect results, then print.
- Test all edge cases: 0, 1, 15, 30.
- Document rule ordering semantics.
By mastering Fizz Buzz’s variants, you’ll be better prepared for designing extensible rule-based systems in real projects.



