目录

题目描述

1574. 删除最短的子数组使剩余数组有序

题意分析

给定整数数组 arr,要求删除恰好一个连续子数组(可以为空),使剩下的元素拼接后是非递减的,问被删子数组的最短长度。

「删除的必须是连续的一段」是全题的骨架:设删掉的是 $[l, r]$,那么剩下的就是一个前缀 $[0, l-1]$ 加一个后缀 $[r+1, n-1]$。要让结果非递减,必须同时满足三件事——前缀自身非递减、后缀自身非递减、且前缀的最后一个元素不超过后缀的第一个元素。答案的形态被彻底限定成「保留一个前缀 + 一个后缀」。

注意「非递减」允许相等,所有比较都要用 $\le$ 而不是 $<$,这一点在有重复元素的数组上会直接决定对错。

数据范围 $n \le 10^5$ 说明要 $O(n)$ 或 $O(n \log n)$:枚举 $[l, r]$ 是 $O(n^2)$ 对,即便判定 $O(1)$ 也超时。

边界有三处:数组本身已经非递减时答案为 $0$(删空段);最坏情况可以删掉 $n - 1$ 个元素只留一个,所以答案上界是 $n-1$,绝不会是 $n$;数组长度为 $1$ 时答案为 $0$。

解法:双指针收缩边界

核心思路

暴力做法是枚举删除区间 $[l, r]$,再检查剩余部分是否有序,$O(n^3)$;用预处理把检查降到 $O(1)$ 也还有 $O(n^2)$ 对区间要枚举。瓶颈在于枚举了大量注定不合法的组合——前缀根本不非递减的 $l$,或后缀根本不非递减的 $r$,怎么配都白搭。

第一个观察把候选范围砍到线性:能保留的前缀必须是原数组「最长非递减前缀」的某个前缀,能保留的后缀必须是「最长非递减后缀」的某个后缀。设最长非递减前缀的末位下标为 left(即 arr[0..left] 非递减且 arr[left] > arr[left+1]),最长非递减后缀的首位下标为 right。那么合法方案只有两类:只保留前缀(删掉 left+1 到 $n-1$,长度 $n - left - 1$)、只保留后缀(删掉 $0$ 到 right-1,长度 right),或者从两边各取一段拼起来。前两类可以直接算出,重点是第三类。

第三类要找的是:在 $i \in [0, left]$、$j \in [right, n-1]$ 中,满足 arr[i] <= arr[j] 且使删除长度 $j - i - 1$ 最小的配对。直接双重循环仍是 $O(n^2)$。

第二个观察带来双指针:arr[0..left]arr[right..n-1] 都是非递减的,于是「对固定的 $i$,最小的可行 $j$」随 $i$ 增大而单调不减——$i$ 变大意味着 arr[i] 变大(或持平),要求的 $j$ 只会更靠右。单调性成立,就可以让两个指针各自单向前进,总步数 $O(n)$。

双指针的不变量是:$i$ 从 $0$ 出发只增,$j$ 从 right 出发只增;每当 arr[i] <= arr[j] 时,当前的 $j$ 就是配得上这个 $i$ 的最小可行右端点,此时删除长度 $j - i - 1$ 是该 $i$ 的最优值,记入答案后把 $i$ 推进一格;否则 arr[i] 太大,只能把 $j$ 右移去找更大的值。因为 $j$ 从不回退,所以每个 $i$ 被处理时 $j$ 恰好停在它的最小可行位置。

三类方案取最小值即为答案。

解题步骤

  • 先从左往右扩出最长非递减前缀的末位 left:只要 arr[left] <= arr[left+1] 就继续右移。循环条件里带 left + 1 < n 防止越界。
  • left == n - 1 直接返回 $0$。这说明整个数组已经非递减,删空段即可。这个提前返回还有一层作用:保证后续 right 的计算不会与 left 产生退化交叠。
  • 再从右往左扩出最长非递减后缀的首位 right:只要 arr[right-1] <= arr[right] 就继续左移。同样带 right > 0 防越界。
  • 先用两个「只保留一侧」的方案初始化答案:res = min(n - left - 1, right)。前者是删掉前缀之后的全部,后者是删掉后缀之前的全部。这两种必须单独算,因为双指针循环里 $i$ 和 $j$ 都必须落在有效范围内,无法表达「一侧完全不保留」。
  • 双指针从 i = 0j = right 出发,循环条件 i <= left && j < n。$i$ 的上界是 left 而非 $n$,因为超出最长非递减前缀后,保留的前缀自身就不有序了;$j$ 的下界是 right,理由对称。
  • arr[i] <= arr[j],用 j - i - 1 更新答案并 i++。删除区间是 $[i+1, j-1]$,长度正是 $j - i - 1$。此时该 $i$ 已找到最优 $j$,推进 $i$ 去处理下一个;不推进 $j$ 是因为更大的 $i$ 可能仍配得上这个 $j$,那样删得更少。
  • 否则 j++arr[i] > arr[j] 说明当前 $j$ 太小接不上,而 $j$ 左侧的更小,只能往右找。这一支不更新答案。
  • 返回 res

