Facebook Pixel

1115. Print FooBar Alternately

MediumConcurrency
Leetcode Link

Problem Description

You have a class FooBar with two methods: foo() and bar(). Each method prints its respective string ("foo" or "bar") n times in a loop.

Two separate threads will be created:

  • Thread A will call the foo() method
  • Thread B will call the bar() method

Both threads will run concurrently on the same instance of FooBar. Without synchronization, the output could be in any order (like "foofoofoobarbar" or "barbarfoofoo").

Your task is to modify the program using synchronization mechanisms to ensure that the output always alternates between "foo" and "bar", producing exactly "foobar" repeated n times. For example:

  • If n = 1, the output should be: "foobar"
  • If n = 2, the output should be: "foobarfoobar"
  • If n = 3, the output should be: "foobarfoobarfoobar"

The solution uses two semaphores to coordinate the threads:

  • Semaphore f (initialized to 1) controls when "foo" can be printed
  • Semaphore b (initialized to 0) controls when "bar" can be printed

The synchronization works as follows:

  1. Thread A acquires semaphore f, prints "foo", then releases semaphore b
  2. Thread B acquires semaphore b, prints "bar", then releases semaphore f
  3. This pattern repeats n times, ensuring strict alternation

The semaphores act as signals between threads - when one thread finishes printing, it signals the other thread that it's their turn to print. This guarantees the correct "foobar" pattern regardless of thread scheduling.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we have two threads that need to alternate their execution in a strict pattern, we need a way for them to communicate and coordinate. Think of it like two people taking turns speaking - one person needs to signal when they're done so the other knows it's their turn.

The key insight is that we need to enforce a specific ordering: "foo" must always come before "bar", and after "bar" is printed, "foo" can print again. This is a classic producer-consumer pattern where one thread produces a signal for the other to consume.

We can achieve this coordination using semaphores, which act like permission slips. A semaphore with value 1 means "you have permission to proceed," while a value of 0 means "you must wait."

Here's the reasoning:

  1. Initially, we want "foo" to print first, so we give the foo thread permission to start (semaphore f = 1) while making the bar thread wait (semaphore b = 0).

  2. After "foo" prints, it needs to tell "bar" that it's now bar's turn. This is done by releasing semaphore b (changing it from 0 to 1), giving the bar thread permission to proceed.

  3. Similarly, after "bar" prints, it needs to tell "foo" that foo can go again. This is done by releasing semaphore f.

  4. The acquire operations ensure that each thread waits for its turn - foo waits for semaphore f to be available, and bar waits for semaphore b to be available.

This creates a ping-pong effect: foo → signals bar → bar → signals foo → foo → signals bar... continuing for n iterations. The semaphores essentially create a token-passing mechanism where only the thread holding the token can execute, and after execution, it passes the token to the other thread.

Solution Approach

The implementation uses two semaphores to control the execution order of the threads. Let's walk through how this works:

Initialization:

  • Create semaphore f with initial value 1 - this allows the foo thread to execute first
  • Create semaphore b with initial value 0 - this blocks the bar thread initially

The foo method execution:

for _ in range(self.n):
    self.f.acquire()  # Wait for permission to print "foo"
    printFoo()        # Print "foo"
    self.b.release()  # Give permission for "bar" to print
  1. self.f.acquire() - The thread attempts to acquire semaphore f. On the first iteration, f = 1, so it proceeds immediately and decrements f to 0. On subsequent iterations, it waits until the bar thread releases f.
  2. printFoo() - Once acquired, it prints "foo"
  3. self.b.release() - It then releases semaphore b, incrementing it from 0 to 1, signaling the bar thread that it can now proceed

The bar method execution:

for _ in range(self.n):
    self.b.acquire()  # Wait for permission to print "bar"
    printBar()        # Print "bar"
    self.f.release()  # Give permission for "foo" to print
  1. self.b.acquire() - The thread attempts to acquire semaphore b. Initially b = 0, so it blocks until the foo thread releases b. Once available, it decrements b to 0.
  2. printBar() - Once acquired, it prints "bar"
  3. self.f.release() - It then releases semaphore f, incrementing it from 0 to 1, allowing the foo thread to proceed with its next iteration

Execution Flow for n = 2:

  1. Initial state: f = 1, b = 0
  2. Thread A (iteration 1): acquires f (now f = 0), prints "foo", releases b (now b = 1)
  3. Thread B (iteration 1): acquires b (now b = 0), prints "bar", releases f (now f = 1)
  4. Thread A (iteration 2): acquires f (now f = 0), prints "foo", releases b (now b = 1)
  5. Thread B (iteration 2): acquires b (now b = 0), prints "bar", releases f (now f = 1)
  6. Output: "foobarfoobar"

