Facebook Pixel

1656. Design an Ordered Stream

EasyDesignArrayHash TableData Stream
Leetcode Link

Problem Description

You need to design a data structure that processes a stream of (idKey, value) pairs and returns values in sorted order by their IDs, but with a specific streaming behavior.

The stream will receive n pairs where:

  • Each idKey is a unique integer between 1 and n
  • Each value is a string
  • The pairs arrive in arbitrary (random) order

The key requirement is that when you insert a pair, you should return the largest possible consecutive chunk of values starting from the current pointer position. The pointer tracks which ID position we're currently waiting for.

Here's how it works step by step:

  1. You maintain an array to store values at their corresponding ID positions
  2. You keep a pointer starting at position 1
  3. When you insert (idKey, value), you store the value at position idKey
  4. After insertion, you check from the pointer position: if there's a value there, add it to the result and move the pointer forward. Continue this until you hit an empty position
  5. Return all the consecutive values you found

For example, with n = 5:

  • Insert (3, "ccccc"): Store at position 3, but pointer is at 1 (empty), so return []
  • Insert (1, "aaaaa"): Store at position 1, pointer is at 1 (now filled), so return ["aaaaa"] and move pointer to 2
  • Insert (2, "bbbbb"): Store at position 2, pointer is at 2 (now filled), so we can return ["bbbbb", "ccccc"] since position 3 was already filled earlier. Pointer moves to 4
  • Insert (5, "eeeee"): Store at position 5, but pointer is at 4 (empty), so return []
  • Insert (4, "ddddd"): Store at position 4, pointer is at 4 (now filled), so return ["ddddd", "eeeee"] since position 5 was already filled

The concatenation of all returned chunks gives you the complete sorted sequence: ["aaaaa", "bbbbb", "ccccc", "ddddd", "eeeee"].

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

Intuition

The key insight is recognizing that we need to output values in sorted order, but we can only output a value when all values before it have been inserted and outputted.

Think of it like assembling a jigsaw puzzle where pieces must be placed in numbered order from left to right. Even if you have piece #3, you can't place it until pieces #1 and #2 are in place. But once piece #2 is placed, if piece #3 is already available, you can place both #2 and #3 together in one go.

This naturally leads us to think about using a pointer to track our current position - essentially asking "what's the next ID we're waiting for?" When we insert a value:

  • If it's not at the pointer position, we just store it for later
  • If it fills the pointer position, we can start outputting values

The clever part is realizing that after filling the pointer position, we might be able to output multiple consecutive values. Why? Because earlier insertions might have already filled positions ahead of the pointer. So we keep moving forward and collecting values until we hit a gap.

Using an array makes perfect sense here because:

  1. IDs range from 1 to n, which maps naturally to array indices
  2. We need O(1) access to check if a position is filled
  3. The pointer can simply increment through array positions

The pointer essentially maintains the invariant: "all positions before the pointer have been outputted, and the pointer points to the first position that hasn't been outputted yet." This ensures we always output values in the correct order while maximizing the chunk size returned each time.

Learn more about Data Stream patterns.

Solution Approach

The implementation uses an array-based simulation with a pointer to track the current position in the stream.

Data Structures:

  • data: An array of size n + 1 where data[i] stores the value for idKey = i. We use size n + 1 to make indexing simpler (1-indexed instead of 0-indexed).
  • ptr: A pointer that tracks the next position we're waiting to output. Initially set to 1.

Initialization (__init__):

self.ptr = 1
self.data = [None] * (n + 1)

We create an array filled with None values and set the pointer to position 1, since IDs start from 1.

Insert Operation: The insert method performs two main tasks:

  1. Store the value:

    self.data[idKey] = value

    Place the value at its corresponding position in the array.

  2. Collect consecutive values starting from pointer:

    ans = []
    while self.ptr < len(self.data) and self.data[self.ptr]:
        ans.append(self.data[self.ptr])
        self.ptr += 1
    return ans
    • Start from the current pointer position
    • While the position is valid and contains a value (not None):
      • Add the value to our result list
      • Increment the pointer to the next position
    • Return the collected values

