【LeetCode 每日一题】1493. 删掉一个元素以后全为 1 的最长子数组——(解法一)预处理
Problem: 1493. 删掉一个元素以后全为 1 的最长子数组
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N)
- 空间复杂度:O(Z) 或 O(N)
整体思路
这段代码旨在解决一个特定的数组问题:“删除一个元素后全为 1 的最长子数组”。问题要求在给定的二进制数组 nums
中,必须删除一个元素,然后找出结果数组中只包含 1 的最长子数组的长度。
该算法采用了一种非常巧妙的 预处理 策略,而不是传统的滑动窗口。它首先识别出所有障碍物(即 0),然后计算移除每个障碍物后可能形成的最长连续 1 区间的长度。
其核心逻辑步骤如下:
-
识别并记录所有 0 的位置:
- 算法的核心是
List<Integer> zeroLoc
,它被用来存储数组中所有 0 元素的索引。 - 哨兵(Sentinel)技巧:在开始扫描之前,代码巧妙地向
zeroLoc
中添加了两个“哨兵”值:-1
和n
(数组长度)。-1
代表数组开始之前的一个虚拟的 0。n
代表数组结束之后的一个虚拟的 0。
- 目的:这两个哨兵极大地简化了边界情况的处理。例如,当我们考虑删除第一个实际的 0 时,可以利用
-1
作为它“左边”的 0 的位置;同样,删除最后一个实际的 0 时,可以利用n
作为它“右边”的 0 的位置。这避免了在主循环中写大量的if/else
来处理首尾的特殊情况。
- 算法的核心是
-
处理特殊情况(全为 1):
- 在记录完所有 0 的位置后,代码检查
zeroLoc
的大小。如果size == 2
,这意味着列表中只有两个哨兵-1
和n
,说明原始数组中没有任何 0。 - 根据题意,我们必须删除一个元素。因此,从一个全为 1 的数组中删除任意一个 1,得到的最长子数组长度就是
n - 1
。代码正确地处理了这种情况。
- 在记录完所有 0 的位置后,代码检查
-
计算移除每个 0 后的最长长度:
- 算法的主体是一个
for
循环,它遍历zeroLoc
列表中所有实际的 0 的索引(即排除首尾的哨兵)。 - 对于列表中的第
i
个实际的 0(其索引在zeroLoc
中是i
),如果我们将它删除,那么它原来左边的连续 1 和右边的连续 1 就会合并成一个更长的连续 1 子数组。 - 这个合并后的子数组的边界,恰好是由这个 0 的前一个 0 和后一个 0 的位置所限定的。
- 长度计算:
- 前一个 0 的索引是
zeroLoc.get(i - 1)
。 - 后一个 0 的索引是
zeroLoc.get(i + 1)
。 - 这两个 0 之间的总元素个数是
zeroLoc.get(i + 1) - zeroLoc.get(i - 1) - 1
。 - 这个区间内包含了一个我们想要删除的 0,所以纯 1 的个数就是
(zeroLoc.get(i + 1) - zeroLoc.get(i - 1) - 1) - 1
,即zeroLoc.get(i + 1) - zeroLoc.get(i - 1) - 2
。
- 前一个 0 的索引是
- 算法在循环中不断计算这个值,并用
ans
变量来记录遇到的最大值。
- 算法的主体是一个
-
返回结果:
- 遍历结束后,
ans
中存储的就是最终的答案。
- 遍历结束后,
完整代码
import java.util.ArrayList;
import java.util.List;class Solution {/*** 删除一个元素后,返回数组中全为 1 的最长子数组的长度。* @param nums 二进制数组* @return 最长子数组的长度*/public int longestSubarray(int[] nums) {int n = nums.length;// ans: 用于存储最终结果,即找到的最长子数组的长度int ans = 0;// zeroLoc: 用于存储所有 0 元素的索引List<Integer> zeroLoc = new ArrayList<>();// 关键技巧:添加哨兵(sentinel)值// -1 代表数组开始前的一个虚拟的 0zeroLoc.add(-1); // 步骤 1: 遍历数组,记录所有实际 0 的位置for (int i = 0; i < n; i++) {if (nums[i] == 0) {zeroLoc.add(i);}}// n 代表数组结束后一个虚拟的 0zeroLoc.add(n); // 获取包含哨兵在内的 0 的总数int size = zeroLoc.size();// 步骤 2: 处理特殊情况// 如果 size 为 2,说明列表中只有哨兵[-1, n],即原数组中没有 0。// 题目要求必须删除一个元素,所以从全 1 的数组中删除一个,长度为 n-1。if (size == 2) {return n - 1;} // 步骤 3: 遍历每一个实际的 0,计算删除它之后能形成的最长 1 子数组// 循环从 i=1 到 size-2,正好遍历所有实际的 0,跳过哨兵for (int i = 1; i < size - 1; i++) {// zeroLoc.get(i) 是当前要“删除”的 0 的索引// zeroLoc.get(i - 1) 是它前一个 0 的索引(可能是哨兵 -1)// zeroLoc.get(i + 1) 是它后一个 0 的索引(可能是哨兵 n)// 这两个 0 之间的区间长度,减去两个 0 本身,就是合并后的 1 的数量。int currentLength = zeroLoc.get(i + 1) - zeroLoc.get(i - 1) - 2;ans = Math.max(ans, currentLength);}return ans;}
}
时空复杂度
时间复杂度:O(N)
- 查找 0 的位置:第一个
for
循环遍历整个nums
数组一次,以找到所有 0 的索引。这个过程的时间复杂度是 O(N)。 - 计算最大长度:第二个
for
循环遍历zeroLoc
列表。设nums
数组中 0 的数量为Z
,则zeroLoc
的大小为Z+2
,这个循环执行Z
次。 - 综合分析:
- 总的时间复杂度为 O(N) (扫描数组) + O(Z) (扫描0列表)。
- 由于 0 的数量
Z
不可能超过数组的总长度N
(即Z <= N
),所以 O(N) + O(Z) 可以简化为 O(N)。
空间复杂度:O(Z) 或 O(N)
- 主要存储开销:算法使用了一个
ArrayList
类型的zeroLoc
来存储所有 0 的索引。 - 空间大小:
- 列表
zeroLoc
的大小等于数组中 0 的数量Z
加上两个哨兵,即Z+2
。 - 在最坏的情况下,
nums
数组中可能全是 0,此时Z = N
,zeroLoc
的大小将与输入规模N
成线性关系。
- 列表
- 综合分析:
- 算法所需的额外空间主要由
zeroLoc
列表决定。因此,其空间复杂度为 O(Z),其中Z
是数组中 0 的个数。 - 在最坏情况下,空间复杂度为 O(N)。
- 算法所需的额外空间主要由