The semaphores create a strict alternation pattern where each thread must wait for the other to complete before proceeding, ensuring the output is always in the correct "foobar" order repeated n times.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with n = 2 to see how the semaphores coordinate the threads.

Initial Setup:

  • Semaphore f = 1 (foo can start)
  • Semaphore b = 0 (bar must wait)
  • Thread A runs foo() method
  • Thread B runs bar() method

Step-by-Step Execution:

StepThread A (foo)Thread B (bar)Semaphore fSemaphore bOutput
1f.acquire() - succeeds, f becomes 0b.acquire() - blocks (b is 0)00
2printFoo() - prints "foo"(still blocked)00"foo"
3b.release() - b becomes 1(still blocked)01"foo"
4Starts iteration 2: f.acquire() - blocks (f is 0)b.acquire() - succeeds, b becomes 000"foo"
5(still blocked)printBar() - prints "bar"00"foobar"
6(still blocked)f.release() - f becomes 110"foobar"
7f.acquire() - succeeds, f becomes 0Starts iteration 2: b.acquire() - blocks (b is 0)00"foobar"
8printFoo() - prints "foo"(still blocked)00"foobarfoo"
9b.release() - b becomes 1(still blocked)01"foobarfoo"
10Loop completeb.acquire() - succeeds, b becomes 000"foobarfoo"
11printBar() - prints "bar"00"foobarfoobar"
12f.release() - f becomes 110"foobarfoobar"

Key Observations:

  1. Thread A always waits for semaphore f before printing "foo"
  2. Thread B always waits for semaphore b before printing "bar"
  3. After printing, each thread signals the other by releasing the opposite semaphore
  4. The initial values (f=1, b=0) ensure "foo" prints first
  5. The alternating acquire/release pattern creates a handshake mechanism that enforces strict ordering

This synchronization guarantees that regardless of how the operating system schedules these threads, the output will always be "foobarfoobar" for n=2.

Solution Implementation

