zoukankan      html  css  js  c++  java
  • 寻找两个有序数组的中位数

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

    示例 1:

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

    示例 2:

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

    方法一:子集划分

    为了解决这个问题,我们需要理解 “中位数的作用是什么”。在统计中,中位数被用来:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。
    如果理解了中位数的划分作用,我们就很接近答案了。

    首先,让我们在任一位置 i 将 A划分成两个部分:

          left_A             |        right_A
    A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
    

    由于 A中有 m个元素, 所以我们有 m+1种划分的方法(i=0∼m)。

    我们知道:
    len(left_A)=i,len(right_A)=m−i
    注意:当 i=0 时,left_A为空集, 而当 i=m时, right_A为空集。

    采用同样的方式,我们在任一位置 j将 B划分成两个部分:

          left_B             |        right_B
    B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]
    

    将 left_A和 left_B放入一个集合,并将 right_A和 right_B放入另一个集合。 再把这两个新的集合分别命名为 left_part和 right_part:

          left_part          |        right_part
    A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
    B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]
    

    如果我们可以确认:

        len(left_part)=len(right_part)
        max⁡(left_part)≤min⁡(right_part)
    

    那么,我们已经将 {A,B}中的所有元素划分为相同长度的两个部分,且其中一部分中的元素总是大于另一部分中的元素。那么:median=(max(left_part)+min(right_part)​)/2

    要确保这两个条件,我们只需要保证:

        i+j=m−i+n−j(或:m−i+n−j+1,即m+n为奇数时,把多的一个放left_part)
        如果 n≥m只需要使  i=0∼m, j=(m+n+1)/2−i(因为j是整型,所以m+n为奇数或偶数时,j都是(m+n+1)/2,如m+n=3,j=2;m+n=4,j=2)
        B[j−1]≤A[i] 以及 A[i−1]≤B[j]
    

    ps.1 为了简化分析,假设 A[i−1],B[j−1],A[i],B[j]总是存在,哪怕出现 i=0,i=m,j=0或是 j=n这样的临界条件。我们将在最后讨论如何处理这些临界值。

    ps.2 为什么 n≥m?由于0≤i≤m且 j=(m+n+1)/2−i,我们必须确保 j不是负数。如果 n<m,那么 j 将可能是负数,而这会造成错误的答案。

    所以,我们需要做的是:

    在 [0,m]中搜索并找到目标对象 i,以使:
    
    B[j−1]≤A[i]且 A[i−1]≤B[j], 其中 j=(m+n+1)/2−i
    

    接着,我们可以按照以下步骤来进行二叉树搜索:

    设 imin=0,imax=m, 然后开始在 [imin,imax]中进行搜索。
    
    令 i=(imin+imax2)/2​, j=(m+n+1)/2−i
    
    现在我们有 len(left_part)=len(right_part)。 而且我们只会遇到三种情况:
    
        B[j−1]≤A[i]且 A[i−1]≤B[j]:
        这意味着我们找到了目标对象 i,所以可以停止搜索。
    
        B[j−1]>A[i]:
        这意味着 A[i]太小,我们必须调整 i 以使 B[j−1]≤A[i]。
        我们可以增大 i 吗?
              是的,因为当 i 被增大的时候,j 就会被减小。
              因此 B[j−1]会减小,而 A[i]会增大,那么 B[j−1]≤A[i]就可能被满足。
        我们可以减小 i 吗?
              不行,因为当 i 被减小的时候,j 就会被增大。
              因此 B[j−1]会增大,而 A[i] 会减小,那么 B[j−1]≤A[i]就可能不满足。
        所以我们必须增大 i。也就是说,我们必须将搜索范围调整为 [i+1,imax]。
    
        A[i−1]>B[j]:
        这意味着 A[i−1] 太大,我们必须减小 i以使 A[i−1]≤B[j]。
        也就是说,我们必须将搜索范围调整为 [imin,i−1]。
    

    当找到目标对象 i 时,中位数为:

    max⁡(A[i−1],B[j−1]), 当 m+n为奇数时
    
    max⁡(A[i−1],B[j−1])+min⁡(A[i],B[j])/2, 当 m+n为偶数时
    

    现在,让我们来考虑这些临界值 i=0,i=m,j=0,j=n,此时 A[i−1],B[j−1],A[i],B[j]可能不存在。
    其实这种情况比你想象的要容易得多。

    我们需要做的是确保 max(left_part)≤min(right_part)。 因此,如果 i和 j 不是临界值(这意味着 A[i−1],B[j−1],A[i],B[j]全部存在), 那么我们必须同时检查 B[j−1]≤A[i]以及 A[i−1]≤B[j]是否成立。
    但是如果 A[i−1],B[j−1],A[i],B[j]中部分不存在,那么我们只需要检查这两个条件中的一个(或不需要检查)。
    举个例子,如果 i=0那么 A[i−1]不存在,我们就不需要检查 A[i−1]≤B[j]是否成立。
    所以,我们需要做的是:

    在 [0,m]中搜索并找到目标对象 i,以使:
    
    (j=0 or i=m or B[j−1]≤A[i])或是
    (i=0 or j=n or A[i−1]≤B[j]), 其中 j=(m+n+1)/2−i
    

    在循环搜索中,我们只会遇到三种情况:

        (j=0 or i=m or B[j−1]≤A[i])或是 (i=0 or j=n or A[i−1]≤B[j]),这意味着 i 是完美的,我们可以停止搜索。
        j>0 and i<m and B[j−1]>A[i] 这意味着 i 太小,我们必须增大它。
        i>0 and j<n and A[i−1]>B[j] 这意味着 i 太大,我们必须减小它。
    
    class Solution {
    public:
    	double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    		int m = nums1.size(), n = nums2.size();
    		if (m > n) { //确保n>=m
    			vector<int> temp = nums1;
    			nums1 = nums2, nums2 = temp;
    			m = nums1.size(), n = nums2.size();
    		}
    		int imin = 0, imax = m,half=(m+n+1)/2;
    		int i = (imin + imax) / 2, j = half - i;
    		while (imax>=imin)
    		{
    			i = (imin + imax) / 2, j = half - i;
    			if (j > 0 && i<m && nums2[j - 1]>nums1[i])
    				imin = i + 1;
    			else if (i > 0 && j<n && nums1[i - 1]>nums2[j])
    				imax = i - 1;
    			else
    			{
    				int maxleft, minright;
    				if (i == 0)maxleft = nums2[j - 1];
    				else if (j == 0)maxleft = nums1[i - 1];
    				else
    				{
    					maxleft = nums2[j - 1] > nums1[i - 1] ? nums2[j - 1] : nums1[i - 1];
    				}
    
    				if ((m + n) % 2)
    					return maxleft * 1.0;
    
    				if (i == m)minright = nums2[j];
    				else if (j == n)minright = nums1[i];
    				else 
    				{
    					minright = nums2[j] < nums1[i] ? nums2[j] : nums1[i];
    				}
    
    				return (maxleft + minright) * 1.0 / 2;
    			}
    		}
            return -1;
    			
    	}
    };
    

    方法二:第K小值

    题目是求中位数,其实就是求第 k 小数的一种特殊情况。
    由于数列是有序的,其实我们完全可以一半一半的排除。假设我们要找第 k 小数,我们可以每次循环排除掉 k/2 个数。看下边一个例子。
    假设我们要找第 7 小的数字。

    我们比较两个数组的第 k/2 个数字,如果 k 是奇数,向下取整。也就是比较第 3 个数字,上边数组中的 4 和下边数组中的 3,如果哪个小,就表明该数组的前 k/2 个数字都不是第 k 小数字,所以可以排除。也就是 1,2,3这三个数字不可能是第7 小的数字,我们可以把它排除掉。将 1,3,4,9和 4,5,6,7,8,9,10 两个数组作为新的数组进行比较。

    更一般的情况 A[1] ,A[2] ,A[3],A[k/2] ... ,B[1],B[2],B[3],B[k/2] ... ,如果 A[k/2]<B[k/2] ,那么A[1],A[2],A[3],A[k/2]都不可能是第 k 小的数字。

    橙色的部分表示已经去掉的数字。

    由于我们已经排除掉了 3 个数字,就是这 3 个数字一定在最前边,所以在两个新数组中,我们只需要找第 7 - 3 = 4 小的数字就可以了,也就是 k = 4。此时两个数组,比较第 2 个数字,3 < 5,所以我们可以把小的那个数组中的 1 ,3 排除掉了。

    我们又排除掉 2 个数字,所以现在找第 4 - 2 = 2 小的数字就可以了。此时比较两个数组中的第 k / 2 = 1 个数,4 == 4,怎么办呢?由于两个数相等,所以我们无论去掉哪个数组中的都行,因为去掉 1 个总会保留 1 个的,所以没有影响。为了统一,我们就假设 4 > 4 吧,所以此时将下边的 4 去掉。

    由于又去掉 1 个数字,此时我们要找第 1 小的数字,所以只需判断两个数组中第一个数字哪个小就可以了,也就是 4。

    所以第 7 小的数字是 4。

    我们每次都是取 k/2 的数进行比较,有时候可能会遇到数组长度小于 k/2的时候。


    此时 k / 2 等于 3,而上边的数组长度是 2,我们此时将箭头指向它的末尾就可以了。这样的话,由于 2 < 3,所以就会导致上边的数组 1,2 都被排除。造成下边的情况。


    由于 2 个元素被排除,所以此时 k = 5,又由于上边的数组已经空了,我们只需要返回下边的数组的第 5 个数字就可以了。

    从上边可以看到,无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k 的值都有可能从奇数变为偶数,最终都会变为 1 或者由于一个数组空了,直接返回结果。

    所以我们采用递归的思路,为了防止数组长度小于 k/2,所以每次比较 min(k/2,len(数组) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。递归出口就是当 k=1 或者其中一个数字长度是 0 了。

    class Solution {
    public:
    	int findK(vector<int>& A1, int s1, int e1, vector<int>& A2, int s2, int e2, int k) {
    		int len1 = e1 - s1 + 1, len2 = e2 - s2 + 1;
    		if (len1 == 0)return A2[s2 + k - 1]; //由于数组下标从0开始,所以要减1
    		if (len2 == 0)return A1[s1 + k - 1];
    		if (k == 1)return A1[s1] < A2[s2] ? A1[s1] : A2[s2];
    		int m = k / 2;
    		int i = len1 < m ? s1 + len1 - 1 : s1 + m - 1;
    		int j= len2 < m ? s2 + len2 - 1 : s2 + m - 1;
    		if (A1[i] < A2[j])return findK(A1, i+1, e1, A2, s2, e2, k - (i-s1+1));
    		else
    			return findK(A1, s1, e1, A2, j+1, e2, k -(j-s2+1));
    	}
    	double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    		int m = nums1.size(), n = nums2.size();
    		int k1 = (m + n + 1) / 2, k2 = (m + n + 2) / 2; //如m+n为3,则k1=2,k2=2;如m+n=4,则k1=2,k2=3。从而使得m+n为奇数和偶数得到的中位数公式统一,即(findk1+findk2)/2
    		return (findK(nums1, 0, m - 1, nums2, 0, n - 1, k1) + findK(nums1, 0, m - 1, nums2, 0, n - 1, k2)) * 0.5;
    	}
    };
    
  • 相关阅读:
    MySQL高级【一】索引和视图
    SpringMVC【八】文件上传和下载
    SpringMVC【七】拦截器
    SpringMVC【六】AJAX
    python
    SqlServer优化之批量插入(SqlBulkCopy、表值参数)
    SQL中merge into用法
    Asp.Net MVC Filter权限过滤使用说明 FilterConfig类
    .NET-ORM框架EF-Code First代码优先
    asp.net mvc框架之EF的使用
  • 原文地址:https://www.cnblogs.com/HunterJ/p/11660610.html
Copyright © 2011-2022 走看看