题目:
- 寻找两个有序数组的中位数
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
示例 1:
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
题目解法一:
(自己的解法,不符合复杂度要求)
1.由于自身对递归和分治思想比较不熟悉,所以一开始肯定写不出符合复杂度的算法,因为这个复杂度一看就是要用递归去做
2.该算法的思想其实就是获取到两个数组的长度之和,然后记录中位数的位数(即是第几位数)
3.之后开始定义i,j分别同时遍历两个数组,逐一进行元素比较,哪个小就意味着在前面,进行遍历,对应的数组下标后移即可。在遍历的过程中进行计数,当计数到中位数的位置时就输出即可。
4.该算法的算法复杂度应该是O(m+n) 空间复杂度也是O(1)
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
int sumLength = nums1.length + nums2.length;
int numStore = 0;//中位数存储
int sum = 0;//计数现在定位到了多少个数了
int targetIndex = ((sumLength+1)/2) - 1;//中位数所在的索引
for(int i = 0, j = 0; i < nums1.length || j < nums2.length;){
if(i == nums1.length){
numStore = nums2[j];
j += 1;
}else if(j == nums2.length){
numStore = nums1[i];
i += 1;
}else{
if(nums1[i] <= nums2[j]) {
numStore = nums1[i];
i += 1;
}else{
numStore = nums2[j];
j += 1;
}
}
sum += 1;
if(sum == targetIndex){//找到中位数了
if(sumLength %2 == 0){//总共偶数个数
int n = Math.min(nums1[i],nums2[j]);
return (numStore + n)/2.0;
}else{//总共奇数个数
return numStore;
}
}
}
throw new IllegalArgumentException("No two sum solution");
}
题目解法二(官方解法):
1.首先我们要理解中位数的概念,其实就是将一组有序数组变成两节,左边的永远比右边的小(左边最大的小于右边最小的),这就是中位数的概念。
2.设计如下
-
我们假设从下标 i 开始将数组A切分成俩个数组LeftA,RightA,那么在这两个数组中,LeftA一定小于RightA,在这个条件下,如果LeftA和RightA的长度又正好相同(长度为偶数的情况下),那么我们就可以知道中位数为A【i-1】+A【i】的平均值。如果长度为奇数,那么就让LeftA的长度比RightA多1,那么中位数就为A【i-1】
-
同理,我们从 j 下标开始将数组B切分为两个数组LeftB,RightB,根据上面的思想我们也可以得到一些结论
-
那么如何将这两个数组融合在一起呢?其实很简单,只要我们同时在A,B中进行切分,将LeftA,LeftB都放在左边作为一个LeftPart,将RightA,RightB都放在右边作为一个RightPart,这时候我们来看会出现什么结果
-
设A数组的长度为m,B数组的长度为n,那么我们就可以得出 i 的范围是【0,m】
-
在这个新的LeftPart,RightPart中,为了准确找到中位数的长度,我们要满足两个条件:
- Len(LeftPart) = Len(RightPart) 或者
Len(LeftPart) = Len(RightPart)+ 1 - Max(LeftPart)<= Min(RightPart)
- Len(LeftPart) = Len(RightPart) 或者
-
然后我们将这两个条件用数字公式翻译过来:
- i + j = m - i + n - j 或 i + j = m - i + n - j +1
再转化一次就是:j = (m+n+1)/ 2 - i ( i 和 j 的关系就出来了) - B【j-1】<=A【i】 && B【j】>= A【i-1】
- i + j = m - i + n - j 或 i + j = m - i + n - j +1
-
直到这里我们本题的思路就很清楚了,我们采用递归的方法,不断的找到这个 i 的值,每次都进行判断此时是否符合这两个条件,如果符合那就说明此时切分的很合理,那就意味着可以直接找到中位数了!
-
但是有几个需要注意的地方:
- 由于j = (m+n+1)/ 2 - i ,而 i 的范围是【0,m】,我们要确保 j 不能是负值,所以就必须保证n > m,也就是说必须把短的数组放在前面,长的数组放在后面,其实就是进行一个长度判断,转一下即可
- 第二个问题其实就是关于一些临界点的问题,比如i = m,j = n的问题,这些问题我们可以留到后面再讨论
-
关于如何找到 i 这就是该算法的一个核心思想,这里采用二叉树的搜索思想实现:
- 首先定义iMin = 0,iMin = m,这两个就规定了i的范围,我们每次都取i = (iMin+iMax)/ 2
- 然后先满足第一个长度条件求j:j = (m+n+1)/ 2 - i
- 然后去判断此时的i是否符合我们的那两个条件,如果发现不符合,那就根据不同的情况去调整iMin ,iMax的范围,这样我们最后就可以找到符合条件的i值了
-
对于每次判断i是否符合条件一共会出现三种情况:
- B[j−1]>A[i]:此时意味着A【i】太小了,应该在左半部分,所以那就意味着我们切A的时候,应该多切一点,i应该增大,那么我们应该将范围调整为【i+1,iMax】
- A[i−1]>B[j]:此时意味着A【i-1】太大了,应该在右半部分,所以我们切A的时候应该少切一点,i应该减小,将范围调整为【iMin,i-1】
- B[j−1]≤A[i] 且A[i−1]≤B[j]:那就意味着我们找到了符合要求的切分点,那么此时就可以不用再寻找了,接下来就是找中位数了
-
算法实现框架:(这里不是最终代码,只是为了更好的理解)
public static double findMedianSortedArrays1(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
if(n < m){ //一定要把长的放前面
int temp[] = nums1; nums1 = nums2; nums2 = temp;
int tempa = m; m = n; n = tempa;
}
int iMin = 0, iMax = m, halflen = (m + n + 1) / 2;
while(iMin <= iMax){
//通过已有的关系式,设置i ,j的大小 i每次都从中间砍一刀
int i = (iMin + iMax) / 2;
int j = halflen - i;
//疑问 为什么i < iMax i > iMin 【1,3】 【2】 报错
if( i < iMax && nums2[j - 1] > nums1[i]){ //B[j-1] > A[i] LeftA部分砍少了 i右移
iMin =i + 1;
}else if(i > iMin && nums2[j] < nums1[i - 1]){//B[j] < A[i - 1] leftA部分砍多了 i左移
iMax =i - 1;
}else{ //i砍得正好
//接下来这部分就是求中位数了
}
}
return 0.0;
}
- 对于求中位数部分而言其实很简单,首先无论是总长度是奇数还是偶数,我们都需要先求出LeftPart中的max,因为如果是奇数,那么它就是中位数,如果是偶数,我们还需要求出RightPart的min,这样就可以求出中位数了
- 然后我们这里就出现了刚刚提到的如果i = m, j = n怎么办,直接上代码,基本上都可以看得懂!
public static double findMedianSortedArrays1(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
if(n < m){ //一定要把长的放前面
int temp[] = nums1; nums1 = nums2; nums2 = temp;
int tempa = m; m = n; n = tempa;
}
int iMin = 0, iMax = m, halflen = (m + n + 1) / 2;
while(iMin <= iMax){
//通过已有的关系式,设置i ,j的大小 i每次都从中间砍一刀
int i = (iMin + iMax) / 2;
int j = halflen - i;
//疑问 为什么i < iMax i > iMin 【1,3】 【2】 报错
//因为i如果此时等于iMax,呢么iMin加完之后也等于iMax,
if( i < iMax && nums2[j - 1] > nums1[i]){ //B[j-1] > A[i] LeftA部分砍少了 i右移
iMin =i + 1;
}else if(i > iMin && nums2[j] < nums1[i - 1]){//B[j] < A[i - 1] leftA部分砍多了 i左移
iMax =i - 1;
}else{ //i砍得正好
int maxLeft = 0;
//这一部分的判断其实就是怕数组越界了
if(i == 0){ //若左半部分没有A A全部在右边
maxLeft = nums2[j - 1];
}else if(j == 0){ //若左边部分没有B B全在右边
maxLeft = nums1[i - 1];
}else {
maxLeft = Math.max(nums1[i - 1],nums2[j - 1]);
}
if( (m + n)%2 != 0){//如果是总长度为奇数 那么maxLeft就是中位数 因为分的时候左部分会比有部分多一个
return maxLeft;
}
//如果总长度为偶数
int minRight = 0;
if(i == m){ //右部分没有A
minRight = nums2[j];
}else if(j == n){//右部分没有B
minRight = nums1[i];
}else {
minRight = Math.min(nums1[i],nums2[j]);
}
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}