寻找两个正序数组的中位数(292)

今天为大家分享一道FLAG和BAT都出现过的经典面试题。题目有一定难度,建议大家耐着性子看完!不要说没天赋看不懂。在这个浮躁到努力的人都很少的年代,还谈不上说天赋这件事。

01、题目示例

这道题是非常好的一道题目,也是前面100道里最难的题目之一,相当经典!


第4题:寻找两个正序数组的中位数
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m n))。你可以假设 nums1 和 nums2 不会同时为空。

示例 1:

  1. nums1 = [1, 3]
  2. nums2 = [2]
  3. 则中位数是 2.0

示例 2:

  1. nums1 = [1, 2]
  2. nums2 = [3, 4]
  3. 则中位数是 (2 3)/2 = 2.5

02、题目分析

中位数(Median)又称中值,统计学中的专有名词,是按顺序排列的一组数据中居于中间位置的数,代表一个样本、种群或概率分布中的一个数值,其可将数值集合划分为相等的上下两部分。对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果观察值有偶数个,通常取最中间的两个数值的平均数作为中位数。


题意还是比较简单的,没有什么需要额外补充。当然可以直接暴力求解,因为都是有序数组,直接进行合并,再对合并后的数组通过判断是否是偶数个,求出中位数。但是由于这种方法肯定是达不到O(log(n))的,所以我们直接略过。如果是为了练习code能力的小伙伴,下去可以自行试试。


那如何保证时间复杂度在O(log(n))呢?这里有一个小技巧,一般如果题目要求时间复杂度在O(log(n)),大部分都是可以使用二分的思想来进行求解。当然,本题的二分是有一点反直觉的,可能不是很容易想到,一起看下。


首先,我们考虑一个问题,如果只有一个有序数组,我们需要找中位数,那肯定需要判断元素是奇数个还是偶数个,如果是奇数个那最中间的就是中位数,如果是偶数个的话,那就是最中间两个数的和除以2。

PNG

那如果是两个数组,也是一样的,我们先求出两个数组长度之和。如果为奇数,就找中间的那个数,也就是 (长度之和 1)/2 。如果为偶数,那就找 长度之和/2。比如下面的 (9 5)/2 = 7,那我们最终就是找到排列第7位的值。此时,问题其实已经转化为“找到两个数组中第k小的元素”。找到了第7位之后,第8位我们已经知道了,然后第7位和第8位的和,除以2就是我们要找的中位数(注意:这里的7和8你其实是不知道的,图中画出来,只是为了帮助理解

PNG

现在的问题是,我们如何用二分的思想来找到中间排列第7位的数。这里有一种不太好想到的方式,是用删的方式,因为如果我们可以把多余的数排除掉,最终剩下的那个数,是不是就是我们要找的数? 对于上面的数组,我们可以先删掉 7/2=3 个数。那这里,可以选择删上面的,也可以选择删下面的。那这里因为 i<j,所以我们选择删除上面的3个数。

PNG

(删除前)

PNG

(删除后)

由于我们已经排除掉了 3 个数字,现在对于两个数组,我们需要找到7-3=4的数字,来进行下一步运算。我们可以继续删掉4/2=2个数。我们比较i和j的值,删除小的一边。

PNG

(删除前)

PNG

(删除后)

继续上面的步骤,我们删除 2/2=1 个数。同理,比较7和6的大小,删除小的一边。删完后是下面这样:

PNG

(7和6,删除6)

不要忘记我们的目的,我们是为了找第7小的数。此时,两个数组的第一个元素,哪个小,就是我们要找的那个数。因为7<8,所以7就是我们要找的第7小的数。

PNG

这里有一点比较特殊的,如果在删除过程中,我们要删除的K/2个数,大于其中一边的数组长度,那我们就将小的一侧数组元素都删除。比如下面这个,此时7/2=3,但是下面的数组只有2个元素,我们就将它全部删除。

PNG

删完之后,此时因为只删除了2个元素,所以k变成了5。那我们只需要返回其中一边的第5个元素就ok。

PNG

整个上面的过程,完成了本题的算法架构!


03、证明过程

今天的题目有一定难度,但是并不是没办法攻克!最重要的还是三点:

  • 从整体把握整个问题的算法基础,想明白是如何从找中位数,转化成找“两个有序数组第k大的元素”
  • 能理解折半“删除”元素的过程,将问题转化为二分
  • 独立的思考 一点点的努力


根据分析,完成代码:

  1. //JAVA
  2. class Solution {
  3. public double findMedianSortedArrays(int[] nums1, int[] nums2) {
  4. int len1 = nums1.length;
  5. int len2 = nums2.length;
  6. int total = len1 + len2;
  7. int left = (total + 1) / 2;
  8. int right = (total + 2) / 2;
  9. return (findK(nums1, 0, nums2, 0, left) + findK(nums1, 0, nums2, 0, right)) / 2.0;
  10. }
  11. //找到两个数组中第k小的元素
  12. public int findK(int[] nums1, int i, int[] nums2, int j, int k) {
  13. if (i >= nums1.length)
  14. return nums2[j + k - 1];
  15. if (j >= nums2.length)
  16. return nums1[i + k - 1];
  17. if (k == 1) {
  18. return Math.min(nums1[i], nums2[j]);
  19. }
  20. //计算出每次要比较的两个数的值,来决定 "删除"" 哪边的元素
  21. int mid1 = (i + k / 2 - 1) < nums1.length ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
  22. int mid2 = (j + k / 2 - 1) < nums2.length ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
  23. //通过递归的方式,来模拟删除掉前K/2个元素
  24. if (mid1 < mid2) {
  25. return findK(nums1, i + k / 2, nums2, j, k - k / 2);
  26. }
  27. return findK(nums1, i, nums2, j + k / 2, k - k / 2);
  28. }
  29. }

执行结果:

PNG