数据结构算法题/两个有序数组的中位数

有三种方法,时间复杂度分别是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.csdn.net/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.csdn.net/hjhjhx26364/article/details/80251675 
https://blog.csdn.net/darminz/article/details/77500474

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值