对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode 4. 寻找两个正序数组的中位数详解
1. 题目描述
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为 O(log (m+n))。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3],中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4],中位数 (2 + 3) / 2 = 2.5
提示:
nums1.length == mnums2.length == n0 <= m <= 10000 <= n <= 10001 <= m + n <= 2000-10^6 <= nums1[i], nums2[i] <= 10^6
2. 问题分析
中位数是一组数据中间位置的数:如果数据个数为奇数,中位数是排序后的中间元素;如果为偶数,则是中间两个数的平均值。对于两个正序数组,问题转化为找到合并后数组的中位数,但无需实际合并以优化性能。
核心挑战在于满足 O(log(m+n)) 时间复杂度。直接合并或遍历需要线性时间,不满足要求。因此,必须利用数组有序的特性,通过二分查找或类似分治策略来快速定位中位数位置。关键洞察是:中位数将数据集分成两部分,左边部分的所有元素小于等于右边部分。对于两个数组,我们可以寻找一个划分点,使得两个数组的左边元素个数之和为总元素的一半,且左边最大值小于等于右边最小值。
3. 解题思路
3.1 思路一:合并后查找(暴力法)
将两个数组合并为一个有序数组,然后直接取中位数。这种方法简单直观,但时间复杂度和空间复杂度均为 O(m+n),不满足题目要求,仅作为基础理解。
3.2 思路二:双指针遍历法
使用两个指针模拟合并过程,遍历到中位数位置而不实际合并数组。时间复杂度为 O(m+n),空间复杂度为 O(1)。虽然空间优化,但时间仍为线性,不满足要求。
3.3 思路三:二分查找法(最优解)
在较短数组上执行二分查找,寻找划分点使得两个数组的左边部分合并后恰好为总元素的一半,且满足左边最大值小于等于右边最小值。时间复杂度为 O(log(min(m,n))),空间复杂度为 O(1),满足题目要求,是最优解。原理是:设总长度 total = m+n,左边部分大小 leftSize = (total+1)//2(向上取整)。在 nums1(较短数组)中设划分点 partition1,则 nums2 的划分点 partition2 = leftSize - partition1。通过调整 partition1 确保 nums1[partition1-1] <= nums2[partition2] 且 nums2[partition2-1] <= nums1[partition1],然后根据奇偶性计算中位数。
4. 各思路代码实现
4.1 思路一代码:合并后查找
javascript
var findMedianSortedArrays = function(nums1, nums2) {
const merged = [];
let i = 0, j = 0;
while (i < nums1.length && j < nums2.length) {
if (nums1[i] < nums2[j]) {
merged.push(nums1[i++]);
} else {
merged.push(nums2[j++]);
}
}
while (i < nums1.length) merged.push(nums1[i++]);
while (j < nums2.length) merged.push(nums2[j++]);
const len = merged.length;
if (len % 2 === 0) {
return (merged[len/2 - 1] + merged[len/2]) / 2;
} else {
return merged[Math.floor(len/2)];
}
};
4.2 思路二代码:双指针遍历法
javascript
var findMedianSortedArrays = function(nums1, nums2) {
const m = nums1.length, n = nums2.length;
const total = m + n;
const mid = Math.floor(total / 2);
let i = 0, j = 0;
let prev = 0, curr = 0;
for (let k = 0; k <= mid; k++) {
prev = curr;
if (i < m && (j >= n || nums1[i] < nums2[j])) {
curr = nums1[i++];
} else {
curr = nums2[j++];
}
}
if (total % 2 === 0) {
return (prev + curr) / 2;
} else {
return curr;
}
};
4.3 思路三代码:二分查找法(最优解)
javascript
var findMedianSortedArrays = function(nums1, nums2) {
// 确保 nums1 为较短数组,以优化二分查找
if (nums1.length > nums2.length) {
[nums1, nums2] = [nums2, nums1];
}
const m = nums1.length, n = nums2.length;
const total = m + n;
const leftSize = Math.floor((total + 1) / 2); // 左边部分大小
let left = 0, right = m;
while (left <= right) {
const partition1 = Math.floor((left + right) / 2);
const partition2 = leftSize - partition1;
// 处理边界情况
const maxLeft1 = partition1 === 0 ? -Infinity : nums1[partition1 - 1];
const minRight1 = partition1 === m ? Infinity : nums1[partition1];
const maxLeft2 = partition2 === 0 ? -Infinity : nums2[partition2 - 1];
const minRight2 = partition2 === n ? Infinity : nums2[partition2];
if (maxLeft1 <= minRight2 && maxLeft2 <= minRight1) {
// 划分有效,计算中位数
if (total % 2 === 0) {
return (Math.max(maxLeft1, maxLeft2) + Math.min(minRight1, minRight2)) / 2;
} else {
return Math.max(maxLeft1, maxLeft2);
}
} else if (maxLeft1 > minRight2) {
// 划分太靠右,向左调整
right = partition1 - 1;
} else {
// 划分太靠左,向右调整
left = partition1 + 1;
}
}
throw new Error("Input arrays are not sorted.");
};
5. 各实现思路的复杂度、优缺点对比表格
| 思路 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 合并后查找 | O(m+n) | O(m+n) | 简单直观,易于实现 | 时间和空间效率低,不满足题目要求 |
| 双指针遍历 | O(m+n) | O(1) | 空间效率高,无需额外数组 | 时间效率仍为线性,不满足 O(log(m+n)) |
| 二分查找法 | O(log(min(m,n))) | O(1) | 时间效率高,满足题目要求,是最优解 | 实现稍复杂,需处理边界条件 |