Problem: 3634. 使数组平衡的最少移除数目
文章目录
- [1. 整体思路](#1. 整体思路)
- [2. 完整代码](#2. 完整代码)
- [3. 时空复杂度](#3. 时空复杂度)
-
-
- [时间复杂度: O ( N log N ) O(N \log N) O(NlogN)](#时间复杂度: O ( N log N ) O(N \log N) O(NlogN))
- [空间复杂度: O ( log N ) O(\log N) O(logN)](#空间复杂度: O ( log N ) O(\log N) O(logN))
-
1. 整体思路
核心问题
同样是求保留最长子序列,使得序列中最大值不超过最小值的 k k k 倍。
算法逻辑
-
排序:依然首先对数组进行排序。
-
枚举 + 二分查找:
- 遍历数组中的每个元素
nums[i],将其视为保留序列的最小值。 - 我们需要找到数组中满足
val <= nums[i] * k的最大元素的位置(或者第一个大于该值的元素位置)。 - 由于数组是有序的,查找这个位置可以使用 二分查找 (Binary Search)。
- 查找目标 :我们想找第一个严格大于
nums[i] * k的数的位置。Arrays.binarySearch的行为是:如果找到,返回索引;如果没找到,返回-(插入点) - 1。其中插入点是第一个大于搜索值的位置。- 代码中的搜索目标设为
nums[i] * k + 1,意图是寻找第一个 ≥ \ge ≥ 这个值的元素位置。或者更准确地说,是寻找值的分界线。 - 实际上,更直接的逻辑是找
upper_bound(第一个大于nums[i] * k的位置)。
- 遍历数组中的每个元素
-
细节解析:
- 优化判断 :
if ((long) nums[i] * k <= nums[n - 1])- 如果
nums[i] * k甚至比数组最大的元素还大(或相等),说明从i开始直到数组末尾的所有元素都满足条件,此时不需要二分,j直接取n即可。 - 否则,我们需要用二分查找确定具体的截止位置
j。
- 如果
- 二分处理 :
j = Arrays.binarySearch(nums, nums[i] * k + 1):这里试图查找nums[i]*k + 1。- 如果数组中没有这个值(大概率),
j会返回负数。 j < 0 ? -j - 1 : j:将负数返回值转换为插入点索引。这个索引正好是第一个大于 (或等于,如果刚好存在val+1)nums[i] * k的位置。这个位置作为右边界(exclusive)是正确的。- 注意 :
binarySearch查找的是精确值。如果用target + 1,逻辑上有点取巧。更严谨的做法是自己写一个upperBound函数查找nums[i] * k。不过在整数域上,找> val和找≥ val + 1是等价的。
- 优化判断 :
2. 完整代码
java
import java.util.Arrays;
class Solution {
public int minRemoval(int[] nums, int k) {
// 1. 排序
Arrays.sort(nums);
int maxKeep = 0;
int n = nums.length;
// 2. 枚举每个元素作为最小值
for (int i = 0; i < n; i++) {
// 默认右边界为数组末尾 (exclusive)
int j = n;
// 目标上限值
long limit = (long) nums[i] * k;
// 优化:如果当前最小值乘以 k 已经覆盖了整个数组的最大值,
// 那么从 i 到 n-1 都满足条件,不需要二分。
// 否则,需要二分查找截止位置。
if (limit < nums[n - 1]) {
// 我们要找第一个大于 limit 的数的位置
// 在整数中,这等价于查找第一个 >= limit + 1 的数的位置
// 这种做法有个潜在风险:如果 limit + 1 溢出 int 范围,binarySearch 会抛错或行为异常
// 但这里 binarySearch 接受的是 int key,如果 limit 很大这里需要小心强转问题
// 假设题目数据范围在 int 内
int target = (int)limit + 1; // 潜在的溢出点,如果 nums[i]*k 很大
// 使用 Java 内置二分查找
j = Arrays.binarySearch(nums, target);
// binarySearch 返回值处理:
// 如果找到,返回非负索引。
// 如果没找到,返回 (-(insertion point) - 1)。
// 插入点即为第一个大于 target 的元素位置,或者是数组长度。
// 我们需要的正是这个插入点作为右边界。
if (j < 0) {
j = -j - 1;
}
// 如果 j >= 0,说明数组里刚好有 target (limit + 1)
// 那么 j 指向该元素,作为 exclusive 边界,它左边的元素都 < target,即 <= limit
// 也是正确的。
}
// 更新最大保留长度
maxKeep = Math.max(maxKeep, j - i);
}
return n - maxKeep;
}
}
3. 时空复杂度
假设数组 nums 的长度为 N N N。
时间复杂度: O ( N log N ) O(N \log N) O(NlogN)
- 排序 : O ( N log N ) O(N \log N) O(NlogN)。
- 遍历 + 二分 :
- 外层循环 N N N 次。
- 内层二分查找耗时 O ( log N ) O(\log N) O(logN)。
- 这部分总耗时 O ( N log N ) O(N \log N) O(NlogN)。
- 对比双指针 :
- 双指针(上一版)是 O ( N log N + N ) O(N \log N + N) O(NlogN+N)。
- 二分版是 O ( N log N + N log N ) O(N \log N + N \log N) O(NlogN+NlogN)。
- 虽然大 O 相同,但双指针在第二阶段是 O ( N ) O(N) O(N),通常更快。二分法在常数上略大。
空间复杂度: O ( log N ) O(\log N) O(logN)
- 计算依据 :
- 主要消耗来自排序的栈空间。
- 结论 : O ( log N ) O(\log N) O(logN)。