LeetCode 2454 - Next Greater Element IV
The problem asks us to compute, for every element in the array, its "second greater element" to the right. For an index i, we are interested in elements positioned after i. Among those elements, we only care about values strictly greater than nums[i].
Difficulty: 🔴 Hard
Topics: Array, Binary Search, Stack, Sorting, Heap (Priority Queue), Monotonic Stack
Solution
Problem Understanding
The problem asks us to compute, for every element in the array, its "second greater element" to the right.
For an index i, we are interested in elements positioned after i. Among those elements, we only care about values strictly greater than nums[i].
The second greater element is defined very precisely:
- We need an index
j > i nums[j] > nums[i]- Between
iandj, there must be exactly one element greater thannums[i]
Another way to think about this is:
- Find all elements to the right of
nums[i]that are greater than it - Order them by position
- The second greater element is simply the second such element encountered
For example, consider:
nums = [2,4,0,9,6]
For 2 at index 0:
- First greater element is
4 - Second greater element is
9
So the answer for index 0 is 9.
For 0 at index 2:
- Greater elements to the right are
9and6 - The second one encountered is
6
So the answer is 6.
If fewer than two greater elements exist to the right, the answer is -1.
The constraints are important:
nums.lengthcan be up to100000- A quadratic solution would perform up to
10^10operations in the worst case, which is far too slow
This immediately tells us that we need a near linear solution, typically O(n) or O(n log n).
Several edge cases are important:
- Arrays with all equal values, such as
[3,3,3] - Strictly decreasing arrays, where no greater elements exist
- Strictly increasing arrays, where many second greater elements exist
- Duplicate values, because the condition requires strictly greater values
- Very large arrays, which rule out nested scanning approaches
The problem guarantees:
- All numbers are non-negative
- Array length is at least
1 - Values fit comfortably within standard integer types
Approaches
Brute Force Approach
The most direct solution is to process each index independently.
For every index i:
- Scan all elements to the right
- Count how many elements are greater than
nums[i] - Once the second greater element is found, record it
- If the scan finishes before finding two greater elements, return
-1
For example:
nums = [2,4,0,9,6]
For index 0:
4 > 2, count = 10ignored9 > 2, count = 2, answer = 9
This works because it directly follows the definition of the problem.
However, its performance is poor.
For each element, we may scan nearly the entire remainder of the array. With n = 100000, this becomes too slow.
Optimal Approach
The key observation is that we do not actually need to repeatedly rescan the array.
This problem resembles the classic "next greater element" problem, which is usually solved with a monotonic stack. However, here we need the second greater element, not the first.
The important insight is:
- Some elements are still waiting for their first greater element
- Some elements have already found their first greater element and are now waiting for their second greater element
We can track these two states separately.
We use:
- One monotonic decreasing stack for elements waiting for their first greater element
- Another structure for elements that already found their first greater element and are now waiting for the second
The elegant solution uses:
- A decreasing stack called
first_stack - Another decreasing stack called
second_stack
As we process each number:
- Elements popped from
second_stackhave now found their second greater element - Elements popped from
first_stackhave just found their first greater element, so they move intosecond_stack
This allows every element to move through the process only a small number of times, leading to linear complexity.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Scan right side for every index |
| Optimal | O(n) | O(n) | Uses two monotonic stacks |
Algorithm Walkthrough
Optimal Two Stack Algorithm
- Initialize the result array with
-1.
Every index defaults to -1 because some elements may never find a second greater element.
2. Create two stacks.
The first stack stores indices waiting for their first greater element.
The second stack stores indices that already found their first greater element and are now waiting for their second greater element. 3. Iterate through the array from left to right.
At each position, treat the current value as a possible greater element for earlier indices. 4. First, process the second stack.
While the current number is greater than the value represented by the top index in second_stack, we have found the second greater element for that index.
Record the current value in the answer array. 5. Next, process the first stack.
While the current number is greater than the value represented by the top index in first_stack, those indices have now found their first greater element.
Remove them from first_stack.
6. Transfer those removed indices into second_stack.
This means they are now waiting for their second greater element.
The transfer order matters because we want to preserve the monotonic behavior correctly.
7. Push the current index into first_stack.
The current element has not yet found even its first greater element. 8. Continue until all elements are processed. 9. Return the answer array.
Why it works
The algorithm maintains two clear invariants:
first_stackcontains indices that have not yet encountered any greater elementsecond_stackcontains indices that have encountered exactly one greater element
When a new value arrives:
- Any index removed from
second_stackhas now seen exactly two greater elements, so the current value is its answer - Any index removed from
first_stackhas now seen its first greater element, so it transitions intosecond_stack
Each index moves through the states in order:
waiting for first greater
→ waiting for second greater
→ completed
Because every index is pushed and popped only a constant number of times, the total work is linear.
Python Solution
from typing import List
class Solution:
def secondGreaterElement(self, nums: List[int]) -> List[int]:
n = len(nums)
answer = [-1] * n
first_stack = []
second_stack = []
for current_index, current_value in enumerate(nums):
# Resolve second greater elements
while second_stack and nums[second_stack[-1]] < current_value:
index = second_stack.pop()
answer[index] = current_value
# Elements that just found their first greater element
moved = []
while first_stack and nums[first_stack[-1]] < current_value:
moved.append(first_stack.pop())
# Preserve correct order when moving
while moved:
second_stack.append(moved.pop())
# Current element waits for first greater
first_stack.append(current_index)
return answer
The implementation follows the exact state transition described earlier.
The answer array is initialized with -1, which automatically handles elements that never find a second greater value.
first_stack stores indices waiting for their first greater element. The stack is maintained in decreasing order of values.
Whenever the current number is greater than the top element of first_stack, those indices have now encountered their first greater element. They are removed and temporarily stored in moved.
The temporary moved array is important because the order of insertion into second_stack matters. Reversing the order preserves the monotonic property correctly.
second_stack contains indices that already found one greater element. When the current value exceeds the top of this stack, the current value becomes the second greater element for that index.
Every index is pushed and popped at most once from each stack, which guarantees linear runtime.
Go Solution
func secondGreaterElement(nums []int) []int {
n := len(nums)
answer := make([]int, n)
for i := 0; i < n; i++ {
answer[i] = -1
}
firstStack := []int{}
secondStack := []int{}
for currentIndex, currentValue := range nums {
// Resolve second greater elements
for len(secondStack) > 0 &&
nums[secondStack[len(secondStack)-1]] < currentValue {
index := secondStack[len(secondStack)-1]
secondStack = secondStack[:len(secondStack)-1]
answer[index] = currentValue
}
moved := []int{}
// Resolve first greater elements
for len(firstStack) > 0 &&
nums[firstStack[len(firstStack)-1]] < currentValue {
index := firstStack[len(firstStack)-1]
firstStack = firstStack[:len(firstStack)-1]
moved = append(moved, index)
}
// Move into second stack in reverse order
for len(moved) > 0 {
secondStack = append(secondStack, moved[len(moved)-1])
moved = moved[:len(moved)-1]
}
firstStack = append(firstStack, currentIndex)
}
return answer
}
The Go implementation mirrors the Python logic closely.
Slices are used as stacks. Appending pushes an element, and reslicing removes the top.
The result array must be explicitly initialized to -1 because Go integer slices default to 0.
There are no integer overflow concerns because the constraints fit within Go's standard int type.
Worked Examples
Example 1
nums = [2,4,0,9,6]
Initial state:
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| Start | - | [] | [] | [-1,-1,-1,-1,-1] |
Process index 0, value 2:
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| Push 0 | 2 | [0] | [] | [-1,-1,-1,-1,-1] |
Process index 1, value 4:
4 > 2- Index
0found first greater element - Move
0intosecond_stack
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| After processing | 4 | [1] | [0] | [-1,-1,-1,-1,-1] |
Process index 2, value 0:
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| Push 2 | 0 | [1,2] | [0] | [-1,-1,-1,-1,-1] |
Process index 3, value 9:
9 > 2, so index0gets second greater =99 > 0, index2found first greater9 > 4, index1found first greater
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| After processing | 9 | [3] | [1,2] | [9,-1,-1,-1,-1] |
Process index 4, value 6:
6 > 0, so index2gets second greater =66 > 4, so index1gets second greater =6
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| Final | 6 | [3,4] | [] | [9,6,6,-1,-1] |
Final answer:
[9,6,6,-1,-1]
Example 2
nums = [3,3]
Process index 0:
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| Push 0 | 3 | [0] | [] | [-1,-1] |
Process index 1:
3is not strictly greater than3- Nothing moves
| Step | Current | first_stack | second_stack | answer |
|---|---|---|---|---|
| Final | 3 | [0,1] | [] | [-1,-1] |
Final answer:
[-1,-1]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each index is pushed and popped at most once from each stack |
| Space | O(n) | Stacks and answer array may store all indices |
The runtime is linear because no index is processed excessively. Every element transitions through the stacks a constant number of times.
The space complexity is linear because, in the worst case, both stacks together may hold nearly all indices.
Test Cases
from typing import List
class Solution:
def secondGreaterElement(self, nums: List[int]) -> List[int]:
n = len(nums)
answer = [-1] * n
first_stack = []
second_stack = []
for current_index, current_value in enumerate(nums):
while second_stack and nums[second_stack[-1]] < current_value:
index = second_stack.pop()
answer[index] = current_value
moved = []
while first_stack and nums[first_stack[-1]] < current_value:
moved.append(first_stack.pop())
while moved:
second_stack.append(moved.pop())
first_stack.append(current_index)
return answer
solution = Solution()
assert solution.secondGreaterElement([2,4,0,9,6]) == [9,6,6,-1,-1] # official example
assert solution.secondGreaterElement([3,3]) == [-1,-1] # equal values
assert solution.secondGreaterElement([1]) == [-1] # single element
assert solution.secondGreaterElement([1,2]) == [-1,-1] # only one greater exists
assert solution.secondGreaterElement([1,2,3]) == [3,-1,-1] # increasing sequence
assert solution.secondGreaterElement([5,4,3,2,1]) == [-1,-1,-1,-1,-1] # decreasing sequence
assert solution.secondGreaterElement([1,1,1,1]) == [-1,-1,-1,-1] # duplicates only
assert solution.secondGreaterElement([2,1,3,4]) == [4,4,-1,-1] # multiple second greater values
assert solution.secondGreaterElement([1,5,2,6,3,7]) == [2,7,7,-1,-1,-1] # interleaved behavior
assert solution.secondGreaterElement([10,1,2,3,4]) == [-1,3,4,-1,-1] # large first element
assert solution.secondGreaterElement([1,3,2,4,5]) == [2,5,5,-1,-1] # mixed ordering
| Test | Why |
|---|---|
[2,4,0,9,6] |
Validates official example |
[3,3] |
Ensures strict inequality is enforced |
[1] |
Smallest possible input |
[1,2] |
Only one greater element exists |
[1,2,3] |
Simple increasing sequence |
[5,4,3,2,1] |
No greater elements anywhere |
[1,1,1,1] |
Duplicate handling |
[2,1,3,4] |
Multiple elements resolve together |
[1,5,2,6,3,7] |
Complex stack transitions |
[10,1,2,3,4] |
Large leading element never resolves |
[1,3,2,4,5] |
Mixed ordering and delayed resolution |
Edge Cases
Arrays With Duplicate Values
The comparison must be strictly greater, not greater than or equal.
For example:
[3,3,3]
No element is greater than another, so every answer is -1.
A common bug is accidentally using <= instead of < when comparing stack elements. The implementation correctly uses strict inequality.
Strictly Decreasing Arrays
Consider:
[5,4,3,2,1]
No element ever encounters even a first greater value.
This means all indices remain in first_stack until the end, and the result stays entirely -1.
The implementation handles this naturally because unresolved entries are never overwritten.
Elements With Only One Greater Value
Consider:
[1,2]
The element 1 has only one greater value to its right, not two.
A buggy implementation might incorrectly treat the first greater value as the answer.
This solution explicitly separates:
- waiting for first greater
- waiting for second greater
An element is only assigned an answer after transitioning through both states.
Large Increasing Arrays
For arrays like:
[1,2,3,4,5,...]
Many indices quickly transition between stacks.
A naive nested loop would become quadratic.
The monotonic stack approach ensures each index moves through the system only once, preserving linear complexity even for the largest inputs allowed by the constraints.