arr = [1, 2, 3, 10, 4, 2, 3, 5] 走一遍:

求前缀:$1 \le 2 \le 3 \le 10$,到下标 3 时 $10 > 4$ 停下,left = 3。不等于 $n - 1 = 7$,继续。
求后缀:right 初值 $7$;检查 arr[6] = 3 <= arr[7] = 5 成立,right = 6;检查 arr[5] = 2 <= arr[6] = 3 成立,right = 5;检查 arr[4] = 4 <= arr[5] = 2 不成立,停止。right = 5,最长非递减后缀是 [2, 3, 5]
初始答案:只留前缀要删 $8 - 3 - 1 = 4$ 个;只留后缀要删 right = 5 个。res = 4
双指针:i = 0j = 5arr[0] = 1 <= arr[5] = 2,删除长度 $5 - 0 - 1 = 4$,res 仍为 $4$,i = 1
arr[1] = 2 <= arr[5] = 2(注意用 $\le$),删除长度 $5 - 1 - 1 = 3$,res = 3i = 2
arr[2] = 3 > arr[5] = 2j = 6
arr[2] = 3 <= arr[6] = 3,删除长度 $6 - 2 - 1 = 3$,res 仍为 $3$,i = 3
arr[3] = 10 > arr[6] = 3j = 7
arr[3] = 10 > arr[7] = 5j = 8,循环因 j < n 失败退出。
返回 $3$,对应删掉 [10, 4, 2](下标 3 到 5),剩下 [1, 2, 3, 5]

第二步那个 arr[1] = 2 <= arr[5] = 2 尤其值得注意:若比较写成严格小于,这一档会被跳过,res 停在 $4$,答案就错了——这正是「非递减允许相等」的直接体现。

代码实现

class Solution {
    public int findLengthOfShortestSubarray(int[] arr) {
        int n = arr.length;
        int left = 0;
        while (left + 1 < n && arr[left] <= arr[left + 1]) {
            left++;
        }
        if (left == n - 1) {
            return 0;
        }

        int right = n - 1;
        while (right > 0 && arr[right - 1] <= arr[right]) {
            right--;
        }

        int res = Math.min(n - left - 1, right);

        int i = 0;
        int j = right;
        while (i <= left && j < n) {
            if (arr[i] <= arr[j]) {
                res = Math.min(res, j - i - 1);
                i++;
            } else {
                j++;
            }
        }

        return res;
    }
}
func findLengthOfShortestSubarray(arr []int) int {
    n := len(arr)
    left := 0
    for left+1 < n && arr[left] <= arr[left+1] {
        left++
    }
    if left == n-1 {
        return 0
    }

    right := n - 1
    for right > 0 && arr[right-1] <= arr[right] {
        right--
    }

    res := n - left - 1
    if right < res {
        res = right
    }

    i, j := 0, right
    for i <= left && j < n {
        if arr[i] <= arr[j] {
            if j-i-1 < res {
                res = j - i - 1
            }
            i++
        } else {
            j++
        }
    }

    return res
}

复杂度分析

  • 时间复杂度:$O(n)$。求最长非递减前缀与后缀各是一趟单向扫描;双指针阶段 $i$ 至多前进 left + 1 步、$j$ 至多前进 $n - right$ 步,两者都不回退,合计不超过 $2n$ 次比较。全程没有排序也没有嵌套循环。
  • 空间复杂度:$O(1)$。只用了 leftrightresij 五个整型变量,不复制数组也不建辅助结构。

