尋找兩個正序陣列中的中位數

2022-05-29 21:01:25

作者:Grey

原文地址:尋找兩個正序陣列中的中位數

題目連結

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

例如:nums1陣列是 [1,2], nums2 陣列是 [3,4]
那麼這兩個陣列的合併陣列是[1,2,3,4] ,所以中位數 (2 + 3) / 2 = 2.5

再比如:nums1陣列是 [1,2,3], nums2 陣列是 [4,5]
那麼這兩個陣列的合併陣列是[1,2,3,4,5] ,所以中位數 3

時間和空間都是O(M+N)的解法

這不是最優解,但是是最容易想到的一個解法,即,建立一個合併陣列,這個資料的長度就是兩個資料長度之和,然後通過合併有序陣列的方法將這個合併陣列生成出來,如果合併陣列長度是奇數,則取中間值,如果合併長度是偶數,則取上中位數和下中位數,通過(上中位數+下中位數)/ 2 得到結果。

這個解法的時間複雜度和空間複雜度都是O(M+N), 比較簡單,完整程式碼如下:

public double findMedianSortedArrays1(int[] nums1, int[] nums2) {
        // 題目已經說明nums1和nums2不能同時為空
        if (null == nums1 || nums1.length == 0) {
            return median(nums2);
        }
        if (null == nums2 || nums2.length == 0) {
            return median(nums1);
        }
        int m = nums1.length;
        int n = nums2.length;
        int[] nums = new int[m + n];
        int i = 0;
        int j = 0;
        int index = 0;
  // 合併兩個有序陣列的實現
        while (i < m && j < n) {
            if (nums1[i] >= nums2[j]) {
                nums[index++] = nums2[j++];
            } else {
                nums[index++] = nums1[i++];
            }
        }
        while (i < m) {
            nums[index++] = nums1[i++];
        }
        while (j < n) {
            nums[index++] = nums2[j++];
        }
        return median(nums);
    }
    // 求一個有序陣列的中位數
    // 如果是奇數,直接返回中間位置的值
    // 如果是偶數,則返回(上中位數+下中位數)/ 2的值
    public static double median(int[] arr) {
        int len = arr.length;
        if ((len & 1) == 1) {
            // 奇數
            return arr[len / 2];
        }
        return ((arr[len / 2] + arr[(len - 1) / 2]) / 2d);
    }

時間複雜度O(log(M+N))的解法

如果我們可以高效實現如下方法:

// 在O(log(M+N))時間複雜度下找到num1和num2這兩個有序陣列合併後的第k大的數是什麼
int findKth(int[] nums1, int[] nums2, int k)

那麼題目就可以通過如下方式實現:

   public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 題目已經說明nums1和nums2不能同時為空
        if (null == nums1 || nums1.length == 0) {
            return median(nums2);
        }
        if (null == nums2 || nums2.length == 0) {
            return median(nums1);
        }
        int m = nums1.length;
        int n = nums2.length;
			
        if (((m + n) & 1) == 1) {
           // 合併後的陣列長度是奇數,則取中間這個數
            return findKth(nums1, nums2, (m + n) / 2 + 1);
        }
        // 合併後的陣列長度如果是偶數,則返回(上中位數+下中位數)/ 2的值
        return (findKth(nums1, nums2, (m + n) / 2) + findKth(nums1, nums2, ((m + n) / 2 + 1))) / 2d;
    }

現在所有的目光都聚焦在這個演演算法中,

// 在O(log(M+N))時間複雜度下找到num1和num2這兩個有序陣列合併後的第k大的數是什麼
int findKth(int[] nums1, int[] nums2, int k)

注:以上演演算法中,我們規定k以1開始計算,也就是說:合併陣列中最小的數是第1小的,而不是第0小的。且在呼叫的時候,保證1<= k <= nums1.lengh + nums2.length

在解決這個演演算法之前,假設我們已經有一個方法,可以高效得到:兩個長度相等的排序陣列合併後的上中位數,我們假設這個方法為:

// 獲取兩個長度相等的有序陣列merge後的上中位數
// 如果是偶數,取上中位數
// 呼叫該方法的時候保證[s1...e1] 和 [s2...e2]等長
int getUpMedian(int[] A, int s1, int e1, int[] B, int s2, int e2) 

一旦我們有getUpMedian這個方法,我們就可以針對k的取值,分多種情況來解決findKth方法,我們假設

int findKth(int[] nums1, int[] nums2, int k)

方法中,較長的陣列我們重新定義為名字為longs的陣列,較短的陣列重新定義為名字shorts的陣列。

現在開始討論k的範圍:

第一種情況:k<=shorts.length

