CoreTechED

Loading

Learn Fizz Buzz in C: naive branching, string concatenation, and hash-map table-driven approaches with extended rules. Complete code examples.

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" if i is divisible by both 3 and 5.
  • "Fizz" if i is divisible by 3.
  • "Buzz" if i is divisible by 5.
  • i itself 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

ApproachTime ComplexitySpace ComplexityExtensibility
Naive (individual checks)O(n)O(1)Poor
String concatenationO(n)O(1)Good
Hash map / tableO(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

  1. Naive: Uses explicit branches. Fast and simple; doesn’t scale with more rules.
  2. Concatenation: Appends matching words; scales linearly with rule count.
  3. Table-Driven: Stores rules in an array; adding rules requires no code changes.
  4. Collector: Builds an output string with dynamic memory.
  5. 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 snprintf and strncat for safe string operations.
  • Negative n yields empty output.

Best Practices

  1. Prefer the table-driven approach for extensibility.
  2. Use snprintf and strncat for safe string handling.
  3. Initialize string buffers to "".
  4. Size buffers generously (e.g., 64 bytes).
  5. Free dynamically allocated result strings.
  6. Guard against negative or zero n.
  7. Keep rule arrays const when possible.
  8. Separate logic from presentation — collect results, then print.
  9. Test all edge cases: 0, 1, 15, 30.
  10. Document rule ordering semantics.

By mastering Fizz Buzz’s variants, you’ll be better prepared for designing extensible rule-based systems in real projects.