LeetCode 4. Median of Two Sorted Arrays 尋找兩個有序數組的中位數 C#

前言

本文介紹了 LeetCode 第 4 題 , “Median of Two Sorted Arrays”, 也就是 “尋找兩個有序數組的中位數” 的問題.

本文使用 C# 語言完成題目,介紹了多種方法供大家參考。

題目

English

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)).

You may assume nums1 and nums2 cannot be both empty.

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

中文

LeetCode 4. 尋找兩個有序數組的中位數

給定兩個大小爲 m 和 n 的有序數組 nums1 和 nums2。

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

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

示例 1:

nums1 = [1, 3]
nums2 = [2]

則中位數是 2.0
示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

則中位數是 (2 + 3)/2 = 2.5

解決方案

首先想到的是將兩個有序數組合併爲一個有序數組,再根據長度取中間值,計算中位數即可;另外,還可以 不真正合並數組的情況下找尋中位數; 但是上面兩種方式思想是相同的,均爲合併成一個數組後根據長度找中位數,達不到要求的 O(log(m + n)) 的 時間複雜度要求。根據 LeetCode 題解中 windliang 提供的方法“第k小數”方法,時間複雜度可以達到題目要求;根據 LeetCode 官方題解中提供的方法,時間複雜度可以達到 O( log( min(m,n)) ) ,比題目要求的時間複雜度還要小。下面我們依次來介紹這幾種方法。

方法一 : 合併List根據長度找中位數

new 一個 List , 並將 nums1 和 nums2 都添加到list 中,然後進行排序。對於排序後的 list, 根據長度計算出中位數的index,進而計算出最終結果。

假設合併後的list長度爲13,則從小到大第7個數字爲中位數,resultIndex=6;

假設合併後的list長度爲14,則從小到大第7,8個數字的平均值爲中位數,index 分別爲 6,7,此時resultIndex =7,resultIndex-1 =6 , 結果爲 ( list[resultIndex-1] + list[resultIndex] ) / 2.0 ;

    public double FindMedianSortedArrays(int[] nums1, int[] nums2)
    {
        int m = nums1.Length;
        int n = nums2.Length;
        int len = m + n;
        var resultIndex = len / 2;
        List<int> list = new List<int>(nums1);
        list.AddRange(nums2);
        list.Sort();
        if (len % 2 == 0)
        {
            return (list[resultIndex - 1] + list[resultIndex]) / 2.0;
        }
        else
        {
            return list[resultIndex];
        }
    }

執行結果

執行結果 通過,執行用時 156ms,內存消耗 27.2MB .

複雜度分析

時間複雜度:O( (m+n)(1+log(m+n) ))

將長度爲m,n的兩個數組添加到list,複雜度分別爲常數級的m和n ;list.Sort()的複雜度根據官方文檔可得爲 (m+n)log(m+n),所以該方法時間複雜度爲 O( m+n+(m+n)log(m+n) ) = O( (m+n)(1+log(m+n) ))

空間複雜度:O(m+n)

使用list的長度爲m+n.

方法二 : 歸併排序後根據長度找中位數

方法一使用了list.Sort() 方法,可以對list進行排序,但是,若題目給出的nums1 和 nums2 是無序數組,使用 list.Sort() 纔算是 物有所用。 本題中 nums1 和 nums2 是有序數組,所以使用 list.Sort() 有寫 殺雞用宰牛刀的感覺,換句話說,這裏面存在着效率的浪費。我們可以利用 【nums1 和 nums2 是有序數組】 這個條件,來精簡我們的排序。

 public double FindMedianSortedArrays(int[] nums1, int[] nums2)
    {
        // nums1 與 nums2 有序添加到list中
        List<int> list = new List<int>();
        int i = 0, j = 0;
        int m = nums1.Length;
        int n = nums2.Length;
        int len = m + n;
        var resultIndex = len / 2;

        while (i < m && j < n)
        {
            if (nums1[i] < nums2[j])
                list.Add(nums1[i++]);
            else
                list.Add(nums2[j++]);
        }
        while (i < m) list.Add(nums1[i++]);
        while (j < n) list.Add(nums2[j++]);

        if (len % 2 == 0)
        {
            return (list[resultIndex - 1] + list[resultIndex]) / 2.0;
        }
        else
        {
            return list[resultIndex];
        }
    }

執行結果

執行結果 通過,執行用時 152ms,內存消耗 27.2MB .

複雜度分析

時間複雜度:O(m+n)

i 和 j 一起把長度爲 m 和 n 的兩個數組遍歷了一遍,所以時間複雜度爲 O(m+n)

空間複雜度:O(m+n)

使用list的長度爲m+n.

方法三 : 方法二的優化,不真實添加到list

