1 删除最短的子数组使剩余数组有序
1.1 题目描述
![数组(四)-- LC[1574] 删除最短的子数组使剩余数组有序 数组(四)-- LC[1574] 删除最短的子数组使剩余数组有序](https://image.miaokee.com:8440/aHR0cHM6Ly9pbWctYmxvZy5jc2RuaW1nLmNuL2M2NDBlYjllOTMwZjQ1YjNiZGZjYmY5NThmNGEzOTJkLnBuZyNwaWNfY2VudGVy.png#pic_center?w=700&webp=1)
题目链接:https://leetcode.cn/problems/shortest-subarray-to-be-removed-to-make-array-sorted/description/
1.2 滑动窗口
1. 枚举左端点,移动右端点
核心思路:枚举 left \textit{left} left,增大 right \textit{right} right 直到 arr [ left ] ≤ arr [ right ] \textit{arr}[\textit{left}]\le\textit{arr}[\textit{right}] arr[left]≤arr[right],此时更新子数组长度的最小值。
![数组(四)-- LC[1574] 删除最短的子数组使剩余数组有序 数组(四)-- LC[1574] 删除最短的子数组使剩余数组有序](https://image.miaokee.com:8440/aHR0cHM6Ly9pbWctYmxvZy5jc2RuaW1nLmNuL2FlYjY2YmY3YjViZDRmZDQ4ZTY3MmRmNjFjNzAzMjBjLmdpZiNwaWNfY2VudGVy.gif#pic_center?w=700&webp=1)
解疑答惑:
问:为什么枚举一个新的 left \textit{left} left 时, right \textit{right} right 不会往左移?或者说,是否需要再次枚举之前枚举过的 arr [ right ] \textit{arr}[\textit{right}] arr[right]?
答:在向右移动时,由于 arr [ left ] \textit{arr}[\textit{left}] arr[left] 和 arr [ right ] \textit{arr}[\textit{right}] arr[right] 都是非递减的,所以 right \textit{right} right 左侧之前枚举过的元素必然小于 arr [ left ] \textit{arr}[\textit{left}] arr[left],无需再次枚举。这也是本题可以使用同向双指针(不定长滑动窗口)的前提。
问:在计算子数组长度时,我经常分不清下标是否要 +1 或 −1,请问如何解决?
答:第一,时刻把握住 left \textit{left} left 和 right \textit{right} right 的含义,对于本题来说是开区间 ( left , right ) (\textit{left},\textit{right}) (left,right),这两个指针指向的元素不能删除。第二,可以代入一些数据来验证,比如代入 left = 1 , right = 3 \textit{left}=1, \textit{right}=3 left=1,right=3,此时只需要删除一个 arr [ 2 ] \textit{arr}[2] arr[2],所以公式 right − left − 1 \textit{right}-\textit{left}-1 right−left−1 才是符合要求的。
问:为什么不用判断 left < right \textit{left}<\textit{right} left<right,难道不会出现 left ≥ right \textit{left}\ge\textit{right} left≥right 的情况吗?
答:由于提前判断了 arr \textit{arr} arr 是非递减数组的情况,后面的循环 left \textit{left} left 必定小于 right \textit{right} right。反证:如果某个时刻 left \textit{left} left 达到了 right \textit{right} right,就说明整个数组是有序的,但这种情况已经提前判断了。
问:能不能先把 left \textit{left} left 的最大值算出来,然后再去枚举 left \textit{left} left 或 right \textit{right} right?
答:可以。根据对称性,这种做法和先算 right \textit{right} right 的最小值的做法是一样的,只不过枚举的顺序相反而已。
class Solution:
def findLengthOfShortestSubarray(self, arr: List[int]) -> int:
n = len(arr)
right = n - 1
while right and arr[right - 1] <= arr[right]:
right -= 1
if right == 0: # arr 已经是非递减数组
return 0
# 此时 arr[right-1] > arr[right]
ans = right # 删除 arr[:right]
left = 0 # 枚举 left
while left == 0 or arr[left - 1] <= arr[left]:
while right < n and arr[right] < arr[left]:
right += 1
# 此时 arr[left] <= arr[right],删除 arr[left+1:right]
ans = min(ans, right - left - 1)
left += 1
return ans
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 为 nums \textit{nums} nums 的长度。虽然写了个二重循环,但是内层循环中对 right \textit{right} right 加一的总执行次数不会超过 n n n 次,所以总的时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1),仅用到若干额外变量。
2. 枚举右端点,移动左端点
核心思路:枚举 right \textit{right} right,增大 left \textit{left} left 直到 arr [ left ] > arr [ right ] \textit{arr}[\textit{left}]>\textit{arr}[\textit{right}] arr[left]>arr[right]。在增大过程中去更新子数组长度的最小值。
![数组(四)-- LC[1574] 删除最短的子数组使剩余数组有序 数组(四)-- LC[1574] 删除最短的子数组使剩余数组有序](https://image.miaokee.com:8440/aHR0cHM6Ly9pbWctYmxvZy5jc2RuaW1nLmNuLzY0ZjdjZjFmYzgyNTQwNjg5NDlkM2Y3MjIyOTVhZDc2LmdpZiNwaWNfY2VudGVy.gif#pic_center?w=700&webp=1)
问:为什么枚举一个新的 right \textit{right} right 时, left \textit{left} left 不会往左移?或者说,是否需要再次枚举之前枚举过的 arr [ left ] \textit{arr}[\textit{left}] arr[left]?
答:在向右移动时,由于 arr [ left ] \textit{arr}[\textit{left}] arr[left] 和 arr [ right ] \textit{arr}[\textit{right}] arr[right] 都是非递减的,所以 left \textit{left} left 左侧之前枚举过的元素必然小于等于 arr [ right ] \textit{arr}[\textit{right}] arr[right],由于这样的子数组长度更长,无需再次枚举。这也是本题可以使用同向双指针(不定长滑动窗口)的前提。
问:为什么循环一定会结束?
答:代码中提前判断了 arr \textit{arr} arr 已经是非递减数组的情况,所以后面的循环一定存在 left \textit{left} left,使得 arr [ left ] > arr [ left + 1 ] \textit{arr}[\textit{left}]>\textit{arr}[\textit{left}+1] arr[left]>arr[left+1] 成立。
注:最坏情况下,当 right = n \textit{right}=n right=n 时才会去移动 left \textit{left} left。
class Solution:
def findLengthOfShortestSubarray(self, arr: List[int]) -> int:
n = len(arr)
right = n - 1
while right and arr[right - 1] <= arr[right]:
right -= 1
if right == 0: # arr 已经是非递减数组
return 0
# 此时 arr[right-1] > arr[right]
ans = right # 删除 arr[:right]
left = 0
while True: # 枚举 right
while right == n or arr[left] <= arr[right]:
ans = min(ans, right - left - 1) # 删除 arr[left+1:right]
if arr[left] > arr[left + 1]:
return ans
left += 1
right += 1
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 为 nums \textit{nums} nums 的长度。虽然写了个二重循环,但是内层循环中对 left \textit{left} left 加一的总执行次数不会超过 n n n 次,所以总的时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1),仅用到若干额外变量。