zoukankan      html  css  js  c++  java
  • 寻找两个有序数组的中位数 C++实现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

    这道题让我们求两个有序数组的中位数,而且限制了时间复杂度为 O(log (m+n)),看到这个时间复杂度,自然而然的想到了应该使用二分查找法来求解。但是这道题被定义为 Hard 也是有其原因的,难就难在要在两个未合并的有序数组之间使用二分法,如果这道题只有一个有序数组,让我们求中位数的话,估计就是个 Easy 题。对于这道题来说,我们可以将两个有序数组混合起来成为一个有序数组再做吗,图样图森破,这个时间复杂度限制的就是告诉你金坷垃别想啦。那么我们还是要用二分法,而且是在两个数组之间使用,感觉很高端啊。那么回顾一下中位数的定义,如果某个有序数组长度是奇数,那么其中位数就是最中间那个,如果是偶数,那么就是最中间两个数字的平均值。这里对于两个有序数组也是一样的,假设两个有序数组的长度分别为m和n,由于两个数组长度之和 m+n 的奇偶不确定,因此需要分情况来讨论,对于奇数的情况,直接找到最中间的数即可,偶数的话需要求最中间两个数的平均值。为了简化代码,不分情况讨论,我们使用一个小 trick,分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个,然后求其平均值即可,这对奇偶数均适用。若 m+n 为奇数的话,那么其实 (m+n+1) / 2 和 (m+n+2) / 2 的值相等,相当于两个相同的数字相加再除以2,还是其本身。

    好,这里我们需要定义一个函数来在两个有序数组中找到第K个元素,下面重点来看如何实现找到第K个元素。首先,为了避免拷贝产生新的数组从而增加时间复杂度,我们使用两个变量i和j分别来标记数组 nums1 和 nums2 的起始位置。然后来处理一些 corner cases,比如当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了。还有就是如果 K=1 的话,那么我们只要比较 nums1 和 nums2 的起始位置i和j上的数字就可以了。难点就在于一般的情况怎么处理?因为我们需要在两个有序数组中找到第K个元素,为了加快搜索的速度,我们要使用二分法,那么对谁二分呢,数组么?其实要对K二分,意思是我们需要分别在 nums1 和 nums2 中查找第 K/2 个元素,注意这里由于两个数组的长度不定,所以有可能某个数组没有第 K/2 个数字,所以我们需要先 check 一下,数组中到底存不存在第 K/2 个数字,如果存在就取出来,否则就赋值上一个整型最大值。如果某个数组没有第 K/2 个数字,那么我们就淘汰另一个数组的前 K/2 个数字即可。举个例子来说吧,比如 nums1 = {3},nums2 = {2, 4, 5, 6, 7},K=4,我们要找两个数组混合中第4个数字,那么我们分别在 nums1 和 nums2 中找第2个数字,我们发现 nums1 中只有一个数字,不存在第二个数字,那么 nums2 中的前2个数字可以直接跳过,为啥呢,因为我们要求整个混合数组的第4个数字,不管 nums1 中的那个数字是大是小,第4个数字绝不会出现在 nums2 的前两个数字中,所以可以直接跳过。

    有没有可能两个数组都不存在第 K/2 个数字呢,这道题里是不可能的,因为我们的K不是任意给的,而是给的 m+n 的中间值,所以必定至少会有一个数组是存在第 K/2 个数字的。最后就是二分法的核心啦,比较这两个数组的第 K/2 小的数字 midVal1 和 midVal2 的大小,如果第一个数组的第 K/2 个数字小的话,那么说明我们要找的数字肯定不在 nums1 中的前 K/2 个数字,所以我们可以将其淘汰,将 nums1 的起始位置向后移动 K/2 个,并且此时的K也自减去 K/2,调用递归,举个例子来说吧,比如 nums1 = {1, 3},nums2 = {2, 4, 5},K=4,我们要找两个数组混合中第4个数字,那么我们分别在 nums1 和 nums2 中找第2个数字,nums1 中的第2个数字是3,nums2 中的第2个数字是4,由于3小于4,所以混合数组中第4个数字肯定在 nums2 中,所以我们可以将 nums1 的起始位置向后移动 K/2 个。反之,我们淘汰 nums2 中的前 K/2 个数字,并将 nums2 的起始位置向后移动 K/2 个,并且此时的K也自减去 K/2,调用递归即可,参见代码如下:

     

    C++ 解法一:

     
    class Solution {
    public:
        double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
            int m = nums1.size(), n = nums2.size(), left = (m + n + 1) / 2, right = (m + n + 2) / 2;
            return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
        }
        int findKth(vector<int>& nums1, int i, vector<int>& nums2, int j, int k) {
            if (i >= nums1.size()) return nums2[j + k - 1];
            if (j >= nums2.size()) return nums1[i + k - 1];
            if (k == 1) return min(nums1[i], nums2[j]);
            int midVal1 = (i + k / 2 - 1 < nums1.size()) ? nums1[i + k / 2 - 1] : INT_MAX;
            int midVal2 = (j + k / 2 - 1 < nums2.size()) ? nums2[j + k / 2 - 1] : INT_MAX;
            if (midVal1 < midVal2) {
                return findKth(nums1, i + k / 2, nums2, j, k - k / 2);
            } else {
                return findKth(nums1, i, nums2, j + k / 2, k - k / 2);
            }
        }
    };
     

    Java 解法一:

     
    public class Solution {
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int m = nums1.length, n = nums2.length, left = (m + n + 1) / 2, right = (m + n + 2) / 2;
            return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
        }
        int findKth(int[] nums1, int i, int[] nums2, int j, int k) {
            if (i >= nums1.length) return nums2[j + k - 1];
            if (j >= nums2.length) return nums1[i + k - 1];
            if (k == 1) return Math.min(nums1[i], nums2[j]);
            int midVal1 = (i + k / 2 - 1 < nums1.length) ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
            int midVal2 = (j + k / 2 - 1 < nums2.length) ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
            if (midVal1 < midVal2) {
                return findKth(nums1, i + k / 2, nums2, j, k - k / 2);
            } else {
                return findKth(nums1, i, nums2, j + k / 2, k - k / 2);
            }
        }
    }
     

    上面的解法一直使用的是原数组,同时用了两个变量来分别标记当前的起始位置。我们也可以直接生成新的数组,这样就不要用起始位置变量了,不过拷贝数组的操作可能会增加时间复杂度,也许会超出限制,不过就算当个思路拓展也是极好的。首先我们要判断数组是否为空,为空的话,直接在另一个数组找第K个即可。还有一种情况是当 K = 1 时,表示我们要找第一个元素,只要比较两个数组的第一个元素,返回较小的那个即可。这里我们分别取出两个数组的第 K/2 个数字的位置坐标i和j,为了避免数组没有第 K/2 个数组的情况,我们每次都和数组长度做比较,取出较小值。这里跟上面的解法有些许不同,上面解法我们直接取出的是值,而这里我们取出的是位置坐标,但是思想都是很类似的。不同在于,上面解法中我们每次固定淘汰 K/2 个数字,而这里我们由于取出了合法的i和j,所以我们每次淘汰i或j个。评论区有网友提出,可以让 j = k-i,这样也是对的,可能还更好一些,收敛速度可能会更快一些,参见代码如下:

    C++ 解法二:

     
    class Solution {
    public:
        double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
            int m = nums1.size(), n = nums2.size();
            return (findKth(nums1, nums2, (m + n + 1) / 2) + findKth(nums1, nums2, (m + n + 2) / 2)) / 2.0;
        }
        int findKth(vector<int> nums1, vector<int> nums2, int k) {
            if (nums1.empty()) return nums2[k - 1];
            if (nums2.empty()) return nums1[k - 1];
            if (k == 1) return min(nums1[0], nums2[0]);
            int i = min((int)nums1.size(), k / 2), j = min((int)nums2.size(), k / 2);
            if (nums1[i - 1] > nums2[j - 1]) {
                return findKth(nums1, vector<int>(nums2.begin() + j, nums2.end()), k - j);
            } else {
                return findKth(vector<int>(nums1.begin() + i, nums1.end()), nums2, k - i);
            }
            return 0;
        }
    };
     

    Java 解法二:

     
    public class Solution {
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int m = nums1.length, n = nums2.length, left = (m + n + 1) / 2, right = (m + n + 2) / 2;
            return (findKth(nums1, nums2, left) + findKth(nums1, nums2, right)) / 2.0;
        }
        int findKth(int[] nums1, int[] nums2, int k) {
            int m = nums1.length, n = nums2.length;
            if (m == 0) return nums2[k - 1];
            if (n == 0) return nums1[k - 1];
            if (k == 1) return Math.min(nums1[0], nums2[0]);
            int i = Math.min(m, k / 2), j = Math.min(n, k / 2);
            if (nums1[i - 1] > nums2[j - 1]) {
                return findKth(nums1, Arrays.copyOfRange(nums2, j, n), k - j);
            } else {
                return findKth(Arrays.copyOfRange(nums1, i, m), nums2, k - i);
            }
        }
    }
     

    此题还能用迭代形式的二分搜索法来解,是一种相当巧妙的应用,讲解在这个帖子中写的十分清楚,等有时间我再来写写分析过程:

    C++ 解法三:

     
    class Solution {
    public:
        double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
            int m = nums1.size(), n = nums2.size();
            if (m < n) return findMedianSortedArrays(nums2, nums1);
            if (n == 0) return ((double)nums1[(m - 1) / 2] + (double)nums1[m / 2]) / 2.0;
            int left = 0, right = n * 2;
            while (left <= right) {
                int mid2 = (left + right) / 2;
                int mid1 = m + n - mid2;
                double L1 = mid1 == 0 ? INT_MIN : nums1[(mid1 - 1) / 2];
                double L2 = mid2 == 0 ? INT_MIN : nums2[(mid2 - 1) / 2];
                double R1 = mid1 == m * 2 ? INT_MAX : nums1[mid1 / 2];
                double R2 = mid2 == n * 2 ? INT_MAX : nums2[mid2 / 2];
                if (L1 > R2) left = mid2 + 1;
                else if (L2 > R1) right = mid2 - 1;
                else return (max(L1, L2) + min(R1, R2)) / 2;
            }
            return -1;
        }
    };
     

    Java 解法三:

     
    public class Solution {
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int m = nums1.length, n = nums2.length;
            if (m < n) return findMedianSortedArrays(nums2, nums1);
            if (n == 0) return (nums1[(m - 1) / 2] + nums1[m / 2]) / 2.0;
            int left = 0, right = 2 * n;
            while (left <= right) {
                int mid2 = (left + right) / 2;
                int mid1 = m + n - mid2;
                double L1 = mid1 == 0 ? Double.MIN_VALUE : nums1[(mid1 - 1) / 2];
                double L2 = mid2 == 0 ? Double.MIN_VALUE : nums2[(mid2 - 1) / 2];
                double R1 = mid1 == m * 2 ? Double.MAX_VALUE : nums1[mid1 / 2];
                double R2 = mid2 == n * 2 ? Double.MAX_VALUE : nums2[mid2 / 2];
                if (L1 > R2) left = mid2 + 1;
                else if (L2 > R1) right = mid2 - 1;
                else return (Math.max(L1, L2) + Math.min(R1, R2)) / 2;
            }
            return -1;
        }
    }

    参考资料:

  • 相关阅读:
    设计模式之工厂模式-抽象工厂(02)
    1036 跟奥巴马一起编程 (15 分)
    1034 有理数四则运算 (20 分)
    1033 旧键盘打字 (20 分)
    1031 查验身份证 (15 分)
    大学排名定向爬虫
    1030 完美数列 (25 分)二分
    1029 旧键盘 (20 分)
    1028 人口普查 (20 分)
    1026 程序运行时间 (15 分)四舍五入
  • 原文地址:https://www.cnblogs.com/suibian1/p/10952886.html
Copyright © 2011-2022 走看看