1. 程式人生 > 實用技巧 >[LeetCode] 4. 尋找兩個正序陣列的中位數

[LeetCode] 4. 尋找兩個正序陣列的中位數

目錄




給定兩個大小為 m 和 n 的正序(從小到大)陣列 nums1 和 nums2。

請你找出這兩個正序陣列的中位數,並且要求演算法的時間複雜度為 O(log(m + n))。

你可以假設 nums1 和 nums2 不會同時為空。

nums1 = [1, 3]
nums2 = [2]
則中位數是 2.0

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

分析

  1. 如果對時間複雜度的要求有log,通常都要用到二分查詢,求第k小數,兩個陣列每次迴圈可排除其中一個數組的k/2個數,然後調整k
  2. 分別切分兩陣列,左部分長度和與右部分長度和為固定值,使用二分法確定切點

解法一

  • 求第k小數,兩個陣列每次迴圈可排除其中一個數組的k/2個數,然後調整k,若其中一個數組長度小於k/2,排除這個陣列再調整k
  • 時間複雜度O(log(m+n)),每迴圈一次減少k/2個元素,時間複雜度為O(log(k)),k = (m+n)/2
  • 空間複雜度O(1),遞迴為尾遞迴,不需要不停堆疊
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        int k1 = (m + n + 1) /2;
        int k2 = (m + n + 2) /2;
        // 將偶數和奇數情況合併, 若為奇數則求兩次相同的k
        return (getKth(nums1, 0, m -1, nums2, 0, n -1, k1)
                + getKth(nums1, 0, m -1, nums2, 0, n -1, k2)) /2;
    }

    private double 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
        if(len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
        // len1為空時, 返回len2的第k小數
        if(len1 == 0) return nums2[start2 + k -1];
        // k為1時, 返回兩陣列最小值
        if(k == 1) return Math.min(nums1[start1], nums2[start2]);

        // 當len1小於k/2時, 指標移至末位
        int i = start1 + Math.min(len1, k /2) - 1;
        int j = start2 + Math.min(len1, k /2) - 1;
        // 比較k/2-1處元素大小,移除元素
        if( nums1[i] < nums2[j]){
            return getKth(nums1, i + 1, end1, nums2, start2, end2,
                    k - (i - start1 + 1));
        }
        else {
            return getKth(nums1, start1, end1, nums2, j + 1, end2,
                    k - (j - start2 + 1));
        }
    }
}

//class Test{
//    public static void main(String[] args) {
//        int[] nums1 = {1,2};
//        int[] nums2 = {3,4};
//        Solution s = new Solution();
//        double res = s.findMedianSortedArrays(nums1, nums2);
//        System.out.println(res);
//    }
//}

解法二

  • 切分兩陣列, 左部分長度和與右部分長度和為固定值, 使用二分法確定切點

    長度和為偶數

    • 左半部分長度和 = 右半部分長度和

      i + j = m - i + n - j, 即j = (m + n) / 2 - i

    • 左半部分最大值小於右半部分最小值

      max(A[i - 1], B[j - 1]) <= min(A[i], B[j]), 即A[i - 1] < B[j]且B[j - 1] < A[i]

    長度和為奇數

    • 左半部分長度和 = 右半部分長度和 + 1

      i + j = m - i + n - j, 即j = (m + n + 1) / 2 - i, 取整同上為j = (m + n) / 2 - i

    • 左半部分最大值小於右半部分最小值

      同上為A[i - 1] < B[j]且B[j - 1] < A[i]

    減小i, 增加j, 需保證i != 0, j != n

    增加i, 減小j, 需保證i != m, j != 0

    i = 0, j = 0, i = m, j = n單獨考慮

  • 時間複雜度O(log(min(m,n))),對較短陣列進行二分查詢

  • 空間複雜度O(1)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        // 保證m<=n
        if(m > n) return findMedianSortedArrays(nums2, nums1);

        int iMin = 0, iMax = m;
        while (iMin <= iMax){
            int i = (iMin + iMax) / 2;
            int j = (m + n + 1) / 2 - i;
            // i需要增大
            if(j != 0 && i != m && nums2[j - 1] > nums1[i]) iMin = i + 1;
            // i需要減小
            else if(i != 0 && j != n && nums1[i-1] > nums2[j]) iMax = i - 1;
            else{
                // 達到要求或邊界條件
                int maxLeft = 0;
                if(i == 0) maxLeft = nums2[j - 1];
                else if(j == 0) maxLeft = nums1[i - 1];
                else maxLeft = Math.max(nums1[i - 1], nums2[j - 1]);
                // 奇數情況不需要考慮右半部分
                if((m + n) % 2 != 0) return maxLeft;

                int minRight = 0;
                if(i == m) minRight = nums2[j];
                else if(j == n) minRight = nums1[i];
                else minRight = Math.min(nums2[j], nums1[i]);
                return (maxLeft + minRight) / 2.0;
            }
        }
        return 0.0;
    }
}