Facebook Pixel

1574. Shortest Subarray to be Removed to Make Array Sorted

Problem Description

You are given an integer array arr. Your task is to remove a subarray (which can be empty) from arr such that the remaining elements form a non-decreasing sequence.

A non-decreasing sequence means each element is less than or equal to the next element (i.e., arr[i] <= arr[i+1]).

A subarray is a contiguous portion of the array - meaning you can only remove elements that are next to each other, not individual elements from different positions.

Your goal is to find the minimum length of the subarray that needs to be removed to make the remaining array non-decreasing.

For example:

  • If arr = [1, 2, 3, 10, 4, 2, 3, 5], you could remove the subarray [10, 4, 2] (length 3) to get [1, 2, 3, 3, 5], which is non-decreasing.
  • If the array is already non-decreasing like [1, 2, 3, 4, 5], you don't need to remove anything, so return 0.

The key insight is that after removing a subarray, you're essentially keeping a prefix and a suffix of the original array, and these must combine to form a non-decreasing sequence.

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

Intuition

When we remove a subarray from the middle, we're essentially keeping a prefix (left part) and a suffix (right part) of the original array. For the remaining array to be non-decreasing, two conditions must hold:

  1. The prefix itself must be non-decreasing
  2. The suffix itself must be non-decreasing
  3. The last element of the prefix must be ≤ the first element of the suffix

This observation leads us to think: what if we first identify the longest non-decreasing prefix and the longest non-decreasing suffix?

Let's say the longest non-decreasing prefix ends at index i, and the longest non-decreasing suffix starts at index j.

If i >= j, it means these two parts overlap or touch each other - the entire array is already non-decreasing! We need to remove nothing, so return 0.

Otherwise, we have three strategies:

  1. Keep only the prefix: remove everything after index i, which removes n - i - 1 elements
  2. Keep only the suffix: remove everything before index j, which removes j elements
  3. Keep both a prefix and a suffix: we need to ensure they can connect properly

For strategy 3, we can iterate through each possible endpoint of the prefix (from index 0 to i). For each prefix ending at index l, we need to find where in the suffix we can connect. Since the suffix arr[j..n-1] is non-decreasing, we can use binary search to find the first position r in the suffix where arr[r] >= arr[l]. Then we remove everything between l and r, which is r - l - 1 elements.

The minimum among all these possibilities gives us our answer.

Learn more about Stack, Two Pointers, Binary Search and Monotonic Stack patterns.

Solution Approach

The implementation follows a two-pointer approach combined with binary search:

Step 1: Find the longest non-decreasing prefix

i = 0
while i + 1 < n and arr[i] <= arr[i + 1]:
    i += 1

We iterate from the beginning and stop when we find the first position where arr[i] > arr[i + 1]. The prefix arr[0..i] is non-decreasing.

Step 2: Find the longest non-decreasing suffix

j = n - 1
while j - 1 >= 0 and arr[j - 1] <= arr[j]:
    j -= 1

We iterate from the end and stop when we find the first position where arr[j - 1] > arr[j]. The suffix arr[j..n-1] is non-decreasing.

Step 3: Check if array is already non-decreasing

if i >= j:
    return 0

If the prefix and suffix overlap or meet, the entire array is non-decreasing.

Step 4: Initialize answer with simple cases

ans = min(n - i - 1, j)
  • n - i - 1: Remove everything after the prefix (keep only prefix)
  • j: Remove everything before the suffix (keep only suffix)

Step 5: Try combining prefix and suffix

for l in range(i + 1):
    r = bisect_left(arr, arr[l], lo=j)
    ans = min(ans, r - l - 1)

For each possible prefix ending at index l:

  • Use binary search (bisect_left) to find the first position r in the suffix arr[j..n-1] where arr[r] >= arr[l]
  • The lo=j parameter ensures we only search within the suffix portion
  • Calculate the length of subarray to remove: r - l - 1 (everything between l and r)
  • Update the minimum answer

The algorithm efficiently explores all valid ways to remove a contiguous subarray, ensuring the remaining parts form a non-decreasing sequence. The time complexity is O(n log n) due to the binary search performed for each prefix position.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with arr = [1, 2, 3, 10, 4, 2, 3, 5].

