Builds on Next Greater Element, which you haven’t finished yet.
Circular Array Handling (Virtual Doubling)
nums is a circular integer array: the element after the last one is the first one again. For every element, walk forward from it, wrapping past the end back to the start when you reach it, and report the first value that is strictly larger. If no larger value exists anywhere around the circle, report -1 for that element. Return the answers in the same order as nums.
1 sees 2 next. Nothing is bigger than 2, so it gets -1. The last 1 wraps around to the start and finds 2.Circular Array
circular array ➔ walk indices 0..2n-1 with i % n ➔ see past the end| 1 | def next_greater_elements(nums: list[int]) -> list[int]: |
| 2 | """Next Greater Element II: scan the virtually doubled array right to left. |
| 3 | Time: O(N) | Space: O(N) |
| 4 | """ |
| 5 | n = len(nums) |
| 6 | res = [-1] * n |
| 7 | stack: list[int] = [] # Candidate values, decreasing from bottom to top |
| 8 | |
| 9 | # 1. Scan The Doubled Array Right To Left: |
| 10 | # i % n reads the circular array twice without copying it, so by the time i reaches a real index, every value that wraps around to its right has already been seen. |
| 11 | for i in range(2 * n - 1, -1, -1): |
| 12 | val = nums[i % n] |
| 13 | # 2. Drop Candidates val Blocks: |
| 14 | # A stacked value that is not strictly greater than val can never be the answer for anything further left, since val stands in front of it and is at least as big. |
| 15 | while stack and stack[-1] <= val: |
| 16 | stack.pop() |
| 17 | # 3. Record Only On The Real Pass: |
| 18 | # i >= n is only priming the stack with the wrap-around values, so the answer is written exclusively when i < n, a real index. |
| 19 | if i < n: |
| 20 | res[i] = stack[-1] if stack else -1 |
| 21 | stack.append(val) |
| 22 | |
| 23 | return res |
stack holds the values right of position i in the doubled array that no later value has blocked, strictly decreasing from bottom to top.A circular array written twice in a row contains every wrap-around, so loop i from 2 * n - 1 down to 0 and read nums[i % n] instead of copying. Scanning right to left, the steps with i >= n fill stack with the values that wrap around, so when i reaches a real index, everything after it on the circle has already been seen; that is why res[i] is written only when i < n. The trap is equal values: pop while stack[-1] <= val, or a value equal to val stays on top and is reported as its next greater element.
See it run
Next Greater Element II: next_greater_elements([1, 2, 1])
i -val -stack []res [_, _, _]
The top row is the array written twice, so the column number is i. The stack row fills from the left, and the res row uses the same column numbers as i for i below n.
ithe index being read, and val = nums[i % n]topthe top of stack, which is the last value that was pushedwrittenthe slot of res that was just filled
The doubled array has 6 columns, and i runs from 5 down to 0. stack is empty and res has 3 unwritten slots.
Gotcha 1 of 3
- The trap
Gotcha 1: Popping with
stack[-1] < valinstead of<=Mistakewhile stack and stack[-1] < val:Fixwhile stack and stack[-1] <= val:What happens: A value equal to
valis not greater thanval. With<, an equal value stays on the stack. It is then on top, and it is reported as the next greater element.Example: nums = [5, 5, 5]Mistake: returns [5, 5, 5]Fix: returns [-1, -1, -1]
- The loop is hidden: you follow links (
i -> nums[i]) and must find out whether and where they repeat, as in Find the Duplicate Number (LC 287), rather than being told the array wraps around. See Fast & Slow Pointers
Deep Dive
Why walking 2n indices shows every wrap-around
In a circular array, the element after the last one is nums[0]. For a position i, the elements to look at are the next n - 1 elements on the circle. In a normal array, a scan from i stops at the end. On the circle, it must continue from the start.
Write the array twice in a row. The doubled array has 2 * n positions, and position j holds nums[j % n]. For every i < n, the positions i + 1 to i + n - 1 are in the doubled array, and they are exactly the n - 1 elements that follow i on the circle. The code never builds the doubled array. It reads nums[j % n], so there is no copy and no extra memory for it.
Two laps are enough. The next greater element of i is at most n - 1 positions away. Position i + n is nums[i] again, and a value is never greater than itself.
Tracing [1, 2, 1] from i = 5 down to 0
Here n = 3, so i runs from 5 to 0. The first three steps, i = 5, 4, 3, only fill stack. They read nums[2] = 1, nums[1] = 2 and nums[0] = 1. After them, stack is [2, 1]: the values to the right of the real indices that no later value blocks.
At i = 2, val = 1. The top of stack is 1, which is equal, so it is popped. The top is now 2, so res[2] = 2. The value 2 is at doubled position 4, which is nums[1], so this answer came from the wrap-around.
At i = 1, val = 2. The stack pops 1 and then 2, because 2 is equal to val. stack is empty, so res[1] = -1. At i = 0, val = 1, and the top of stack is 2, so res[0] = 2.
The result is [2, -1, 2], the answer of Next Greater Element II (LC 503), Example 1.
The two traps: equal values and the i < n check
The first trap is equal values. The question asks for a value that is strictly greater. So the pop test is stack[-1] <= val. With <, an equal value stays on top and is returned as the answer. For nums = [5, 5, 5], the correct result is [-1, -1, -1], and the wrong test returns [5, 5, 5].
The second trap is the write. The loop has 2 * n steps, but res has only n slots. The line res[i] = ... must run only when i < n. For i >= n, the index is past the end of res, and Python raises IndexError. Those first n steps have one job: they put the wrap-around values on stack.
A third trap is the copy nums + nums. It gives the same answers, but it uses O(N) extra memory. Reading nums[i % n] uses none.
The cost, and where else i % n appears
The loop runs 2 * n times. Each step pushes one value and pops each value at most once, so the time is O(N). stack and res hold up to n values, so the space is O(N).
The same index i % n appears in other circular problems. In Defuse the Bomb (LC 1652), a window of fixed length wraps around the end. In Minimum Swaps to Group All 1's Together II (LC 2134) and Alternating Groups II (LC 3208), a window also crosses the end, so each index is read as nums[j % n].
This is not the same as a loop you must detect. In Find the Duplicate Number (LC 287), nobody tells you that the sequence repeats, and the Fast & Slow Pointers card finds the repeat. Here the array is circular by definition.
60-Second Pitch
"The array is circular, so the elements after position i include the start of the array. I treat it as the array written twice, and I read nums[i % n] so I do not copy it. I scan from 2 * n - 1 down to 0 with a stack of values that decrease from bottom to top. At each step I pop every value that is not strictly greater than the current one. The first n steps only fill the stack with the wrap-around values. For i < n, the top of the stack is the answer, or -1 if the stack is empty. Then I push the current value. Each value is pushed and popped at most once per lap, so the time is O(N), and the space is O(N)."
Common Questions
- What is the time and space complexity?
The time is
O(N). The loop runs2 * ntimes, and each push has at most one pop, so the total work is linear. The space isO(N)forstackandres. Readingnums[i % n]adds no memory.- Why are two laps enough?
The next greater element of a position is at most
n - 1positions away on the circle. Afternpositions, the scan is back at the same value, and a value is not greater than itself. If no greater value exists inside one lap, none exists in any lap, and the answer is -1.- Why scan from right to left?
The stack then holds the values to the right of
i, and its top is the nearest one, sores[i]is written in the same step. A left-to-right scan also works. It keeps indices on the stack and setsres[j]whenjis popped, and it also needs two laps.- How do you handle a window that wraps around the end?
- What changes if you must return the index of the next greater element?
Store indices on the stack, as
i % n, and comparenums[stack[-1]] <= val. Thenres[i]isstack[-1]. The pops and the two laps stay the same. The index stored is the position on the circle, not the position in the doubled array.
SWE Reasoning
Spot it
- The statement says the array is circular, or that the last element is followed by the first.
- A scan, a stack or a window must look at elements past the end.
- The answer for each position depends on the next
n - 1elements on the circle. - A linear version of the problem exists, such as Next Greater Element (a stack) or a fixed window.
The invariant, step by step
reshasnslots, all-1.stackis empty.iruns from2 * n - 1down to 0, andval = nums[i % n]reads the doubled array without copying it.while stack and stack[-1] <= valpops every value that is not strictly greater thanval. Such a value can never be the answer for a position to the left, becausevalis in front of it and is at least as large.- For
i >= n, nothing is written.stack.append(val)only prepares the stack for the real indices. - For
i < n,res[i] = stack[-1] if stack else -1. The top ofstackis the nearest value to the right ofi, on the circle, that is strictly greater thanval. stack.append(val)keeps the values strictly decreasing from bottom to top.
Traps
- Popping with
<instead of<=. An equal value stays on top and is reported as the next greater element. - Writing
res[i]for everyi. Fori >= n, the index is past the end ofres, and Python raisesIndexError. - Building
nums + nums. It costsO(N)extra memory, andnums[i % n]does the same job. - Scanning one lap only. A value at the end of the array never sees the values at its start.
- Using
3 * nor more steps. Two laps are enough, because the answer is at mostn - 1positions away.
Practice
Builds on Next Greater Element, which you haven’t finished yet.
Circular Array Handling (Virtual Doubling)
nums is a circular integer array: the element after the last one is the first one again. For every element, walk forward from it, wrapping past the end back to the start when you reach it, and report the first value that is strictly larger. If no larger value exists anywhere around the circle, report -1 for that element. Return the answers in the same order as nums.
1 sees 2 next. Nothing is bigger than 2, so it gets -1. The last 1 wraps around to the start and finds 2.Circular Array
circular array ➔ walk indices 0..2n-1 with i % n ➔ see past the end| 1 | def next_greater_elements(nums: list[int]) -> list[int]: |
| 2 | """Next Greater Element II: scan the virtually doubled array right to left. |
| 3 | Time: O(N) | Space: O(N) |
| 4 | """ |
| 5 | n = len(nums) |
| 6 | res = [-1] * n |
| 7 | stack: list[int] = [] # Candidate values, decreasing from bottom to top |
| 8 | |
| 9 | # 1. Scan The Doubled Array Right To Left: |
| 10 | # i % n reads the circular array twice without copying it, so by the time i reaches a real index, every value that wraps around to its right has already been seen. |
| 11 | for i in range(2 * n - 1, -1, -1): |
| 12 | val = nums[i % n] |
| 13 | # 2. Drop Candidates val Blocks: |
| 14 | # A stacked value that is not strictly greater than val can never be the answer for anything further left, since val stands in front of it and is at least as big. |
| 15 | while stack and stack[-1] <= val: |
| 16 | stack.pop() |
| 17 | # 3. Record Only On The Real Pass: |
| 18 | # i >= n is only priming the stack with the wrap-around values, so the answer is written exclusively when i < n, a real index. |
| 19 | if i < n: |
| 20 | res[i] = stack[-1] if stack else -1 |
| 21 | stack.append(val) |
| 22 | |
| 23 | return res |
stack holds the values right of position i in the doubled array that no later value has blocked, strictly decreasing from bottom to top.A circular array written twice in a row contains every wrap-around, so loop i from 2 * n - 1 down to 0 and read nums[i % n] instead of copying. Scanning right to left, the steps with i >= n fill stack with the values that wrap around, so when i reaches a real index, everything after it on the circle has already been seen; that is why res[i] is written only when i < n. The trap is equal values: pop while stack[-1] <= val, or a value equal to val stays on top and is reported as its next greater element.
See it run
Next Greater Element II: next_greater_elements([1, 2, 1])
i -val -stack []res [_, _, _]
The top row is the array written twice, so the column number is i. The stack row fills from the left, and the res row uses the same column numbers as i for i below n.
ithe index being read, and val = nums[i % n]topthe top of stack, which is the last value that was pushedwrittenthe slot of res that was just filled
The doubled array has 6 columns, and i runs from 5 down to 0. stack is empty and res has 3 unwritten slots.
Gotcha 1 of 3
- The trap
Gotcha 1: Popping with
stack[-1] < valinstead of<=Mistakewhile stack and stack[-1] < val:Fixwhile stack and stack[-1] <= val:What happens: A value equal to
valis not greater thanval. With<, an equal value stays on the stack. It is then on top, and it is reported as the next greater element.Example: nums = [5, 5, 5]Mistake: returns [5, 5, 5]Fix: returns [-1, -1, -1]
- The loop is hidden: you follow links (
i -> nums[i]) and must find out whether and where they repeat, as in Find the Duplicate Number (LC 287), rather than being told the array wraps around. See Fast & Slow Pointers
Deep Dive
Why walking 2n indices shows every wrap-around
In a circular array, the element after the last one is nums[0]. For a position i, the elements to look at are the next n - 1 elements on the circle. In a normal array, a scan from i stops at the end. On the circle, it must continue from the start.
Write the array twice in a row. The doubled array has 2 * n positions, and position j holds nums[j % n]. For every i < n, the positions i + 1 to i + n - 1 are in the doubled array, and they are exactly the n - 1 elements that follow i on the circle. The code never builds the doubled array. It reads nums[j % n], so there is no copy and no extra memory for it.
Two laps are enough. The next greater element of i is at most n - 1 positions away. Position i + n is nums[i] again, and a value is never greater than itself.
Tracing [1, 2, 1] from i = 5 down to 0
Here n = 3, so i runs from 5 to 0. The first three steps, i = 5, 4, 3, only fill stack. They read nums[2] = 1, nums[1] = 2 and nums[0] = 1. After them, stack is [2, 1]: the values to the right of the real indices that no later value blocks.
At i = 2, val = 1. The top of stack is 1, which is equal, so it is popped. The top is now 2, so res[2] = 2. The value 2 is at doubled position 4, which is nums[1], so this answer came from the wrap-around.
At i = 1, val = 2. The stack pops 1 and then 2, because 2 is equal to val. stack is empty, so res[1] = -1. At i = 0, val = 1, and the top of stack is 2, so res[0] = 2.
The result is [2, -1, 2], the answer of Next Greater Element II (LC 503), Example 1.
The two traps: equal values and the i < n check
The first trap is equal values. The question asks for a value that is strictly greater. So the pop test is stack[-1] <= val. With <, an equal value stays on top and is returned as the answer. For nums = [5, 5, 5], the correct result is [-1, -1, -1], and the wrong test returns [5, 5, 5].
The second trap is the write. The loop has 2 * n steps, but res has only n slots. The line res[i] = ... must run only when i < n. For i >= n, the index is past the end of res, and Python raises IndexError. Those first n steps have one job: they put the wrap-around values on stack.
A third trap is the copy nums + nums. It gives the same answers, but it uses O(N) extra memory. Reading nums[i % n] uses none.
The cost, and where else i % n appears
The loop runs 2 * n times. Each step pushes one value and pops each value at most once, so the time is O(N). stack and res hold up to n values, so the space is O(N).
The same index i % n appears in other circular problems. In Defuse the Bomb (LC 1652), a window of fixed length wraps around the end. In Minimum Swaps to Group All 1's Together II (LC 2134) and Alternating Groups II (LC 3208), a window also crosses the end, so each index is read as nums[j % n].
This is not the same as a loop you must detect. In Find the Duplicate Number (LC 287), nobody tells you that the sequence repeats, and the Fast & Slow Pointers card finds the repeat. Here the array is circular by definition.
60-Second Pitch
"The array is circular, so the elements after position i include the start of the array. I treat it as the array written twice, and I read nums[i % n] so I do not copy it. I scan from 2 * n - 1 down to 0 with a stack of values that decrease from bottom to top. At each step I pop every value that is not strictly greater than the current one. The first n steps only fill the stack with the wrap-around values. For i < n, the top of the stack is the answer, or -1 if the stack is empty. Then I push the current value. Each value is pushed and popped at most once per lap, so the time is O(N), and the space is O(N)."
Common Questions
- What is the time and space complexity?
The time is
O(N). The loop runs2 * ntimes, and each push has at most one pop, so the total work is linear. The space isO(N)forstackandres. Readingnums[i % n]adds no memory.- Why are two laps enough?
The next greater element of a position is at most
n - 1positions away on the circle. Afternpositions, the scan is back at the same value, and a value is not greater than itself. If no greater value exists inside one lap, none exists in any lap, and the answer is -1.- Why scan from right to left?
The stack then holds the values to the right of
i, and its top is the nearest one, sores[i]is written in the same step. A left-to-right scan also works. It keeps indices on the stack and setsres[j]whenjis popped, and it also needs two laps.- How do you handle a window that wraps around the end?
- What changes if you must return the index of the next greater element?
Store indices on the stack, as
i % n, and comparenums[stack[-1]] <= val. Thenres[i]isstack[-1]. The pops and the two laps stay the same. The index stored is the position on the circle, not the position in the doubled array.
SWE Reasoning
Spot it
- The statement says the array is circular, or that the last element is followed by the first.
- A scan, a stack or a window must look at elements past the end.
- The answer for each position depends on the next
n - 1elements on the circle. - A linear version of the problem exists, such as Next Greater Element (a stack) or a fixed window.
The invariant, step by step
reshasnslots, all-1.stackis empty.iruns from2 * n - 1down to 0, andval = nums[i % n]reads the doubled array without copying it.while stack and stack[-1] <= valpops every value that is not strictly greater thanval. Such a value can never be the answer for a position to the left, becausevalis in front of it and is at least as large.- For
i >= n, nothing is written.stack.append(val)only prepares the stack for the real indices. - For
i < n,res[i] = stack[-1] if stack else -1. The top ofstackis the nearest value to the right ofi, on the circle, that is strictly greater thanval. stack.append(val)keeps the values strictly decreasing from bottom to top.
Traps
- Popping with
<instead of<=. An equal value stays on top and is reported as the next greater element. - Writing
res[i]for everyi. Fori >= n, the index is past the end ofres, and Python raisesIndexError. - Building
nums + nums. It costsO(N)extra memory, andnums[i % n]does the same job. - Scanning one lap only. A value at the end of the array never sees the values at its start.
- Using
3 * nor more steps. Two laps are enough, because the answer is at mostn - 1positions away.