Facebook Pixel

1116. Print Zero Even Odd

MediumConcurrency
Leetcode Link

Problem Description

This is a multithreading synchronization problem where you need to coordinate three threads to print a specific sequence of numbers.

You're given a class ZeroEvenOdd that will be shared among three threads:

  • Thread A repeatedly calls zero() to print 0
  • Thread B repeatedly calls even() to print even numbers (2, 4, 6, ...)
  • Thread C repeatedly calls odd() to print odd numbers (1, 3, 5, ...)

The goal is to synchronize these threads so they produce the output pattern: "010203040506..." up to number n.

The pattern follows this rule:

  • Before each positive number (1, 2, 3, ..., n), a 0 must be printed
  • The positive numbers alternate between odd and even in ascending order
  • The total length of the output sequence is 2n (n zeros and n positive numbers)

For example, if n = 5, the output should be: "0102030405"

The class provides these methods:

  • ZeroEvenOdd(int n): Constructor that initializes with the maximum number n
  • zero(printNumber): Should print 0 using the provided printNumber function
  • even(printNumber): Should print the next even number using printNumber
  • odd(printNumber): Should print the next odd number using printNumber

The challenge is to implement proper synchronization using semaphores so that:

  1. Zero is always printed before each positive number
  2. Odd and even numbers alternate correctly in sequence
  3. No race conditions or deadlocks occur

The solution uses three semaphores to control the execution order:

  • Semaphore z (initially 1): Controls when zero() can execute
  • Semaphore e (initially 0): Controls when even() can execute
  • Semaphore o (initially 0): Controls when odd() can execute

The zero() method determines whether to release the odd or even semaphore based on which positive number comes next in the sequence.

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

Intuition

The key insight is recognizing that this is a producer-consumer pattern with a specific ordering requirement. We need to enforce a strict sequence: 0 → odd → 0 → even → 0 → odd → ...

Let's think about the execution flow:

  1. A zero must always come first
  2. After each zero, we need either an odd or even number
  3. After each positive number, we need another zero

This creates a cyclic dependency where each thread must wait for specific other threads to complete before proceeding.

The natural approach is to use semaphores as "permission tokens" that threads pass to each other. Think of it like a relay race where only the runner with the baton can run:

  • Initially, only the zero() thread has permission to run (baton)
  • After zero() prints, it needs to decide who gets the baton next
  • The pattern shows odd numbers come at positions 1, 3, 5... and even numbers at positions 2, 4, 6...
  • So zero() can track which iteration it's on: if i % 2 == 0, pass the baton to odd(), otherwise to even()

Why three semaphores? Each thread needs its own "wait signal":

  • z semaphore: Controls when zero() can run (starts with 1 since zero goes first)
  • o semaphore: Controls when odd() can run (starts with 0, must wait)
  • e semaphore: Controls when even() can run (starts with 0, must wait)

The beauty of this solution is that each thread, after completing its work, "wakes up" the next thread in the sequence by releasing the appropriate semaphore. The even() and odd() threads always give control back to zero(), while zero() alternates between giving control to odd() and even().

This creates a perfect choreography where threads take turns in the exact order needed to produce the desired output pattern.

Solution Approach

The implementation uses three semaphores to orchestrate the thread execution order. Let's walk through how each component works:

Initialization:

self.z = Semaphore(1)  # Zero thread can start immediately
self.e = Semaphore(0)  # Even thread must wait
self.o = Semaphore(0)  # Odd thread must wait

The semaphore values represent how many times a thread can proceed without blocking. Setting z to 1 allows the zero() thread to run first, while e and o at 0 means those threads will block until signaled.

Zero Thread Logic:

def zero(self, printNumber):
    for i in range(self.n):
        self.z.acquire()    # Wait for permission to print zero
        printNumber(0)      # Print 0
        if i % 2 == 0:
            self.o.release()  # Allow odd thread to run
        else:
            self.e.release()  # Allow even thread to run

The zero() function runs n times (once before each positive number). After printing 0, it determines which thread should run next:

  • On iterations 0, 2, 4... (even indices), it releases the odd semaphore for numbers 1, 3, 5...
  • On iterations 1, 3, 5... (odd indices), it releases the even semaphore for numbers 2, 4, 6...

