【算法】删掉一个元素以后全为 1 的最长子数组 & 使数组平衡的最少移除数目

🎬 博主名称: 超级苦力怕
🔥 个人专栏: 《LeetCode 题解》
🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!
本文收录两道中等难度的 LeetCode 题目------1493. 删掉一个元素以后全为 1 的最长子数组 和 3634. 使数组平衡的最少移除数目 ,均使用 Java 语言完成。两道题的核心思路都属于 滑动窗口(Sliding Window)/ 双指针 范畴,但在窗口的判定条件上有本质区别:前者维护窗口内 至多一个 0 ,后者在排序后维护窗口内元素满足 最大值 ≤ 最小值 × k 的倍数关系。适用范围覆盖 O(n) 和 O(n log n) 两种典型复杂度场景,是锻炼双指针边界控制的绝佳组合。
1493. 删掉一个元素以后全为 1 的最长子数组
题目介绍
1493. 删掉一个元素以后全为 1 的最长子数组
直达链接:LeetCode 1493
给你一个二进制数组 nums,你需要从中恰好删除一个元素 。请你返回删除后得到的数组中,只包含 1 的最长子数组的长度。如果不存在这样的子数组,返回 0。
注意,你必须删除一个元素,即使数组中全是 1,也必须删除一个(此时最长全 1 子数组的长度为原数组长度减一)。

提示:
1 <= nums.length <= 10^5nums[i]为0或1
题目示例
示例 1:
输入:nums = [1,1,0,1]
输出:3
解释:删除 nums[2] = 0,得到 [1,1,1],全为 1 的子数组长度为 3。
示例 2:
输入:nums = [0,1,1,1,0,1,1,0,1]
输出:5
解释:删除 nums[4] = 0,得到 [0,1,1,1,1,1,0,1],最长全 1 子数组为 [1,1,1,1,1],长度为 5。
示例 3:
输入:nums = [1,1,1]
输出:2
解释:必须删除一个元素。删除任意一个 1,剩下两个 1,长度为 2。
算法思路
核心思想:滑动窗口中至多允许一个 0。
本题要求恰好删除一个元素,等价于在数组中找一个最长的区间,该区间内至多包含一个 0 。因为我们可以把那一个 0 删除,剩下的就全是 1 了。找到满足此条件的最大窗口后,窗口内的 0 不计入结果,实际全 1 长度 = right - left(窗口元素数 − 1,其中 −1 对应那个被删除的 0)。
具体步骤:
- 维护两个指针
left和right,以及当前窗口内0的个数zeroCount。 right从左到右遍历数组:- 若
nums[right] == 0,zeroCount加 1。 - 当
zeroCount > 1时,说明当前窗口包含了超过一个 0,需要收缩左边界。移动left,若nums[left] == 0则将zeroCount减 1,直到zeroCount ≤ 1为止。 - 此时窗口
[left, right]内至多一个 0,更新maxLen = max(maxLen, right - left)(无需 +1,因为窗口内如果有 0,它会被删掉;如果全是 1,也必须删一个)。
- 若
- 遍历结束后返回
maxLen。
易错点:
maxLen更新为right - left而非right - left + 1------因为必须删除一个元素。- 当数组全为 1 时(如示例 3),最大窗口长度为
n,maxLen最终等于n - 1(因为right = n - 1,left = 0,right - left = n - 1),恰好是删除一个元素后的结果,无需特殊处理。
核心代码
java
class Solution {
public int longestSubarray(int[] nums) {
int left = 0, zeroCount = 0, maxLen = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] == 0) {
zeroCount++;
}
while (zeroCount > 1) {
if (nums[left] == 0) {
zeroCount--;
}
left++;
}
maxLen = Math.max(maxLen, right - left);
}
return maxLen;
}
}
示例测试(总代码)
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Solution sol = new Solution();
// 示例1测试
int[] nums1 = {1, 1, 0, 1};
System.out.println("示例1输出:" + sol.longestSubarray(nums1)); // 预期输出3
// 示例2测试
int[] nums2 = {0, 1, 1, 1, 0, 1, 1, 0, 1};
System.out.println("示例2输出:" + sol.longestSubarray(nums2)); // 预期输出5
// 示例3测试
int[] nums3 = {1, 1, 1};
System.out.println("示例3输出:" + sol.longestSubarray(nums3)); // 预期输出2
}
}
3634. 使数组平衡的最少移除数目
题目介绍
3634. 使数组平衡的最少移除数目
直达链接:LeetCode 3634
给你一个整数数组 nums 和一个整数 k。如果一个数组的最大 元素的值至多 是其最小 元素的 k 倍,则该数组被称为是平衡 的。你可以从 nums 中移除任意 数量的元素,但不能使其变为空数组。返回为了使剩余数组平衡,需要移除的元素的最小数量。
注意:大小为 1 的数组被认为是平衡的,因为其最大值和最小值相等,条件总是成立。

