寻找两个正序数组的中位数
1. 题目描述
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
示例 1:
css
输入: nums1 = [1,3], nums2 = [2]
输出: 2.00000
解释: 合并数组 = [1,2,3] ,中位数 2
示例 2:
ini
输入: nums1 = [1,2], nums2 = [3,4]
输出: 2.50000
解释: 合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-106 <= nums1[i], nums2[i] <= 106
2. 解决方案
1. 合并数组后找中位数
- 思路:
- 首先将两个有序数组合并成一个新的有序数组。
- 然后根据新数组的长度是奇数还是偶数来计算中位数。如果新数组长度
len
是奇数,中位数就是新数组第Math.floor(len / 2)
个元素;如果len
是偶数,中位数就是新数组第len / 2 - 1
个元素和第len / 2
个元素的平均值。
- 代码实现:
ts
function findMedianSortedArraysMerge(nums1: number[], nums2: number[]): number {
const merged: number[] = [];
let i = 0, j = 0;
while (i < nums1.length && j < nums2.length) {
if (nums1[i] < nums2[j]) {
merged.push(nums1[i]);
i++;
} else {
merged.push(nums2[j]);
j++;
}
}
while (i < nums1.length) {
merged.push(nums1[i]);
i++;
}
while (j < nums2.length) {
merged.push(nums2[j]);
j++;
}
const len = merged.length;
if (len % 2 === 1) {
return merged[Math.floor(len / 2)];
} else {
return (merged[len / 2 - 1] + merged[len / 2]) / 2;
}
}
- 分析:
- 时间复杂度:(O(m + n)),因为需要遍历两个数组一次,将它们合并。
- 空间复杂度 :(O(m + n)),需要创建一个大小为
m + n
的新数组来存储合并后的结果。
- 优点:
- 思路简单直接,容易理解和实现,对于初学者来说是比较容易想到的方法。
- 缺点:
- 空间复杂度较高,需要额外的空间来存储合并后的数组。如果两个数组非常大,可能会导致内存问题。
2. 二分查找法(优化解法)
- 思路:
- 目标是将两个数组合并后找到中位数,我们可以通过二分查找的方式,在不实际合并数组的情况下找到中位数。
- 假设两个数组分别为
nums1
和nums2
,长度分别为m
和n
。我们将两个数组划分为两部分,使得左半部分的所有元素都小于等于右半部分的所有元素。 - 通过二分查找在较短的数组(假设为
nums1
)上进行,确定一个划分点i
,同时根据总长度计算出另一个数组nums2
的划分点j = (m + n + 1) / 2 - i
。 - 然后检查划分是否满足条件:
nums1[i - 1] <= nums2[j]
且nums2[j - 1] <= nums1[i]
。如果满足,根据数组总长度的奇偶性计算中位数;如果不满足,调整划分点i
继续查找。
- 代码实现:
ts
function findMedianSortedArrays(nums1: number[], nums2: number[]): number {
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}
const m = nums1.length;
const n = nums2.length;
let low = 0, high = m;
while (low <= high) {
const i = Math.floor((low + high) / 2);
const j = Math.floor((m + n + 1) / 2) - i;
const maxLeft1 = i === 0? -Infinity : nums1[i - 1];
const minRight1 = i === m? Infinity : nums1[i];
const maxLeft2 = j === 0? -Infinity : nums2[j - 1];
const minRight2 = j === n? Infinity : nums2[j];
if (maxLeft1 <= minRight2 && maxLeft2 <= minRight1) {
if ((m + n) % 2 === 0) {
return (Math.max(maxLeft1, maxLeft2) + Math.min(minRight1, minRight2)) / 2;
} else {
return Math.max(maxLeft1, maxLeft2);
}
} else if (maxLeft1 > minRight2) {
high = i - 1;
} else {
low = i + 1;
}
}
return 0;
}
- 分析:
- 时间复杂度:(O(log(min(m, n)))),因为二分查找是在较短的数组上进行的,每次迭代都将搜索区间减半。
- 空间复杂度:(O(1)),只需要几个指针和变量来辅助计算,不需要额外的数组空间。
- 优点:
- 时间复杂度较低,在处理大规模数据时效率更高。空间复杂度为常数级别,大大节省了内存。
- 缺点:
- 算法思路相对复杂,代码实现难度较大,需要对二分查找和数组划分有较深入的理解。
3. 最优解及原因
- 最优解:二分查找法是最优解。
- 原因:二分查找法在时间复杂度和空间复杂度上都优于合并数组后找中位数的方法。在处理大规模数据时,其 (O(log(min(m, n)))) 的时间复杂度能够显著提高算法效率,并且 (O(1)) 的空间复杂度使得它在内存使用上非常高效。虽然实现相对复杂,但对于追求高性能的场景,这种方法是最合适的。
3. 拓展和题目变形
拓展:
- 如果数组不是有序的,如何找到两个数组合并后的中位数?
思路:
- 首先对两个数组进行排序,然后可以使用合并数组后找中位数的方法或者二分查找法来求解。排序可以使用高效的排序算法,如快速排序或归并排序,时间复杂度为 (O((m + n)log(m + n)))。
- 另一种方法是使用两个堆(一个最大堆和一个最小堆)来维护数据流,在插入元素的同时保持堆的平衡,使得两个堆的元素个数满足一定关系,从而可以快速找到中位数。每次插入元素的时间复杂度为 (O(log(m + n))),最终获取中位数的时间复杂度为 (O(1))。
代码实现(使用堆) :
ts
import { MaxPriorityQueue, MinPriorityQueue } from '@datastructures-js/priority-queue';
function findMedianUnsortedArrays(nums1: number[], nums2: number[]): number {
const maxHeap = new MaxPriorityQueue();
const minHeap = new MinPriorityQueue();
const allNums = [...nums1, ...nums2];
for (const num of allNums) {
if (maxHeap.size() === 0 || num <= maxHeap.front()) {
maxHeap.enqueue(num);
} else {
minHeap.enqueue(num);
}
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.enqueue(maxHeap.dequeue()!.element);
} else if (minHeap.size() > maxHeap.size()) {
maxHeap.enqueue(minHeap.dequeue()!.element);
}
}
if (maxHeap.size() === minHeap.size()) {
return (maxHeap.front() + minHeap.front()) / 2;
} else {
return maxHeap.front();
}
}
题目变形:
- 给定三个正序数组,找到这三个数组合并后的中位数。
思路:
- 可以将其扩展为类似于两个数组的二分查找方法。先在其中一个较短的数组上进行二分查找确定划分点,然后根据总长度和其他两个数组的划分点关系来确定其他数组的划分点,再检查划分是否满足条件,不断调整划分点直到找到中位数。
- 也可以先将三个数组合并为一个有序数组(时间复杂度为 (O(m + n + p)),其中
m
,n
,p
分别为三个数组的长度),然后按照常规方法找中位数。
代码实现(扩展二分查找) :
ts
function findMedianOfThreeSortedArrays(nums1: number[], nums2: number[], nums3: number[]): number {
const totalLength = nums1.length + nums2.length + nums3.length;
const isEven = totalLength % 2 === 0;
const halfLength = Math.floor(totalLength / 2);
if (nums1.length > nums2.length) {
return findMedianOfThreeSortedArrays(nums2, nums1, nums3);
}
if (nums1.length > nums3.length) {
return findMedianOfThreeSortedArrays(nums3, nums2, nums1);
}
let low = 0, high = nums1.length;
while (low <= high) {
const i = Math.floor((low + high) / 2);
const remainingLength = halfLength - i;
const j = Math.max(0, Math.min(nums2.length, remainingLength));
const k = remainingLength - j;
const maxLeft1 = i === 0? -Infinity : nums1[i - 1];
const minRight1 = i === nums1.length? Infinity : nums1[i];
const maxLeft2 = j === 0? -Infinity : nums2[j - 1];
const minRight2 = j === nums2.length? Infinity : nums2[j];
const maxLeft3 = k === 0? -Infinity : nums3[k - 1];
const minRight3 = k === nums3.length? Infinity : nums3[k];
if (maxLeft1 <= minRight2 && maxLeft1 <= minRight3 && maxLeft2 <= minRight1 && maxLeft2 <= minRight3 && maxLeft3 <= minRight1 && maxLeft3 <= minRight2) {
if (isEven) {
return (Math.max(maxLeft1, maxLeft2, maxLeft3) + Math.min(minRight1, minRight2, minRight3)) / 2;
} else {
return Math.max(maxLeft1, maxLeft2, maxLeft3);
}
} else if (maxLeft1 > minRight2 || maxLeft1 > minRight3) {
high = i - 1;
} else {
low = i + 1;
}
}
return 0;
}