Facebook Pixel

3903. Smallest Stable Index I

LeetCode ↗

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 index 0 up to and including index i. In other words, it's the maximum of the prefix ending at i.
  • min(nums[i..n - 1]) is the smallest value among all elements from index i up to and including the last index n - 1. In other words, it's the minimum of the suffix starting at i.

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Prefix Sums?

This problem maps to Prefix Sums through a short path in the full flowchart.

Subarray/substringproblem?noSum/additiveproblem?yesPrefix Sums

Precomputing prefix sums answers range queries in constant time.

Open in Flowchart

Intuition

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 the right array and one forward pass to check each index, both linear in the length of nums.
  • Space complexity: O(n), due to the extra right array storing the suffix minimums. The left variable 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]

inums[i]right[i + 1]right[i] = min(right[i+1], nums[i])
43— (base)3 (initialized)
313min(3, 1) = 1
251min(1, 5) = 1
121min(1, 2) = 1
041min(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).

ix = nums[i]left = max(left, x)right[i]score = left - right[i]score <= 2?
04max(0, 4) = 414 - 1 = 3❌ No
12max(4, 2) = 414 - 1 = 3❌ No
25max(4, 5) = 515 - 1 = 4❌ No
31max(5, 1) = 515 - 1 = 4❌ No
43max(5, 3) = 535 - 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
30
1class 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}
64
1class 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};
31
1/**
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}
40

Time and Space Complexity

Time Complexity: O(n), where n is the length of the nums array.

The analysis is as follows:

  • Initializing the right array with [nums[-1]] * n takes O(n) time.
  • The first loop iterates from n - 2 down to 0, performing constant-time operations (a min comparison and assignment) at each step, contributing O(n).
  • The second loop iterates over all n elements once, performing constant-time operations (a max comparison and a conditional check) at each step, contributing O(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 right array stores n elements, requiring O(n) extra space.
  • The remaining variables (n, left, i, x) use only constant O(1) space.
  • Therefore, the dominant term is the right array, giving an overall space complexity of O(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 0 is -5.
  • The suffix minimum at index 0 is min(-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 <= 0 here.

The error becomes outcome-changing in cases like nums = [-5, -3], k = 2:

  • Correct: prefix max at i=0 is -5, suffix min is min(-5,-3) = -5, score = -5 - (-5) = 0 <= 2 → return 0.
  • Buggy: prefix max at i=0 is max(0, -5) = 0, suffix min = -5, score = 0 - (-5) = 5, not <= 2. At i=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]) includes nums[i].
  • min(nums[i..n-1]) includes nums[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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Problem: 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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More