對於方法二,我們在已知 resultIndex 的情況下,也可以不把 nums1 和 nums2 真實添加到 list 中,只需要在i 和 j 不斷向右移動的過程中,計算是否到達了 resultIndex 即可。 若到達了 resultIndex,可以直接返回結果,而不必再處理後面的數據。但是相對的,我們需要在 i 或者 j 向右移動時,判斷是否到達了resultIndex.

    public double FindMedianSortedArrays(int[] nums1, int[] nums2)
    {
        int i = 0, j = 0, m = nums1.Length, n = nums2.Length;
        int len = m + n;
        int resultIndex = len / 2;
        int resultIndexPre = resultIndex - 1;
        int result = 0, resultPre = 0;  
        bool isTwoResult = len % 2 == 0;
        while (i < m || j < n)
        {
            var nums1ii = i < m ? nums1[i] : int.MaxValue;
            var nums2jj = j < n ? nums2[j] : int.MaxValue;
            if (nums1ii < nums2jj)
            {
                if (i + j == resultIndexPre) resultPre = nums1[i];
                if (i + j == resultIndex)
                {
                    result = nums1[i];
                    if (isTwoResult) return (resultPre + result) / 2.0;
                    else return result;
                }
                i++;
            }
            else
            {
                if (i + j == resultIndexPre) resultPre = nums2[j];
                if (i + j == resultIndex)
                {
                    result = nums2[j];
                    if (isTwoResult) return (resultPre + result) / 2.0;
                    else return result;
                }
                j++;
            }
        }
        return 0;
    }

執行結果

執行結果 通過,執行用時 144ms,內存消耗 26.8MB .

複雜度分析

時間複雜度:O(m+n)

i 和 j 一起把長度爲 m 和 n 的兩個數組遍歷了一半,但是每一步都需要判斷當前i+j的值是否等於resultIndex,所以時間複雜度仍可認爲 O(m+n)

空間複雜度:O(1)

對比方法二,不再使用list,只使用了幾個變量來存值,所以空間複雜度爲O(1)

方法四 : 第k小數

此方法來自 leetcode題解中 windliang 貢獻的題解。在此感謝 windliang,文末給出了他的題解的鏈接。

前面的幾種方法,時間複雜度都沒有達到題目要求的 O(log(m+n)) 。 看到log,很明顯需要使用二分法。根據 windliang提供的思路,題目求中位數,實際上是求第 k 小數的一種特殊情況,而求第 k 小數 有一種算法。

方法三中,i 和 j 每次向右移動一位時,相當於去掉了一個不可能是中位數的值,也就是一個一個的排除。由於給定的兩個數組是有序的,所以我們完全可以一半一半的排除。假設我們要找第 k 小數,我們每次循環可以安全的排除掉 k/2 個數,下面看一個例子。

假設 nums1=[ 1,4,7,9 ] , nums2=[ 1,2,3,4,5,6,7,8 ] ,我們要找第7小的數字,即 k=7, 則 k/2 = 3 (向下取整).

################# 1

因爲 B的第三位爲3 ,A的第三位爲7,所以B的第三位較小,所以B的前三位 1,2,3 都不可能是第7小的數字。

上面這句話如果沒有理解的話,可以換個說法:我們的目標 result 是第7小的數字,也就是說 A和B中 比 result 小的值 合計共有6個纔對。但是對於B的前三位 1,2,3 來說,B後面的值因爲B本身是有序數組,所以都比B的前三位大,而A的第三位又比B的第三位大,所以對於B的第三位B[2]來說,A和B中,沒有合計的6個值比B[2]小的。B的前兩位又一定是比第三位小的,就更不用說了。

################# 2

因爲減少了3位,所以我們要求的第7小的值,變爲了剩下數據中第4小的值 k=4, k/2=2 ,所以需要比較當前A與B剩餘數據的第二位。

################# 3

當前A的第二位爲4,B的第二位爲5,A<B,所以A的前兩位1和4被排除掉。

################# 4

此時已經排除了5位,我們需要找的第7小的數,變成了剩下數據中第2小的數, k=2, k/2 =1 ,所以需要比較當前A與B剩餘數據的第1位。

################# 5

此時B<A,所以B剩餘數據的第一位 4 被排除掉。

################# 6

我們需要找的是第7小的數,而現在已經排除了6個,所需現在需要找的是剩餘數據中第1小的數, k=1 ; 當k=1時,達到了我們的循環出口條件:下面只需要比較A和B剩餘數據的第一位,取小的那個數,就是我們的答案。

################# 7

因爲5<7,所以5爲最終答案。 即 對於A與B的第7小的數是5.