1from threading import Semaphore
2from typing import Callable
3
4
5class FooBar:
6    """
7    A class that coordinates two threads to print "foo" and "bar" alternately.
8  
9    This implementation uses semaphores to ensure that "foo" and "bar" are printed
10    in alternating order, regardless of which thread runs first or how the threads
11    are scheduled.
12    """
13  
14    def __init__(self, n: int) -> None:
15        """
16        Initialize the FooBar synchronization object.
17      
18        Args:
19            n: The number of times each method (foo and bar) should print.
20        """
21        self.n = n
22        # Semaphore for foo method - initially available (value=1)
23        # This allows foo to run first
24        self.foo_semaphore = Semaphore(1)
25      
26        # Semaphore for bar method - initially blocked (value=0)
27        # This ensures bar waits for foo to complete first
28        self.bar_semaphore = Semaphore(0)
29
30    def foo(self, printFoo: Callable[[], None]) -> None:
31        """
32        Print "foo" n times, alternating with bar method.
33      
34        Args:
35            printFoo: A callable that prints "foo" when invoked.
36        """
37        for _ in range(self.n):
38            # Wait for permission to print foo
39            # First iteration: semaphore is available (value=1)
40            # Subsequent iterations: wait for bar to release this semaphore
41            self.foo_semaphore.acquire()
42          
43            # printFoo() outputs "foo". Do not change or remove this line.
44            printFoo()
45          
46            # Signal bar method that foo has completed
47            # This allows bar to proceed with its print
48            self.bar_semaphore.release()
49
50    def bar(self, printBar: Callable[[], None]) -> None:
51        """
52        Print "bar" n times, alternating with foo method.
53      
54        Args:
55            printBar: A callable that prints "bar" when invoked.
56        """
57        for _ in range(self.n):
58            # Wait for foo to complete before printing bar
59            # First iteration: blocked until foo releases this semaphore
60            # Subsequent iterations: wait for foo to release this semaphore
61            self.bar_semaphore.acquire()
62          
63            # printBar() outputs "bar". Do not change or remove this line.
64            printBar()
65          
66            # Signal foo method that bar has completed
67            # This allows foo to proceed with its next iteration
68            self.foo_semaphore.release()
69
1class FooBar {
2    private int n;
3    // Semaphore for controlling "foo" execution - initialized with 1 permit (foo goes first)
4    private Semaphore fooSemaphore = new Semaphore(1);
5    // Semaphore for controlling "bar" execution - initialized with 0 permits (bar waits)
6    private Semaphore barSemaphore = new Semaphore(0);
7
8    /**
9     * Constructor to initialize the FooBar instance with the number of iterations
10     * @param n Number of times to print "foobar"
11     */
12    public FooBar(int n) {
13        this.n = n;
14    }
15
16    /**
17     * Method executed by the "foo" thread
18     * Prints "foo" n times, alternating with bar
19     * @param printFoo Runnable that prints "foo"
20     * @throws InterruptedException if thread is interrupted while waiting
21     */
22    public void foo(Runnable printFoo) throws InterruptedException {
23        for (int i = 0; i < n; i++) {
24            // Wait for permission to print "foo" (acquire permit from fooSemaphore)
25            fooSemaphore.acquire();
26          
27            // printFoo.run() outputs "foo". Do not change or remove this line.
28            printFoo.run();
29          
30            // Signal that "bar" can now execute (release permit to barSemaphore)
31            barSemaphore.release();
32        }
33    }
34
35    /**
36     * Method executed by the "bar" thread
37     * Prints "bar" n times, alternating with foo
38     * @param printBar Runnable that prints "bar"
39     * @throws InterruptedException if thread is interrupted while waiting
40     */
41    public void bar(Runnable printBar) throws InterruptedException {
42        for (int i = 0; i < n; i++) {
43            // Wait for permission to print "bar" (acquire permit from barSemaphore)
44            barSemaphore.acquire();
45          
46            // printBar.run() outputs "bar". Do not change or remove this line.
47            printBar.run();
48          
49            // Signal that "foo" can now execute (release permit to fooSemaphore)
50            fooSemaphore.release();
51        }
52    }
53}
54
1#include <semaphore.h>
2#include <functional>
3
4class FooBar {
5private:
6    int n;                    // Number of times to print "foobar"
7    sem_t semaphoreFoo;       // Semaphore to control foo() execution
8    sem_t semaphoreBar;       // Semaphore to control bar() execution
9
10public:
11    FooBar(int n) {
12        this->n = n;
13        // Initialize semaphoreFoo with value 1 (foo goes first)
14        sem_init(&semaphoreFoo, 0, 1);
15        // Initialize semaphoreBar with value 0 (bar waits initially)
16        sem_init(&semaphoreBar, 0, 0);
17    }
18
19    void foo(std::function<void()> printFoo) {
20        for (int i = 0; i < n; i++) {
21            // Wait for permission to print "foo"
22            sem_wait(&semaphoreFoo);
23          
24            // printFoo() outputs "foo". Do not change or remove this line.
25            printFoo();
26          
27            // Signal bar() that it can now print "bar"
28            sem_post(&semaphoreBar);
29        }
30    }
31
32    void bar(std::function<void()> printBar) {
33        for (int i = 0; i < n; i++) {
34            // Wait for foo() to complete before printing "bar"
35            sem_wait(&semaphoreBar);
36          
37            // printBar() outputs "bar". Do not change or remove this line.
38            printBar();
39          
40            // Signal foo() that it can print "foo" again
41            sem_post(&semaphoreFoo);
42        }
43    }
44};
45
1// Number of times to print "foobar"
2let n: number;
3
4// Semaphore-like structures using Promises
5let semaphoreFoo: Promise<void>;
6let semaphoreBar: Promise<void>;
7let resolveFoo: (() => void) | null = null;
8let resolveBar: (() => void) | null = null;
9
10/**
11 * Initializes the synchronization mechanism
12 * @param count - Number of times to print "foobar"
13 */
14function initializeFooBar(count: number): void {
15    n = count;
16  
17    // Initialize semaphoreFoo with immediate resolution (foo goes first)
18    semaphoreFoo = Promise.resolve();
19  
20    // Initialize semaphoreBar with pending promise (bar waits initially)
21    semaphoreBar = new Promise<void>((resolve) => {
22        resolveBar = resolve;
23    });
24}
25
26/**
27 * Prints "foo" n times, alternating with bar()
28 * @param printFoo - Callback function that prints "foo"
29 */
30async function foo(printFoo: () => void): Promise<void> {
31    for (let i = 0; i < n; i++) {
32        // Wait for permission to print "foo"
33        await semaphoreFoo;
34      
35        // Create new promise for next iteration of foo
36        semaphoreFoo = new Promise<void>((resolve) => {
37            resolveFoo = resolve;
38        });
39      
40        // printFoo() outputs "foo". Do not change or remove this line.
41        printFoo();
42      
43        // Signal bar() that it can now print "bar"
44        if (resolveBar) {
45            resolveBar();
46        }
47    }
48}
49
50/**
51 * Prints "bar" n times, alternating with foo()
52 * @param printBar - Callback function that prints "bar"
53 */
54async function bar(printBar: () => void): Promise<void> {
55    for (let i = 0; i < n; i++) {
56        // Wait for foo() to complete before printing "bar"
57        await semaphoreBar;
58      
59        // Create new promise for next iteration of bar
60        semaphoreBar = new Promise<void>((resolve) => {
61            resolveBar = resolve;
62        });
63      
64        // printBar() outputs "bar". Do not change or remove this line.
65        printBar();
66      
67        // Signal foo() that it can print "foo" again
68        if (resolveFoo) {
69            resolveFoo();
70        }
71    }
72}
73