Odd Thread Logic:

def odd(self, printNumber):
    for i in range(1, self.n + 1, 2):  # Generate 1, 3, 5, ...
        self.o.acquire()    # Wait for permission from zero thread
        printNumber(i)      # Print the odd number
        self.z.release()    # Give control back to zero thread

The odd thread iterates through odd numbers from 1 to n. Each iteration:

  1. Waits for the o semaphore to be released by zero()
  2. Prints the current odd number
  3. Releases the z semaphore to allow zero() to continue

Even Thread Logic:

def even(self, printNumber):
    for i in range(2, self.n + 1, 2):  # Generate 2, 4, 6, ...
        self.e.acquire()    # Wait for permission from zero thread
        printNumber(i)      # Print the even number
        self.z.release()    # Give control back to zero thread

Similar to the odd thread, but iterates through even numbers starting from 2.

Execution Flow Example (n=3):

  1. zero() acquires z (now 0), prints 0, releases o (now 1)
  2. odd() acquires o (now 0), prints 1, releases z (now 1)
  3. zero() acquires z (now 0), prints 0, releases e (now 1)
  4. even() acquires e (now 0), prints 2, releases z (now 1)
  5. zero() acquires z (now 0), prints 0, releases o (now 1)
  6. odd() acquires o (now 0), prints 3, releases z (now 1)

Output: 010203

The semaphores act as a token-passing mechanism, ensuring only one thread can execute at a time and in the correct order. Each thread blocks on acquire() until another thread calls release() on its semaphore, creating a perfectly synchronized sequence.

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 trace through the execution with n = 2 to see how the semaphores coordinate the threads to produce output "0102".

Initial State:

  • Semaphore z = 1 (zero thread can start)
  • Semaphore e = 0 (even thread blocked)
  • Semaphore o = 0 (odd thread blocked)
  • Thread A will call zero() twice (i = 0, 1)
  • Thread B will call even() once (i = 2)
  • Thread C will call odd() once (i = 1)

Step 1: First Zero (Thread A, i=0)

  • zero() calls z.acquire() → succeeds (z: 1→0)
  • Prints 0
  • Since i=0 is even, calls o.release() → (o: 0→1)
  • Thread A blocks on next iteration waiting for z

Step 2: First Odd (Thread C, i=1)

  • odd() was waiting on o.acquire() → now succeeds (o: 1→0)
  • Prints 1
  • Calls z.release() → (z: 0→1)
  • Thread C completes (no more odd numbers)

Step 3: Second Zero (Thread A, i=1)

  • zero() calls z.acquire() → succeeds (z: 1→0)
  • Prints 0
  • Since i=1 is odd, calls e.release() → (e: 0→1)
  • Thread A completes (reached n iterations)

Step 4: First Even (Thread B, i=2)

  • even() was waiting on e.acquire() → now succeeds (e: 1→0)
  • Prints 2
  • Calls z.release() → (z: 0→1)
  • Thread B completes (no more even numbers)

Final Output: 0102

The key insight is how the semaphores create a handoff pattern:

  • Zero always goes first (z starts at 1)
  • Zero decides who goes next based on iteration count
  • Odd/Even always hand control back to Zero
  • This creates the pattern: Zero→Odd→Zero→Even, exactly as needed

Solution Implementation

