Java LeetCode每日一题-从易到难带你领略算法的魅力(五):寻找两个正序数组的中位数-5700字匠心出品

LeetCode每日一题 专栏收录该内容
21 篇文章 2 订阅

1.题目要求

  • 给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。

  • 进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?

2.题目示例

  • 示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
  • 示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
  • 示例 3:
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000
  • 示例 4:
输入:nums1 = [], nums2 = [1]
输出:1.00000
  • 示例 5:
输入:nums1 = [2], nums2 = []
输出:2.00000

3.提示

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -106 <= nums1[i], nums2[i] <= 106

4.解题

4.1 解题思路

  1. 不考虑进阶的情况下:在读完题后第一反应就是简单的,把两个数组合并,然后根据合并数组长度是奇数还是偶数来判断中位数的取法(太简单就不写出解决方案了)
  2. 然后我们来看进阶的要求,要求要以时间复杂度为 O(log (m+n))的算法来实现,第一反应是二分查找
  3. 根据中位数的定义,当m+n是奇数时,中位数就是两个有序数组中第(m+n)/2个元素,当m+n是偶数是,中位数是两个有序数组中第(m+n)/2个元素和(m+n)/2+1个元素的平均值。
  4. 所以这道题可以转变为,寻找这两个有序数组中第k小的元素,k=中位数的位置(比如A数组长度为3,B数组长度为2.那么k=(2+3)/2(取整),k=3)
  5. 假设有AB两个有序数组,要找到第k个元素,我们就可以比较A[k/2-1]和B[k/2-1],对于AB[k/2-1]前面会有k2-1个元素,所以A[k/2-1]和B[k/2-1]的其中一个的较小值最多只有(k/2-1)+(k/2-1)<=k-2个值比他小,那么他就不会是第k个小的数,就可以把比较输了的一方的前k/2-1个元素全部剔除
  6. 处理过后的两个数组又变成新的数组,重新进行二分查找,并且根据我们排除的元素个数来减少k的数值。
  7. 特殊情况:
    如果A[k/2-1]或者B[k/2-1]越界,需要选择对应数组的最大值,然后必须根据排除元素的个数来减去k,不能直接k/2-1
    如果另一个数组为空,直接返回有数据数组的第k个元素即可
    如果k=1,那么直接返回数组首元素最小值即可
  • 例子:
//有AB两个数组,长度为2和长度为3.
//那么k=(2+3)/2
//k=3
A[13]
B[123]

//比较两个数组中下标为k/2-1的数,也就是A[0]、B[0]。-*-代表比较数字
A[-1-3]
B[-1-23]

//1=1
//所以AB数组的前k/2-1个元素被排除都行,括号内代表被排除
//同时更新k的值,k=k-k/2=3-1=2
A[1)、3]
B[123]

//比较两个数组中下标为k/2-1的数,也就是A[1]、B[0]。
A[1)、-3-]   也可以看做   A[-3-]  A[0]
B[-1-23]   也可以看做   B[-1-23]  B[0]

//3>1
//所以排除B数组前k/2-1个元素
//更新k的值,k=k-k/2=2-1=1
A[1)、3]
B[(1)23]

//因为k=1,所以两个数组中未被排除的最小的数就是第k个数也就是中位数,所以直接比较大小即可
A[1)、-3-]
B[(1)-2-3] 

//3>2
//所以中位数就是2

4.2 业务代码

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int length1 = nums1.length, length2 = nums2.length;
        int totalLength = length1 + length2;
        //区分奇数和偶数的k值计算情况
        if (totalLength % 2 == 1) {
            int midIndex = totalLength / 2;
            double median = getKthElement(nums1, nums2, midIndex + 1);
            return median;
        } else {
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
            return median;
        }
    }

    public int getKthElement(int[] nums1, int[] nums2, int k) {
        int length1 = nums1.length, length2 = nums2.length;
        //下标偏移量,就是删除了多少个元素
        int index1 = 0, index2 = 0;
        int kthElement = 0;

        while (true) {
            // 边界情况
            if (index1 == length1) {
                return nums2[index2 + k - 1];
            }
            if (index2 == length2) {
                return nums1[index1 + k - 1];
            }
            //最后结果计算
            if (k == 1) {
                return Math.min(nums1[index1], nums2[index2]);
            }
            
            // 正常情况
            int half = k / 2;
            int newIndex1 = Math.min(index1 + half, length1) - 1;
            int newIndex2 = Math.min(index2 + half, length2) - 1;
            int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
            //判断大小,决定删除那个数组的数据
            if (pivot1 <= pivot2) {
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;
            } else {
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;
            }
        }
    }
}

4.3 运行结果

结果

5.优化