Time and Space Complexity

The time complexity is O(n), where n is the parameter passed to the constructor. This is because both the foo and bar methods execute their loops exactly n times. Each iteration performs a constant amount of work - acquiring a semaphore, calling a print function, and releasing a semaphore are all O(1) operations. Therefore, the total time complexity is O(n) * O(1) = O(n).

The space complexity is O(1). The class only uses a fixed amount of extra space regardless of the input size n. It stores:

  • One integer variable self.n
  • Two semaphore objects self.f and self.b

The semaphores themselves use constant space as they only maintain internal state (counter and queue) that doesn't grow with n. The loop variables in both methods also use constant space. Since the space usage doesn't scale with the input size, the space complexity is O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Semaphore Initialization Values

One of the most common mistakes is initializing both semaphores with the same value or swapping their initial values. This can lead to deadlock or incorrect output ordering.

Incorrect Implementation:

def __init__(self, n: int) -> None:
    self.n = n
    # Wrong: Both initialized to 1
    self.foo_semaphore = Semaphore(1)
    self.bar_semaphore = Semaphore(1)  # Should be 0!

Problem: If both semaphores start at 1, both threads can execute simultaneously on their first iteration, potentially producing "barfoo" instead of "foobar".

Another Incorrect Version:

def __init__(self, n: int) -> None:
    self.n = n
    # Wrong: Swapped initial values
    self.foo_semaphore = Semaphore(0)  # Should be 1!
    self.bar_semaphore = Semaphore(1)  # Should be 0!

Problem: This causes "bar" to print first, resulting in "barfoo" pattern instead of "foobar".

2. Swapping acquire() and release() Operations

Another critical mistake is confusing which semaphore to acquire versus which to release in each method.

Incorrect Implementation:

def foo(self, printFoo: Callable[[], None]) -> None:
    for _ in range(self.n):
        self.bar_semaphore.acquire()  # Wrong semaphore!
        printFoo()
        self.foo_semaphore.release()  # Wrong semaphore!

Problem: This creates a deadlock immediately. The foo thread tries to acquire a semaphore that starts at 0, blocking forever since only the bar thread can release it, but bar is also waiting.

3. Using a Single Semaphore

Some might attempt to use just one semaphore, thinking it's simpler.

Incorrect Implementation:

def __init__(self, n: int) -> None:
    self.n = n
    self.semaphore = Semaphore(1)

def foo(self, printFoo: Callable[[], None]) -> None:
    for _ in range(self.n):
        self.semaphore.acquire()
        printFoo()
        self.semaphore.release()

def bar(self, printBar: Callable[[], None]) -> None:
    for _ in range(self.n):
        self.semaphore.acquire()
        printBar()
        self.semaphore.release()

Problem: A single semaphore only provides mutual exclusion but doesn't enforce ordering. The output could be "foofoobarbar" or "barbarfoofoo" or any other combination.

4. Missing the Loop Structure

Forgetting that each method needs to execute n times, not just once.

Incorrect Implementation:

def foo(self, printFoo: Callable[[], None]) -> None:
    # Missing loop - only executes once!
    self.foo_semaphore.acquire()
    printFoo()
    self.bar_semaphore.release()

Problem: This would only print "foobar" once regardless of the value of n.

5. Race Condition in Initialization

While less common, attempting to use other synchronization primitives incorrectly:

Incorrect Implementation Using Lock:

def __init__(self, n: int) -> None:
    self.n = n
    self.lock = threading.Lock()
    self.turn = "foo"

def foo(self, printFoo: Callable[[], None]) -> None:
    for _ in range(self.n):
        while True:
            with self.lock:
                if self.turn == "foo":
                    printFoo()
                    self.turn = "bar"
                    break
            # Busy waiting - inefficient!

def bar(self, printBar: Callable[[], None]) -> None:
    for _ in range(self.n):
        while True:
            with self.lock:
                if self.turn == "bar":
                    printBar()
                    self.turn = "foo"
                    break
            # Busy waiting - inefficient!

Problem: While this might work, it uses busy-waiting (spinning), which wastes CPU cycles. Semaphores are more efficient as they block the thread properly.

Correct Solution Reminder:

The correct approach uses two semaphores with specific initial values:

  • foo_semaphore starts at 1 (allowing foo to go first)
  • bar_semaphore starts at 0 (forcing bar to wait)
  • Each method acquires its own semaphore and releases the other's semaphore
  • This creates a ping-pong effect ensuring strict alternation
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More