根據上面的分析,我們C#代碼如下:

    public double FindMedianSortedArrays(int[] nums1, int[] nums2)
    {
        int n = nums1.Length;
        int m = nums2.Length;
        int len = n + m;
        int kPre = (len + 1) / 2;
        int k = (len + 2) / 2;
        if (len % 2 == 0)
            return (GetKth(nums1, 0, n - 1, nums2, 0, m - 1, kPre) + GetKth(nums1, 0, n - 1, nums2, 0, m - 1, k)) * 0.5;
        else
            return GetKth(nums1, 0, n - 1, nums2, 0, m - 1, k);
    }

    private int 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 的長度小於 len2,這樣就能保證如果有數組空了,一定是 len1 
        if (len1 > len2) return GetKth(nums2, start2, end2, nums1, start1, end1, k);
        if (len1 == 0) return nums2[start2 + k - 1];
        if (k == 1) return Math.Min(nums1[start1], nums2[start2]);
        int i = start1 + Math.Min(len1, k / 2) - 1;
        int j = start2 + Math.Min(len2, k / 2) - 1;
        if (nums1[i] > nums2[j])
            return GetKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        else
            return GetKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
    }

執行結果

執行結果 通過,執行用時 136ms,內存消耗 27.1MB .

複雜度分析

時間複雜度:O(log(m+n))

每進行依次循環,就減少 k/2個元素,所以時間複雜度爲 O(log(k)) , 而 k = (m+n)/2 , 所以最終複雜度是 O(log(m+n))

空間複雜度:O(1)

只使用了幾個變量來存值,遞歸是尾遞歸不佔用堆棧, 所以空間複雜度爲O(1)

方法五 : 從中位數的概念定義入手

該方法參考了 LeetCode 題解的 官方題解 以及 windliang 的題解。

首先我們來看一下百度百科中位數的定義:https://baike.baidu.com/item/%E4%B8%AD%E4%BD%8D%E6%95%B0/3087401?fr=aladdin

Ø 中位數(Median)又稱中值,統計學中的專有名詞,是按順序排列的一組數據中居於中間位置的數,代表一個樣本、種羣或概率分佈中的一個數值,其可將數值集合劃分爲相等的上下兩部分。對於有限的數集,可以通過把所有觀察值高低排序後找出正中間的一個作爲中位數。如果觀察值有偶數個,通常取最中間的兩個數值的平均數作爲中位數。

所以我們可以對數組進行切割。

一個長度爲 m 的數組,有 0 到 m 總共 m+1 個位置可以切。

################# 8

我們把數組 A 和數組 B 分別在 I 和 j 進行切割。

################# 9

將i的左邊和j的左邊組成[左半部分],將i的右邊和j的右邊組成[右半部分]。我們將左半部分稱爲left,右半部分稱爲right。

當 A數組 和 B數組 的總長度是偶數時,如果我們能夠保證下面兩點,

1. len(left) = len(right),也就是 i+j = m-i+n-j, 即j=(m+n)/2-i
2. max(left) <= min(right),也就是 max( A[i-1] , B[j-1] ) <= min( A[i] , B[j] )

那麼 中位數就是 ( max(left) + min(right) ) / 2 , 也就是 ( max( A[i-1] , B[j-1] ) + min( A[i] , B[j] ) )/2

同樣的,當 A數組 和 B數組 的總長度爲奇數時,如果我們能夠保證下面兩點:

1. len(left)=len(right)+1, 即 j=(m+n+1)/2-i 
2. max(left) <= min(right),也就是 max( A[i-1] , B[j-1] ) <= min( A[i] , B[j] )

那麼 中位數就是 max(left),即max( A[i-1] , B[j-1] ) , 也就是left比right多出來的那一個數。

上面的第一個條件,我們可以將 奇數和偶數 兩種情況合併爲 j=(m+n+1)/2-i ,因爲如果m+n是偶數,再加1之後,由於除以2是int類型的操作,所以對結果沒有影響。

於是我們得到了 j與i的關係 j=(m+n+1)/2-i . 由於 0<=i<=m ,爲了保證 0<=j<=n ,我們必須保證 m<=n . 這是因爲

j= (m+n+1)/2-i = m/2 + n/2 + 1/2 - i = (m-2i+n)/2 
 因爲0<=j<=n, 所以 0<=(m-2i+n)/2<=n
先計算左邊 
	(m-2i+n)/2 >= 0    
	即  n>=2i-m
	因爲 0<=i<=m,所以 0<=2i<=2m,
	所以2i-m的取值範圍爲 -m <= 2i-m <= m
	又因爲 n>=2i-m,需要大於等於2i-m的最大值m
	所以n>=m; 即 m <= n;