Step 1: Find longest non-decreasing prefix

  • Start at index 0: 1 <= 2 ✓, continue
  • Index 1: 2 <= 3 ✓, continue
  • Index 2: 3 <= 10 ✓, continue
  • Index 3: 10 <= 4 ✗, stop

The longest non-decreasing prefix is [1, 2, 3, 10], ending at i = 3.

Step 2: Find longest non-decreasing suffix

  • Start at index 7: 3 <= 5 ✓, continue
  • Index 6: 2 <= 3 ✓, continue
  • Index 5: 4 <= 2 ✗, stop

The longest non-decreasing suffix is [2, 3, 5], starting at j = 5.

Step 3: Check if already non-decreasing

  • Is i >= j? Is 3 >= 5? No, so we continue.

Step 4: Initialize with simple cases

  • Keep only prefix: remove indices 4-7, length = 8 - 3 - 1 = 4
  • Keep only suffix: remove indices 0-4, length = 5
  • Initial ans = min(4, 5) = 4

Step 5: Try combining prefix and suffix

For each prefix ending position l from 0 to 3:

  • l = 0 (prefix = [1]):

    • Binary search in suffix [2, 3, 5] for first element ≥ 1
    • Found at index 5 (value = 2)
    • Remove indices 1-4, length = 5 - 0 - 1 = 4
    • ans = min(4, 4) = 4
  • l = 1 (prefix = [1, 2]):

    • Binary search in suffix [2, 3, 5] for first element ≥ 2
    • Found at index 5 (value = 2)
    • Remove indices 2-4, length = 5 - 1 - 1 = 3
    • ans = min(4, 3) = 3
  • l = 2 (prefix = [1, 2, 3]):

    • Binary search in suffix [2, 3, 5] for first element ≥ 3
    • Found at index 6 (value = 3)
    • Remove indices 3-5, length = 6 - 2 - 1 = 3
    • ans = min(3, 3) = 3
  • l = 3 (prefix = [1, 2, 3, 10]):

    • Binary search in suffix [2, 3, 5] for first element ≥ 10
    • Not found (returns index 8, beyond array)
    • Remove indices 4-7, length = 8 - 3 - 1 = 4
    • ans = min(3, 4) = 3

Final answer: 3

The optimal solution removes the subarray [10, 4, 2] at indices 3-5, leaving [1, 2, 3, 3, 5] which is non-decreasing.

Solution Implementation