Time Complexity:

  • insert: O(k) where k is the number of consecutive values returned. In the worst case, this could be O(n) for a single insertion, but amortized over all n insertions, each position is visited exactly once by the pointer, giving us O(1) amortized time per insertion.
  • Overall for n insertions: O(n)

Space Complexity: O(n) for storing the array of n+1 elements.

The elegance of this solution lies in its simplicity - the pointer ensures we always output values in order, and the array allows us to handle out-of-order insertions efficiently. Each value is processed exactly once when the pointer passes through it, making the algorithm optimal.

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 small example with n = 4 to illustrate how the solution works.

Initial Setup:

  • data = [None, None, None, None, None] (size 5, index 0 unused)
  • ptr = 1 (waiting for ID 1)

Step 1: Insert (2, "bb")

  • Store "bb" at position 2: data = [None, None, "bb", None, None]
  • Check from ptr=1: position 1 is empty (None)
  • Return: []
  • State: ptr = 1, waiting for ID 1

Step 2: Insert (4, "dd")

  • Store "dd" at position 4: data = [None, None, "bb", None, "dd"]
  • Check from ptr=1: position 1 is empty (None)
  • Return: []
  • State: ptr = 1, still waiting for ID 1

Step 3: Insert (1, "aa")

  • Store "aa" at position 1: data = [None, "aa", "bb", None, "dd"]
  • Check from ptr=1: position 1 has "aa" ✓
    • Add "aa" to result, move ptr to 2
  • Check from ptr=2: position 2 has "bb" ✓
    • Add "bb" to result, move ptr to 3
  • Check from ptr=3: position 3 is empty (None) ✗
  • Return: ["aa", "bb"]
  • State: ptr = 3, waiting for ID 3

Step 4: Insert (3, "cc")

  • Store "cc" at position 3: data = [None, "aa", "bb", "cc", "dd"]
  • Check from ptr=3: position 3 has "cc" ✓
    • Add "cc" to result, move ptr to 4
  • Check from ptr=4: position 4 has "dd" ✓
    • Add "dd" to result, move ptr to 5
  • Check from ptr=5: ptr >= len(data), stop
  • Return: ["cc", "dd"]
  • State: ptr = 5, all values processed

Final Result: Concatenating all returned chunks: ["aa", "bb"] + ["cc", "dd"] = ["aa", "bb", "cc", "dd"]

This example demonstrates the key behaviors:

  1. Values are stored immediately but only returned when their turn comes
  2. The pointer ensures we output in sorted order
  3. Multiple consecutive values can be returned in one operation (like steps 3 and 4)
  4. Each value is outputted exactly once when the pointer reaches and passes its position

Solution Implementation