1from threading import Semaphore
2from typing import Callable
3
4
5class ZeroEvenOdd:
6    """
7    A class that coordinates three threads to print numbers in the pattern:
8    0, 1, 0, 2, 0, 3, 0, 4, ... up to n
9    where zero() prints 0s, odd() prints odd numbers, and even() prints even numbers.
10    """
11  
12    def __init__(self, n: int) -> None:
13        """
14        Initialize the synchronization mechanism for n numbers.
15      
16        Args:
17            n: The maximum number to print (inclusive)
18        """
19        self.n = n
20        # Semaphore for zero thread - starts with 1 permit (zero goes first)
21        self.zero_semaphore = Semaphore(1)
22        # Semaphore for even thread - starts with 0 permits
23        self.even_semaphore = Semaphore(0)
24        # Semaphore for odd thread - starts with 0 permits
25        self.odd_semaphore = Semaphore(0)
26
27    def zero(self, printNumber: Callable[[int], None]) -> None:
28        """
29        Thread function that prints zeros.
30        Alternates releasing permits to odd and even threads.
31      
32        Args:
33            printNumber: Function to call to print a number
34        """
35        for i in range(self.n):
36            # Wait for permission to print zero
37            self.zero_semaphore.acquire()
38          
39            # Print zero
40            printNumber(0)
41          
42            # Determine which thread should go next based on iteration
43            # i=0 -> release odd (for printing 1)
44            # i=1 -> release even (for printing 2)
45            # i=2 -> release odd (for printing 3)
46            # and so on...
47            if i % 2 == 0:
48                # Release odd thread for odd numbers (1, 3, 5, ...)
49                self.odd_semaphore.release()
50            else:
51                # Release even thread for even numbers (2, 4, 6, ...)
52                self.even_semaphore.release()
53
54    def even(self, printNumber: Callable[[int], None]) -> None:
55        """
56        Thread function that prints even numbers.
57        Prints 2, 4, 6, ... up to n (if n is even) or n-1 (if n is odd).
58      
59        Args:
60            printNumber: Function to call to print a number
61        """
62        # Iterate through even numbers from 2 to n
63        for number in range(2, self.n + 1, 2):
64            # Wait for permission to print even number
65            self.even_semaphore.acquire()
66          
67            # Print the even number
68            printNumber(number)
69          
70            # Release zero thread to print next zero
71            self.zero_semaphore.release()
72
73    def odd(self, printNumber: Callable[[int], None]) -> None:
74        """
75        Thread function that prints odd numbers.
76        Prints 1, 3, 5, ... up to n (if n is odd) or n-1 (if n is even).
77      
78        Args:
79            printNumber: Function to call to print a number
80        """
81        # Iterate through odd numbers from 1 to n
82        for number in range(1, self.n + 1, 2):
83            # Wait for permission to print odd number
84            self.odd_semaphore.acquire()
85          
86            # Print the odd number
87            printNumber(number)
88          
89            # Release zero thread to print next zero
90            self.zero_semaphore.release()
91
1class ZeroEvenOdd {
2    private int n;
3  
4    // Semaphore for controlling zero printing (initially available)
5    private Semaphore zeroSemaphore = new Semaphore(1);
6  
7    // Semaphore for controlling even number printing (initially blocked)
8    private Semaphore evenSemaphore = new Semaphore(0);
9  
10    // Semaphore for controlling odd number printing (initially blocked)
11    private Semaphore oddSemaphore = new Semaphore(0);
12
13    /**
14     * Constructor to initialize the maximum number to print
15     * @param n The maximum number in the sequence
16     */
17    public ZeroEvenOdd(int n) {
18        this.n = n;
19    }
20
21    /**
22     * Method to print zeros
23     * Prints 0 before each number from 1 to n
24     * @param printNumber Function to print the number
25     */
26    public void zero(IntConsumer printNumber) throws InterruptedException {
27        for (int i = 0; i < n; i++) {
28            // Wait for permission to print zero
29            zeroSemaphore.acquire();
30          
31            // Print zero
32            printNumber.accept(0);
33          
34            // Determine which thread to release next based on the iteration
35            if (i % 2 == 0) {
36                // Release odd thread for odd numbers (1, 3, 5, ...)
37                oddSemaphore.release();
38            } else {
39                // Release even thread for even numbers (2, 4, 6, ...)
40                evenSemaphore.release();
41            }
42        }
43    }
44
45    /**
46     * Method to print even numbers
47     * Prints even numbers from 2 to n
48     * @param printNumber Function to print the number
49     */
50    public void even(IntConsumer printNumber) throws InterruptedException {
51        for (int i = 2; i <= n; i += 2) {
52            // Wait for permission to print even number
53            evenSemaphore.acquire();
54          
55            // Print the even number
56            printNumber.accept(i);
57          
58            // Release zero thread to print the next zero
59            zeroSemaphore.release();
60        }
61    }
62
63    /**
64     * Method to print odd numbers
65     * Prints odd numbers from 1 to n
66     * @param printNumber Function to print the number
67     */
68    public void odd(IntConsumer printNumber) throws InterruptedException {
69        for (int i = 1; i <= n; i += 2) {
70            // Wait for permission to print odd number
71            oddSemaphore.acquire();
72          
73            // Print the odd number
74            printNumber.accept(i);
75          
76            // Release zero thread to print the next zero
77            zeroSemaphore.release();
78        }
79    }
80}
81
1#include <semaphore.h>
2#include <functional>
3
4class ZeroEvenOdd {
5private:
6    int n;
7    sem_t zeroSem;  // Semaphore for controlling zero printing
8    sem_t evenSem;  // Semaphore for controlling even number printing
9    sem_t oddSem;   // Semaphore for controlling odd number printing
10
11public:
12    /**
13     * Constructor: Initializes the class with the given upper limit n
14     * @param n: The maximum number to print (excluding zero)
15     */
16    ZeroEvenOdd(int n) {
17        this->n = n;
18      
19        // Initialize semaphores
20        sem_init(&zeroSem, 0, 1);  // Zero thread starts first (initial value = 1)
21        sem_init(&evenSem, 0, 0);  // Even thread waits initially (initial value = 0)
22        sem_init(&oddSem, 0, 0);   // Odd thread waits initially (initial value = 0)
23    }
24
25    /**
26     * Thread function for printing zeros
27     * Prints '0' before each number and coordinates with odd/even threads
28     * @param printNumber: Function to print the number
29     */
30    void zero(std::function<void(int)> printNumber) {
31        for (int i = 0; i < n; ++i) {
32            // Wait for permission to print zero
33            sem_wait(&zeroSem);
34          
35            // Print zero
36            printNumber(0);
37          
38            // Determine which thread should go next based on the iteration
39            if (i % 2 == 0) {
40                // After even iterations (0, 2, 4...), release odd thread to print odd numbers (1, 3, 5...)
41                sem_post(&oddSem);
42            } else {
43                // After odd iterations (1, 3, 5...), release even thread to print even numbers (2, 4, 6...)
44                sem_post(&evenSem);
45            }
46        }
47    }
48
49    /**
50     * Thread function for printing even numbers
51     * Prints even numbers in sequence: 2, 4, 6, ...
52     * @param printNumber: Function to print the number
53     */
54    void even(std::function<void(int)> printNumber) {
55        for (int i = 2; i <= n; i += 2) {
56            // Wait for permission to print even number
57            sem_wait(&evenSem);
58          
59            // Print the current even number
60            printNumber(i);
61          
62            // Signal zero thread to print next zero
63            sem_post(&zeroSem);
64        }
65    }
66
67    /**
68     * Thread function for printing odd numbers
69     * Prints odd numbers in sequence: 1, 3, 5, ...
70     * @param printNumber: Function to print the number
71     */
72    void odd(std::function<void(int)> printNumber) {
73        for (int i = 1; i <= n; i += 2) {
74            // Wait for permission to print odd number
75            sem_wait(&oddSem);
76          
77            // Print the current odd number
78            printNumber(i);
79          
80            // Signal zero thread to print next zero
81            sem_post(&zeroSem);
82        }
83    }
84};
85
1// Global variables
2let n: number;
3let zeroSemValue: number;  // Semaphore value for controlling zero printing
4let evenSemValue: number;  // Semaphore value for controlling even number printing
5let oddSemValue: number;   // Semaphore value for controlling odd number printing
6
7// Mutex locks for thread synchronization
8let zeroLock: Promise<void> | null = null;
9let evenLock: Promise<void> | null = null;
10let oddLock: Promise<void> | null = null;
11
12// Resolvers for the locks
13let zeroResolver: (() => void) | null = null;
14let evenResolver: (() => void) | null = null;
15let oddResolver: (() => void) | null = null;
16
17/**
18 * Initializes the synchronization mechanism with the given upper limit
19 * @param maxNumber - The maximum number to print (excluding zero)
20 */
21function initializeZeroEvenOdd(maxNumber: number): void {
22    n = maxNumber;
23  
24    // Initialize semaphore values
25    zeroSemValue = 1;  // Zero thread starts first (initial value = 1)
26    evenSemValue = 0;  // Even thread waits initially (initial value = 0)
27    oddSemValue = 0;   // Odd thread waits initially (initial value = 0)
28  
29    // Create initial lock for zero (unlocked state)
30    zeroLock = Promise.resolve();
31  
32    // Create initial locks for even and odd (locked state)
33    evenLock = new Promise<void>((resolve) => {
34        evenResolver = resolve;
35    });
36    oddLock = new Promise<void>((resolve) => {
37        oddResolver = resolve;
38    });
39}
40
41/**
42 * Simulates sem_wait operation - waits for the semaphore to be available
43 * @param semType - Type of semaphore ('zero', 'even', or 'odd')
44 */
45async function semWait(semType: 'zero' | 'even' | 'odd'): Promise<void> {
46    if (semType === 'zero') {
47        await zeroLock;
48        zeroLock = new Promise<void>((resolve) => {
49            zeroResolver = resolve;
50        });
51    } else if (semType === 'even') {
52        await evenLock;
53        evenLock = new Promise<void>((resolve) => {
54            evenResolver = resolve;
55        });
56    } else if (semType === 'odd') {
57        await oddLock;
58        oddLock = new Promise<void>((resolve) => {
59            oddResolver = resolve;
60        });
61    }
62}
63
64/**
65 * Simulates sem_post operation - releases the semaphore
66 * @param semType - Type of semaphore to release ('zero', 'even', or 'odd')
67 */
68function semPost(semType: 'zero' | 'even' | 'odd'): void {
69    if (semType === 'zero' && zeroResolver) {
70        zeroResolver();
71    } else if (semType === 'even' && evenResolver) {
72        evenResolver();
73    } else if (semType === 'odd' && oddResolver) {
74        oddResolver();
75    }
76}
77
78/**
79 * Thread function for printing zeros
80 * Prints '0' before each number and coordinates with odd/even threads
81 * @param printNumber - Callback function to print the number
82 */
83async function zero(printNumber: (x: number) => void): Promise<void> {
84    for (let i = 0; i < n; i++) {
85        // Wait for permission to print zero
86        await semWait('zero');
87      
88        // Print zero
89        printNumber(0);
90      
91        // Determine which thread should go next based on the iteration
92        if (i % 2 === 0) {
93            // After even iterations (0, 2, 4...), release odd thread to print odd numbers (1, 3, 5...)
94            semPost('odd');
95        } else {
96            // After odd iterations (1, 3, 5...), release even thread to print even numbers (2, 4, 6...)
97            semPost('even');
98        }
99    }
100}
101
102/**
103 * Thread function for printing even numbers
104 * Prints even numbers in sequence: 2, 4, 6, ...
105 * @param printNumber - Callback function to print the number
106 */
107async function even(printNumber: (x: number) => void): Promise<void> {
108    for (let i = 2; i <= n; i += 2) {
109        // Wait for permission to print even number
110        await semWait('even');
111      
112        // Print the current even number
113        printNumber(i);
114      
115        // Signal zero thread to print next zero
116        semPost('zero');
117    }
118}
119
120/**
121 * Thread function for printing odd numbers
122 * Prints odd numbers in sequence: 1, 3, 5, ...
123 * @param printNumber - Callback function to print the number
124 */
125async function odd(printNumber: (x: number) => void): Promise<void> {
126    for (let i = 1; i <= n; i += 2) {
127        // Wait for permission to print odd number
128        await semWait('odd');
129      
130        // Print the current odd number
131        printNumber(i);
132      
133        // Signal zero thread to print next zero
134        semPost('zero');
135    }
136}
137