1from typing import List
2from bisect import bisect_left
3
4class Solution:
5    def findLengthOfShortestSubarray(self, arr: List[int]) -> int:
6        n = len(arr)
7      
8        # Find the longest non-decreasing prefix
9        left_end = 0
10        while left_end + 1 < n and arr[left_end] <= arr[left_end + 1]:
11            left_end += 1
12      
13        # Find the longest non-decreasing suffix
14        right_start = n - 1
15        while right_start - 1 >= 0 and arr[right_start - 1] <= arr[right_start]:
16            right_start -= 1
17      
18        # If the entire array is already non-decreasing
19        if left_end >= right_start:
20            return 0
21      
22        # Option 1: Keep only the prefix or only the suffix
23        min_removal = min(n - left_end - 1,  # Remove everything after prefix
24                         right_start)         # Remove everything before suffix
25      
26        # Option 2: Keep some prefix and some suffix, remove middle part
27        # For each element in the prefix, find where it can connect in the suffix
28        for prefix_idx in range(left_end + 1):
29            # Find the leftmost position in suffix where arr[prefix_idx] can connect
30            suffix_idx = bisect_left(arr, arr[prefix_idx], lo=right_start)
31          
32            # Calculate elements to remove between prefix_idx and suffix_idx
33            removal_length = suffix_idx - prefix_idx - 1
34            min_removal = min(min_removal, removal_length)
35      
36        return min_removal
37
1class Solution {
2    /**
3     * Finds the length of the shortest subarray to remove to make the array sorted.
4     * Strategy: Find the longest sorted prefix and suffix, then try to connect them.
5     * 
6     * @param arr the input array
7     * @return the length of the shortest subarray to remove
8     */
9    public int findLengthOfShortestSubarray(int[] arr) {
10        int n = arr.length;
11      
12        // Find the longest non-decreasing prefix ending at index leftEnd
13        int leftEnd = 0;
14        while (leftEnd + 1 < n && arr[leftEnd] <= arr[leftEnd + 1]) {
15            leftEnd++;
16        }
17      
18        // Find the longest non-decreasing suffix starting at index rightStart
19        int rightStart = n - 1;
20        while (rightStart - 1 >= 0 && arr[rightStart - 1] <= arr[rightStart]) {
21            rightStart--;
22        }
23      
24        // If the entire array is already sorted (prefix and suffix overlap)
25        if (leftEnd >= rightStart) {
26            return 0;
27        }
28      
29        // Option 1: Keep only the prefix (remove everything after leftEnd)
30        // Option 2: Keep only the suffix (remove everything before rightStart)
31        int minLength = Math.min(n - leftEnd - 1, rightStart);
32      
33        // Option 3: Try to connect prefix and suffix by finding valid combinations
34        // For each element in the prefix, find the first valid element in the suffix
35        for (int prefixIndex = 0; prefixIndex <= leftEnd; prefixIndex++) {
36            // Binary search for the first element in suffix >= arr[prefixIndex]
37            int suffixIndex = binarySearchFirstGreaterOrEqual(arr, arr[prefixIndex], rightStart);
38            // Calculate the length of elements to remove between prefixIndex and suffixIndex
39            minLength = Math.min(minLength, suffixIndex - prefixIndex - 1);
40        }
41      
42        return minLength;
43    }
44  
45    /**
46     * Binary search to find the first index where arr[index] >= target.
47     * Searches in the range [startIndex, arr.length).
48     * 
49     * @param arr the array to search in
50     * @param target the target value to compare against
51     * @param startIndex the starting index for the search
52     * @return the first index where arr[index] >= target
53     */
54    private int binarySearchFirstGreaterOrEqual(int[] arr, int target, int startIndex) {
55        int left = startIndex;
56        int right = arr.length;
57      
58        while (left < right) {
59            int mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
60            if (arr[mid] >= target) {
61                // Found a valid element, but check if there's an earlier one
62                right = mid;
63            } else {
64                // Current element is too small, search in the right half
65                left = mid + 1;
66            }
67        }
68      
69        return left;
70    }
71}
72
1class Solution {
2public:
3    int findLengthOfShortestSubarray(vector<int>& arr) {
4        int n = arr.size();
5      
6        // Find the longest non-decreasing prefix
7        int leftEnd = 0;
8        while (leftEnd + 1 < n && arr[leftEnd] <= arr[leftEnd + 1]) {
9            ++leftEnd;
10        }
11      
12        // Find the longest non-decreasing suffix
13        int rightStart = n - 1;
14        while (rightStart - 1 >= 0 && arr[rightStart - 1] <= arr[rightStart]) {
15            --rightStart;
16        }
17      
18        // If the entire array is already non-decreasing
19        if (leftEnd >= rightStart) {
20            return 0;
21        }
22      
23        // Calculate minimum removal length by either:
24        // 1. Keeping only the prefix (remove everything after leftEnd)
25        // 2. Keeping only the suffix (remove everything before rightStart)
26        int minRemovalLength = min(n - 1 - leftEnd, rightStart);
27      
28        // Try to connect prefix and suffix by finding valid combinations
29        for (int prefixIdx = 0; prefixIdx <= leftEnd; ++prefixIdx) {
30            // Find the first element in suffix that is >= arr[prefixIdx]
31            int suffixIdx = lower_bound(arr.begin() + rightStart, arr.end(), arr[prefixIdx]) - arr.begin();
32          
33            // Calculate elements to remove between prefixIdx and suffixIdx
34            minRemovalLength = min(minRemovalLength, suffixIdx - prefixIdx - 1);
35        }
36      
37        return minRemovalLength;
38    }
39};
40
1function findLengthOfShortestSubarray(arr: number[]): number {
2    const n = arr.length;
3  
4    // Find the longest non-decreasing prefix
5    let leftEnd = 0;
6    while (leftEnd + 1 < n && arr[leftEnd] <= arr[leftEnd + 1]) {
7        leftEnd++;
8    }
9  
10    // Find the longest non-decreasing suffix
11    let rightStart = n - 1;
12    while (rightStart - 1 >= 0 && arr[rightStart - 1] <= arr[rightStart]) {
13        rightStart--;
14    }
15  
16    // If the entire array is already non-decreasing
17    if (leftEnd >= rightStart) {
18        return 0;
19    }
20  
21    // Calculate minimum removal length by either:
22    // 1. Keeping only the prefix (remove everything after leftEnd)
23    // 2. Keeping only the suffix (remove everything before rightStart)
24    let minRemovalLength = Math.min(n - 1 - leftEnd, rightStart);
25  
26    // Try to connect prefix and suffix by finding valid combinations
27    for (let prefixIdx = 0; prefixIdx <= leftEnd; prefixIdx++) {
28        // Find the first element in suffix that is >= arr[prefixIdx]
29        let suffixIdx = lowerBound(arr, rightStart, n, arr[prefixIdx]);
30      
31        // Calculate elements to remove between prefixIdx and suffixIdx
32        minRemovalLength = Math.min(minRemovalLength, suffixIdx - prefixIdx - 1);
33    }
34  
35    return minRemovalLength;
36}
37
38// Helper function to find the first element >= target in arr[start...end)
39function lowerBound(arr: number[], start: number, end: number, target: number): number {
40    let left = start;
41    let right = end;
42  
43    while (left < right) {
44        const mid = Math.floor((left + right) / 2);
45        if (arr[mid] < target) {
46            left = mid + 1;
47        } else {
48            right = mid;
49        }
50    }
51  
52    return left;
53}
54

