今天我们来拆解LeetCode第1385题"两个数组间的距离值"。这道题看似简单,但要想写出高效的解法,需要我们对二分查找有深入的理解。题目要求我们找出arr1中满足特定条件的元素个数:对于arr1中的每个元素,arr2中所有元素与该元素的绝对差都必须大于给定的d值。
给定两个整数数组arr1和arr2,以及一个整数d,我们需要计算arr1中满足以下条件的元素数量:对于arr1中的元素x,arr2中所有元素y都满足|x - y| > d。换句话说,arr1中的元素x与arr2中最近的元素的距离也要大于d。
最直观的解法是暴力枚举:对于arr1中的每个元素,遍历arr2中的所有元素,检查是否所有绝对差都大于d。这种解法的时间复杂度是O(n*m),其中n和m分别是arr1和arr2的长度。当数组较大时(比如都达到1000),这种解法会达到百万次操作,效率较低。
我们可以通过预处理arr2来优化算法。具体思路是:
这种优化后的算法时间复杂度为O(m log m + n log m),当m和n较大时,效率提升明显。
首先我们需要对arr2进行排序。C++中可以直接使用sort函数:
cpp复制sort(arr2.begin(), arr2.end());
排序后,arr2中的元素按升序排列,这样我们就可以使用二分查找来快速定位元素。
对于arr1中的每个元素x,我们需要在arr2中找到:
这两个元素是arr2中距离x最近的元素,我们只需要比较这两个元素与x的差值即可。
cpp复制ind = lower_bound(arr2.begin(), arr2.end(), arr1[i]) - arr2.begin();
二分查找时需要注意边界情况:
计算x与找到的最近元素的绝对差,并判断是否大于d:
cpp复制if(ind == m) mi = abs(arr1[i] - arr2[m-1]);
else {
mi = abs(arr1[i] - arr2[ind]);
if(ind - 1 >= 0) mi = min( mi, abs(arr1[i] - arr2[ind-1]) );
}
if(mi > d) ret++;
让我们完整分析一下给出的代码:
cpp复制class Solution {
public:
int findTheDistanceValue(vector<int>& arr1, vector<int>& arr2, int d) {
sort(arr2.begin(), arr2.end()); // 排序arr2
int n = arr1.size(), m = arr2.size(), ind, mi, ret = 0;
for(int i = 0; i < n; i++) {
// 找到arr2中第一个大于等于arr1[i]的元素位置
ind = lower_bound(arr2.begin(), arr2.end(), arr1[i]) - arr2.begin();
// 处理边界情况并计算最小差值
if(ind == m) mi = abs(arr1[i] - arr2[m-1]);
else {
mi = abs(arr1[i] - arr2[ind]);
if(ind - 1 >= 0) mi = min( mi, abs(arr1[i] - arr2[ind-1]) );
}
// 如果最小差值大于d,计数加一
if(mi > d) ret++;
}
return ret;
}
};
sort(arr2.begin(), arr2.end()):预处理arr2,使其有序,这是后续二分查找的基础lower_bound:STL提供的二分查找函数,返回第一个不小于目标值的迭代器当n和m同数量级时,可以简化为O(n log n),比暴力解法的O(n²)高效很多。
除了二分查找,这道题还可以使用双指针的方法解决。思路是:
这种解法的时间复杂度也是O(n log n + m log m),但在某些情况下可能比纯二分查找更高效。
对于特别大的数据集,可以考虑使用分治法:
这种方法适合分布式处理大规模数据。
如果数据范围不大,可以考虑使用哈希表:
这种方法的时间复杂度是O(n + m),但空间复杂度较高,且只适用于元素范围不大的情况。
好的测试用例应该包括:
这类数组距离计算问题在实际中有很多应用:
理解这类算法有助于我们在实际工程中选择合适的解决方案。例如,当需要频繁查询最近邻时,预处理(排序)加二分查找的方案就非常适用;而对于一次性计算,可能暴力解法就足够了。
如果数组元素不是简单的数字,而是多维向量,如何计算距离?这时我们可以:
如果arr2会动态增删元素,如何高效维护?可以考虑:
除了绝对差,还可以考虑其他距离度量:
不同的距离度量适用于不同的应用场景。
在实际实现这个算法时,我有几点深刻体会:
预处理的重要性:排序arr2虽然增加了O(m log m)的开销,但使得后续查询效率大幅提升。这在算法设计中很常见——用预处理换取查询效率。
STL算法的威力:C++的lower_bound实现了高效的二分查找,比自己手写更可靠且不易出错。熟练掌握STL可以大大提高编码效率。
边界条件的隐蔽性:最初实现时我忽略了ind == m的情况,导致某些测试用例失败。这提醒我要特别注意循环和查找的边界条件。
测试的必要性:即使算法看起来简单,也要用各种边界用例测试。我习惯先写几个简单的测试用例,再逐步增加复杂度。
性能优化的平衡:虽然暴力解法在某些情况下也能通过,但学习更优解法能培养算法思维。在实际工程中,要根据数据规模选择合适的方法。