关键点总结

  • 「删除一个连续段」等价于「保留一个前缀加一个后缀」,这个转换把二维的区间枚举降成了两端各选一个切点的问题,是本题的第一步也是最关键的一步。
  • 可行解的范围往往能先被单调性夹住:保留的前缀必在最长非递减前缀之内、后缀必在最长非递减后缀之内。先把候选空间砍到线性,再考虑怎么配对。
  • 两个有序序列上找满足 a[i] <= b[j] 且间隔最小的配对,是相向/同向双指针的标准场景;成立的依据是「随着 $i$ 增大,最小可行 $j$ 单调不减」,这一条必须能证明,否则双指针不成立。
  • 「只保留一侧」的两种极端方案必须单独初始化答案,双指针的循环范围表达不了它们。凡是双指针题都要检查一遍端点情形是否被覆盖。
  • 面试视角:面试官会依次问三件事——为什么答案一定是「前缀 + 后缀」的形态;为什么 $i$ 只需在最长非递减前缀内枚举;双指针的移动依据是什么(arr[i] > arr[j] 时为什么只能移 $j$)。如果被追问优化前的思路,可以说「对每个 $i$ 在后缀上二分找第一个不小于 arr[i] 的位置」,那是 $O(n \log n)$ 的自然过渡版本,双指针是它的线性化。

易错点总结

  • 比较写成严格小于arr = [1, 2, 3, 10, 4, 2, 3, 5]arr[1] = 2arr[5] = 2 相等的那次匹配会被跳过,返回 $4$,正确答案是 $3$。
  • left 当成前缀长度而不是末位下标arr = [1, 2, 3, 10, 4]left = 3 表示下标,若按长度理解写成 n - left,只留前缀的方案会算成 $2$ 而非 $1$,最终返回值偏大。
  • 答案初始化只取 n - left - 1 而漏掉 rightarr = [5, 4, 3, 2, 1]left = 0right = 4,只保留前缀要删 $4$ 个、只保留后缀也要删 $4$ 个,漏掉一侧会在类似 arr = [10, 1, 2, 3] 的用例上返回 $3$ 而非 $1$。
  • 双指针里 arr[i] > arr[j] 时移动 $i$arr = [1, 2, 3, 10, 4, 2, 3, 5]i = 2j = 5 处 $3 > 2$,若移 $i$ 会跳过下标 2 这个本可与下标 6 配对的位置,最终返回值偏大。
  • 匹配成功后同时推进 $i$ 和 $j$:会漏掉「更大的 $i$ 配同一个 $j$」这类更优解,arr = [1, 1, 1, 5, 2] 上会返回比最优值更大的删除长度。
  • 删除长度写成 j - iarr = [1, 2, 3, 10, 4, 2, 3, 5] 中最优那次会算成 $4$ 而非 $3$,因为删的是开区间 $(i, j)$ 内的元素,长度是 $j - i - 1$。
  • $i$ 的循环上界写成 i < ni 越过 left 后前缀自身已不非递减,arr = [1, 2, 3, 10, 4]i = 4 会与某个 $j$ 配对并给出一个非法方案,返回值偏小。
  • right 时循环条件漏掉 right > 0arr = [1, 2, 3] 若没有提前返回,right 会一直左移到 $-1$ 并访问 arr[-1] 越界。
  • left 时用 arr[left] < arr[left + 1]arr = [1, 1, 1, 2] 会在第一步就停下,left = 0,把本已有序的数组判成需要删除 $3$ 个元素,正确答案是 $0$。
  • 认为答案可能是 $n$arr = [5, 4, 3, 2, 1] 时至少可以只保留一个元素,删 $4$ 个即可,返回 $5$ 说明把「保留空数组」当成了唯一出路。

相似题目

题目 难度 考察点
581. 最短无序连续子数组 中等 求最短的「排序后即整体有序」的段,靠前后缀极值夹逼而非删除拼接
665. 非递减数列 中等 允许修改一个元素使数组非递减,考的是在拐点处改左还是改右的贪心
209. 长度最小的子数组 中等 同向双指针求最短合法窗口,但窗口是被保留的对象且判据是和
26. 删除有序数组中的重复项 简单 原地删除的双指针写法,删除的是分散元素而非连续段
167. 两数之和 II - 输入有序数组 中等 有序数组上双指针配对的最简形态,移动依据同为「当前和偏大还是偏小」
300. 最长递增子序列 中等 允许删除任意多个不连续元素,问题从两端切点变成子序列 DP 或贪心加二分
80. 删除有序数组中的重复项 II 中等 快慢指针原地保留,判据是与前第二个保留元素比较,考的是双指针的语义设计
11. 盛最多水的容器 中等 相向双指针,移动依据来自「短板不可能更优」的反证而非有序性