Time and Space Complexity

The time complexity is O(n log n), where n is the length of the array. This complexity comes from:

  • The first while loop runs at most n times to find the longest non-decreasing prefix: O(n)
  • The second while loop runs at most n times to find the longest non-decreasing suffix: O(n)
  • The for loop iterates through at most i + 1 elements (which is at most n): O(n)
  • Inside the for loop, bisect_left performs binary search on the suffix portion of the array, which takes O(log n) time
  • Therefore, the overall time complexity is O(n) + O(n) + O(n × log n) = O(n log n)

The space complexity is O(1) as the algorithm only uses a constant amount of extra space for variables i, j, l, r, ans, and n, regardless of the input size.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrect Binary Search Usage for Finding Valid Suffix Connection

The Problem: Using bisect_left(arr, arr[prefix_idx], lo=right_start) doesn't guarantee a valid connection between prefix and suffix. The binary search finds where arr[prefix_idx] would fit in the sorted suffix, but it doesn't verify that the value at that position actually exists or forms a valid non-decreasing connection.

Why It Happens:

  • bisect_left returns an insertion point, which could be n (beyond array bounds)
  • Even if the index is valid, we need arr[suffix_idx] >= arr[prefix_idx] for a valid connection
  • The suffix starting at right_start is non-decreasing, but we need to ensure the connection point exists

Correct Solution:

for prefix_idx in range(left_end + 1):
    # Use binary search to find insertion point
    suffix_idx = bisect_left(arr, arr[prefix_idx], lo=right_start)
  
    # Ensure suffix_idx is within bounds
    if suffix_idx < n:
        # Valid connection exists
        removal_length = suffix_idx - prefix_idx - 1
        min_removal = min(min_removal, removal_length)

Pitfall 2: Off-by-One Error in Removal Length Calculation

The Problem: Confusion about what indices represent (inclusive vs exclusive) leads to incorrect removal length calculations.

Why It Happens:

  • left_end is the last index of the valid prefix (inclusive)
  • right_start is the first index of the valid suffix (inclusive)
  • When calculating removal length between prefix_idx and suffix_idx, it's easy to miscount

Correct Understanding:

  • If keeping prefix up to index i and suffix from index j, we remove indices [i+1, j-1]
  • Removal length = j - i - 1 = (j-1) - (i+1) + 1
  • Example: If i=2 and j=5, we keep [0,1,2] and [5,...], removing [3,4] (length 2)

Pitfall 3: Not Handling Edge Cases Properly

The Problem: Missing edge cases like single-element arrays or arrays with all equal elements.

Why It Happens:

  • The algorithm assumes there are distinct increasing/decreasing patterns
  • Equal consecutive elements need special consideration for "non-decreasing" vs "strictly increasing"

Correct Solution: Ensure the comparison uses <= (not <) for non-decreasing sequences:

# Correct: non-decreasing allows equal elements
while left_end + 1 < n and arr[left_end] <= arr[left_end + 1]:
    left_end += 1

# Incorrect: would stop at equal elements
while left_end + 1 < n and arr[left_end] < arr[left_end + 1]:
    left_end += 1
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement priority queue?


Recommended Readings

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

Load More