【算法面试题】寻找两个正序数组的中位数


今天是一道面试中比较常见的算法题,来自leetcode,难度为hard,Acceptance为38.2%

题目如下


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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/median-of-two-sorted-arrays

回复0000查看更多题目

解题思路


思路一

初看这道题,寻找中位数,在一个有序数组中寻找一个数第一个想到的当然是二分查找,可以做到O(logn)是时间复杂度。那个对于两个有序数组怎么处理呢?

第一个想到的就是将两个数组合并成一个有序数组,这样就可以直接取合并后数组的第len+1 / 2个值(长度是奇数)或者是第len+1 / 2len+1 / 2 - 1的平均值(长度是偶数);
而合并两个有序数组,显然是归并排序的一个步骤,时间复杂度是O(m+n)。思路有了,代码如下

代码如下


Java版

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int[] nums;
    int m = nums1.length;
    int n = nums2.length;
    nums = new int[m + n];
    if (m == 0) {
        if (n % 2 == 0) {
            return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0;
        } else {

            return nums2[n / 2];
        }
    }
    if (n == 0) {
        if (m % 2 == 0) {
            return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0;
        } else {
            return nums1[m / 2];
        }
    }

    int count = 0;
    int i = 0, j = 0;
    while (count != (m + n)) {
        if (i == m) {
            while (j != n) {
                nums[count++] = nums2[j++];
            }
            break;
        }
        if (j == n) {
            while (i != m) {
                nums[count++] = nums1[i++];
            }
            break;
        }

        if (nums1[i] < nums2[j]) {
            nums[count++] = nums1[i++];
        } else {
            nums[count++] = nums2[j++];
        }
    }

    if (count % 2 == 0) {
        return (nums[count / 2 - 1] + nums[count / 2]) / 2.0;
    } else {
        return nums[count / 2];
    }
}

因为涉及到了归并排序的一步,所以其时间复杂度是O(m+n)
空间复杂度:因为在合并数组时开辟了一个数组,保存合并后的两个数组 O(m+n)

思路二

在上面的步骤中,我们先合并两个数组,然后再取中间的值。其实仔细想一下的话就会发现,我们并不需要将两个数组真的合并,只需要知道合并后中位数在哪个数组的哪个位置就可以了。

因为在合并两个数组之前,已经知道了两个数组的长度m,n,也就知道了合并后的长度len=m+n

这样我们在合并两个数组的时候(不需要真的开辟一块存储空间保持合并的数组,只需要按归并的逻辑遍历两个数组),就可以在访问到len+1 / 2的元素时停止就可以了。这也就是我们要的值。

当然仍然需要注意奇偶数的区别,长度是奇数是第len/2个值,长度是偶数是第len+1 /2len+1 / 2 - 1的平均值。

代码如下


Java版

public double findMedianSortedArrays(int[] A, int[] B) {
    int m = A.length;
    int n = B.length;
    int len = m + n;
    int left = -1, right = -1;
    int aStart = 0, bStart = 0;
    for (int i = 0; i <= len / 2; i++) {
        left = right;
        if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) {
            right = A[aStart++];
        } else {
            right = B[bStart++];
        }
    }
    if ((len & 1) == 0)
        return (left + right) / 2.0;
    else
        return right;
}

时间复杂度分析,因为从头遍历到len+1 / 2len=m+n,所以时间复杂度依旧是 O(m+n)
空间复杂度上,因为我们申不需要真的开辟一块存储空间保持合并的数组,只需要存储aStart,bStart等几个记录位置的值。所以空间复杂度是 O(1)

思路三

