这道题目要求我们在一个已排序的数组中,找到与给定目标值x最接近的k个元素。这类问题在实际开发中非常常见,比如在推荐系统中为用户推荐最相关的商品,或者在日志分析中查找与特定时间点最接近的事件记录。
题目给出了明确的判断标准:一个元素a比元素b更接近x的条件是:
这意味着我们需要考虑两种排序条件:首先是距离的绝对值,其次是元素本身的大小。这种双重排序条件在实际问题中很常见,需要特别注意。
让我们仔细看看题目给出的示例:
示例1:
输入:arr = [1,2,3,4,5], k = 4, x = 3
输出:[1,2,3,4]
这里x=3正好是数组中的一个元素。最接近的4个元素自然包含3本身,以及它左右两边的元素。
示例2:
输入:arr = [1,2,3,4,5], k = 4, x = -1
输出:[1,2,3,4]
这个例子更有意思,x=-1不在数组中,且比所有元素都小。此时最接近的4个元素就是数组中最小的4个元素。
这个解法的核心思想是:既然我们需要保留k个最接近的元素,那么可以反过来思考,我们需要删除n-k个最不接近的元素。
由于数组是有序的,最不接近x的元素一定出现在数组的两端。我们可以使用双指针技术,从数组的两端向中间移动,每次比较两个指针指向的元素与x的距离,删除距离较远的一个。
具体步骤:
java复制class Solution {
public List<Integer> findClosestElements(int[] arr, int k, int x) {
int n = arr.length;
int left = 0, right = n - 1;
int remove = n - k;
while (remove > 0) {
if (compare(arr[left], arr[right], x) > 0) {
left++;
} else {
right--;
}
remove--;
}
List<Integer> closestElements = new ArrayList<Integer>();
for (int i = left; i <= right; i++) {
closestElements.add(arr[i]);
}
return closestElements;
}
public int compare(int num1, int num2, int x) {
int diff1 = Math.abs(num1 - x);
int diff2 = Math.abs(num2 - x);
if (diff1 != diff2) {
return diff1 - diff2;
} else {
return num1 - num2;
}
}
}
比较函数compare的设计很关键:
时间复杂度:O(n),因为最坏情况下我们需要遍历整个数组。
空间复杂度:O(1),除了结果外没有使用额外空间。
这个解法简单直观,但对于大型数组可能不够高效。特别是当k远小于n时,我们仍然需要做接近n次比较。
这个更高效的解法分为两个阶段:
定位阶段:使用二分查找找到数组中第一个大于或等于x的元素位置。这个位置将作为我们扩展搜索的起点。
扩展阶段:从这个位置向两边扩展,使用双指针技术选择最接近的k个元素。指针一个向左移动,一个向右移动,每次选择更接近x的元素加入结果。
这种方法利用了数组已排序的特性,通过二分查找快速定位到可能的最接近区域,然后只需要考虑这个区域附近的元素。
java复制class Solution {
public List<Integer> findClosestElements(int[] arr, int k, int x) {
int n = arr.length;
// 二分查找找到第一个大于等于x的元素位置
int low = 0, high = n;
while (low < high) {
int mid = low + (high - low) / 2;
if (arr[mid] >= x) {
high = mid;
} else {
low = mid + 1;
}
}
// 初始化双指针
int left = low - 1, right = low;
while (k > 0) {
if (left >= 0 && right < n) {
if (compare(arr[left], arr[right], x) < 0) {
left--;
} else {
right++;
}
} else if (left >= 0) {
left--;
} else {
right++;
}
k--;
}
// 收集结果
List<Integer> closestElements = new ArrayList<Integer>();
for (int i = left + 1; i < right; i++) {
closestElements.add(arr[i]);
}
return closestElements;
}
// 相同的比较函数
public int compare(int num1, int num2, int x) {
int diff1 = Math.abs(num1 - x);
int diff2 = Math.abs(num2 - x);
if (diff1 != diff2) {
return diff1 - diff2;
} else {
return num1 - num2;
}
}
}
时间复杂度:O(logn + k)
空间复杂度:O(1),不包括结果存储
这种解法在k较小而n较大时优势明显,因为它避免了遍历整个数组。
在实际编码中,我们需要特别注意以下几种边界情况:
我们的两种解法都能自然地处理这些边界情况,这是它们设计巧妙之处。
选择哪种解法取决于具体场景:
指针越界:特别是在解法二中,left可能小于0,right可能超过数组长度。
距离比较错误:忘记处理距离相等的情况。
结果顺序错误:忘记结果需要保持升序。
好的测试用例应该包括:
例如:
java复制// x在数组中
arr = [1,3,5,7,9], k=3, x=5 → [3,5,7]
// x不在数组中
arr = [1,3,5,7,9], k=2, x=4 → [3,5]
// x小于所有元素
arr = [10,20,30], k=2, x=5 → [10,20]
// x大于所有元素
arr = [10,20,30], k=2, x=35 → [20,30]
// k=1
arr = [1,2,3,4,5], k=1, x=3 → [3]
// k=n
arr = [1,2,3], k=3, x=2 → [1,2,3]
这个问题有几个有趣的变种:
非排序数组:如果数组未排序,我们需要先计算所有元素与x的距离,然后进行排序选择。时间复杂度会增加到O(nlogn)。
动态数组:如果数组会动态变化,可能需要更复杂的数据结构,如平衡二叉搜索树。
多维数据:在多维空间中寻找最接近的点,需要使用空间划分数据结构如KD-Tree。
流式数据:对于数据流场景,可能需要使用堆结构来维护最接近的k个元素。
这种算法在实际中有广泛应用:
理解这类算法的核心思想,能够帮助我们在面对类似问题时快速找到解决方案。