1from typing import List, Optional
2
3class OrderedStream:
4    def __init__(self, n: int):
5        """
6        Initialize the OrderedStream with capacity n.
7      
8        Args:
9            n: The maximum number of elements that can be stored (1-indexed)
10        """
11        # Pointer to track the next expected position for consecutive output
12        self.ptr = 1
13        # Initialize storage array with n+1 slots (index 0 unused, 1 to n used)
14        self.data: List[Optional[str]] = [None] * (n + 1)
15
16    def insert(self, idKey: int, value: str) -> List[str]:
17        """
18        Insert a value at the given idKey position and return consecutive values
19        starting from the current pointer position if available.
20      
21        Args:
22            idKey: The position (1-indexed) where the value should be inserted
23            value: The string value to insert
24          
25        Returns:
26            List of consecutive string values starting from current pointer,
27            or empty list if no consecutive sequence is available
28        """
29        # Store the value at the specified position
30        self.data[idKey] = value
31      
32        # Collect consecutive values starting from current pointer
33        result: List[str] = []
34      
35        # Advance pointer while consecutive values exist
36        while self.ptr < len(self.data) and self.data[self.ptr] is not None:
37            result.append(self.data[self.ptr])
38            self.ptr += 1
39          
40        return result
41
42
43# Your OrderedStream object will be instantiated and called as such:
44# obj = OrderedStream(n)
45# param_1 = obj.insert(idKey, value)
46
1/**
2 * OrderedStream class maintains an ordered stream of string values.
3 * It stores values at specific positions and returns consecutive values
4 * starting from the current pointer position when they become available.
5 */
6class OrderedStream {
7    // Current pointer position in the stream
8    private int currentPointer = 1;
9  
10    // Array to store stream data (1-indexed, so size is n+1)
11    private String[] streamData;
12
13    /**
14     * Constructor initializes the stream with capacity n.
15     * @param n The maximum number of elements in the stream
16     */
17    public OrderedStream(int n) {
18        // Create array with size n+1 to accommodate 1-based indexing
19        streamData = new String[n + 1];
20    }
21
22    /**
23     * Inserts a value at the specified position and returns all consecutive
24     * values starting from the current pointer position.
25     * @param idKey The position (1-indexed) where the value should be inserted
26     * @param value The string value to insert
27     * @return List of consecutive non-null values from current pointer position
28     */
29    public List<String> insert(int idKey, String value) {
30        // Store the value at the specified position
31        streamData[idKey] = value;
32      
33        // Initialize result list to collect consecutive values
34        List<String> result = new ArrayList<>();
35      
36        // Collect all consecutive non-null values starting from current pointer
37        while (currentPointer < streamData.length && streamData[currentPointer] != null) {
38            result.add(streamData[currentPointer]);
39            currentPointer++;
40        }
41      
42        return result;
43    }
44}
45
46/**
47 * Your OrderedStream object will be instantiated and called as such:
48 * OrderedStream obj = new OrderedStream(n);
49 * List<String> param_1 = obj.insert(idKey,value);
50 */
51
1class OrderedStream {
2private:
3    int currentPointer;           // Tracks the next position to be processed
4    vector<string> dataStream;    // Stores the stream values (1-indexed)
5
6public:
7    /**
8     * Constructor: Initializes the ordered stream with capacity n
9     * @param n: The size of the stream (number of elements)
10     */
11    OrderedStream(int n) {
12        currentPointer = 1;                    // Start from index 1 (1-indexed system)
13        dataStream = vector<string>(n + 1);    // Create vector with n+1 slots (index 0 unused)
14    }
15  
16    /**
17     * Inserts a key-value pair into the stream and returns consecutive values if available
18     * @param idKey: The position/ID where the value should be inserted
19     * @param value: The string value to insert at the given position
20     * @return: A vector of consecutive non-empty strings starting from currentPointer
21     */
22    vector<string> insert(int idKey, string value) {
23        // Store the value at the specified position
24        dataStream[idKey] = value;
25      
26        // Prepare result vector to collect consecutive values
27        vector<string> result;
28      
29        // Collect all consecutive non-empty values starting from currentPointer
30        while (currentPointer < dataStream.size() && !dataStream[currentPointer].empty()) {
31            result.push_back(dataStream[currentPointer]);
32            currentPointer++;  // Move pointer to next position
33        }
34      
35        return result;
36    }
37};
38
39/**
40 * Your OrderedStream object will be instantiated and called as such:
41 * OrderedStream* obj = new OrderedStream(n);
42 * vector<string> param_1 = obj->insert(idKey,value);
43 */
44
1// Pointer to track the next expected position in the stream
2let ptr: number;
3// Array to store the stream data with 1-based indexing
4let data: string[];
5
6/**
7 * Initializes the ordered stream with a given size
8 * @param n - The size of the stream
9 */
10function OrderedStream(n: number): void {
11    // Initialize pointer to start at position 1 (1-based indexing)
12    ptr = 1;
13    // Create array with size n+1 to accommodate 1-based indexing
14    data = Array(n + 1);
15}
16
17/**
18 * Inserts a value at the specified position and returns consecutive values starting from ptr
19 * @param idKey - The position (1-based) where the value should be inserted
20 * @param value - The string value to insert
21 * @returns Array of consecutive values from current ptr position, or empty array if no consecutive values exist
22 */
23function insert(idKey: number, value: string): string[] {
24    // Store the value at the specified position
25    data[idKey] = value;
26  
27    // Collect all consecutive values starting from current pointer position
28    const result: string[] = [];
29  
30    // Continue collecting values while they exist at consecutive positions
31    while (data[ptr]) {
32        result.push(data[ptr]);
33        ptr++;
34    }
35  
36    return result;
37}
38

