题目描述
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2
请你找出这两个正序数组的中位数
进阶:你能设计一个时间复杂度为 O(log(m + n)) 的算法解决此问题吗?
提示:
• 0 <= m、n <= 1000
• m+n >= 1
• -106 <= nums1[i] 、nums2[i] <= 106
输入:nums1 = [1, 3],nums2 = [2]
输出:2.0
解释:合并数组=[1,2,3],因此中位数是 2
输入:nums1 = [1, 2],nums2 = [3, 4]
输出:2.5
解释:合并数组=[1,2,3,4],因此中位数是 (2 + 3)/2 = 2.5
数据结构
- 数组
算法思维
- 归并排序、二分查找
关键知识点:归并排序(Merge Sort)
- 将序列拆成两个或两个以上分别排序,然后再合并成一个
采用分治思想,基于归并操作的一种稳定排序算法
稳定排序:对于关键字相等的元素在排序前后相对顺序保持不变 -
归并排序原理
寻找两个有序数组的最小值,即两个数组最小值中较小的那个,放入合并后数组的第一位
寻找两个数组第2小的值
寻找两个数组第3小的值
......
依次进行直到结束
首先将数组拆分成两部分
对这两部分分别递归排序
元素个数大于1,继续拆分
只有一个元素时无需排序,结束递归
在对有序数组进行两两合并
class Solution{
public static int[] mergeSort(int[] arr) {
if (arr.length < 2) return arr;
//计算中间位置
int mid = arr.length / 2;
//分解为左右两部分,分别排序
int[] left = Arrays.copyOfRange(arr, 0, mid);
left = mergeSort(left);
int[] right=Arrays.copyOfRange(arr,mid,arr.length);
right = mergeSort(right);
//合并两个排序后的数组为一个数组
return merge(left, right);
}
private static int[] merge(int[] l, int[] r) {
int[] result = new int[l.length + r.length];
int lIndex = 0;
int rIndex = 0;
for (int i = 0; i < result.length; i++) {
if (lIndex < l.length && rIndex < r.length) {
if (l[lIndex] <= r[rIndex]) {
result[i] = l[lIndex++];
} else {
result[i] = r[rIndex++];
}
} else if (lIndex >= l.length) {
result[i] = r[rIndex++];
} else {
result[i] = l[lIndex++];
}
}
return result;
}
}
时间复杂度:O(nlogn)
• 需要递归的将数组切割 logn 次,然后进行两两归并,时间复杂度为 O(nlogn)
空间复杂度:O(n)
• 递归深度是 O(logn)
• 每次递归在合并时需额外辅助空间,长度与待排序的数组长度相等
• 每次递归都会释放掉所占的辅助空间,最大辅助空间为 O(n)
• 所以空间复杂度为 O(n+logn) = O(n)
关键知识点:二分查找(折半查找)
使用折半的方式在有序数组中查找某一特定元素
- 从数组的中间元素开始查找,如果中间元素等于目标元素,查找结束;
- 如果中间元素小于目标元素,则在右半部分继续查找;
- 如果中间元素大于目标元素,则在左半部分继续查找;
- 如果在某一步骤数组为空,则代表找不到,查找结束;
每一次比较都使查找范围缩小一半
//二分查找算法,在a[start] ~ a[end]中查找key
public int binarySearch(int key, int a[], int start, int end) {
if (start > end) //未找到key,返回-1
return -1;
int m = (start + end) / 2;
if (a[m] == key) //找到key,返回key的id
return m;
if (a[m] > key)
return binarySearch(key, a, start, m - 1);//在m左侧继续查找
return binarySearch(key, a, m + 1, end);//在m右侧继续查找
}
解题步骤
一. Comprehend 理解题意
寻找两个数组的中位数
- 两个数组都是正序数组
- 寻找两个数组所有元素的中位数
进阶要求
- 算法的时间复杂度为 O(log(m+n))
宽松限制
- m + n >= 1:数组不同时为空(中位数一定存在)
细节问题
- 某个数组可能为空
- 元素总数为偶数时,中位数是中间两数平均值(不一定是整数)
二. Choose 选择数据结构与算法
数据结构选择
- 输入的数据类型为两个整形数组
- 输出的数据类型为一个浮点数
- 因为需要处理的是两个有序的整数集合,我们采用数组作为数据结构
算法思维选择
- 题目要求寻找中位数,我们可以对两个数组进行合并和排序
- 在排序后的数组中我们可以很容易地找到中位数
- 有没有更好的合并、排序方案? -- 归并排序
解题思路
• 题目中给定两个有序数组
• 直接使用归并排序的最后一步对两个数组进行合并即可
时间复杂度
• 需要对两个数组各浏览一遍,进行 m+n-1 次比较,时间复杂度为 O(m+n)
空间复杂度
• 需要额外的空间存储合并排序后的数组,空间复杂度为 O(m+n)
算法思维优化
- 题目要求寻找中位数,其实我们并不需要得到合并后的有序数组
- 归并排序进行到一半的时候,我们其实已经找到了中位数
- 算法可以被改进为遍历两个有序数组 num1 和 nums2:
• 若 |nums1|+|nums2| 为奇数,寻找第 (|nums1|+|nums2|+1)/2 小的数
• 若 |nums1|+|nums2| 为偶数,寻找第 (|nums1|+|nums2|)/2 和 (|nums1|+|nums2|+2)/2 小的数
三. Code 编码实现基本解法
解题思路剖析
- 遍历有序数组 nums1 和 nums2,寻找中位数
- 使用归并排序的思想,但是并不真正构建归并后的数组
- 遍历有序数组nums1和nums2,寻找中位数
- |nums1|+|nums2|= 4+3 = 7, 中位数为第 (7+1)/2=4 小的数
代码实现
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
//定义指针p1,p2 分别指代nums1和nums2的当前元素
int p1 = 0, p2 = 0;
//定义中位数m1和m2,指代当前第i-1大的数和第i大的数
int m1 = 0, m2 = 0;
for (int i = 0; i <= (m + n) / 2; i++) {
m1 = m2; //指针右移
//若nums1未处理完,并且 nums2已处理完 或 p1元素小于p2元素
//则第i大的元素为nums1当前元素
if (p1 < m && (p2 >= n || nums1[p1] < nums2[p2])) {
m2 = nums1[p1++];
} else { //否则第i大的元素为nums2当前元素
m2 = nums2[p2++];
}
}
//若m+n为奇数,返回m2,若m+n为偶数,返回(m1+m2)/2.0
if (((m + n) % 2) == 0) return (m1 + m2) / 2.0;
else return m2;
}
}
时间复杂度:O(m+n)
• 需要多次比较两个数组中元素的大小
• 只需要找到中间位置的元素,并不需要完成整个归并,总的比较次数为 (m+n)/2 + 1
• 时间复杂度为 O(m+n)
空间复杂度:O(1)
• 常数级的变量空间 O(1)
执行耗时:3 ms,击败了 69.10% 的Java用户
内存消耗:41 MB,击败了 24.82% 的Java用户
四. Consider 思考更优解
剔除无效代码 优化空间消耗
寻找更好的算法思维
- 当前浏览了输入 nums1 和 nums2 总数据的一半
- 有没有可能浏览更少的元素就可以确定中位数?
中位数的性质
- 中位数可以将数组分成 2 份
• 小于等于中位数的元素
• 大于中位数的元素
这两个数组的元素个数之差小于等于 1 - 寻找中位数的问题 变为 寻找满足下列条件的分界线:
1)分界线左边的元素都小于等于分界线右边的元素
2)0 <= 分界线左边的元素个数 - 分界线右边的元素个数 <= 1 - 如何查找分界线? -- 二分查找
五. Code 编码实现最优解
解题思路剖析
- 找到 nums1 和 nums2 的分界线,将所有元素划分为两部分,使得:
分界线左边的元素都小于等于分界线右边的元素
0 <= 分界线左边的元素个数-分界线右边的元素个数 <= 1 - 在 nums1 和 nums2 中元素较少的数组进行二分查找分界线
- 若 |nums1|+|nums2| 为奇数,返回分界线左侧元素的最大值
- 若 |nums1|+|nums2| 为偶数,返回分界线左侧元素最大值与右侧元素最小值的平均
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
//较短的数组在前,确保m <= n
if (m > n) return findMedianSortedArrays(nums2, nums1);
//定义p、q为nums1分界线范围,共m+1个可能的划分位置
int p = 0, q = m;
int i = 0, j = 0; //nums1、num2的分界位置
//使用循环代替递归,减少空间消耗
while (p <= q) {
i = (p + q) / 2; //二分确定nums1当前分界位置
//根据i确定nums2的分界位置,使得左侧元素数-右侧元素数为0或1
j = (m + n + 1) / 2 - i;
//nums1右侧最小值小于nums2左侧最大值,nums1划分位置在[i+1, q]之间
if (j != 0 && i != m && nums1[i] < nums2[j - 1]) p = i + 1;
//nums1左侧最大值大于nums2右侧最小值,nums1划分位置在[p, i-1]之间
else if (i != 0 && j != n && nums1[i - 1] > nums2[j]) q = i - 1;
//当前划分位置左侧的最大值小于右侧的最小值,满足要求
else break;
}
//m+n为奇数,返回左侧的最大值 左侧最大值:三种情况 nums1为空、nums2为空、都不为空
int maxLeft = i == 0 ? nums2[j - 1] : (j == 0 ? nums1[i - 1] : Math.max(nums1[i - 1], nums2[j - 1]));
if ((m + n) % 2 == 1) return maxLeft;
//m+n为偶数,返回左侧最大值与右侧最小值的平均 右侧最小值:三种情况 nums1为空、nums2为空、都不为空
int minRight = i == m ? nums2[j] : (j == n ? nums1[i] : Math.min(nums1[i], nums2[j]));
return (maxLeft + minRight) / 2.0;
}
}
时间复杂度:O(log(min(m,n)))
• 只需要对 nums1 和 nums2 中较短数组进行二分查找
• 二分查找的时间复杂度为 O(log(min(m,n)))
空间复杂度:O(1)
• 常数级内存空间 O(1)
执行耗时:2 ms,击败了 100.00% 的Java用户
内存消耗:40.9 MB,击败了 35.53% 的Java用户
六. Change 变形与延伸
题目变形
- (练习)如果输入 nums1 和 nums2 变为逆序数组该如何实现?
延伸扩展
- 二分查找是一种基础高效的算法思维
- 实际工作中应用非常广泛
数据库中使用最频繁的查找算法select * from tb where dt = 2021-01-30
数学方程求根