有三种方法,时间复杂度分别是O(m+n) ,O(k),O(log(m+n))
注意点:
判断合并后的数组的元素个数是奇数还是偶数
如果是奇数取中间值;如果是偶数取中间2个数的平均值。
两种求中位数的方法:
方法1,判断奇数个还是偶数个
if (lengthall % 2 == 0){
result = ( all[lengthall/2-1] + all[lengthall/2] )*1.0/2;
}else{
result = all[lengthall/2];
}
方法2,不需要判断奇数个还是偶数个
int l = (lengthall+1)/2-1;
int r = (lengthall+2)/2-1;
//上面减1的原因是坐标从0开始的
result = (all[l] + all[r])*1.0/2;
//这种求中位数的方法不需要判断是奇数还是偶数个
思考:
中位数相当于合并之后的top(n/2),如果是其他top数也是可以的。所以如果是一个无序数组的top k就只用小顶堆。如果是两个有序数组合并之后的top k就可以用如下的方法。
(1)O(m+n)合并两有序数组成一个新有序数组,再按中间位置取值。
两个有序数组合并为一个有序数组的时间复杂度O(m+n)。请参考https://blog.youkuaiyun.com/fkyyly/article/details/83145276
/**1
* O(m+n)
* 合并两有序数组成一个新有序数组,再按中间位置取值
* @param nums1
* @param nums2
* @return
*/
public static double findMedianSortedArrays1(int[] nums1, int[] nums2) {
int length1 = nums1.length;
int length2 = nums2.length;
int lengthall = length1 + length2;
int[] all = new int[length1+length2];
int i=0,j=0,k=0;
while (i<length1 && j <length2){
if(nums1[i]<nums2[j]){
all[k]=nums1[i];
i++;
k++;
}else{
all[k] = nums2[j];
j++;
k++;
}
}
while (i<length1){
all[k]=nums1[i];
i++;
k++;
}
while (j<length2){
all[k] = nums2[j];
j++;
k++;
}
double result;
if (lengthall % 2 == 0){
result = ( all[lengthall/2-1] + all[lengthall/2] )*1.0/2;
}else{
result = all[lengthall/2];
}
return result;
}
(2)O(k)两个指针分别从两数组开头指,比较两指针处的数,谁小谁往后移,直到两指针共移动k次,k为中间位置
/**2
* O(k)
* 两个指针分别从两数组开头指,比较两指针处的数,谁小谁往后移,
* 直到两指针共移动k次,k为中间位置
* 注意总数有奇数个还是偶数个计算中位数的方法
* @param nums1
* @param nums2
* @return
*/
public static double findMedianSortedArrays2(int[] nums1, int[] nums2) {
int length1 = nums1.length;
int length2 = nums2.length;
int lengthall = length1 + length2;
int inx1 = 0;
int inx2 = 0;
int x = -1;
int y = -1;
while (inx1 + inx2 < lengthall){
if (inx1 < length1) {
while (inx2 == length2 || nums1[inx1] <= nums2[inx2] ) {
//nums1到头了或者nums1此时的元素小于nums2此时的元素,这两种情况下从nums1取值(也就是nums1和nums2小的移动)
inx1++;
//下面是总长度是奇数还是偶数对于中位数的计算
if (inx1 + inx2 == (lengthall + 1) / 2) {
x = nums1[inx1 - 1];
}
if (inx1 + inx2 == (lengthall + 2) / 2) {
y = nums1[inx1 - 1];
return (x + y) * 1.0 / 2;
}
if (inx1 == length1){
break;
}
}
}
if (inx2 < length2){
while ( inx1 == length1 || nums2[inx2] <= nums1[inx1] ) {
inx2++;
if (inx1 + inx2 == (lengthall + 1)/2){
x = nums2[inx2-1];
}
if (inx1 + inx2 == (lengthall + 2)/2){
y = nums2[inx2-1];
return (x+y)*1.0/2;
}
if (inx2 == length2){
break;
}
}
}
}
return -1;
}
(3)O(log(m+n))两个数组分别采用二分法查找
思想:上面的是数组num1[],下面是数组num2[]。
使用递归的思想分别对num1和num2求中位数,因为是有序的,所以直接取n/2处的值即可。
有个前提:合并后数组的中位数一定大于等于单个数组的中位数。
(1)如果num1的中位数=num2的中位数,那么就是最终的结果。
(2)如果num1的中位数<num2的中位数,那么下次就在[B,C]和[D,F]两个新数组直接找中位数。
(3)如果num1的中位数>num2的中位数,那么下次就在[A,C]和[E,F]两个新数组直接找中位数。
(4)重复这个步骤,直到新数组的长度为2
方法:二分+递归
--看到有序,并且明确要求时间复杂度为log级的,肯定需要二分。但是请注意,很多人一看到二分,就想也不想的开始对数组进行二分,但是对于这题,对数组二分就进了死胡同了。这题是对另一个量进行二分。
--再分析中位数的特征,就是求数组中的某一个(或两个)数,其左右两边的个数相等
--设两个有序数组分别为,a,长度m;b,长度n
--那么这个中位数,就是,a和b合并后的第(m+n+1)/2个(先不管有2个中位数的情况,后面会有个小技巧来屏蔽奇偶差异)
--那么现在的问题就变成了,求两个有序数组中,从小到大的第k个值
--所以二分其实是对k进行二分,两个数组同时从0开始,每次往后跳k/2个,当然不是同时跳,谁更小才能跳,保证已经跳过的数在整个数组中是最小的
private int findKth(int[] array1, int start1, int end1, int[] array2, int start2, int end2, int k){
if(start1>end1){
return array2[start2+k-1];
}
if(start2>end2){
return array1[start1+k-1];
}
if(k==1){
return Math.min(array1[start1], array2[start2]);
}
int mid1=Integer.MAX_VALUE;
int mid2=Integer.MAX_VALUE;
//这里的思想,其实不是每次移动整个end-start的一半,而是移动k的一半
if(start1+k/2-1<=end1){
mid1=array1[start1+k/2-1];
}
if(start2+k/2-1<=end2){
mid2=array2[start2+k/2-1];
}
if(mid1<mid2){
return findKth(array1, start1+k/2, end1, array2, start2, end2, k-k/2);
}
return findKth(array1, start1, end1, array2, start2+k/2, end2, k-k/2);
}
public double findMiddle(int[] array1, int[] array2){
int totalLength=array1.length+array2.length;
if(totalLength==0){
return -1;
}
//屏蔽奇偶差异,当是奇数时,k1,k2找到的是同一个中位数;当是偶数时,k1,k2找到的是两个中位数
int k1=(totalLength+1)/2;
int k2=(totalLength+2)/2;
int mid1, mid2;
if(array1.length==0){
mid1=array2[k1-1];
mid2=array2[k2-1];
}else if(array2.length==0){
mid1=array1[k1-1];
mid2=array1[k2-1];
}else {
mid1=findKth(array1, 0, array1.length-1, array2, 0, array2.length-1, k1);
mid2=findKth(array1, 0, array1.length-1, array2, 0, array2.length-1, k2);
}
return (double) (mid1+mid2)/2;
}
public static void main(String[] args) {
int[] nums1 = {1,2,3};
int[] nums2 = {3,4,5,6,7};
MediaTwoArr mediaTwoArr = new MediaTwoArr();
System.out.println(mediaTwoArr.findMiddle(nums1, nums2));
}
如果数组a的中位数小于数组b的中位数,那么整体的中位数只可能出现在a的右区间加上b的左区间之中;
如果数组a的中位数大于等于数组b的中位数,那么整体的中位数只可能出现在a的左区间加上b的右区间之中。
关键就是利用分治的思想逐渐缩小a的区间和b的区间来找到中位数。
首先假设数组A和B的元素个数都大于k/2,我们比较A[k/2-1]和B[k/2-1]两个元素,这两个元素分别表示A的第k/2小的元素和B的第k/2小的元素。这两个元素比较共有三种情况:>、<和=。如果A[k/2-1]<B[k/2-1],这表示A[0]到A[k/2-1]的元素都在A和B合并之后的前k小的元素中。换句话说,A[k/2-1]不可能大于两数组合并之后的第k小值,所以我们可以将其抛弃。
当A[k/2-1]>B[k/2-1]时存在类似的结论。
当A[k/2-1]=B[k/2-1]时,我们已经找到了第k小的数,也即这个相等的元素,我们将其记为m。由于在A和B中分别有k/2-1个元素小于m,所以m即是第k小的数。(这里可能有人会有疑问,如果k为奇数,则m不是中位数。这里是进行了理想化考虑,在实际代码中略有不同,是先求k/2,然后利用k-k/2获得另一个数。)
通过上面的分析,我们即可以采用递归的方式实现寻找第k小的数。此外我们还需要考虑几个边界条件:
如果A或者B为空,则直接返回B[k-1]或者A[k-1];
如果k为1,我们只需要返回A[0]和B[0]中的较小值;
如果A[k/2-1]=B[k/2-1],返回其中一个;
https://blog.youkuaiyun.com/hjhjhx26364/article/details/80251675
https://blog.youkuaiyun.com/darminz/article/details/77500474