3903. Smallest Stable Index I
Problem Description
You are given an integer array nums of length n and an integer k.
For each index i, we define an instability score that measures how "unstable" that position is. The instability score at index i is calculated as:
max(nums[0..i]) - min(nums[i..n - 1])
Let's break down what each part means:
max(nums[0..i])is the largest value among all elements from index0up to and including indexi. In other words, it's the maximum of the prefix ending ati.min(nums[i..n - 1])is the smallest value among all elements from indexiup to and including the last indexn - 1. In other words, it's the minimum of the suffix starting ati.
The instability score is the difference between these two values: the maximum of the left part (including i) minus the minimum of the right part (including i).
An index i is called stable if its instability score is less than or equal to k. That is, index i is stable when:
max(nums[0..i]) - min(nums[i..n - 1]) <= k
Your task is to find the smallest index i that is stable, and return it. If there is no index that satisfies the stability condition, return -1.
How We Pick the Algorithm
Why Prefix Sums?
This problem maps to Prefix Sums through a short path in the full flowchart.
Precomputing prefix sums answers range queries in constant time.
Open in FlowchartIntuition
The key observation is that the instability score at index i depends on two separate quantities: the maximum of the prefix nums[0..i] and the minimum of the suffix nums[i..n - 1]. If we could quickly know both values for every index i, we could simply check each index and return the first one that satisfies the condition.
Computing the prefix maximum for each i is easy: as we move from left to right, the maximum of nums[0..i] only grows or stays the same. We can keep a running variable left that tracks the largest value seen so far, updating it with left = max(left, nums[i]) at each step.
The suffix minimum is trickier to compute on the fly while scanning from left to right, because at index i we would need to know the minimum of all elements from i to the end—values we haven't "looked at" yet in a left-to-right pass. The natural fix is to precompute these suffix minimums in advance. By traversing nums from back to front, we can build an array right, where right[i] stores the minimum value among nums[i..n - 1]. Each entry is computed using right[i] = min(right[i + 1], nums[i]), since the minimum of a suffix is just the current element compared against the minimum of the suffix one position to its right.
Once we have the right array ready, we make a single left-to-right pass. At each index i, the prefix maximum is available in left, and the suffix minimum is available in right[i]. The instability score is simply left - right[i]. Because we want the smallest stable index, we check this condition as we go and return immediately the first time it holds, guaranteeing the earliest qualifying index. If the entire scan finishes without finding any stable index, we return -1.
Pattern Learn more about Prefix Sum patterns.
Solution Approach
Solution 1: Preprocessing + Enumeration
This solution combines a preprocessing step (suffix minimum array) with a single enumeration pass to find the answer efficiently.
Step 1: Preprocess the suffix minimum array right.
We create an array right of length n, where right[i] represents the minimum value among the elements nums[i..n - 1]. We initialize every entry to nums[-1] (the last element), since the suffix that starts at the last index contains only that one element.
Then we traverse nums from back to front, starting at index n - 2 and going down to 0:
right = [nums[-1]] * n
for i in range(n - 2, -1, -1):
right[i] = min(right[i + 1], nums[i])
The recurrence right[i] = min(right[i + 1], nums[i]) works because the minimum of the suffix starting at i is just the current element nums[i] compared against the already-known minimum of the suffix starting at i + 1.
Step 2: Enumerate each index from left to right.
We maintain a variable left, initialized to 0, which tracks max(nums[0..i])—the maximum value seen so far in the prefix. As we iterate over each index i with value x, we update left using left = max(left, x).
At every index i, both pieces of the instability score are now available:
- The prefix maximum is held in
left. - The suffix minimum is held in
right[i].
So the instability score is simply left - right[i]. If this value is <= k, index i is stable, and since we are scanning left to right, it must be the smallest such index, so we return it immediately:
left = 0
for i, x in enumerate(nums):
left = max(left, x)
if left - right[i] <= k:
return i
return -1
If the loop completes without finding any stable index, we return -1.
Complexity Analysis:
- Time complexity:
O(n). We make one backward pass to build therightarray and one forward pass to check each index, both linear in the length ofnums. - Space complexity:
O(n), due to the extrarightarray storing the suffix minimums. Theleftvariable uses only constant additional space.
The pattern here is the classic prefix/suffix preprocessing technique: when a per-index query depends on information from both directions, precompute one direction in an auxiliary array and stream the other direction with a running variable, reducing what could be an O(n^2) brute force into a clean O(n) solution.
Example Walkthrough
Let's trace through a concrete example to see how the preprocessing + enumeration approach finds the smallest stable index.
Input: nums = [4, 2, 5, 1, 3], k = 2, so n = 5.
We want the smallest index i where max(nums[0..i]) - min(nums[i..n-1]) <= 2.
Step 1: Build the suffix minimum array right.
We initialize every entry to nums[-1] = 3, then fill from back to front using right[i] = min(right[i + 1], nums[i]).
Starting state: right = [3, 3, 3, 3, 3]
i | nums[i] | right[i + 1] | right[i] = min(right[i+1], nums[i]) |
|---|---|---|---|
| 4 | 3 | — (base) | 3 (initialized) |
| 3 | 1 | 3 | min(3, 1) = 1 |
| 2 | 5 | 1 | min(1, 5) = 1 |
| 1 | 2 | 1 | min(1, 2) = 1 |
| 0 | 4 | 1 | min(1, 4) = 1 |
Final: right = [1, 1, 1, 1, 3]
Each right[i] correctly holds the minimum of the suffix nums[i..n-1]. For instance, right[3] = 1 is the min of [1, 3], and right[4] = 3 is the min of just [3].
Step 2: Scan left to right, tracking prefix max left.
We start with left = 0 and update left = max(left, x) at each step, then check whether left - right[i] <= k (i.e., <= 2).
i | x = nums[i] | left = max(left, x) | right[i] | score = left - right[i] | score <= 2? |
|---|---|---|---|---|---|
| 0 | 4 | max(0, 4) = 4 | 1 | 4 - 1 = 3 | ❌ No |
| 1 | 2 | max(4, 2) = 4 | 1 | 4 - 1 = 3 | ❌ No |
| 2 | 5 | max(4, 5) = 5 | 1 | 5 - 1 = 4 | ❌ No |
| 3 | 1 | max(5, 1) = 5 | 1 | 5 - 1 = 4 | ❌ No |
| 4 | 3 | max(5, 3) = 5 | 3 | 5 - 3 = 2 | ✅ Yes |
At index 4, the prefix max is 5 (from element 5 at index 2) and the suffix min is 3 (the lone element at the end). The score 5 - 3 = 2 satisfies <= k, so we return 4 immediately.
Result: The smallest stable index is 4.
Why this is correct: Because we scan left to right and return on the first match, the answer is guaranteed to be the smallest qualifying index. The left variable always reflects max(nums[0..i]) since maxima are monotonically non-decreasing as the prefix grows, and right[i] was precomputed to give the suffix minimum in O(1) per query—turning what could be an O(n^2) brute-force search into a clean O(n) solution.
Solution Implementation
1from typing import List
2
3
4class Solution:
5 def firstStableIndex(self, nums: List[int], k: int) -> int:
6 n = len(nums)
7
8 # suffix_min[i] holds the minimum value in nums[i:].
9 # Initialize every slot with the last element as a safe default.
10 suffix_min = [nums[-1]] * n
11
12 # Fill suffix_min from right to left:
13 # the min of nums[i:] is min(nums[i], min of nums[i+1:]).
14 for i in range(n - 2, -1, -1):
15 suffix_min[i] = min(suffix_min[i + 1], nums[i])
16
17 # prefix_max tracks the maximum value among nums[0..i] as we scan.
18 prefix_max = 0
19 for i, value in enumerate(nums):
20 # Update the running maximum of the prefix.
21 prefix_max = max(prefix_max, value)
22
23 # If the gap between the prefix max and the suffix min
24 # at this index is within k, this is the first stable index.
25 if prefix_max - suffix_min[i] <= k:
26 return i
27
28 # No index satisfies the condition.
29 return -1
301class Solution {
2 public int firstStableIndex(int[] nums, int k) {
3 int n = nums.length;
4
5 // suffixMin[i] holds the minimum value in nums from index i to n - 1
6 int[] suffixMin = new int[n];
7 suffixMin[n - 1] = nums[n - 1];
8
9 // Build the suffix-minimum array by scanning from right to left
10 for (int i = n - 2; i >= 0; i--) {
11 suffixMin[i] = Math.min(suffixMin[i + 1], nums[i]);
12 }
13
14 // prefixMax tracks the maximum value in nums from index 0 up to the current index
15 int prefixMax = 0;
16 for (int i = 0; i < n; i++) {
17 // Update the running prefix maximum to include nums[i]
18 prefixMax = Math.max(prefixMax, nums[i]);
19
20 // If the gap between the prefix max and suffix min is within k,
21 // this is the first stable index
22 if (prefixMax - suffixMin[i] <= k) {
23 return i;
24 }
25 }
26
27 // No index satisfies the condition
28 return -1;
29 }
30}
31```
32
33**A note on a potential edge case:** `prefixMax` is initialized to `0`. If all values in `nums` are negative, the initial `0` would incorrectly act as the maximum. A safer initialization would be `Integer.MIN_VALUE` (or `nums[0]`). I kept the original behavior to preserve your logic, but here is the more robust variant:
34
35```java
36class Solution {
37 public int firstStableIndex(int[] nums, int k) {
38 int n = nums.length;
39
40 // suffixMin[i] holds the minimum value in nums from index i to n - 1
41 int[] suffixMin = new int[n];
42 suffixMin[n - 1] = nums[n - 1];
43
44 // Build the suffix-minimum array from right to left
45 for (int i = n - 2; i >= 0; i--) {
46 suffixMin[i] = Math.min(suffixMin[i + 1], nums[i]);
47 }
48
49 // Initialize to the smallest possible int so negatives are handled correctly
50 int prefixMax = Integer.MIN_VALUE;
51 for (int i = 0; i < n; i++) {
52 // Extend the prefix maximum to include nums[i]
53 prefixMax = Math.max(prefixMax, nums[i]);
54
55 // First index where the prefix max and suffix min differ by at most k
56 if (prefixMax - suffixMin[i] <= k) {
57 return i;
58 }
59 }
60
61 return -1;
62 }
63}
641class Solution {
2public:
3 int firstStableIndex(vector<int>& nums, int k) {
4 int n = static_cast<int>(nums.size());
5
6 // suffixMin[i] = minimum value among nums[i..n-1]
7 vector<int> suffixMin(n);
8 suffixMin[n - 1] = nums[n - 1];
9
10 // Build the suffix-minimum array from right to left
11 for (int i = n - 2; i >= 0; --i) {
12 suffixMin[i] = min(suffixMin[i + 1], nums[i]);
13 }
14
15 // prefixMax tracks the maximum value among nums[0..i]
16 int prefixMax = 0;
17 for (int i = 0; i < n; ++i) {
18 prefixMax = max(prefixMax, nums[i]);
19
20 // Return the first index where the gap between the
21 // running prefix-maximum and the suffix-minimum is within k
22 if (prefixMax - suffixMin[i] <= k) {
23 return i;
24 }
25 }
26
27 // No qualifying index found
28 return -1;
29 }
30};
311/**
2 * Finds the first index where the difference between the maximum value
3 * in the prefix (nums[0..i]) and the minimum value in the suffix (nums[i..n-1])
4 * is less than or equal to k.
5 *
6 * @param nums - The input array of numbers.
7 * @param k - The allowed threshold for the difference.
8 * @returns The first index satisfying the condition, or -1 if none exists.
9 */
10function firstStableIndex(nums: number[], k: number): number {
11 const length: number = nums.length;
12
13 // suffixMin[i] holds the minimum value among nums[i..length-1]
14 const suffixMin: number[] = new Array<number>(length);
15
16 // Initialize the last element; the suffix starting at the last index is itself
17 suffixMin[length - 1] = nums[length - 1];
18
19 // Build the suffix minimum array from right to left
20 for (let i = length - 2; i >= 0; i--) {
21 suffixMin[i] = Math.min(suffixMin[i + 1], nums[i]);
22 }
23
24 // prefixMax tracks the maximum value among nums[0..i] as we iterate
25 let prefixMax: number = 0;
26
27 // Scan from left to right, updating the prefix maximum at each step
28 for (let i = 0; i < length; i++) {
29 prefixMax = Math.max(prefixMax, nums[i]);
30
31 // Check if the difference between the prefix max and suffix min is within k
32 if (prefixMax - suffixMin[i] <= k) {
33 return i;
34 }
35 }
36
37 // No index satisfies the condition
38 return -1;
39}
40Time and Space Complexity
Time Complexity: O(n), where n is the length of the nums array.
The analysis is as follows:
- Initializing the
rightarray with[nums[-1]] * ntakesO(n)time. - The first loop iterates from
n - 2down to0, performing constant-time operations (amincomparison and assignment) at each step, contributingO(n). - The second loop iterates over all
nelements once, performing constant-time operations (amaxcomparison and a conditional check) at each step, contributingO(n). - Combining these, the total time complexity is
O(n) + O(n) + O(n) = O(n).
Space Complexity: O(n), where n is the length of the nums array.
The analysis is as follows:
- The
rightarray storesnelements, requiringO(n)extra space. - The remaining variables (
n,left,i,x) use only constantO(1)space. - Therefore, the dominant term is the
rightarray, giving an overall space complexity ofO(n).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Initializing prefix_max to 0 instead of negative infinity
The most subtle and dangerous bug in this solution lies in how prefix_max is initialized:
prefix_max = 0
This assumes that every value in nums is non-negative. The intent of prefix_max is to track max(nums[0..i]), but seeding it with 0 silently injects an artificial value of 0 into the maximum computation. When the array contains negative numbers, this produces an incorrect prefix maximum.
Concrete failure case:
Consider nums = [-5, -3, -8] and k = 0.
- The true prefix maximum at index
0is-5. - The suffix minimum at index
0ismin(-5, -3, -8) = -8. - The correct instability score is
-5 - (-8) = 3, which is not<= 0.
But with prefix_max = 0, the code computes:
prefix_max = max(0, -5) = 0(wrong—it should be-5).- Score
= 0 - (-8) = 8, still not<= 0here.
The error becomes outcome-changing in cases like nums = [-5, -3], k = 2:
- Correct: prefix max at
i=0is-5, suffix min ismin(-5,-3) = -5, score= -5 - (-5) = 0 <= 2→ return0. - Buggy: prefix max at
i=0ismax(0, -5) = 0, suffix min= -5, score= 0 - (-5) = 5, not<= 2. Ati=1: prefix max= max(0, -3) = 0, suffix min= -3, score= 0 - (-3) = 3, still not<= 2→ returns-1.
The buggy version returns -1 while the correct answer is 0.
Solution: Initialize prefix_max to negative infinity so the first real element always establishes the true maximum:
prefix_max = float('-inf')
for i, value in enumerate(nums):
prefix_max = max(prefix_max, value)
if prefix_max - suffix_min[i] <= k:
return i
return -1
Since the problem guarantees n >= 1 (the code already relies on nums[-1] existing), prefix_max is always updated to a real element before being used, so the -inf sentinel is never compared against suffix_min directly.
Pitfall: Assuming nums is non-empty
The line suffix_min = [nums[-1]] * n will raise an IndexError if nums is empty. While most LeetCode constraints guarantee n >= 1, if you reuse this code in a context without that guarantee, guard against it:
n = len(nums)
if n == 0:
return -1
Pitfall: Misreading the inclusive boundaries at index i
A frequent conceptual error is treating the prefix as nums[0..i-1] or the suffix as nums[i+1..n-1], i.e., excluding the current index. Both ranges in this problem are inclusive of i:
max(nums[0..i])includesnums[i].min(nums[i..n-1])includesnums[i].
This is why prefix_max is updated before the comparison (so nums[i] is folded in), and why suffix_min[i] itself already covers nums[i]. Updating the running maximum after the check, or building the suffix array with an off-by-one shift, would break the inclusive semantics and yield wrong scores.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapProblem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
Subarray Sum Equals Target This problem applies the prefix sum technique from the introduction problems prefix_sum_intro instead of testing every possible subarray a running prefix sum turns the search into a hash table lookup Given an integer array arr and a target value return a subarray whose sum equals the target Return the answer
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!