提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^91 <= k <= 10^5
题目示例
示例 1:
输入:nums = [2,1,5], k = 2
输出:1
解释:移除 nums[2] = 5 得到 nums = [2, 1]。现在 max = 2, min = 1,且 max <= min * k 即 2 <= 1 * 2。因此,答案是 1。
示例 2:
输入:nums = [1,6,2,9], k = 3
输出:2
解释:移除 nums[0] = 1 和 nums[3] = 9 得到 nums = [6, 2]。现在 max = 6, min = 2,且 max <= min * k 即 6 <= 2 * 3。因此,答案是 2。
示例 3:
输入:nums = [4,6], k = 2
输出:0
解释:由于 nums 已经平衡,因为 6 <= 4 * 2,所以不需要移除任何元素。
算法思路
核心思想:排序后,双指针维护满足「最大值 ≤ 最小值 × k」的最长连续子数组。
本题的平衡条件只与最大值和最小值有关,中间元素的值不影响判定。而「移除元素使剩余数组平衡」等价于「保留一个尽可能大的子集,使其满足平衡条件」。为了让保留的子集尽量大,我们应该优先考虑排序后的连续子数组------因为排序后相邻元素最接近,比值约束最容易被满足。
于是问题转化为:先对数组排序,然后找一段最长的连续子数组,使得它的最后一个元素 ≤ 第一个元素 × k。答案 = n − 该最长子数组的长度。
具体步骤:
- 对
nums进行升序排序。 - 使用双指针
i(区间最小值位置)和j(区间最大值位置的下一位置,即开区间右端点)。 - 遍历
i从 0 到n - 1,对于每个i:- 尽量右移
j,使得nums[j] <= nums[i] * k始终成立。 - 当
j无法继续右移时(即nums[j] > nums[i] * k),以nums[i]为最小值的最大平衡区间为[i, j - 1],长度为j - i。 - 更新最大平衡区间长度
maxWindow = max(maxWindow, j - i)。
- 尽量右移
- 随着
i右移,j不需要回退------因为nums[i]增大,nums[i] × k也增大,j的右移条件只会更宽松。整个过程j最多移动n次,总复杂度 O(n)。 - 最终答案 =
n - maxWindow。
易错点:
nums[i]最大可达10^9,k最大可达10^5,乘积nums[i] * k可达10^14,远超int上限(约2.1 × 10^9)。比较时必须转为long,否则乘法溢出会导致判断结果错误。maxWindow至少为 1(任意单元素数组都是平衡的),无需额外判断空数组。
核心代码
java
class Solution {
public int minRemovals(int[] nums, int k) {
Arrays.sort(nums);
int n = nums.length;
int maxWindow = 0;
int j = 0;
for (int i = 0; i < n; i++) {
while (j < n && (long) nums[j] <= (long) nums[i] * k) {
j++;
}
maxWindow = Math.max(maxWindow, j - i);
}
return n - maxWindow;
}
}
示例测试(总代码)
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Solution sol = new Solution();
// 示例1测试
int[] nums1 = {2, 1, 5};
System.out.println("示例1输出:" + sol.minRemovals(nums1, 2)); // 预期输出1
// 示例2测试
int[] nums2 = {1, 6, 2, 9};
System.out.println("示例2输出:" + sol.minRemovals(nums2, 3)); // 预期输出2
// 示例3测试
int[] nums3 = {4, 6};
System.out.println("示例3输出:" + sol.minRemovals(nums3, 2)); // 预期输出0
}
}
总结
| 题目 | 核心技巧 | 时间复杂度 | 空间复杂度 | 思路转化 |
|---|---|---|---|---|
| 1493. 删一个元素全为 1 的最长子数组 | 滑动窗口(维护 0 的个数) | O(n) | O(1) | "删除一个元素" → 窗口内至多一个 0 |
| 3634. 使数组平衡的最少移除数目 | 排序 + 双指针 | O(n log n) | O(1) | "最少移除" → 排序后找最长平衡连续子数组 |
核心要点:
- 将"删除"转化为"窗口内允许的瑕疵":1493 题中,恰好删除一个元素 = 窗口内至多一个 0。滑动时只需维护 0 的计数,逻辑清晰且易于扩展。
- 排序是很多「比值条件」问题的前置步骤:3634 题涉及最大值与最小值的倍数关系,排序后该关系在连续子数组中最容易满足,将原问题转化为简单的双指针滑动,避免了复杂的子集枚举。
- 注意整数溢出 :当数据范围达到
10^9级别,且涉及乘法比较时,务必使用long类型进行转换后再比较,这是 3634 题最容易翻车的点。
