Facebook Pixel

3023. Find Pattern in Infinite Stream I 🔒

MediumArrayString MatchingSliding WindowHash FunctionRolling Hash
Leetcode Link

Problem Description

You have an infinite stream of binary bits (0s and 1s) and need to find where a specific pattern first appears in this stream.

Given:

  • A binary array pattern containing only 0s and 1s
  • An InfiniteStream object that provides access to an infinite sequence of bits
  • The stream has a next() method that returns one bit at a time

Your task is to find the first starting index where the pattern matches consecutive bits in the stream.

For example, if pattern = [1, 0] and the stream produces [0, 1, 0, 1, ...], the pattern first matches at index 1 (the subsequence [1, 0] starting at position 1).

The solution uses bit manipulation to efficiently compare patterns. Since the pattern length is at most 100, it splits the pattern into two halves and represents each half as a 64-bit integer (a and b). As bits are read from the stream, it maintains a sliding window represented by two corresponding integers (x and y). When the window size matches the pattern length, it checks if both halves match. If they do, it returns the starting index of the match.

The key insight is using bit operations for fast pattern matching:

  • Left shift operations (<<) to build binary representations
  • Bitwise OR (|) to set specific bits
  • Bitwise AND (&) with masks to maintain fixed-width windows
  • Direct integer comparison for pattern matching
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The straightforward approach would be to read bits one by one and check if the last m bits match the pattern every time we read a new bit. However, comparing arrays element by element repeatedly would be inefficient.

The key observation is that we can treat the pattern as a binary number. For instance, pattern [1, 0, 1] can be represented as the binary number 101. This transforms our pattern matching problem into a number comparison problem - we just need to check if the current window of bits equals our pattern when interpreted as a number.

Why split into two halves? Since the pattern can be up to 100 bits long, it exceeds what a single 64-bit integer can hold. By splitting the pattern into two parts, we ensure each part fits within a 64-bit integer. The left half goes into integer a, and the right half goes into integer b.

As we read the stream bit by bit, we maintain a sliding window of the same size as our pattern. This window is also represented as two integers (x and y). Each new bit shifts our window:

  • The new bit enters from the right
  • The oldest bit falls off from the left
  • We use bit masks (mask1 and mask2) to ensure our window integers don't grow beyond their designated sizes

The beauty of this approach is that pattern matching becomes a simple integer comparison: if a == x and b == y, we've found our match. This is much faster than comparing arrays element by element, as we're leveraging the CPU's ability to compare integers in a single operation.

The bit manipulation operations (<<, |, &) allow us to efficiently update our sliding window and maintain the binary representation, turning what could be an O(m) comparison at each step into an O(1) operation.

Learn more about Sliding Window patterns.

Solution Approach

The implementation uses bit manipulation with a sliding window technique to efficiently find the pattern in the stream.

Step 1: Split and Convert Pattern to Binary Numbers