Time and Space Complexity

Time Complexity: O(n)

The code implements a thread synchronization mechanism where three methods (zero, even, and odd) work together to print a sequence. Analyzing each method:

  • The zero method runs a loop n times, performing constant-time operations (acquire semaphore, print, release semaphore) in each iteration: O(n)
  • The even method iterates through even numbers from 2 to n, which is approximately n/2 iterations with constant-time operations: O(n/2) = O(n)
  • The odd method iterates through odd numbers from 1 to n, which is approximately n/2 iterations (or (n+1)/2 for odd n) with constant-time operations: O(n/2) = O(n)

Since these methods run concurrently but the total number of operations across all threads is proportional to n, the overall time complexity is O(n).

Space Complexity: O(1)

The space usage consists of:

  • Three semaphore objects (z, e, o), each using constant space
  • One integer variable n storing the input value
  • Loop variables (i) in each method

All of these use constant space regardless of the input size n. The semaphores themselves don't grow with n - they simply maintain internal counters. Therefore, the space complexity is O(1).

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

Common Pitfalls

1. Incorrect Semaphore Release Logic in Zero Thread

One of the most common mistakes is getting the logic wrong for determining which thread (odd or even) to release after printing zero. Developers often confuse the iteration index with the actual number being printed next.

Incorrect Implementation:

