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

LeetCode刷題實戰4:尋找兩個正序陣列的中位數

題目描述

給定兩個大小為 m 和 n 的正序(從小到大)陣列nums1nums2。請你找出並返回這兩個正序陣列的中位數。

示例 1

輸入:nums1 = [1,3], nums2 = [2]
輸出:2.00000
解釋:合併陣列 = [1,2,3] ,中位數 2

示例 2

輸入:nums1 = [1,2], nums2 = [3,4]
輸出:2.50000
解釋:合併陣列 = [1,2,3,4] ,中位數 (2 + 3) / 2 = 2.5

示例 3

輸入:nums1 = [0,0], nums2 = [0,0]
輸出:0.00000

示例 4

輸入:nums1 = [], nums2 = [1]
輸出:1.00000

示例 5

輸入:nums1 = [2], nums2 = []
輸出:2.00000

解答

方法 1

給定兩個正序的陣列,要求返回兩個正序陣列的中位數,最容易想到的方法就是先將兩個陣列進行合併,然後再返回合併後的大陣列的中位數即可。我們的第一種方法,也是用到數組合並的思想,但並不是完全合併兩個陣列。

兩個陣列的長度分別為 m 和 n,總長度 total_len = m + n,當 total_len 為奇數時,中位數的下標 mid 為 total_len // 2;為偶數時,mid = total_len / 2,此時中位數為 mid-1 和 mid 的下標對應值之和再除以 2。

先定義一個空的列表 new_arry。i,j 分別對應兩個陣列對應的下標,並比較 nums1[i] 和 nums2[j] 的大小,將值小的插入到 new_arry 中。

當 new_arry 的長度等於 mid 時,迴圈停止。

