LeetCode 1115 - Print FooBar Alternately
This problem is a classic concurrency synchronization task. We are given a class FooBar with two methods, foo() and bar().
Difficulty: 🟡 Medium
Topics: Concurrency
Solution
Problem Understanding
This problem is a classic concurrency synchronization task. We are given a class FooBar with two methods, foo() and bar(). Each method is executed by a separate thread:
- Thread A repeatedly calls
foo() - Thread B repeatedly calls
bar()
The goal is to ensure the output sequence becomes:
foobarfoobarfoobar...
exactly n times.
The challenge is that threads execute asynchronously. Without synchronization, the operating system scheduler may run one thread much faster than the other. For example, the foo() thread might print all "foo" values before the bar() thread runs at all, producing incorrect output like:
foofoofoobarbarbar
instead of:
foobarfoobarfoobar
The problem is therefore not about computation or data processing, but about coordinating execution order between threads.
The input consists of a single integer n, which specifies how many times the combined sequence "foobar" must appear. The expected output is an alternating pattern where every "foo" is immediately followed by "bar".
The constraints are small:
1 <= n <= 1000
This tells us performance is not the difficult part. The important challenge is correctness under concurrent execution. Even though n is small, thread synchronization bugs can still occur if the implementation is not carefully designed.
A few important observations emerge immediately:
- The
foo()method must always execute before the correspondingbar()call in each iteration. - The two threads must coordinate repeatedly, not just once.
- The solution must avoid race conditions.
- The solution must not deadlock, meaning both threads waiting forever.
- The output order must remain deterministic despite nondeterministic thread scheduling.
Edge cases mainly involve synchronization behavior:
n = 1, the smallest valid input, still requires strict ordering.- Fast execution by one thread should not break ordering.
- Repeated alternation across many iterations must remain correct.
Approaches
Brute Force Approach
A brute force idea would be to let both threads run independently and attempt to rely on timing or busy waiting to preserve ordering.
For example, one could repeatedly check a shared variable in a loop:
while turn != "foo":
pass
and similarly for "bar".
This technically works if implemented carefully, because each thread waits until it is its turn before printing. However, this approach wastes CPU cycles because the thread continuously spins while waiting. This is known as busy waiting or spin locking.
Busy waiting is inefficient because the processor repeatedly checks a condition instead of sleeping until notified. In real systems, this can severely degrade performance.
Another poor brute force strategy would be inserting artificial delays using sleep(). This is unreliable because thread scheduling timing is nondeterministic and depends on the operating system.
Optimal Approach
The correct solution uses synchronization primitives designed specifically for thread coordination.
A semaphore is an ideal choice here. A semaphore maintains an internal counter:
- If the counter is positive, a thread may proceed.
- If the counter is zero, the thread blocks until another thread releases the semaphore.
We use two semaphores:
- One controls when
foo()may execute. - One controls when
bar()may execute.
Initially:
fooSemaphore = 1barSemaphore = 0
This means:
foo()is allowed to run first.bar()is blocked initially.
After foo() prints "foo", it signals bar() by releasing the second semaphore.
After bar() prints "bar", it signals foo() by releasing the first semaphore.
This creates a strict alternating execution pattern.
The major insight is that synchronization primitives allow threads to sleep efficiently while waiting, rather than continuously consuming CPU time.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(1) | Uses busy waiting or timing hacks, inefficient and unreliable |
| Optimal | O(n) | O(1) | Uses semaphores for precise thread synchronization |
Algorithm Walkthrough
- Create two synchronization primitives, one for the
foo()thread and one for thebar()thread. - Initialize the semaphore controlling
foo()with value1. This allows thefoo()thread to execute immediately. - Initialize the semaphore controlling
bar()with value0. This forces thebar()thread to wait initially. - Inside
foo():
- Before printing, acquire the
foosemaphore. - If the semaphore is unavailable, the thread blocks automatically.
- Print
"foo". - Release the
barsemaphore so thebar()thread may proceed.
- Inside
bar():
- Acquire the
barsemaphore. - This guarantees
"foo"has already been printed for the current iteration. - Print
"bar". - Release the
foosemaphore so the next"foo"may execute.
- Repeat this process exactly
ntimes in both threads.
The semaphores enforce a strict alternating execution order:
foo -> bar -> foo -> bar
regardless of thread scheduling order.
Why it works
The key invariant is:
Only one thread is allowed to proceed at a time.
Initially, only foo() may run because its semaphore count is positive. After printing, foo() transfers execution permission to bar() by releasing the second semaphore.
Similarly, bar() transfers permission back to foo() after printing.
Because execution permission alternates after every print operation, the output must always follow the exact sequence:
foobarfoobar...
No race condition can violate this ordering because the semaphores serialize execution.
Python Solution
from threading import Semaphore
from typing import Callable
class FooBar:
def __init__(self, n: int):
self.n = n
self.foo_semaphore = Semaphore(1)
self.bar_semaphore = Semaphore(0)
def foo(self, printFoo: 'Callable[[], None]') -> None:
for _ in range(self.n):
self.foo_semaphore.acquire()
# printFoo() outputs "foo". Do not change or remove this line.
printFoo()
self.bar_semaphore.release()
def bar(self, printBar: 'Callable[[], None]') -> None:
for _ in range(self.n):
self.bar_semaphore.acquire()
# printBar() outputs "bar". Do not change or remove this line.
printBar()
self.foo_semaphore.release()
The constructor initializes two semaphores.
self.foo_semaphore = Semaphore(1)
allows the foo() thread to start immediately.
self.bar_semaphore = Semaphore(0)
forces the bar() thread to wait until "foo" has been printed.
Inside foo(), the thread first acquires its semaphore. Once permitted, it prints "foo" and then releases the bar semaphore. This wakes the bar() thread.
Inside bar(), the reverse process occurs. The thread waits until foo() signals it, prints "bar", and then releases the foo semaphore for the next iteration.
The implementation directly mirrors the synchronization strategy described in the algorithm walkthrough.
Go Solution
package main
import "sync"
type FooBar struct {
n int
fooChan chan struct{}
barChan chan struct{}
}
func NewFooBar(n int) *FooBar {
fb := &FooBar{
n: n,
fooChan: make(chan struct{}, 1),
barChan: make(chan struct{}, 1),
}
fb.fooChan <- struct{}{}
return fb
}
func (fb *FooBar) Foo(printFoo func()) {
for i := 0; i < fb.n; i++ {
<-fb.fooChan
// printFoo() outputs "foo". Do not change or remove this line.
printFoo()
fb.barChan <- struct{}{}
}
}
func (fb *FooBar) Bar(printBar func()) {
for i := 0; i < fb.n; i++ {
<-fb.barChan
// printBar() outputs "bar". Do not change or remove this line.
printBar()
fb.fooChan <- struct{}{}
}
}
Go does not have built in semaphores in the standard library, so buffered channels are commonly used for synchronization.
The channels behave similarly to semaphores:
- Receiving from a channel blocks until a value is available.
- Sending to a full buffered channel blocks until space exists.
The fooChan channel starts with one token, allowing Foo() to execute first. The barChan starts empty, forcing Bar() to wait.
After each print operation, execution permission is transferred by sending a token to the other channel.
Unlike Python, Go uses channels as a first class concurrency primitive, which makes the implementation concise and idiomatic.
Worked Examples
Example 1
Input:
n = 1
Initial semaphore state:
| Semaphore | Value |
|---|---|
| foo | 1 |
| bar | 0 |
Execution trace:
| Step | Thread | Action | foo Semaphore | bar Semaphore | Output |
|---|---|---|---|---|---|
| 1 | foo | acquire foo | 0 | 0 | "" |
| 2 | foo | print "foo" | 0 | 0 | "foo" |
| 3 | foo | release bar | 0 | 1 | "foo" |
| 4 | bar | acquire bar | 0 | 0 | "foo" |
| 5 | bar | print "bar" | 0 | 0 | "foobar" |
| 6 | bar | release foo | 1 | 0 | "foobar" |
Final output:
foobar
Example 2
Input:
n = 2
Execution trace:
| Iteration | Thread | Output After Step |
|---|---|---|
| 1 | foo | "foo" |
| 1 | bar | "foobar" |
| 2 | foo | "foobarfoo" |
| 2 | bar | "foobarfoobar" |
Final output:
foobarfoobar
The semaphores continue alternating correctly across multiple iterations.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each thread performs exactly n synchronization and print operations |
| Space | O(1) | Only a constant number of synchronization primitives are stored |
The time complexity is linear because each iteration performs a fixed amount of work. Semaphore operations are constant time.
The space complexity remains constant because the algorithm only stores two synchronization objects regardless of n.
Test Cases
import threading
def run_test(n):
output = []
def printFoo():
output.append("foo")
def printBar():
output.append("bar")
fb = FooBar(n)
thread1 = threading.Thread(target=fb.foo, args=(printFoo,))
thread2 = threading.Thread(target=fb.bar, args=(printBar,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
return "".join(output)
assert run_test(1) == "foobar" # minimum valid input
assert run_test(2) == "foobarfoobar" # basic alternating behavior
assert run_test(3) == "foobarfoobarfoobar" # multiple alternations
assert run_test(5) == "foobar" * 5 # repeated synchronization
assert run_test(10) == "foobar" * 10 # larger iteration count
assert len(run_test(100)) == 600 # stress test for repeated execution
assert run_test(100).startswith("foobar") # ordering preserved at start
assert run_test(100).endswith("foobar") # ordering preserved at end
| Test | Why |
|---|---|
n = 1 |
Validates smallest input |
n = 2 |
Verifies basic alternation |
n = 3 |
Ensures repeated synchronization works |
n = 5 |
Tests multiple thread handoffs |
n = 10 |
Checks stability over more iterations |
n = 100 length check |
Stress tests concurrency coordination |
startswith("foobar") |
Verifies correct initial ordering |
endswith("foobar") |
Verifies correct final ordering |
Edge Cases
One important edge case is the smallest possible input:
n = 1
Even with only one iteration, synchronization is still required. Without proper coordination, the bar() thread could theoretically attempt to print first. The semaphore initialization guarantees that foo() always executes before bar().
Another important edge case occurs when one thread runs significantly faster than the other. For example, the foo() thread might repeatedly attempt to execute before bar() has completed. Without synchronization, this could produce outputs like:
foofoofoo...
The semaphore blocks the fast thread until the slower thread finishes its turn, ensuring correct alternation regardless of scheduling speed.
A third important edge case involves many iterations, such as:
n = 1000
Repeated synchronization can expose deadlock bugs if semaphore releases are forgotten or misplaced. The implementation carefully releases the opposite semaphore after every successful print operation, guaranteeing progress throughout all iterations.