Time and Space Complexity

Time Complexity: O(n) for the overall operations, where n is the number of elements in the stream.

  • For the __init__ method: O(n) to initialize the data array with n+1 elements.
  • For each insert operation: While a single insert might seem O(1) in the best case (when idKey != ptr), we need to consider the amortized complexity. The while loop processes elements starting from ptr and advances it. Each element in the stream is processed exactly once by the while loop across all insert operations. Therefore, if we perform n insertions, the total time spent in all while loops combined is O(n), giving us an amortized time complexity of O(1) per insert operation, but O(n) total for all operations.

Space Complexity: O(n) where n is the length of the data stream.

  • The data array stores n+1 elements (indexed from 0 to n, though index 0 is unused).
  • The ptr variable uses O(1) space.
  • The ans list in each insert operation can contain at most n elements in the worst case, but this is temporary space that gets returned.
  • The dominant space usage is the persistent storage of the data array, which is O(n).

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

Common Pitfalls

1. Off-by-One Error with Array Indexing

One of the most common mistakes is mishandling the 1-indexed nature of the problem. Since idKey ranges from 1 to n, developers often make these errors:

Pitfall Example:

# Wrong: Using 0-indexed array without adjustment
self.data = [None] * n  # Creates array of size n
self.data[idKey] = value  # This will cause IndexError when idKey = n

Solution: Either create an array of size n + 1 (wasting index 0) or adjust indices:

# Option 1: Size n+1 array (simpler, used in our solution)
self.data = [None] * (n + 1)
self.data[idKey] = value  # Direct indexing works

# Option 2: Size n array with index adjustment
self.data = [None] * n
self.data[idKey - 1] = value  # Adjust for 0-indexing

2. Forgetting to Update the Pointer

Another critical mistake is returning consecutive values without advancing the pointer, causing duplicate outputs in subsequent calls.

Pitfall Example:

def insert(self, idKey: int, value: str) -> List[str]:
    self.data[idKey] = value
    result = []
    temp_ptr = self.ptr
    while temp_ptr < len(self.data) and self.data[temp_ptr]:
        result.append(self.data[temp_ptr])
        temp_ptr += 1
    # Forgot to update self.ptr!
    return result

Solution: Always update the actual pointer after collecting values:

while self.ptr < len(self.data) and self.data[self.ptr]:
    result.append(self.data[self.ptr])
    self.ptr += 1  # Directly update the pointer

3. Incorrect Boundary Check

Failing to properly check array boundaries before accessing elements can lead to IndexError.

Pitfall Example:

# Wrong: Checking data[self.ptr] before bounds check
while self.data[self.ptr] is not None and self.ptr < len(self.data):
    # This will fail when ptr reaches len(self.data)

Solution: Always check bounds before accessing array elements:

while self.ptr < len(self.data) and self.data[self.ptr] is not None:
    # Bounds check comes first

4. Using Wrong Initial Pointer Value

Starting the pointer at 0 instead of 1 is a common mistake when dealing with 1-indexed problems.

Pitfall Example:

def __init__(self, n: int):
    self.ptr = 0  # Wrong: IDs start from 1, not 0
    self.data = [None] * (n + 1)

Solution: Initialize pointer to 1 since that's the first valid ID:

self.ptr = 1  # Correct starting position
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More