def zero(self, printNumber):
    for i in range(self.n):
        self.zero_semaphore.acquire()
        printNumber(0)
        # WRONG: Using the wrong condition
        if (i + 1) % 2 == 0:  # Checking if next number is even
            self.even_semaphore.release()
        else:
            self.odd_semaphore.release()

Why it's wrong: The iteration index i starts at 0, and when i=0, we want to print 1 (odd) next. The correct check is i % 2 == 0 releases odd, not (i + 1) % 2 == 1.

Solution: Remember that the iteration index corresponds to which positive number comes next:

  • Index 0 → Number 1 (odd)
  • Index 1 → Number 2 (even)
  • Index 2 → Number 3 (odd)
  • Index 3 → Number 4 (even)

2. Deadlock from Incorrect Initial Semaphore Values

Incorrect Implementation:

def __init__(self, n):
    self.n = n
    # WRONG: All semaphores initialized to 0
    self.zero_semaphore = Semaphore(0)
    self.even_semaphore = Semaphore(0)
    self.odd_semaphore = Semaphore(0)

Why it causes deadlock: If all semaphores start at 0, every thread will block on acquire() waiting for someone else to release() first. No thread can proceed, creating a circular wait condition.

Solution: Always ensure the zero semaphore starts with value 1, allowing the sequence to begin properly.