在這種情況下比較簡單,我們可以將longs陣列取前k個數,shorts陣列取前k個數,然後呼叫getMedian方法,拿到中位數,即是longs和shorts陣列合併後的第k小的數。

例如:shorts陣列為[1,3,5,7],longs陣列為[2,4,6,8,10,12]
如果要取第2小的數, 客觀上第2小的數是2,可能存在的範圍是shorts的前2個數中,也可能存在在longs的前2個數中,除此之外,不能是其他範圍,因為超過這個範圍,就不止第2小了。

第二種情況:shorts.length<k<=longs.length

在這種情況下,shorts陣列中的每個數都有可能,longs陣列可以排除掉 前(k - shorts.len - 1) 個數,以及[kth + 1......longs.length]區間的所有數,但是要手動驗證一下long中第(k- shorts.len)位置上的數是不是比shorts最後一個數大,
如果是,longs中第(k - shorts.length)位置上的數即為第k小的數,
如果不是,longs中第(k - shorts.length)位置上的數直接排除掉,
longs中剩餘數和shorts中所有數用一次getUpMedian方法求得的值即為第k小的數。

例如:shorts陣列為[1,3,5,7],longs陣列為[2,4,6,8,10,12,14,16,18]
求第7小的數,此時shorts中的數都有可能是第7小的數,但是,longs中,可以排除如下位置的數

首先:從第8小的數開始一直到第longs.length小的數都可以排除。

其次, 7 - shorts.length - 1 即:7 - 4 - 1 = 3 ,longs中第3小之前的數都可以排除,

排除完畢後,驗證一下longs中第3小的數是不是比shorts中最後一個數大,如果是,則longs中第3小的數即位兩個陣列的第7小的數。

如果不是,則longs中剩餘可選的數繼續和shorts呼叫getUpMedian方法。

第三種情況:longs.length<=k<=(shorts.length+longs.length)

這種情況下,shorts中可排除掉前面(k - longs.length)個數,longs中排除掉前( k - shorts.length - 1) 個數,然後手動判斷下shorts和longs中剩餘數中最左邊的數是不是第k小的數,如果是,直接返回,如果不是,排除掉這些數,然後將shorts和longs剩餘數用getUpMedian獲取的結果即為答案。
例如:shorts陣列為 [1,3,5,7,9,11] 長度是6,longs陣列為 [2,4,6,8,10,12,14,16,18,20,22,24] 長度是12,假設要求第15小的數,那麼在shorts中可以排除掉前面(15 - longs.length - 1)= 2個數, 因為即便shorts中第1小的數比longs中所有數都大,它也只能算第13小的數,第2小的數即便比longs中所有數都大,也只能算全域性第14小的數。longs中可以排除掉第1一直到第8小(即:15 - shorts.length - 1 = 8)的數,因為longs中第8小的數即便比shorts數所有數都大,也只能是全域性第14小的數。經過排除後,shorts中和longs中可選範圍為(以下陣列中沒打x的數位)

shorts中為[x,x,5,7,9,11]

longs中為[x,x,x,x,x,x,x,x,18,20,22,24]

先手動判斷一下,longs中的18和shorts中的5是否是第15小的數,如果是則直接返回,如果不是,shorts中[7,9,11] 和 longs中 [20,22,24] 使用getUpMedian獲取的結果即為答案。

所有情況說明完畢,而且三種情況僅依賴getUpMedian 方法。所以,現在看getUpMedian 方法的實現,具體說明見註釋:

// 獲取兩個長度相等的有序陣列merge後的上中位數
// 如果是偶數,取上中位數
// 呼叫該方法的時候保證[s1...e1] 和 [s2...e2]等長
    public static int getUpMedian(int[] A, int s1, int e1, int[] B, int s2, int e2) {
        if (s1 == e1) {
          // 說明A和B分別只有一個數,因為求上中位數,所以取A[s1]和B[s2]的最小值
            return Math.min(A[s1], B[s2]);
        }
        int mid1 = (s1 + e1) / 2;
        int mid2 = (s2 + e2) / 2;
      // 如果A和B的中位數值一樣,則這個中位數值也是A和B合併後的中位數值
        if (A[mid1] == B[mid2]) {
            return A[mid1];
        }
        boolean even = ((e1 - s1) & 1) != 0; // 是否是偶數
        if (even) {
          // 通過類似二分的方式,判斷A和B的中位數可能出現的位置,
            if (A[mid1] > B[mid2]) {
               // 如果A[mid1] > B[mid2]
              // 則全域性的中位數只可能在A的[s1..mid1]以及B的[mid2+1..e2]範圍內產生
              // 下面的判斷類似。
                return getUpMedian(A, s1, mid1, B, mid2 + 1, e2);
            } else {
                return getUpMedian(A, mid1 + 1, e1, B, s2, mid2);
            }
        } else {
            if (A[mid1] > B[mid2]) {
                if (B[mid2] > A[mid1 - 1]) {
                    return B[mid2];
                }
                return getUpMedian(A, s1, mid1 - 1, B, mid2 + 1, e2);
            } else {
                if (A[mid1] > B[mid2 - 1]) {
                    return A[mid1];
                }
                return getUpMedian(A, mid1 + 1, e1, B, s2, mid2 - 1);
            }
        }
    }

