二分法求解两个有序数组的中位数,竟然如此高效!

愿每次回忆,对生活都不感到负疚。

今天忘忧来跟大家一起搞定leetcode第四题,也是我曾经面试过程中真实遇到的题目。

题目描述

给定两个大小为 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张图片

代码实现(含详细注释):

/**
 * 常规法
 * @param nums1 数组1
 * @param nums2 数组2
 * @return 中位数
 * 公众号:算法之灵魂拷问
 */
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
     
    int length1 = nums1.length;
    int length2 = nums2.length;
    //计算两个数组总长度
    int totalLength = length1 + length2;
    //标示中间数中比较小的一个数
    int middle1 = Integer.MIN_VALUE;
    //标示中间数中比较大的一个数
    int middle2 = Integer.MIN_VALUE;
    //数组1的索引,用于遍历数组1
    int index1 = 0;
    //数组2的索引,用于遍历数组2
    int index2 = 0;
    for (int i = 0; i <= totalLength / 2; i++) {
     
        //将稍大的中间值复制给较小的中间值,因为即将到来一个更大的
        middle1 = middle2;
        //使用数组1中的元素的前提是数组1没有越界并且数组1在该位置的元素小于数组2或者数组2已越界
        if (index2 >= length2 || (index1 < length1 && nums1[index1] < nums2[index2])) {
     
            //从数组1中取值,作为较大的中间值
            middle2 = nums1[index1++];
        } else {
     
            //从数组2中取值,作为较大的中间值
            middle2 = nums2[index2++];
        }
    }
    //如果总元素个数是偶数,那么取两数的平均值
    if ((totalLength % 2) == 0){
     
        return (middle1 + middle2) / 2.0;
    } else{
     
        //否则直接取较大的中间数即可
        return middle2;
    }
}

注:关键点在于找对中间位置即可,当m+n为基数时,中位数即为第(m+n) / 2 + 1个元素的值,当m+n为偶数时, 为第(m+n)/2个元素与第(m+n)/2+1个元素的平均数

提交代码后,虽然运行成功,但是仔细观察一下,这个思路的时间复杂度为O((m+n)/2),也就是O(m+n)的复杂度,很明显并不符合题目中O(log(m + n))的要求,所以,可以再尝试一下其他思路。

思路二(二分法)

在此忘忧给大家分享一个小技巧,当看到时间复杂度是O(log XXX)的时候,首先要去考虑一下二分法。
此题按照二分法的思路,可以将题目转变为在两个有序数组中求第(m+n+1)/2小的数和第(m+n)/2+1小的数的平均值,(小技巧:如果m+n是基数,则(m+n+1)/2与(m+n)/2+1相等)
思路:话不多说,直接上图!

二分法求解两个有序数组的中位数,竟然如此高效!_第2张图片

代码实现:
求第k大的元素


/**
 * 求两个有序数组中第k小的元素
 * @param nums1 数组1
 * @param nums2 数组2
 * @param left1 数组1参与计算的元素的左侧索引位置
 * @param right1 数组1参与计算的元素的右侧索引位置
 * @param left2 数组2参与计算的元素的左侧索引位置
 * @param right2 数组2参与计算的元素的右侧索引位置
 * @param k k
 * @return
 * 公众号:算法之灵魂拷问
 */
private static int getFirstK(int[] nums1, int[] nums2, int left1, int right1, int left2, int right2, int k){
     
    //如果数组2中参与计算的元素为0,则直接返回数组1中第k个参与计算的元素
    if(right2 - left2 + 1 == 0){
     
        return nums1[left1 + k - 1];
    }
    //如果数组1中参与计算的元素为0,则直接返回数组2中第k个参与计算的元素
    if(right1 - left1 + 1 == 0){
     
        return nums2[left2 + k - 1];
    }
    //如果k为1,直接返回数组1和数组2的参与计算的元素中的最小值
    if(k == 1){
     
        return Math.min(nums1[left1], nums2[left2]);
    }
    //标记下次二分的中间位置,每次排除k/2个元素
    int middelIndex1 = Math.min((left1 + right1) / 2, left1 + k / 2 - 1);
    int middelIndex2 = Math.min((left2 + right2) / 2, left2 + k / 2 - 1);
    //递归,二分法
    if(nums1[middelIndex1] > nums2[middelIndex2]){
     
        return getFirstK(nums1, nums2, left1, right1, middelIndex2 + 1, right2, k - (middelIndex2 + 1 - left2));
    }else{
     
        return getFirstK(nums1, nums2, middelIndex1 + 1, right1, left2, right2, k - (middelIndex1 + 1 - left1));
    }
}

二分法查找中位数代码:

/**
 * 二分法
 * @param nums1 数组1
 * @param nums2 数组2
 * @return 中位数
 * 公众号:算法之灵魂拷问
 */
public static double findMedianSortedArrays1(int[] nums1, int[] nums2){
     
    int length1 = nums1.length;
    int length2 = nums2.length;
    int totalLength = length1 + length2;

    int middel1 = (totalLength + 1) / 2;
    int middel2 = totalLength / 2 + 1;
    //经过以上两部计算后,总长度为基数的得到的middel1等于middel2

    //经过处理后能保证nums1始终是短的数组
    if(length1 > length2){
     
        return findMedianSortedArrays1(nums2, nums1);
    }
    //当nums1为空时,中位数就是数组2的中位数
    if(length1 == 0){
     
        return (nums2[middel1 - 1] + nums2[middel2 - 1]) / 2.0;
    }
    //如果nums1的最大值小于nums2的最小值,则可以将num1和num2想像成一个合并的数组进行求解
    if(nums1[length1 - 1] < nums2[0]){
     
        //如果两个数组长度相等,那么中位数一定是(num1的最大值+num2的最小值)/2
        if(length1 == length2){
     
            return (nums1[length1 - 1] + nums2[0]) / 2.0;
        }else{
     
            //如果不想等,即num2的长度大于nums1,那么中位数就是nums2当中(middel1 - nums1.length)位置和(middel2 - nums1.length)的元素的和/2
            return (nums2[middel1 - nums1.length - 1] + nums2[middel2 - nums1.length - 1]) / 2.0;
        }
    }
    return (getFirstK(nums1, nums2, 0, length1 - 1, 0, length2 - 1, middel1) + getFirstK(nums1, nums2, 0, length1 - 1,  0,length2 - 1, middel2)) / 2.0;
}

利用二分法,最后的时间复杂度为O(log((m+n)/2)),即O(log(m+n)),显然已经达到了题目的要求,提交结果。

二分法求解两个有序数组的中位数,竟然如此高效!_第3张图片

结束语:人在旅途,难免会遇到荆棘和坎坷,但风雨过后,一定会有美丽的彩虹。

你可能感兴趣的:(算法)