题目

leetcode题目链接
设给出的两个已从小到大排序的数组为nums1和nums2,size1=nums1.size(),size2=nums2.size()。
分析题目可以知道,要找到中位数,最好能从2个已排序数组中找到第k小的数字,后者是更具一般性的问题。

题解1

我们当然可以先merge这两个已经排序的数组,然后直接返回合并后数组的第k个数字。时间复杂度为O(size1+size2)。但是这个时间复杂度超出了题目限制的O(log (size1+size2))。

题解2

合并两个数组太耗时了,我们不能这样做。
我们的思路是,不断删掉数组中肯定不是第k小的那些数字,从而能够不断地减小数组,在这个过程中,我们要找的那个数字的序号(k)也会不断地减小。
数组中的哪些数字可以删除呢?
让我们假设k是4:
nums1: [a1, a2, a3, ...]
nums2: [b1, b2, b3, ...]
如果a2<b2,那么a2肯定可以删除。因为有可能比a2小的数字只有

  1. a1。它肯定比a2小,因为数组已排序。
  2. b1。它有可能比a2小。

因此,a2最多只能是第3小的数字,肯定比我们要找的第4数字要小!从而a2,以及比a2还小的a1,都可以删除。

删除这两个数字以后,问题变成了:
nums1: [a3, ...]
nums2: [b1, b2, b3, ...]
从以上两个已排序数组中找出第2小的数字。(k已经变了,因为我们已经删除了两个比我们要找的那个数字还小的数字。)

同理,我们可以删除a3和b1中较小的那个数字,然后问题变成从剩余数字中找到第1小的数字。这个问题不就简单了吗?

推广以上方法,我们可以写出一个get_kth_smallest函数:

class Solution {
public:
    // findMedianSortedArrays只是简单地调用get_kth_smallest。后者才是算法的核心。
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int total_size = nums1.size()+nums2.size();
        if (0 == total_size) throw std::invalid_argument( "received empty arrays" );
        if (total_size%2 == 0) {
            return (get_kth_smallest(nums1, nums2, total_size/2)+get_kth_smallest(nums1, nums2, total_size/2+1))/2.0;
        } else {
            return get_kth_smallest(nums1, nums2, total_size/2+1);
        }
    }
    int get_kth_smallest(vector<int> nums1, vector<int> nums2, int k) {
        // 保证nums1.size() <= nums2.size(),方便分析
        if (nums1.size() > nums2.size()) return get_kth_smallest(nums2, nums1, k);
        if (nums1.empty()) return nums2[k-1];
        if (1 == k) return nums1[0] < nums2[0]? nums1[0]:nums2[0];
        
        // 在nums1和nums2中分别取第k/2个数字。当然,如果k/2已经超出数组边界,我们只取数组最后的那个数字。
        int k1 = min(static_cast<int>(nums1.size()), k/2);
        int k2 = min(static_cast<int>(nums2.size()), k/2);
        
        if (nums1[k1-1] < nums2[k2-1]) {
            // 如果nums1[k1-1] < nums2[k2-1],说明nums1[k1-1]小于我们要找的第k小的数字。
            nums1.erase(nums1.begin(), nums1.begin()+k1);
            return get_kth_smallest(nums1, nums2, k-k1);
        } else {
            // 如果nums1[k1-1] > nums2[k2-1],说明nums2[k2-1]小于我们要找的第k小的数字。
            // 如果nums1[k1-1] == nums2[k2-1],说明nums2[k2-1]小于或等于我们要找的第k小的数字。即使nums2[k2-1]等于我们要找的第k小的数字,我们依然可以删掉它,因为还有nums1[k1-1]和它相等呢!
            nums2.erase(nums2.begin(), nums2.begin()+k2);
            return get_kth_smallest(nums1, nums2, k-k2);
        }
    }
};

时间复杂度计算

注意到get_kth_smallest中没有循环语句,只有递归,因此时间复杂度完全取决于get_kth_smallest的调用次数。

  • 最坏的情况:两个数组都很大,以至于每次在nums1和nums2中取第k/2个数字的时候,k/2都没有超出数组边界。因此每次调用get_kth_smallest都会使k减小一半。k减小为1时得到结果。从而时间复杂度为O(log_2(k))。因为k<size1+size2,所以时间复杂度符合题目要求。
  • 较好的情况:其中有一个数组比较小,以至于在某次取第k/2个数字的时候,k/2超出了数组边界,此时,如果这个数组中的最后一个数字 < 另一个数组中取出的第k/2个数字,那么这个数组的所有数字都可以删掉。那么剩下的问题就成为“从一个已排序数组中取出第k'小的数字”,可以直接得到结果,提前结束递归。

csRyan
1.1k 声望198 粉丝

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.