Since the pattern can be up to 100 bits (exceeding 64-bit integer capacity), we split it into two halves:

  • half = m >> 1 calculates the midpoint using right shift (equivalent to m // 2)
  • First half (indices 0 to half-1) is stored in integer a
  • Second half (indices half to m-1) is stored in integer b

For each half, we build the binary representation:

for i in range(half):
    a |= pattern[i] << (half - 1 - i)

This places each bit at its correct position. For example, if pattern's first half is [1, 0, 1], we get a = 101 in binary.

Step 2: Create Bit Masks

We create masks to ensure our sliding window integers stay within bounds:

  • mask1 = (1 << half) - 1 creates a mask with half ones (e.g., for half=3, mask1 = 0b111)
  • mask2 = (1 << (m - half)) - 1 creates a mask for the second half

Step 3: Process Stream with Sliding Window

Initialize window integers x = 0 and y = 0, then for each bit from the stream:

  1. Read the next bit: v = stream.next()
  2. Update the right window (y):
    • Shift left and add new bit: y = y << 1 | v
    • Extract overflow bit for left window: v = y >> (m - half) & 1
    • Keep only valid bits: y &= mask2
  3. Update the left window (x):
    • Shift left and add overflow from y: x = x << 1 | v
    • Keep only valid bits: x &= mask1

Step 4: Check for Pattern Match

Once we've read at least m bits (i >= m), we check if both halves match:

  • If a == x and b == y, we found the pattern
  • Return i - m as the starting index (current position minus pattern length)

The algorithm continues reading bits until a match is found. The count(1) creates an infinite counter starting from 1, tracking our position in the stream.

This approach achieves O(1) comparison per bit read, making it highly efficient compared to naive array comparison which would be O(m) per position.

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 finding pattern [1, 0, 1] in a stream that produces [0, 1, 0, 1, 1, ...].

Setup Phase:

  • Pattern length m = 3
  • Split point: half = 3 >> 1 = 1 (first half has 1 bit, second half has 2 bits)
  • Convert first half [1] to integer a:
    • a = 0
    • a |= 1 << (1-1-0) = 1 << 0 = 1
    • Result: a = 1
  • Convert second half [0, 1] to integer b:
    • b = 0
    • b |= 0 << (2-1-0) = 0 << 1 = 0 → b = 0
    • b |= 1 << (2-1-1) = 1 << 0 = 1 → b = 1
    • Result: b = 1 (binary: 01)
  • Create masks:
    • mask1 = (1 << 1) - 1 = 1 (binary: 1)
    • mask2 = (1 << 2) - 1 = 3 (binary: 11)

Stream Processing:

Position 1: Read bit 0

  • Update y: y = 0 << 1 | 0 = 0
  • Overflow to x: v = 0 >> 2 & 1 = 0
  • Update x: x = 0 << 1 | 0 = 0
  • Window: x=0, y=0 (represents [0])
  • No check yet (need 3 bits)

Position 2: Read bit 1

  • Update y: y = 0 << 1 | 1 = 1
  • Overflow: v = 1 >> 2 & 1 = 0
  • Update x: x = 0 << 1 | 0 = 0
  • Window: x=0, y=1 (represents [0, 1])
  • No check yet

Position 3: Read bit 0

  • Update y: y = 1 << 1 | 0 = 2 (binary: 10)
  • Overflow: v = 2 >> 2 & 1 = 0
  • Update x: x = 0 << 1 | 0 = 0
  • Window: x=0, y=2 (represents [0, 1, 0])
  • Check: a=1 ≠ x=0 - no match

Position 4: Read bit 1

  • Update y: y = 2 << 1 | 1 = 5 (binary: 101)
  • Overflow: v = 5 >> 2 & 1 = 1
  • Apply mask to y: y = 5 & 3 = 1 (keeps last 2 bits: 01)
  • Update x: x = 0 << 1 | 1 = 1
  • Apply mask to x: x = 1 & 1 = 1
  • Window: x=1, y=1 (represents [1, 0, 1])
  • Check: a=1 == x=1 AND b=1 == y=1 - Match found!
  • Return: 4 - 3 = 1 (starting index)

The pattern [1, 0, 1] first appears at index 1 in the stream, matching the subsequence at positions 1, 2, and 3.

Solution Implementation

1from itertools import count
2from typing import Optional, List
3
4# Definition for an infinite stream.
5# class InfiniteStream:
6#     def next(self) -> int:
7#         pass
8
9class Solution:
10    def findPattern(
11        self, stream: Optional["InfiniteStream"], pattern: List[int]
12    ) -> int:
13        """
14        Finds the starting index of a pattern in an infinite binary stream.
15        Uses bit manipulation with a sliding window approach split into two halves.
16      
17        Args:
18            stream: An infinite stream of binary values (0 or 1)
19            pattern: The binary pattern to search for
20          
21        Returns:
22            The starting index (0-based) where the pattern first appears
23        """
24        # Initialize bit representations for pattern halves
25        pattern_first_half = 0
26        pattern_second_half = 0
27        pattern_length = len(pattern)
28      
29        # Calculate the midpoint for splitting the pattern
30        half_length = pattern_length >> 1  # Equivalent to pattern_length // 2
31      
32        # Create bit masks for the two halves
33        # mask1 covers the first half bits
34        first_half_mask = (1 << half_length) - 1
35        # mask2 covers the second half bits
36        second_half_mask = (1 << (pattern_length - half_length)) - 1
37      
38        # Build bit representation of the first half of pattern
39        # Store bits from left to right in the integer
40        for i in range(half_length):
41            pattern_first_half |= pattern[i] << (half_length - 1 - i)
42      
43        # Build bit representation of the second half of pattern
44        # Store bits from left to right in the integer
45        for i in range(half_length, pattern_length):
46            pattern_second_half |= pattern[i] << (pattern_length - 1 - i)
47      
48        # Initialize sliding window buffers for stream
49        stream_first_half = 0   # Buffer for first half of current window
50        stream_second_half = 0  # Buffer for second half of current window
51      
52        # Process stream elements one by one
53        for position in count(1):  # Start counting from 1 (1-indexed positions)
54            # Read next bit from stream
55            current_bit = stream.next()
56          
57            # Update second half buffer: shift left and add new bit
58            stream_second_half = (stream_second_half << 1) | current_bit
59          
60            # Extract the bit that will overflow from second half to first half
61            overflow_bit = (stream_second_half >> (pattern_length - half_length)) & 1
62          
63            # Keep only the relevant bits in second half buffer
64            stream_second_half &= second_half_mask
65          
66            # Update first half buffer: shift left and add overflow bit
67            stream_first_half = (stream_first_half << 1) | overflow_bit
68          
69            # Keep only the relevant bits in first half buffer
70            stream_first_half &= first_half_mask
71          
72            # Check if we have read enough bits and if pattern matches
73            if (position >= pattern_length and 
74                pattern_first_half == stream_first_half and 
75                pattern_second_half == stream_second_half):
76                # Return 0-based starting index of the pattern
77                return position - pattern_length
78
1/**
2 * Definition for an infinite stream.
3 * class InfiniteStream {
4 *     public InfiniteStream(int[] bits);
5 *     public int next();
6 * }
7 */
8class Solution {
9    public int findPattern(InfiniteStream infiniteStream, int[] pattern) {
10        // Split pattern into two halves for efficient matching
11        int patternLength = pattern.length;
12        int firstHalfLength = patternLength >> 1;  // Divide by 2 using bit shift
13        int secondHalfLength = patternLength - firstHalfLength;
14      
15        // Create bit masks for each half
16        // mask1 has 'firstHalfLength' bits set to 1
17        long firstHalfMask = (1L << firstHalfLength) - 1;
18        // mask2 has 'secondHalfLength' bits set to 1
19        long secondHalfMask = (1L << secondHalfLength) - 1;
20      
21        // Convert first half of pattern to binary representation
22        long firstHalfPattern = 0;
23        for (int i = 0; i < firstHalfLength; ++i) {
24            // Pack bits from left to right
25            firstHalfPattern |= (long) pattern[i] << (firstHalfLength - 1 - i);
26        }
27      
28        // Convert second half of pattern to binary representation
29        long secondHalfPattern = 0;
30        for (int i = firstHalfLength; i < patternLength; ++i) {
31            // Pack bits from left to right, adjusting index for second half
32            secondHalfPattern |= (long) pattern[i] << (patternLength - 1 - i);
33        }
34      
35        // Sliding window variables to track current stream segments
36        long firstHalfWindow = 0;   // Tracks bits for first half matching
37        long secondHalfWindow = 0;  // Tracks bits for second half matching
38      
39        // Process stream one bit at a time
40        for (int currentPosition = 1; ; ++currentPosition) {
41            // Read next bit from stream
42            int currentBit = infiniteStream.next();
43          
44            // Update second half window: shift left and add new bit
45            secondHalfWindow = secondHalfWindow << 1 | currentBit;
46          
47            // Extract the bit that will overflow from second half to first half
48            int overflowBit = (int) ((secondHalfWindow >> secondHalfLength) & 1);
49          
50            // Keep only the relevant bits in second half window
51            secondHalfWindow &= secondHalfMask;
52          
53            // Update first half window with the overflow bit
54            firstHalfWindow = firstHalfWindow << 1 | overflowBit;
55            firstHalfWindow &= firstHalfMask;
56          
57            // Check if we have read enough bits and if pattern matches
58            if (currentPosition >= patternLength && 
59                firstHalfPattern == firstHalfWindow && 
60                secondHalfPattern == secondHalfWindow) {
61                // Return the starting index of the pattern (0-indexed)
62                return currentPosition - patternLength;
63            }
64        }
65    }
66}
67
1/**
2 * Definition for an infinite stream.
3 * class InfiniteStream {
4 * public:
5 *     InfiniteStream(vector<int> bits);
6 *     int next();
7 * };
8 */
9class Solution {
10public:
11    int findPattern(InfiniteStream* stream, vector<int>& pattern) {
12        // Initialize hash values for the two halves of the pattern
13        long long firstHalfHash = 0;
14        long long secondHalfHash = 0;
15      
16        int patternLength = pattern.size();
17        int halfLength = patternLength >> 1;  // Divide by 2 using bit shift
18      
19        // Create bit masks for the two halves
20        // These masks ensure we only keep the relevant bits when updating hashes
21        long long firstHalfMask = (1LL << halfLength) - 1;
22        long long secondHalfMask = (1LL << (patternLength - halfLength)) - 1;
23      
24        // Build the hash for the first half of the pattern
25        // Each bit is shifted to its position in the hash
26        for (int i = 0; i < halfLength; ++i) {
27            firstHalfHash |= (long long)pattern[i] << (halfLength - 1 - i);
28        }
29      
30        // Build the hash for the second half of the pattern
31        for (int i = halfLength; i < patternLength; ++i) {
32            secondHalfHash |= (long long)pattern[i] << (patternLength - 1 - i);
33        }
34      
35        // Rolling hash values for the current window in the stream
36        long long currentFirstHalf = 0;
37        long long currentSecondHalf = 0;
38      
39        // Process the stream bit by bit
40        for (int position = 1; ; ++position) {
41            // Get the next bit from the stream
42            int currentBit = stream->next();
43          
44            // Update the second half hash by shifting left and adding the new bit
45            currentSecondHalf = (currentSecondHalf << 1) | currentBit;
46          
47            // Extract the bit that will overflow from second half to first half
48            int overflowBit = (int)((currentSecondHalf >> (patternLength - halfLength)) & 1);
49          
50            // Keep only the relevant bits in the second half
51            currentSecondHalf &= secondHalfMask;
52          
53            // Update the first half hash with the overflow bit
54            currentFirstHalf = (currentFirstHalf << 1) | overflowBit;
55            currentFirstHalf &= firstHalfMask;
56          
57            // Check if we have read enough bits and if the pattern matches
58            if (position >= patternLength && 
59                firstHalfHash == currentFirstHalf && 
60                secondHalfHash == currentSecondHalf) {
61                // Return the starting index of the pattern (0-indexed)
62                return position - patternLength;
63            }
64        }
65    }
66};
67
1/**
2 * Definition for an infinite stream.
3 * class InfiniteStream {
4 *     next(): number;
5 * }
6 */
7
8/**
9 * Finds the starting index of a pattern in an infinite stream using rolling hash
10 * @param stream - The infinite stream to search in
11 * @param pattern - The pattern to search for
12 * @returns The starting index (0-indexed) where the pattern first appears
13 */
14function findPattern(stream: InfiniteStream, pattern: number[]): number {
15    // Initialize hash values for the two halves of the pattern
16    let firstHalfHash: bigint = 0n;
17    let secondHalfHash: bigint = 0n;
18  
19    const patternLength: number = pattern.length;
20    const halfLength: number = patternLength >> 1;  // Divide by 2 using bit shift
21  
22    // Create bit masks for the two halves
23    // These masks ensure we only keep the relevant bits when updating hashes
24    const firstHalfMask: bigint = (1n << BigInt(halfLength)) - 1n;
25    const secondHalfMask: bigint = (1n << BigInt(patternLength - halfLength)) - 1n;
26  
27    // Build the hash for the first half of the pattern
28    // Each bit is shifted to its position in the hash
29    for (let i = 0; i < halfLength; i++) {
30        firstHalfHash |= BigInt(pattern[i]) << BigInt(halfLength - 1 - i);
31    }
32  
33    // Build the hash for the second half of the pattern
34    for (let i = halfLength; i < patternLength; i++) {
35        secondHalfHash |= BigInt(pattern[i]) << BigInt(patternLength - 1 - i);
36    }
37  
38    // Rolling hash values for the current window in the stream
39    let currentFirstHalf: bigint = 0n;
40    let currentSecondHalf: bigint = 0n;
41  
42    // Process the stream bit by bit
43    for (let position = 1; ; position++) {
44        // Get the next bit from the stream
45        const currentBit: number = stream.next();
46      
47        // Update the second half hash by shifting left and adding the new bit
48        currentSecondHalf = (currentSecondHalf << 1n) | BigInt(currentBit);
49      
50        // Extract the bit that will overflow from second half to first half
51        const overflowBit: number = Number((currentSecondHalf >> BigInt(patternLength - halfLength)) & 1n);
52      
53        // Keep only the relevant bits in the second half
54        currentSecondHalf &= secondHalfMask;
55      
56        // Update the first half hash with the overflow bit
57        currentFirstHalf = (currentFirstHalf << 1n) | BigInt(overflowBit);
58        currentFirstHalf &= firstHalfMask;
59      
60        // Check if we have read enough bits and if the pattern matches
61        if (position >= patternLength && 
62            firstHalfHash === currentFirstHalf && 
63            secondHalfHash === currentSecondHalf) {
64            // Return the starting index of the pattern (0-indexed)
65            return position - patternLength;
66        }
67    }
68}
69

Time and Space Complexity

The time complexity is O(n + m), where n is the number of elements read from the infinite stream until the pattern is found, and m is the length of the pattern.

Breaking down the time complexity:

  • The first loop iterates half times, which is O(m/2) = O(m)
  • The second loop iterates m - half times, which is also O(m/2) = O(m)
  • The main loop reads from the stream and performs constant-time bitwise operations for each element until the pattern is found, taking O(n) time
  • Total: O(m) + O(m) + O(n) = O(n + m)

The space complexity is O(1) because:

  • The algorithm uses a fixed number of integer variables (a, b, m, half, mask1, mask2, x, y, i, v)
  • These variables store bit-packed representations of the pattern halves and sliding windows
  • The space used does not grow with the input size, as the bit operations ensure the values stay within fixed bounds determined by the masks
  • Even though the pattern has length m, it's already provided as input and the algorithm only uses constant additional space to process it

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

Common Pitfalls

1. Incorrect Bit Positioning When Building Pattern Representation

A common mistake is placing bits in the wrong positions when converting the pattern array to integers. Many developers might write:

# WRONG: This reverses the bit order
for i in range(half_length):
    pattern_first_half |= pattern[i] << i  # Places first bit at position 0

This would represent pattern [1, 0, 1] as 0b101 with the first element at the least significant bit, when it should be at the most significant bit within the window.

Solution: Always place bits from left to right (most significant to least significant):

# CORRECT: Maintains proper bit order
for i in range(half_length):
    pattern_first_half |= pattern[i] << (half_length - 1 - i)

2. Off-by-One Error in Index Calculation

The code uses count(1) which starts counting from 1, but many might use count(0) or a manual counter starting from 0, leading to incorrect index calculations:

# WRONG: Using 0-based position counter
for position in count(0):  # or position = 0
    # ... process bit ...
    if position >= pattern_length - 1:  # Wrong adjustment
        if matches:
            return position - pattern_length + 1  # Incorrect formula

Solution: Be consistent with your indexing. If using 1-based counting (as in the solution), the return statement should be position - pattern_length. If using 0-based counting, adjust all related calculations accordingly.

3. Forgetting to Apply Masks After Bit Operations

Without applying masks, the integers can grow beyond their intended bit width:

# WRONG: Missing mask application
stream_second_half = (stream_second_half << 1) | current_bit
# stream_second_half can now have more bits than needed

This causes incorrect pattern matching as extra bits contaminate the comparison.

Solution: Always apply masks after shifting operations:

# CORRECT: Apply mask to maintain fixed width
stream_second_half = (stream_second_half << 1) | current_bit
stream_second_half &= second_half_mask  # Keep only relevant bits

4. Incorrect Overflow Bit Extraction

When extracting the overflow bit from the second half to feed into the first half, using the wrong bit position is a common error:

# WRONG: Extracting from wrong position
overflow_bit = stream_second_half >> pattern_length & 1  # Wrong shift amount

Solution: Extract from the correct position based on the second half's length:

# CORRECT: Extract the bit that would overflow
overflow_bit = (stream_second_half >> (pattern_length - half_length)) & 1

5. Premature Pattern Checking

Checking for pattern match before reading enough bits from the stream:

# WRONG: Checking too early
for position in count(1):
    # ... update buffers ...
    if pattern_first_half == stream_first_half:  # Might check with incomplete data
        return position - pattern_length

Solution: Only check for matches after reading at least pattern_length bits:

# CORRECT: Wait until window is full
if position >= pattern_length:
    if pattern_first_half == stream_first_half and pattern_second_half == stream_second_half:
        return position - pattern_length
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement priority queue?


Recommended Readings

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

Load More