5.1 优化思路

  1. 为了使用划分的方法解决这个问题,需要理解「中位数的作用是什么」。在统计中,中位数被用来:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。
  2. 首先,在任意位置 i 将 A 划分成两个部分:
  3. 由于A中有m个元素,所以有m+1种划分方法(i∈[0,m])
    len(left_A)=i,len(right_A)=m-i
    注意:当i=0时,left_A为空集,而当i=m时,right_A为空集
  4. 采用同样的方式,在任意位置j将B划分成两个部分
  5. 将left_A和left_B放入一个集合,并将right_A和right_B放入另一个集合。再把这两个新的集合分别命名为left_part和right_part
  6. 当AB的总长度是偶数时,如果可以确认:
    len(left_part)=len(right_part)
    max(left_part)<=min(right_part)
  7. 那么,{A,B}中的所有元素已经被划分为相同长度的两个部分,且前一部分中的元素总是小于或等于后一部分中的元素。中位数就是前一部分的最大值和后一部分的最小值的平均值
    median=[max(left_part)+min(right_part)]/2
  8. 当AB的总长度是奇数时,如果可以确认:
    len(left_part)=len(right_part)+1
    max(left_part)<=min(right_part)
  9. 那么,{A,B}中的所有元素已经被划分为两个部分,前一部分比后一部分多一个元素,且前一部分中的元素总是小于或等于后一部分的中的元素。中位数就是前一部分的最大值:
    median=max(left_part)
  10. 第一个条件对于总长度是偶数和奇数的情况有所不同,但是可以将两种情况合并。第二个条件对于纵长度是偶数和奇数的情况是一样的。
  11. 要确保这两个条件,只需要保证:
    (1) i+j=m-i+n-j(当m+n为偶数)或i+j=m-i+n-j+1(当m+n为奇数)。等号左侧为前一部分的元素个数,等号右侧为后一部分的元素个数。将i和j全部移到等号左侧,我们就可以得到i+j=(m+n+1)/2。这里的分数结果只报了证书部分。
    (2)0≤i≤m,0≤j≤n。如果我们规定A的长度小于等于B的长度,即m≤n。这样对于任意的i∈[0,m]都有j=(m+n+1)/2-i∈[0,n],那么我们在[0,m]的范围内枚举i并得到j,就不需要额外的性质了。如果A的长度较大,那么我们只要交换A和B即可。如果m>n那么得出的j有可能是复数。
    (3)B[j-1]≤A[i]以及A[i-1]≤B[j],即前一部分的最大值小于等于后一部分的最小值
  12. 为了简化分析,假设A[i-1],B[j-1],A[i],B[j]总是存在。对于i=0,i=m,j=0,j=n这样的临界条件,我们只需要规定A[-1]=B[-1]=-∞,A[m]=B[n]=∞即可。这也是比较直观的:当一个数组不出现在前一部分时,对应的值为负无穷,就不会对前一部分的最大值产生影响。当一个数组不出现在后一部分时,对应的值为正无穷,就不会对后一部分的最小值产生影响。
  13. 对此我们需要做的是:
    在[0,m]中找到i,使得:B[j-1]≤A[i]且A[i-1]≤B[j],其中j=(m+n+1)/2-i
  14. 我们证明它等价于:
    在[0,m]中找到最大的i,使得:A[i-1]≤B[j],其中j=(m+n+1)/2-i
  15. 这是因为:
    (1)当i从0~m递增时,A[i-1]递增,B[j]递减,所以一定存在一个最大的i满足A[i-1]≤B[j]
    (2)如果i是最大的,那么说明i+1不满足。将i+1带入可以得到A[i]≤B[j-1],也就是B[j-1]<A[i],就和我们进行等价变换前i的性质一致了
  16. 因此我们可以对i在[0,m]的区间上进行二分搜索,找到最大的满足A[i-1]≤B[j]的i值,就得到了划分的方法。此时,划分前一部分元素中的最大值,已经划分后一部分中的最小值,才能作为就是这两个数组的中位数。

5.2 优化业务代码

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.length;
        int n = nums2.length;
        int left = 0, right = m;
        // median1:前一部分的最大值
        // median2:后一部分的最小值
        int median1 = 0, median2 = 0;

        while (left <= right) {
            // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
            int i = (left + right) / 2;
            int j = (m + n + 1) / 2 - i;

            // nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
            int nums_im1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
            int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
            int nums_jm1 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
            int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);

            if (nums_im1 <= nums_j) {
                median1 = Math.max(nums_im1, nums_jm1);
                median2 = Math.min(nums_i, nums_j);
                left = i + 1;
            } else {
                right = i - 1;
            }
        }

        return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
    }
}

5.3 优化结果

结果

6.总结

  • 总体下来优化了0.3MB,还是可以的。这道题的优化方案是真的难,我都是有所借鉴才想通的。大家如果有更好的方案的话欢迎在下方留言哟!
  • 要吐了
  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 鲸 设计师:meimeiellie 返回首页

打赏

地球村公民

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值