1656. Design an Ordered Stream
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 andn
- 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:
- You maintain an array to store values at their corresponding ID positions
- You keep a pointer starting at position 1
- When you insert
(idKey, value)
, you store the value at positionidKey
- 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
- 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"]
.
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:
- IDs range from 1 to
n
, which maps naturally to array indices - We need O(1) access to check if a position is filled
- 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 sizen + 1
wheredata[i]
stores the value foridKey = i
. We use sizen + 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:
-
Store the value:
self.data[idKey] = value
Place the value at its corresponding position in the array.
-
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 EvaluatorExample 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:
- Values are stored immediately but only returned when their turn comes
- The pointer ensures we output in sorted order
- Multiple consecutive values can be returned in one operation (like steps 3 and 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 withn+1
elements. - For each
insert
operation: While a single insert might seemO(1)
in the best case (whenidKey != ptr
), we need to consider the amortized complexity. The while loop processes elements starting fromptr
and advances it. Each element in the stream is processed exactly once by the while loop across all insert operations. Therefore, if we performn
insertions, the total time spent in all while loops combined isO(n)
, giving us an amortized time complexity ofO(1)
per insert operation, butO(n)
total for all operations.
Space Complexity: O(n)
where n
is the length of the data stream.
- The
data
array storesn+1
elements (indexed from 0 to n, though index 0 is unused). - The
ptr
variable usesO(1)
space. - The
ans
list in each insert operation can contain at mostn
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 isO(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
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!