再計算右邊   
	(m-2i+n)/2 <= n   
	=>  m-2i+n <= 2n
	=>  m-2i <=n
	=>  n >= m-2i
	因爲 0<=i<=m, 所以 0<=2i<=2m,
	所以m-2i的範圍爲 -m <= m-2i <= m
	又因爲 n >= m-2i , 需要大於等於 m-2i的最大值m
	所以 n>=m;即 m <= n;

有了上面的證明,可以得知m<=n,即A數組的長度需要小於等於B數組的長度。所以在計算時若出現A數組長度大於B數組長度的情況時,交換兩個數組的位置再繼續計算即可。

對於上面的第二個條件,奇數和偶數的情況時一樣的,都是 max(left) <= min(right),也就是 max( A[i-1] , B[j-1] ) <= min( A[i] , B[j] ) . 我們進一步分析,因爲由題意已知A數組和B數組是有序的,所以一定有 A[i-1] <= A[i] 及 B[j-1] <= B[j] ,所以我們只需要保證 B[j-1] <= A[i] 和 A[i-1] <= B[j] ,就可以保證 max(left) <= min(right).

所以我們先討論以下兩種情況:

1. 當 B[j-1] > A[i] 時,我們需要增加i,並且爲了平衡left和right的數量,我們還需要減少j。 幸運的是 j=(m+n+1)/2-i, 當i增大時,j自然就會減小。
2. 當A[i-1] > B[j] 時,與上面情面類似,我們需要增大j,即減小i。

兩種情況示例圖如下:

################# 10

上述兩種情況,我們沒有考慮切割位置在數組邊界的情況。即 i的取值可能爲0或者m;j的取值可能爲0或n.

當i=0時,max(left) = B[j-1] ; 當 i=m時,min(right) = B[j];

################# 11

類似的,當j=0時, max(left)= A[i-1] , 當 j=n時, min(right) = A[i] .

分析至此,所有的思路都理清了,最後一個問題,增加i的方式,當然是用二分了,因爲題目要求的時間複雜度爲log。 初始化i爲中間的值,然後增加或減少一半的值,再增加或減少一半的值,類似有序數組中二分查找指定值。就這樣二分查找直到最終結果。

代碼也是參考了 windliang題解的代碼。

    public double FindMedianSortedArrays(int[] A, int[] B)
    {
        int m = A.Length;
        int n = B.Length;
        //保證第一個數組是較短的
        if (m > n) return FindMedianSortedArrays(B, A);
        //正在尋找的範圍爲 [ A[iMin],A[iMax] ) , 左閉右開。二分查找取i=(iMin+iMax)/2
        int iMin = 0, iMax = m;
        while (iMin <= iMax)
        {
            int i = (iMin + iMax) / 2;
            int j = (m + n + 1) / 2 - i;
            if (j != 0 && i != m && B[j - 1] > A[i])
            { // i 需要增大
                iMin = i + 1;
            }
            else if (i != 0 && j != n && A[i - 1] > B[j])
            { // i 需要減小
                iMax = i - 1;
            }
            else
            { // 達到要求,並且將邊界條件列出來單獨考慮
                int maxLeft = 0;
                if (i == 0) { maxLeft = B[j - 1]; }
                else if (j == 0) { maxLeft = A[i - 1]; }
                else { maxLeft = Math.Max(A[i - 1], B[j - 1]); }
                if ((m + n) % 2 == 1) { return maxLeft; } // 奇數的話不需要考慮右半部分

                int minRight = 0;
                if (i == m) { minRight = B[j]; }
                else if (j == n) { minRight = A[i]; }
                else { minRight = Math.Min(B[j], A[i]); }

                return (maxLeft + minRight) / 2.0; //如果是偶數的話返回結果
            }
        }
        return 0.0;
    }

執行結果

執行結果 通過,執行用時 128ms,內存消耗 27.2MB .

複雜度分析

時間複雜度:O(log(min(m,n))

我們對較短的數組進行了二分查找,所以時間複雜度是 O(log(min(m,n))

空間複雜度:O(1)

只使用了幾個變量來存值,所以空間複雜度爲O(1)

參考資料彙總

題目:

https://leetcode-cn.com/problems/median-of-two-sorted-arrays/

官方題解:

https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-shu-b/

windliang 題解:

https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-2/

微軟官方 List.Sort() 文檔:

https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.generic.list-1.sort?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev16.query%3FappId%3DDev16IDEF1%26l%3DZH-CN%26k%3Dk(System.Collections.Generic.List%601.Sort);k(DevLang-csharp)%26rd%3Dtrue&view=netframework-4.8

百度百科中位數的定義:
https://baike.baidu.com/item/%E4%B8%AD%E4%BD%8D%E6%95%B0/3087401?fr=aladdin

百度百科 二分查找:
https://baike.baidu.com/item/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/10628618?fr=aladdin

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章