zoukankan      html  css  js  c++  java
  • [LeetCode] 4. Median of Two Sorted Arrays ☆☆☆☆☆

    There are two sorted arrays nums1 and nums2 of size m and n respectively.

    Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

    Example 1:

    nums1 = [1, 3]
    nums2 = [2]
    
    The median is 2.0

    Example 2:

    nums1 = [1, 2]
    nums2 = [3, 4]
    
    The median is (2 + 3)/2 = 2.5

    解法1: 将问题转化为经典的“求两个有序数组中的第k小值”问题,即:

      首先假设有序数组A和B的长度都大于k/2(下取整),比较A[k/2-1]和B[k/2-1]两个元素,这两个元素分别表示A和B中的第k/2小的元素。这两个元素比较共有三种情况:>、<和=。

      (1)如果A[k/2-1]<B[k/2-1],说明A[k/2-1]及其之前的元素肯定都在A和B所有元素的前(k-1)小元素中,也就是说,A[k/2-1]不可能大于两个数组所有元素的第k小值。因此,将A的前(k/2)个元素删去之后(设剩余部分为A'),进一步求A'和B中的第(k-k/2)大的数。

      (2)如果A[k/2-1]<B[k/2-1],同理。

      (3)如果A[k/2-1]=B[k/2-1],如果k为偶数,则A[k/2-1]和B[k/2-1]即为第k小数,因为两个数组中分别有(k/2-1)个元素小于该值。但考虑到k可能不为偶数,可将其中一个的前(k/2)个元素删去之后,求剩余部分的第(k-k/2)大的数。

      需要注意的是,如果A的长度m<k/2,应取A[m-1]与B[k/2-1](或B[k-m-1])比较;B的长度小于k/2时同理。

      通过上面的分析,我们即可以采用递归的方式实现寻找第k小的数。此外我们还需要考虑几个边界条件: 

      • 如果A或者B为空,则直接返回B[k-1]或者A[k-1];
      • 如果k为1,我们只需要返回A[0]和B[0]中的较小值。
    public class Solution {
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int len1 = nums1.length;
            int len2 = nums2.length;
            int left = (len1 + len2 + 1) / 2;
            int right = (len1 + len2 + 2) / 2;
            return (findKthSmallest(nums1, nums2, left) + findKthSmallest(nums1, nums2, right)) / 2.0;
        }
        
        // 将问题转化为求两个数组的第k小数
        public int findKthSmallest(int[] nums1, int[] nums2, int k) {
            int len1 = nums1.length;
            int len2 = nums2.length;
            int half = k / 2;
            
            // 如果有其中一个长度为零,直接返回另一个的第k小数
            if (len1 == 0)  return nums2[k - 1];
            if (len2 == 0)  return nums1[k - 1];
            // 如果k为1,则直接返回两个数组的最小数
            if (k == 1)  return Math.min(nums1[0], nums2[0]);
            
            // 判断half是否超过了数组长度
            int cutPoint1 = Math.min(len1, half);
            int cutPoint2 = Math.min(len2, half);
            
            // 判断两个数组切割点处的值大小,将小的数组从切割点处截去
            if (nums1[cutPoint1 - 1] < nums2[cutPoint2 - 1]) {
                return findKthSmallest(Arrays.copyOfRange(nums1, cutPoint1, len1), nums2, k - cutPoint1);
            } else {
                return findKthSmallest(nums1, Arrays.copyOfRange(nums2, cutPoint2, len2), k - cutPoint2);
            }
        }
    }

    解法2:

    -------------------- 准备工作 --------------------

      对于长为N的数组A来说,用L和R分别表示中位数切割点的值(N为奇数)或者左右两侧的值(N为偶数),则L=(N-1)/2, R=N/2, 所以中位数可以表示为: (L+R) / 2 = (A[(N-1)/2] + A[N/2]) / 2。

      在数组的每两个数字之间添加“虚拟位置”(用#表示),同时把数字也当成“位置”,如:

    [6 9 13 18]  ->   [# 6 # 9 # 13 # 18 #]    (N = 4)
    position index     0 1 2 3 4 5  6 7  8     (N_Position = 9)
    		  
    [6 9 11 13 18]->   [# 6 # 9 # 11 # 13 # 18 #]   (N = 5)
    position index      0 1 2 3 4 5  6 7  8 9 10    (N_Position = 11)

      可以看出,对于长度为N的数组,总共有(2*N+1)个位置,无论N为奇数还是偶数,而中位数切割点的位置也总是第N个(下标从0开始)。

    -------------------- 算法原理 --------------------

      设有序数组A1和A2,A1的长度>A2的长度:

    A1: [# 1 # 2 # 3 # 4 # 5 #]    (N1 = 5, N1_positions = 11)
    pos: 0 1 2 3 4 5 6 7 8 9 10 A2: [# 1 # 1 # 1 # 1 #] (N2 = 4, N2_positions = 9)
    pos: 0 1 2 3 4 5 6 7 8

      与一个数组的中位数问题一样,我们需要对两个数组进行切割,使得左侧的所有数字 < 右侧的所有数字。

      可以注意到:

    1. 总共有(2N1 + 2N2 + 2)个位置。因此切割点的左右两侧应该分别有(N1 + N2)个位置,切割点本身占了两个位置。
    2. 因此,假设A2的切割点 C2 = k,那么A1的切割点位置必为 C1 = N1 + N2 -k。例如:如果 C2 = 2,则 C1 = 4 + 5 - C2 = 7。
       [# 1 # 2 # 3 # (4/4) # 5 #]    
      
       [# 1 / 1 # 1 # 1 #]  
    3. 切割之后,A1切割为L1+R1,A2切割为L2+R2,即
       L1 = A1[(C1-1)/2]; R1 = A1[C1/2];
       L2 = A2[(C2-1)/2]; R2 = A2[C2/2];

      在上述例子中,

          L1 = A1[(7-1)/2] = A1[3] = 4; R1 = A1[7/2] = A1[3] = 4;
          L2 = A2[(2-1)/2] = A2[0] = 1; R2 = A1[2/2] = A1[1] = 1;

      现在如何确定切割点是不是我们想要的?由于L1和L2是左侧的两个最大值,而R1和R2是右侧的两个最小值,我们只需满足:

    L1 <= R1  &&  L1 <= R2  &&  L2 <= R1  &&  L2 <= R2

    从而保证左侧的数字 <= 右侧的数字。由于A1和A2是有序的,即满足L1 <= R1 和 L2 <= R2,因此只需满足

    L1 <= R2 和 L2 <= R1即可。

      现在我们可以应用二分法进行查找:

    • 如果 L1 > R2,说明A1左侧有较多大数,因此必须将C1左移(C2右移);
    • 如果 L2 > R1,说明A2左侧有较多大数,因此必须将C2左移(C1右移);
    • 除了以上两种情况,得到的切割点即为正确的,此时可以计算出中位数为 (max(L1, L2) + min(R1, R2)) / 2。

      需要注意的是:

    • 由于C1和C2可以由彼此推出,一般选择较短的A2采用二分法确定C2的位置之后,再计算C1的位置,因此时间复杂度为O(log(min(N1, N2)))。
    • 边界情况:边界情况出现在当切割点位于 0th 或者 2Nth 的时候。例如,当 C2 = 2N2 时,R2 = A2[2 * N2 / 2] = A2[N2] 将超出数组下标范围。为解决此问题,假设在数组两侧有两个额外的数,即 A[-1] = Integer.MIN_VALUE, A[N] = Integer.MAX_VALUE。
    public class Solution {
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int m = nums1.length;
            int 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;
            int right = 2 * n;
            while (left <= right) {
                int mid2 = (left + right) / 2;
                int mid1 = m + n - mid2;
                int L1 = mid1 == 0 ? Integer.MIN_VALUE : nums1[(mid1 - 1) / 2];
                int R1 = mid1 == m * 2 ? Integer.MAX_VALUE : nums1[mid1 / 2];
                int L2 = mid2 == 0 ? Integer.MIN_VALUE : nums2[(mid2 - 1) / 2];
                int R2 = mid2 == n * 2 ? Integer.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.0;
            }
            return -1;
        }
    }

    ps: 如果没有时间复杂度为 O(og(m+n)) 的限制,也可以定义两个指针,分别从两个数组的头部开始,比较指向元素的大小,较小的指针往后移,然后再次比较。。。。若 (m+n) 为奇数,则移动 (m+n-1)/2 次后,指针指向的数为中位数;若(m+n) 为偶数,则移动 (m+n)/2-1 和 (m+n)/2 次后,指针分别指向的两个数的平均值为中位数。 

    参考资料:

    https://discuss.leetcode.com/topic/16797/very-concise-o-log-min-m-n-iterative-solution-with-detailed-explanation

  • 相关阅读:
    基数排序
    阅读笔记
    构建之法阅读笔记
    找水王续
    单元测试
    个人总结
    表单整数的校验问题
    HTML 表格的书写方式:
    rgba兼容IE系列
    "position:relative"在IE中的Bug
  • 原文地址:https://www.cnblogs.com/strugglion/p/6387677.html
Copyright © 2011-2022 走看看