getUpMedian通過類似二分查詢的方法,來得到中位數資訊,複雜度就是O(log(M+N))。完整程式碼見:

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 題目已經說明nums1和nums2不能同時為空
        if (null == nums1 || nums1.length == 0) {
            return median(nums2);
        }
        if (null == nums2 || nums2.length == 0) {
            return median(nums1);
        }
        int m = nums1.length;
        int n = nums2.length;

        if (((m + n) & 1) == 1) {
            return findKth(nums1, nums2, (m + n) / 2 + 1);
        }
        return (findKth(nums1, nums2, (m + n) / 2) + findKth(nums1, nums2, ((m + n) / 2 + 1))) / 2d;
    }
    public static double median(int[] arr) {
        int len = arr.length;
        if ((len & 1) == 1) {
            // 奇數
            return arr[len / 2];
        }
        return ((arr[len / 2] + arr[(len - 1) / 2]) / 2d);
    }
   
    public static int findKth(int[] nums1, int[] nums2, int k) {
        int[] longs = nums1.length > nums2.length ? nums1 : nums2;
        int[] shorts = nums1 == longs ? nums2 : nums1;
        int maxLen = longs.length;
        int minLen = shorts.length;
        // k<= minLen
        // longs擷取前k個數,shorts擷取前k個數,兩個長度相等的陣列取上中位數
        if (k <= minLen) {
            return getUpMedian(shorts, 0, k - 1, longs, 0, k - 1);
        }
        // k > minLen 且 k <= maxLen
        // 到這裡一定k > minLen
        if (k <= maxLen) {
            // 可以排除
            // longs中比第k小的數還大的所有數都可以排除,即下標為:[k....maxLen - 1]
            // longs中第1小到第(k - minLen - 1)小的數都可以排除
            // shorts中所有數都有可能

            // 手動驗證
            // longs中第 (k - minLen) 大的數是否比shorts中最後一個數大,如果是,這個數直接就是結果
            if (longs[k - minLen - 1] >= shorts[minLen - 1]) {
                return longs[k - minLen - 1];
            }
            // 其他的數
            return getUpMedian(shorts, 0, minLen - 1, longs, k - minLen, k - 1);
        }
        // k > maxLen 且 k <= maxLen + minLen
        // 到這裡一定 k > maxLen 且 k <= maxLen + minLen
      
      
        // 可以排除
        // shorts中從第1小一直到第k - maxLen - 1小的數都可以排除
        // longs中第1小的數一直到第k - minLen - 1小的數都可以排除

        // 手動驗證
        if (shorts[minLen - 1] <= longs[k - minLen - 1]) {
            return longs[k - minLen - 1];
        }
        if (longs[maxLen - 1] <= shorts[k - maxLen - 1]) {
            return shorts[k - maxLen - 1];
        }
        // 其他的數
        return getUpMedian(shorts, k - maxLen, minLen - 1, longs, k - minLen, maxLen - 1);
    }

    public static int getUpMedian(int[] A, int s1, int e1, int[] B, int s2, int e2) {
        if (s1 == e1) {
            return Math.min(A[s1], B[s2]);
        }
        int mid1 = (s1 + e1) / 2;
        int mid2 = (s2 + e2) / 2;
        if (A[mid1] == B[mid2]) {
            return A[mid1];
        }
        boolean even = ((e1 - s1) & 1) != 0; // 是否是偶數
        if (even) {
            if (A[mid1] > B[mid2]) {
                return getUpMedian(A, s1, mid1, B, mid2 + 1, e2);
            } else {
                return getUpMedian(A, mid1 + 1, e1, B, s2, mid2);
            }
        } else {
            if (A[mid1] > B[mid2]) {
                if (B[mid2] > A[mid1 - 1]) {
                    return B[mid2];
                }
                return getUpMedian(A, s1, mid1 - 1, B, mid2 + 1, e2);
            } else {
                if (A[mid1] > B[mid2 - 1]) {
                    return A[mid1];
                }
                return getUpMedian(A, mid1 + 1, e1, B, s2, mid2 - 1);
            }
        }
    }
}

更多

演演算法和資料結構筆記