3. Range Calculation Errors in Odd/Even Threads

Incorrect Implementation:

def odd(self, printNumber):
    # WRONG: Off-by-one error or incorrect range
    for i in range(0, self.n, 2):  # Generates 0, 2, 4...
        self.odd_semaphore.acquire()
        printNumber(i + 1)  # Trying to fix it here
        self.zero_semaphore.release()

Why it's wrong: Using complex calculations or adjustments can lead to:

  • Printing wrong numbers (0, 2, 4 instead of 1, 3, 5)
  • Missing the last number when n is odd
  • Thread executing wrong number of iterations

Solution: Use clear, explicit ranges:

  • Odd: range(1, self.n + 1, 2) generates 1, 3, 5...
  • Even: range(2, self.n + 1, 2) generates 2, 4, 6...

4. Missing Edge Case Handling for n=1

Potential Issue: When n=1, only the odd thread should execute once (printing "01"). The even thread's loop range(2, 2, 2) correctly produces no iterations, but developers sometimes add unnecessary special case handling that breaks the synchronization.

Incorrect "Fix":

def even(self, printNumber):
    if self.n < 2:
        return  # WRONG: Exits without proper synchronization
    for i in range(2, self.n + 1, 2):
        self.even_semaphore.acquire()
        printNumber(i)
        self.zero_semaphore.release()

Why it's wrong: The natural behavior of range(2, 2, 2) producing an empty sequence already handles this correctly. Adding early returns can disrupt the expected thread coordination.

Solution: Trust the range function to handle edge cases naturally. The empty loop body for even thread when n=1 is the correct behavior.

5. Race Condition in Thread Termination

Incorrect Assumption: Assuming threads will naturally terminate in order or that the main thread can immediately proceed after starting all threads.

Solution: When using this class, always properly join all threads:

import threading

zoo = ZeroEvenOdd(5)
threads = [
    threading.Thread(target=zoo.zero, args=(print,)),
    threading.Thread(target=zoo.odd, args=(print,)),
    threading.Thread(target=zoo.even, args=(print,))
]

for t in threads:
    t.start()

for t in threads:
    t.join()  # Essential: Wait for all threads to complete

Without proper joining, the main program might terminate before threads finish, causing incomplete output.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More