上边的两种解题思路,时间复杂度都是O(m+n),都达不到题目的要求 O(log(m+n)。看到 时间复杂度是log,很明显,我们又回到一开始说的用二分查找的思路解决问题。题目是要求中位数,可以将其看成是求合并后第 k 小数的一种特殊情况,而求第 k 小数是可以做到时间复杂度O(log(m+n)的。

在上面的思路二中,我们一次遍历就相当于去掉一个不是中位数的值,而两个数组都是有序的,其实我们完全可以按照二分查找的思路,一次遍历去掉一半儿不是中位数的值。即,设我们要找的中位数是第 k 小数,我们可以每次循环排除掉 k/2 个数。
例:
假设我们有两个数组,长度分别是5, 10,具体数值如下:

A = [1, 2, 6, 8, 9]
B = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

则,我们要找的中位数是第(5 + 10 + 1) / 2=8的数,即k = 8。j假设我们先合并得到1,1,2,2,3,4,5,6,6,7,8,8,9,9,10,可以看到我们要找的是第8个数是6.

我们比较两个数组的第 k / 2个数字,也就是比较第4个数字,A数组中是6 和B数组中是4,如果哪个小,就表明该数组的前 k / 2个数字都在搜索空间内,可以直接排除。这样B数据的前3个值1,2,3, 4都不是第7小的数字,我们将其排除掉。我们要求的值在

A=[1,2,6,8,9]
B=[5,6,7,8,9,10]

中,将这两个数组作为新的数组进行比较。

由于我们要找的是最小的第k=8个数字,而此时已经排除掉了 3 个数字,所以在两个新数组中,我们要找的是第k = 8 - 3 = 5 个数字。此时重复上面的流程,比较第 k / 2 = 2个数字,A[2] = 2 < B[2] = 6,所以我们可以把A数组的 [1, 2] 排除掉。我们要求的值在

A=[6,8,9]
B=[5,6,7,8,9,10]

中,继续将这两个数组作为新的数组进行比较。

我们要找的是最小的第k=8个数字,而此时又排除掉了 2 个数字,所以在两个新数组中,我们要找的是第k = 8 - 3 - 2 = 3 个数字。此时重复上面的流程,比较第 k / 2 = 1个数字,A[1] = 6 > B[1] = 5,所以我们可以把B数组的 [5] 排除掉。我们要求的值在

A=[6,8,9]
B=[6,7,8,9,10]

中,继续将这两个数组作为新的数组进行比较。
我们要找的是最小的第k=8个数字,而此时又排除掉了 1 个数字,所以在两个新数组中,我们要找的是第k = 8 - 3 - 2 - 1 = 2 个数字。此时重复上面的流程,比较第 k / 2 = 1个数字,A[1] = 6 = B[1] = 6,此时我们随意选择一个渠道,这里我们可以把B数组的 [6] 排除掉。我们要求的值在

A=[6,8,9]
B=[7,8,9,10]

我们要找的是最小的第k=8个数字,而此时又排除掉了 1 个数字,所以在两个新数组中,我们要找的是第k = 8 - 3 - 2 - 1 - 1 = 1 个数字,此时只需比较数组A,B的第一个元素那个小就可以了,此时显然A[1] = 6就是我们要求的值。

代码如下


public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;
    //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
    return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;  
}
    
    private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
        int len1 = end1 - start1 + 1;
        int len2 = end2 - start2 + 1;
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 
        if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
        if (len1 == 0) return nums2[start2 + k - 1];

        if (k == 1) return Math.min(nums1[start1], nums2[start2]);

        int i = start1 + Math.min(len1, k / 2) - 1;
        int j = start2 + Math.min(len2, k / 2) - 1;

        if (nums1[i] > nums2[j]) {
            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        }
        else {
            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
        }
    }

总结
这道题重点考察对于二分查找的理解和使用,用二分查找的方法将时间复杂度降为 log 级别。当然还有编程细节需要注意,即奇偶数的问题,要做到一次bug free还需要多加练习,注意这些特殊情况和边界条件。

关注我

回复0000查看更多题目

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,540评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,640评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,643评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,672评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,525评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,387评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,809评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,450评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,743评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,787评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,560评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,406评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,824评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,048评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,335评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,784评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,997评论 2 337