1. 问题分析与基础解法
这道题目要求我们在两个已排序的数组中找到中位数,并且要求算法的时间复杂度为O(log(m+n))。我们先从一个更直观的解法开始,逐步深入理解这个问题。
1.1 中位数的定义与简单解法
中位数是指将一组数据按大小顺序排列后,位于中间位置的数。如果数据个数是奇数,中位数就是中间那个数;如果是偶数,则是中间两个数的平均值。
最直观的解法是将两个数组合并后排序,然后直接取中位数。这种方法虽然简单,但时间复杂度为O((m+n)log(m+n)),不满足题目要求。
cpp复制// 简单但不符合要求的解法
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
vector<int> merged(nums1);
merged.insert(merged.end(), nums2.begin(), nums2.end());
sort(merged.begin(), merged.end());
int n = merged.size();
if (n % 2 == 1) {
return merged[n/2];
} else {
return (merged[n/2-1] + merged[n/2]) / 2.0;
}
}
1.2 优化解法:双指针归并
我们可以利用两个数组已经有序的特性,使用双指针进行归并。这种方法的时间复杂度是O(m+n),虽然比排序法好,但仍未达到题目要求的O(log(m+n))。
cpp复制// 双指针归并解法
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
vector<int> merged;
int i = 0, j = 0;
int m = nums1.size(), n = nums2.size();
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) {
merged.push_back(nums1[i++]);
} else {
merged.push_back(nums2[j++]);
}
}
while (i < m) merged.push_back(nums1[i++]);
while (j < n) merged.push_back(nums2[j++]);
int total = m + n;
if (total % 2 == 1) {
return merged[total / 2];
} else {
return (merged[total/2 - 1] + merged[total/2]) / 2.0;
}
}
注意:这里使用push_back而不是预先分配空间,是为了避免初始化时引入不必要的0值。如果使用vector
merged(m+n),会先创建m+n个0,这在某些情况下可能影响结果。
2. 最优解法:二分查找法
为了达到O(log(m+n))的时间复杂度,我们需要采用二分查找的思想。这个解法较为复杂,需要仔细理解。
2.1 算法思路
我们可以将问题转化为在两个有序数组中寻找第k小的元素。具体思路是:
- 比较两个数组的第k/2个元素
- 删除较小元素所在数组的前k/2个元素
- 递归查找第k - k/2小的元素
2.2 实现细节
cpp复制// 寻找两个有序数组的第k小元素
int findKth(vector<int>& nums1, int start1, vector<int>& nums2, int start2, int k) {
// 如果其中一个数组已经全部排除,直接从另一个数组取第k个
if (start1 >= nums1.size()) return nums2[start2 + k - 1];
if (start2 >= nums2.size()) return nums1[start1 + k - 1];
// 如果k=1,直接返回两个数组首元素中的较小者
if (k == 1) return min(nums1[start1], nums2[start2]);
// 比较两个数组的第k/2个元素
int midVal1 = (start1 + k/2 - 1 < nums1.size()) ?
nums1[start1 + k/2 - 1] : INT_MAX;
int midVal2 = (start2 + k/2 - 1 < nums2.size()) ?
nums2[start2 + k/2 - 1] : INT_MAX;
if (midVal1 < midVal2) {
return findKth(nums1, start1 + k/2, nums2, start2, k - k/2);
} else {
return findKth(nums1, start1, nums2, start2 + k/2, k - k/2);
}
}
// 主函数
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int total = nums1.size() + nums2.size();
if (total % 2 == 1) {
return findKth(nums1, 0, nums2, 0, total / 2 + 1);
} else {
return (findKth(nums1, 0, nums2, 0, total / 2) +
findKth(nums1, 0, nums2, 0, total / 2 + 1)) / 2.0;
}
}
2.3 复杂度分析
每次递归调用都会将k的值减半,因此时间复杂度为O(log(m+n)),满足题目要求。
3. 边界条件与特殊处理
在实际编码中,有几个边界条件需要特别注意:
3.1 空数组处理
当其中一个数组为空时,直接返回另一个数组的中位数:
cpp复制if (nums1.empty()) {
int n = nums2.size();
return n % 2 == 1 ? nums2[n/2] : (nums2[n/2-1] + nums2[n/2])/2.0;
}
if (nums2.empty()) {
int m = nums1.size();
return m % 2 == 1 ? nums1[m/2] : (nums1[m/2-1] + nums1[m/2])/2.0;
}
3.2 数组越界检查
在二分查找实现中,需要特别注意数组越界的情况:
cpp复制int midVal1 = (start1 + k/2 - 1 < nums1.size()) ?
nums1[start1 + k/2 - 1] : INT_MAX;
这里使用INT_MAX作为默认值,确保当数组长度不足时,可以正确比较。
4. 实际编码中的常见错误
4.1 整数除法问题
在计算中位数时,如果数组长度为偶数,需要取中间两个数的平均值。这里必须确保进行浮点数除法:
cpp复制// 错误写法:整数除法会丢失小数部分
return (a + b) / 2;
// 正确写法:确保进行浮点运算
return (a + b) / 2.0;
4.2 数组初始化问题
使用vector时,如果预先指定大小,所有元素会被初始化为0:
cpp复制vector<int> merged(m + n); // 会创建m+n个0
这在某些情况下可能导致错误结果,特别是当数组包含负数时。更好的做法是使用push_back动态添加元素。
4.3 递归终止条件
在二分查找实现中,递归终止条件必须正确处理:
cpp复制if (k == 1) return min(nums1[start1], nums2[start2]);
这个条件确保当k减到1时,我们能够正确返回两个数组当前首元素中的较小者。
5. 性能优化与替代方案
5.1 非递归实现
递归实现虽然直观,但可能存在栈开销。我们可以将其改写为迭代形式:
cpp复制int findKth(vector<int>& nums1, vector<int>& nums2, int k) {
int m = nums1.size(), n = nums2.size();
int index1 = 0, index2 = 0;
while (true) {
if (index1 == m) return nums2[index2 + k - 1];
if (index2 == n) return nums1[index1 + k - 1];
if (k == 1) return min(nums1[index1], nums2[index2]);
int newIndex1 = min(index1 + k/2 - 1, m - 1);
int newIndex2 = min(index2 + k/2 - 1, n - 1);
if (nums1[newIndex1] <= nums2[newIndex2]) {
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
} else {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
}
}
5.2 内存优化
我们可以进一步优化空间复杂度,避免创建额外的合并数组:
cpp复制double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size();
int total = m + n;
int i = 0, j = 0;
int prev = 0, curr = 0;
for (int count = 0; count <= total / 2; count++) {
prev = curr;
if (i < m && (j >= n || nums1[i] <= nums2[j])) {
curr = nums1[i++];
} else {
curr = nums2[j++];
}
}
if (total % 2 == 1) {
return curr;
} else {
return (prev + curr) / 2.0;
}
}
这种方法只需要O(1)的额外空间,但时间复杂度仍然是O(m+n)。
6. 测试用例设计
为了确保代码的正确性,应该设计全面的测试用例:
- 两个数组都为空(题目保证至少有一个元素)
- 其中一个数组为空
- 两个数组长度相等
- 两个数组长度相差很大
- 所有元素都在一个数组中
- 包含重复元素的情况
- 包含负数的情况
- 边界值测试(最大最小长度)
cpp复制// 示例测试用例
void test() {
vector<int> nums1 = {1, 3};
vector<int> nums2 = {2};
assert(abs(findMedianSortedArrays(nums1, nums2) - 2.0) < 1e-5);
nums1 = {1, 2};
nums2 = {3, 4};
assert(abs(findMedianSortedArrays(nums1, nums2) - 2.5) < 1e-5);
nums1 = {0, 0};
nums2 = {0, 0};
assert(abs(findMedianSortedArrays(nums1, nums2) - 0.0) < 1e-5);
nums1 = {};
nums2 = {1};
assert(abs(findMedianSortedArrays(nums1, nums2) - 1.0) < 1e-5);
nums1 = {2};
nums2 = {};
assert(abs(findMedianSortedArrays(nums1, nums2) - 2.0) < 1e-5);
}
7. 实际应用与扩展
虽然这道题目看起来是纯粹的算法练习,但它的思想在实际开发中有重要应用:
- 数据库系统中的合并排序
- 大数据处理中的分布式排序
- 流式数据处理中的中位数计算
- 实时系统中的性能监控(计算响应时间的中位数)
理解这个算法不仅有助于通过技术面试,更能培养解决实际问题的思维方式。在实际项目中,我们经常需要处理来自多个源的有序数据,这种分治思想非常有用。