LeetCode 刷題記錄 |004:兩個排序數組的中位數

image.png

背景知識

我發現leetcode上的題,都有一個特色,就是初一看,這麼簡單。再一看,真tm難算法

我把這道題發羣裏,很多人以爲這道題簡單。我只能說你去試着作一下就清楚了。數組

這道題,在難度係數爲5,這是什麼概念呢。是LeetCode是難度係數最高的。
ide

image.png

這道題,要想作對。函數

須要知道兩個知識。spa

  • 什麼是中位數code

  • 什麼是時間複雜度爲O(log(n+m))blog

第一點,什麼是中位數
比方說:
奇位數:[3, 6, 9]的中位數是6
偶位數:[3, 6, 8, 9]的中位數是6+8/2 = 7排序

第二點:時間複雜度
什麼是時間複雜度,我也很難講得明白。若是你須要瞭解更多,你能夠去找本算法的書,亦或者網上搜下。我這裏只能講本節須要瞭解的知識噢。遞歸

log級別的時間複雜度,老司機都會聯想到「二分查找法」。索引

我們舉個例子說.

如今讓你在一個有序列表 [1,3,6,7,9,13,15,17,23] 裏找出17在哪一個位置上。

使用二分查找法,是先拿這個列表最中的數9,先和17進行比較,發現17>9,那再拿9後面的子列表[13,15,17,23]的最中間的數,因爲是偶數,我們取中位數1617對比,發現小於16<17,再後面的子列表[17,23]的中位數2017對比,發現17<20,最後只剩下17這一個數了,取出其索引,就知道他的位置了。

上面這個列表長度是9,用時間複雜度來看,就是 log29=3.1699250014

其意思是說,最多須要3次。和咱們上面例子,找出17的位置用了三次(這是最壞的狀況下用的次數)是吻合的。

必定要注意的是,使用二分查找法的前提是列表已是有序。這和咱們本道題,並不同

個人答案

慚愧。這道題三個小時我都沒作出來,沒有想到一個好的思路。遂放棄。

即便這樣,在看別人的答案時,依然有一種醍醐灌頂的感受(前提是你有思考過)。具體的解析,在下面我會來剖析,否則相信有很多人會看不懂。

網上的答案

對於一個長度爲n的已排序數列a。
若n爲奇數,中位數爲a[n / 2 + 1], 若n爲偶數,則中位數(a[n / 2] + a[n / 2 + 1]) / 2; 若是咱們能夠在兩個數列中求出第K小的元素,即可以解決該問題; 不妨設數列A元素個數爲n,數列B元素個數爲m,各自升序排序,求第k小元素; 取A[k / 2] B[k / 2] 比較; 若是 A[k / 2] > B[k / 2] 那麼,所求的元素必然不在B的前k / 2個元素中(證實反證法); 
反之,必然不在A的前k / 2個元素中,因而咱們能夠將A或B數列的前k / 2元素刪去,求剩下兩個數列的; k - k / 2小元素,因而獲得了數據規模變小的同類問題,遞歸解決; 若是 k / 2 大於某數列個數,所求元素必然不在另外一數列的前k / 2個元素中,同上操做就好。

若是人讀上面的思路以爲難以理解,讓小明來試着解釋一番。

若是是奇數,那麼中位數一定在兩個列表A和B中的某個元素。
若是是偶數,那麼中位數必定是兩個元素的平均數。

那麼問題,咱們能夠將其轉化一下。
若是是奇數,就要再兩個列表裏找到一個第m小的數。
若是是偶數,就要再兩個列表裏找到一個第n小第n+1小的數,而後再取平均數。

如今思路比較清晰了,不管是哪一種狀況,咱們都得找到第k小的數。因此咱們得實現一個這樣的公共函數。

整個問題的精髓都在這個公共函數裏。包括上面的二分查找法的思想,也將在這裏面體現。

class Solution(object):
   def findMedianSortedArrays(self, nums1, nums2):
       """
       :type nums1: List[int]
       :type nums2: List[int]
       :rtype: float
       """

       len1, len2 = len(nums1), len(nums2)
       if (len1 + len2) % 2 == 1:
           # 是奇數
           return self.getKth(nums1, nums2, (len1 + len2)//2 + 1)
       else:
           # 是偶數
           return (self.getKth(nums1, nums2, (len1 + len2)//2) +
                   self.getKth(nums1, nums2, (len1 + len2)//2 + 1)) * 0.5

   # 公共函數:查找兩個列表裏第k小的數
   def getKth(self, A, B, k):
       m, n = len(A), len(B)
       # 保證A比B長度短
       if m > n:
           return self.getKth(B, A, k)

       left, right = 0, m
       # 這個while循環,是找A中<=(整個有序數組中第k個數)的最大數
       while left < right:
           mid = left + (right - left) // 2
           # --------------------------------------------
           # x = k - 1 - mid:表示在B中前k-mid個的索引
           if 0 <= k - 1 - mid < n and A[mid] >= B[k - 1 - mid]:
               # 進入這裏,代表B中的前k-mid個都比中位數小,被剔除
               right = mid
           else:
               # 進入這裏,代表A中的前mid個都比中位數小,被剔除
               left = mid + 1
           # --------------------------------------------

       Ai_minus_1 = A[left - 1] if left - 1 >= 0 else float("-inf")
       # 這個是找B中<=(整個有序數組中第k個數)的最大數
       Bj = B[(k - left) - 1] if k - 1 - left >= 0 else float("-inf")

       return max(Ai_minus_1, Bj)

在看代碼的時候,上面兩長線包圍的代碼塊,是最難以理解的部分,這裏再解釋一下。

假設咱們要找兩個列表裏第k小的數,那麼必將有x個在A列表,k-x個在B列表。
如何找出這個x是何值呢,就使用二分查找法,一次一次試探。

先從A列表中找到最中間的數(這邊使用//,向下取整,假設其索引爲m),去和B列表中的第k-m,對比,若是A[m]<B[k-m],那麼說明A的前m個元素都比第k小的那個數小,能夠剔除了。

接下來,到A[m+1:]的最中間的數(假設其索引爲n)和B[:]中每(k-m)-n對比,和上面同樣,再剔除一半,直到最後,只剩下一個數。這個數在A中比第k小的數小的最大數。聽起來很繞,請必定去仔細閱讀代碼。

好啦。剖析就到這裏。小明已經儘量把這個解法講得簡單明瞭。相信比網上大多數的講解都更加讓人容易理解。

總結

在這道題目中,我陷入了一個死衚衕,說是求中位數,我就硬抓着中位數不放。到最後也沒能想出一個很好的思路。

到最後看了大神們的解答,才知道,有的時候能夠將問題轉換一個思路,問題就迎刃而解了。

相關文章
相關標籤/搜索