1441. Build an Array With Stack Operations
Problem Description
You need to build a specific array using stack operations while reading from a stream of consecutive integers.
You're given:
- An integer array
target
that you want to build - An integer
n
representing the upper limit of available numbers
You have access to:
- A stream containing integers from
[1, n]
in order (1, 2, 3, ..., n) - An initially empty stack
- Two operations:
"Push"
: adds the next integer from the stream to the top of the stack"Pop"
: removes the integer from the top of the stack
The goal is to make the stack contents (from bottom to top) exactly match the target
array.
Rules to follow:
- When you perform a
"Push"
operation, you must take the next available integer from the stream (you can't skip numbers or go back) - You can
"Pop"
elements from the stack whenever needed - Once your stack matches
target
, stop immediately - don't read more numbers or perform more operations - The stream provides numbers sequentially (1, then 2, then 3, etc.)
Example: If target = [1, 3]
and n = 3
:
- You need 1 and 3 in your stack, but not 2
- Since the stream gives you 1, 2, 3 in order, you must:
- Push 1 (now stack has [1])
- Push 2 (now stack has [1, 2])
- Pop 2 (back to [1])
- Push 3 (now stack has [1, 3])
- Result:
["Push", "Push", "Pop", "Push"]
The solution simulates this process by tracking which number from the stream we're currently at (cur
). For each target number x
, if we need to skip some numbers (when cur < x
), we push and immediately pop those numbers. When we reach the desired number, we push it and keep it.
Intuition
The key insight is recognizing that we're forced to read numbers from the stream sequentially - we can't skip ahead or go back. This means if we want number 3
in our stack but haven't read 1
and 2
yet, we must read them first.
Think of it like a conveyor belt of numbers moving past you. You must take each number as it comes, but you can immediately throw it away if you don't need it. The "Push"
followed by "Pop"
combination is our way of "skipping" unwanted numbers.
Let's trace through the thought process:
-
We need specific numbers from a sequential stream - The target array might be
[1, 3, 6]
, but the stream gives us1, 2, 3, 4, 5, 6, ...
in order. -
We can't ignore numbers in the stream - If we want
3
, we must first deal with1
and2
. The only way to "deal with" unwanted numbers is to push them onto the stack and immediately pop them off. -
The pattern emerges - For each target number
x
:- All numbers before
x
that aren't in our target need to be pushed and popped - The number
x
itself needs to be pushed and kept
- All numbers before
-
Why this works - By pushing and immediately popping unwanted numbers, we're essentially discarding them while maintaining our position in the stream. When we reach a number we want, we push it without popping, keeping it in our final stack.
The solution naturally follows: iterate through each target number, skip (push-pop) all numbers between our current position and the target number, then push the target number itself. This ensures our stack builds up exactly the target array from bottom to top.
Learn more about Stack patterns.
Solution Approach
The implementation uses a simulation approach with a simple tracking variable to build the required sequence of operations.
Variables and Data Structures:
ans
: A list to store the sequence of operations ("Push" and "Pop")cur
: An integer tracking the current number from the stream (starts at 1)- We iterate through the
target
array to process each desired number
Algorithm Steps:
-
Initialize tracking variables
- Set
cur = 1
to represent the first number in the stream - Create an empty list
ans
for storing operations
- Set
-
Process each target number
For each number
x
in thetarget
array:-
Skip unwanted numbers: While
cur < x
, we need to read and discard numbers from the streamwhile cur < x: ans.extend(["Push", "Pop"]) # Push then immediately Pop cur += 1 # Move to next number in stream
This loop handles all numbers between our current position and the target number we want.
-
Keep the target number: Once
cur == x
, push the current number and keep itans.append("Push") cur += 1
We only push without popping, keeping this number in our stack.
-
-
Return the result
- After processing all target numbers,
ans
contains the complete sequence of operations
- After processing all target numbers,
Example Walkthrough:
For target = [1, 3]
:
- Start with
cur = 1
,ans = []
- Process
x = 1
:cur == 1
, so skip the while loop- Push 1:
ans = ["Push"]
,cur = 2
- Process
x = 3
:cur = 2 < 3
, so enter while loop- Push and Pop 2:
ans = ["Push", "Push", "Pop"]
,cur = 3
- Now
cur == 3
, exit while loop - Push 3:
ans = ["Push", "Push", "Pop", "Push"]
,cur = 4
- Return
["Push", "Push", "Pop", "Push"]
The beauty of this solution is its simplicity - we just track where we are in the stream and push-pop to skip unwanted numbers, only keeping the ones we need.
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 target = [2, 4]
with n = 5
:
Initial State:
- Stream provides: 1, 2, 3, 4, 5 (in order)
- Stack: empty
cur = 1
(tracking our position in the stream)ans = []
(to store our operations)
Step 1: Build to target[0] = 2
- We want 2, but
cur = 1 < 2
- Must read 1 first (can't skip in stream)
- Push 1, then Pop 1 (discard it):
ans = ["Push", "Pop"]
- Move to next:
cur = 2
- Now
cur == 2
, so Push 2 and keep it:ans = ["Push", "Pop", "Push"]
- Stack now contains: [2]
- Move to next:
cur = 3
Step 2: Build to target[1] = 4
- We want 4, but
cur = 3 < 4
- Must read 3 first
- Push 3, then Pop 3 (discard it):
ans = ["Push", "Pop", "Push", "Push", "Pop"]
- Move to next:
cur = 4
- Now
cur == 4
, so Push 4 and keep it:ans = ["Push", "Pop", "Push", "Push", "Pop", "Push"]
- Stack now contains: [2, 4]
- Move to next:
cur = 5
Final Result:
- Stack: [2, 4] ✓ (matches target)
- Operations:
["Push", "Pop", "Push", "Push", "Pop", "Push"]
The pattern is clear: we Push-Pop to skip numbers we don't want (1 and 3), and just Push to keep numbers we do want (2 and 4).
Solution Implementation
1class Solution:
2 def buildArray(self, target: List[int], n: int) -> List[str]:
3 """
4 Build an array using a stack machine with Push and Pop operations.
5 The stack machine pushes numbers from 1 to n sequentially.
6
7 Args:
8 target: Target array to build (sorted in ascending order)
9 n: Maximum number that can be pushed (1 to n)
10
11 Returns:
12 List of operations ("Push" or "Pop") to build the target array
13 """
14 operations = []
15 current_number = 1
16
17 # Iterate through each target number
18 for target_number in target:
19 # Push and immediately pop all numbers before the target number
20 # These numbers are not needed in the final array
21 while current_number < target_number:
22 operations.extend(["Push", "Pop"])
23 current_number += 1
24
25 # Push the target number (it will stay in the stack)
26 operations.append("Push")
27 current_number += 1
28
29 return operations
30
1class Solution {
2 public List<String> buildArray(int[] target, int n) {
3 // Initialize the result list to store the operations
4 List<String> result = new ArrayList<>();
5
6 // Start with the first number in the stream (1-indexed)
7 int currentNumber = 1;
8
9 // Iterate through each target number we need to build
10 for (int targetNumber : target) {
11 // For numbers between current and target, we need to push and immediately pop
12 // since they are not in our target array
13 while (currentNumber < targetNumber) {
14 result.add("Push");
15 result.add("Pop");
16 currentNumber++;
17 }
18
19 // When we reach the target number, push it onto the stack
20 result.add("Push");
21 currentNumber++;
22 }
23
24 return result;
25 }
26}
27
1class Solution {
2public:
3 vector<string> buildArray(vector<int>& target, int n) {
4 vector<string> result;
5 int currentNumber = 1; // Start from 1 as the stream begins with 1
6
7 // Iterate through each target number we need to build
8 for (int targetNumber : target) {
9 // For numbers between current and target that we don't need,
10 // push them and immediately pop them
11 while (currentNumber < targetNumber) {
12 result.push_back("Push");
13 result.push_back("Pop");
14 currentNumber++;
15 }
16
17 // Push the target number we actually want to keep
18 result.push_back("Push");
19 currentNumber++;
20 }
21
22 return result;
23 }
24};
25
1/**
2 * Builds an array of stack operations to create the target array
3 * from a stream of numbers 1 to n
4 * @param target - The target array to build
5 * @param n - The upper limit of the number stream (1 to n)
6 * @returns Array of "Push" and "Pop" operations
7 */
8function buildArray(target: number[], n: number): string[] {
9 // Array to store the sequence of operations
10 const operations: string[] = [];
11
12 // Current number in the stream (starting from 1)
13 let currentNumber: number = 1;
14
15 // Iterate through each target number
16 for (const targetNumber of target) {
17 // Push and immediately pop all numbers before the target number
18 // These numbers are not needed in the final array
19 for (; currentNumber < targetNumber; ++currentNumber) {
20 operations.push('Push', 'Pop');
21 }
22
23 // Push the target number (this one stays in the stack)
24 operations.push('Push');
25
26 // Move to the next number in the stream
27 ++currentNumber;
28 }
29
30 return operations;
31}
32
Time and Space Complexity
The time complexity is O(m)
, where m = target[-1]
(the last element in the target array). This is because in the worst case, we need to iterate from 1 to the largest element in the target array. For each number from 1 to target[-1]
, we either perform a "Push" operation (if it's in target) or a "Push" followed by "Pop" operation (if it's not in target). The while loop runs target[-1] - len(target)
times for missing numbers, and we process each element in target once.
The space complexity is O(1)
if we exclude the output array ans
. We only use a constant amount of extra space with the variable cur
to track the current number. If we include the output array, the space complexity would be O(m)
where m = target[-1]
, as we need at most 2 * (target[-1] - 1)
operations in the worst case.
Note: The reference answer states the time complexity as O(n)
where n
is the length of the target array, but this is incorrect. The actual time complexity depends on the value of the last element in target, not just the length of the target array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Stop at the Target Array's End
A common mistake is continuing to process numbers beyond what's needed. Once you've built the target array, you should stop immediately - don't read more numbers from the stream.
Incorrect approach:
# Wrong: Processing all numbers up to n
for i in range(1, n + 1):
if i in target:
operations.append("Push")
else:
operations.extend(["Push", "Pop"])
Why it's wrong: If target = [1, 3]
and n = 100
, this would generate operations for numbers 4 through 100, which is unnecessary and incorrect.
Correct approach: Stop after the last target element is processed (as shown in the solution).
2. Mishandling the Current Number Tracking
Another pitfall is incorrectly managing the current_number
variable, especially forgetting to increment it after operations.
Incorrect approach:
for target_number in target: while current_number < target_number: operations.extend(["Push", "Pop"]) # Forgot to increment current_number! operations.append("Push") # Forgot to increment here too!
Why it's wrong: This creates an infinite loop when current_number < target_number
because current_number
never changes.
Correct approach: Always increment current_number
after each Push operation (whether followed by Pop or not).
3. Using Set Membership Instead of Sequential Processing
Some might try to check if each number is in the target using set membership, but this misses the sequential nature of the problem.
Incorrect approach:
target_set = set(target)
for i in range(1, max(target) + 1):
if i in target_set:
operations.append("Push")
else:
operations.extend(["Push", "Pop"])
Why it's wrong: While this might seem to work, it doesn't leverage the fact that target
is already sorted, and it processes numbers unnecessarily if there are gaps at the end.
Correct approach: Process the target array sequentially, which naturally handles the sorted order and stops at the right time.
4. Off-by-One Errors with Initial Value
Starting current_number
at 0 instead of 1 is a subtle but critical error.
Incorrect approach:
current_number = 0 # Wrong starting point for target_number in target: while current_number < target_number: operations.extend(["Push", "Pop"]) current_number += 1 operations.append("Push") current_number += 1
Why it's wrong: The stream starts at 1, not 0. This would cause you to push and pop an extra number at the beginning.
Correct approach: Initialize current_number = 1
since the stream provides integers starting from 1.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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!