如果 total_len 為奇數,則返回 new_arry[mid] 即可;如果為偶數,則返回 (new_arry[mid-1] + new_arry[mid]) / 2.0。

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        res = 0.0
        new_array = []
        m, n = len(nums1), len(nums2)
        total_len = m + n
        if total_len % 2 != 0:
            mid = int(total_len//2)
        else:
            mid = int(total_len/2)
        i,j = 0,0
        while len(new_array) -1 < mid:
            if i < m and j < n :
                if nums1[i] < nums2[j]:
                    new_array.append(nums1[i])
                    i+=1
                    continue
                if nums1[i] >= nums2[j]:
                    new_array.append(nums2[j])
                    j+=1
                    continue
            if i == m and j < n:
                new_array.append(nums2[j])
                j+=1
            if i < m and j == n:
                new_array.append(nums1[i])
                i+=1
        if total_len %2 != 0:
            res = new_array[mid]
        else:
            res = (new_array[mid-1] + new_array[mid])/2.0
        return res

方法 2

中位數是一個數組中間位置的數,其實中位數把一個升序陣列分成了長度相等的兩部分,其中左半部分的最大值永遠小於或等於右半部分的最小值。我們用一個例子來說明,比如現在有如下陣列:

[1,2,3,5,67,7,8,9,15]

[1,2,3,5,6] < [7,7,8,9,15]

如上所示,對於偶數長度的陣列,可以根據中位數分成長度相等的兩部分,左半部分的最大元素(6),永遠小於或等於右半部分的最小元素(7)。

對於奇數長度的陣列,同樣可以根據中位數分成兩部分:

[1,2,3,4,5,5,6,7,9,10,11]

[1,2,3,4,5,5] < [6,7,9,10,11]

如上所示,對於奇數長度的陣列,如果把中位數本身歸入左半部分,則左半部分長度 = 右半部分長度 + 1。

左半部分的最大元素(5),永遠小於或等於右半部分的最小元素(6)。

大陣列被中位數等分的左右兩部分,每一部分根據來源又可以再劃分成兩部分,其中一部分來自陣列 nums1 的元素,另一部分來自陣列 nums2 的元素:

陣列 nums1        陣列 nums2
[3,5,7,8,9] < [1,2,6,7,15]
      i                   j

[1,2,3,5,6,7,7,8,9,15]
大陣列         i+j

如上所示,原始陣列 nums1 和陣列 nums2,各自分成黑色和橙色兩部分。其中數值較小的黑色元素組成了大陣列的左半部分,陣列較大的橙色元素組成了大陣列的右半部分。

最重要的是,黑色元素和橙色元素的數量是相等的(偶數情況),而且最大的黑色元素小於或等於最小的橙色元素。

假設陣列 nums1 的長度是 m,黑色和橙色元素的分界點是 i,陣列 nums2 的長度是 n,黑色和橙色元素的分界點是 j,那麼為了讓大陣列的左右兩部分相等,則 i 和 j 需要符合如下條件: i + j = (m + n + 1) / 2(之所以 m + n 後面要再加 1,是為了應對大陣列長度為奇數的情況),max(nums1[i-1], nums2[j-1])<= min(nums1[i], nums2[j])(直白地說,就是最大的黑色元素小於或等於最小的橙色元素)。

由於 m + n 的值是恆定的,所以我們只要確定一個合適的 i,就可以確定 j,從而找到大陣列左半部分和右半部分的分界,也就找到了歸併之後大陣列的中位數。

對於這個合適的 i 值,我們採用二分查詢的思路來尋找。下面通過一個具體的例子來看看二分查詢確定 i 值:

陣列 nums1:[3,5, 6, 7, 8, 15,20]
陣列 nums2:[1,10,12,18,21,24,25,27]

第一步,就像二分查詢那樣,把 i 設在陣列 nums1 的正中位置,也就是讓 i = 3;

陣列 nums1:[3,5, 6, 7, 8, 15,20]
i 陣列 nums2:[1,10,12,18,21,24,25,27]

第二步,根據 i 的值來確定 j 的值,j = (m + n + 1) / 2 - i = 5;

陣列 nums1:[3,5, 6, 7, 8, 15,20]
                  i
陣列 nums2:[1,10,12,18,21,24,25,27]
j

第三步,驗證 i 和 j,分為下面三種情況:

1. nums2[j-1] <= nums1[i] && nums1[i-1] <= nums2[j]

說明 i 和 j 左側的元素都小於或等於右側的元素,這一組 i 和 j 是我們想要的。

2. nums1[i] < nums2[j-1]

說明 i 對應的元素偏小了,i 應該向右側移動。

3. nums1[i-1] > nums2[j]

說明 i-1 對應的元素偏大了,i 應該向左側移動。

顯然,上面的例子中屬於情況 2,nums1[3] < nums2[5],所以 i 應該向右移動。

第四步,在陣列 nums1 的右半部分,重新確定 i 的位置,就像二分查詢一樣:

陣列 nums1:[3,5, 6, 7, 8, 15,20]
                          i
陣列 nums2:[1,10,12,18,21,24,25,27]

第五步,同第二步,根據 i 來確定 j 的值,j = (m + n + 1) / 2 - i = 3:

陣列 nums1:[3,5, 6, 7, 8, 15,20]
                          i
陣列 nums2:[1,10,12,18,21,24,25,27]
                  j

第六步,同第三步,驗證 i 和 j:

由於nums1[5] >= nums2[2] 且 nums2[3] >= nums1[4],所以這一組 i 和 j 是合適的。

第七步,找出中位數:

如果大陣列的長度是奇數,那麼:中位數 = max(nums1[i-1], nums2[j-1])(也就是大陣列左半部分的最大值)。

如果大陣列的長度是偶數,那麼:中位數 = (max(nums1[i-1], nums2[j-1]) + min(nums1[i],nums2[j])) / 2(也就是大陣列左半部分的最大值和大陣列右半部分的最小值取平均)。

對於一些特殊情況

1. 陣列 nums1 的長度大於陣列 nums2

提前把陣列 nums1 和陣列 nums2 進行交換,較短的陣列放在前面,i 從較短的陣列中取。這樣做還有一個好處,由於陣列 nums1 是較短陣列,i 的搜尋次數減少了。

2. 無法找到合適的 i 值

什麼情況下會無法找到合適的 i 值呢?有兩種情況:

陣列 nums1 的長度小於陣列 nums2 的長度,並且陣列 nums1 的所有元素都大於陣列 nums2 的元素。

在這種情況下,無法通過二分查詢尋找符合 nums2[j-1] <= nums1[i] && nums1[i-1] <= nums2[j] 的 i 值,一直到 i=0 為止。此時我們可以跳出二分查詢的迴圈,所求的中位數是 nums2[j-1]。(僅限奇數情況)

陣列 nums1 的長度小於陣列 nums2 的長度,並且陣列 nums1 的所有元素都小於陣列 nums2 的元素

在這種情況下,同樣無法通過二分查詢尋找符合 nums2[j-1] <= nums1[i] && nums1[i-1] <= nums2[j] 的 i 值,一直到 i=陣列 nums1 的長度-1 為止。此時我們可以跳出二分查詢的迴圈,所求的中位數是 max(nums1[i-1], nums2[j-1])。(僅限奇數情況)

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        m, n = len(nums1), len(nums2)
        if m>n:
            nums1, nums2, m, n=nums2, nums1, n, m
        start, end, half_len = 0, m, (m + n + 1)//2
        while start<=end:
            i = (start+end)//2
            j = half_len - i
            if i < m and nums2[j-1] > nums1[i]:
                start = i + 1
            elif i > 0 and nums1[i-1] > nums2[j]:
                end = i - 1
            else:
                if i == 0:
                    max_of_left = nums2[j-1]
                elif j == 0:
                    max_of_left = nums1[i-1]
                else:
                    max_of_left = max(nums1[i-1], nums2[j-1])
                if (m + n)%2 == 1:
                    return max_of_left
                if i == m:
                    min_of_right = nums2[j]
                elif j == n:
                    min_of_right = nums1[i]
                else:
                    min_of_right = min(nums1[i], nums2[j])
                return (max_of_left + min_of_right)/2.0