力扣演算法題:尋找兩個正序陣列的中位數

LYX6666發表於2023-02-07

記錄這幾天刷題做到的一個陣列的演演算法題的過程:

給定兩個大小分別為 mn 的正序(從小到大)陣列 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

演演算法的時間複雜度應該為 O(log (m+n))

來源:力扣(LeetCode)
連結:https://leetcode.cn/problems/...
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
  
    }

分析

此題目在力扣的難度為困難,乍一看題目並不是很難,用陣列合並的笨方法就可以解決。

真正的難點在於時間複雜度是 Log 級別

我們先來看一下不要求的時間複雜度的情況下如何處理。

奇偶判斷

如果總數是奇數,返回中間值,如果總數是偶數,返回中間值和中間值+1(由於給出的資料結構是陣列,所有的下標-1)

如果不想用 IF 實現判斷,也可以使用條件值的判斷,如:

return ( nums2_length % 2 == 0 ) ?  // 是0嗎?
       ( nums2[middle - 1] + nums2[middle] ) / 2.0 :  // 如果是0說明是偶數,返回中間兩個樹的平均值
       nums2[middle];    // 如果不是0說明是奇數,返回中間的數

邊界條件

首先考慮邊界條件,題目的輸入可能會出現一個陣列為空的情況,如:

輸入:nums1 = [1,3], nums2 = []
輸入:nums1 = [], nums2 = [2,3,4]

這種情況單獨加一個判斷,如果有一個陣列為空,就直接用下標求出另一個陣列的中位數並直接返回,不需要合併陣列:

// 長度
int nums1_length = nums1.length;
int nums2_length = nums2.length;
int total_length = nums1_length + nums2_length;
// 如果陣列1為空,求陣列2的中位數
if (nums1_length == 0) {
    int middle = nums2_length / 2;
    return ( nums2_length % 2 == 0 ) ?
           ( nums2[middle - 1] + nums2[middle] ) / 2.0 :
           nums2[middle];
}
// 如果陣列2為空,求陣列1的中位數
if (nums2_length == 0) {
    int middle = nums1_length / 2;
    return ( nums1_length % 2 == 0 ) ?
           ( nums1[middle - 1] + nums1[middle] ) / 2.0 :
           nums1[middle];
}   

一般情況

在兩個陣列都非空的情況下,嘗試合併陣列,然後取最終陣列的中位數

首先定義了三個索引,分別是合併後的索引,nums1的索引,nums2的索引,然後建立一個永真迴圈:

int[] nums = new int[total_length];
// i:合併後的列表的索引,j:nums1的索引,k:nums2的索引
int i = 0, j = 0, k = 0;
//
while (true) {

}

接下來看幾種情況:

如果 nums1的索引等於它的長度,說明這個陣列迴圈結束了,只把 nums2拼接到合併後陣列的後面即可

    if (j == nums1_length && k != nums2_length) {
        nums[i] = nums2[k];
        k++;
        i++;
    }

    else if (j != nums1_length && k == nums2_length) {
        nums[i] = nums1[j];
        j++;
        i++;
    }

如果兩個陣列的索引都等於他們的長度,說明迴圈結束,跳出迴圈,返回合併後陣列的中位數

    else if (j == nums1_length && k == nums2_length) {
        int middle = total_length / 2;
        return ( total_length % 2 == 0 ) ?
               ( nums[middle - 1] + nums[middle] ) / 2.0 :
               nums[middle];
    }

其他情況就是正常的情況了,由於原有陣列是升序,合併後的陣列也是升序, 也就是插入每次比較時更小的那個

    else {
        if (nums1[j] > nums2[k]) {
            nums[i] = nums2[k];
            k++;
        } else {
            nums[i] = nums1[j];
            j++;
        }
        i++;
    }

小結

整個流程就變成了:

①看是否有一個陣列是空,如果有,直接返回另一個陣列的中位數

②開始迴圈,每次迴圈驗證是否有陣列的索引等於陣列長度,
如果一個陣列滿足,說明這個陣列迴圈完畢,直接把另一個陣列的尾部插入到合併後陣列的尾部
如果兩個陣列滿足,說明迴圈結束,返回合併後陣列的中位數
如果沒有陣列滿足,進入③

③正常情況下:比較兩個數,插入更小的數字,進行下一次迴圈

最後發現這個演演算法時間複雜度是 O(m+n),空間複雜度 O(m+n)

完整程式碼:

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 長度
        int nums1_length = nums1.length;
        int nums2_length = nums2.length;
        int total_length = nums1_length + nums2_length;
        //
        if (nums1_length == 0) {
            int middle = nums2_length / 2;
            return ( nums2_length % 2 == 0 ) ?
                   ( nums2[middle - 1] + nums2[middle] ) / 2.0 :
                   nums2[middle];
        }
        if (nums2_length == 0) {
            int middle = nums1_length / 2;
            return ( nums1_length % 2 == 0 ) ?
                   ( nums1[middle - 1] + nums1[middle] ) / 2.0 :
                   nums1[middle];
        }
        // 定義陣列
        int[] nums = new int[total_length];
        // i:合併後的列表的索引,j:nums1的索引,k:nums2的索引
        int i = 0, j = 0, k = 0;
        //  開始迴圈
        while (true) {
            // 陣列1迴圈結束
            if (j == nums1_length && k != nums2_length) {
                nums[i] = nums2[k];
                k++;
                i++;
            }
            // 陣列2迴圈結束
            else if (j != nums1_length && k == nums2_length) {
                nums[i] = nums1[j];
                j++;
                i++;
            }
            // 整體迴圈結束
            else if (j == nums1_length && k == nums2_length) {
                int middle = total_length / 2;
                return ( total_length % 2 == 0 ) ?
                       ( nums[middle - 1] + nums[middle] ) / 2.0 :
                       nums[middle];
            }
            // 一般情況
            else {
                if (nums1[j] > nums2[k]) {
                    nums[i] = nums2[k];
                    k++;
                } else {
                    nums[i] = nums1[j];
                    j++;
                }
                i++;
            }
        }
        return 0;
    }
}

初步最佳化

在不改變整體思路的情況下,最佳化思路就是

①不迴圈整個陣列,而是索引達到中位數就停止迴圈,這樣迴圈次數減少一半,但複雜度仍是 O(m+n)

②不記錄所有的合併結果,而是隻保留幾個固定元素,達到中位數就停止迴圈,這樣空間複雜度能降到O(1)

但這樣還是無法滿足題目要求,所以只能把整個思路都推翻掉,合併陣列的方案在一開始就是不滿足要求的。

改變思路——二分法

這個思路是真正的解法,但目前看起來比較難
暫時附上鍊接:https://leetcode.cn/problems/...

到筆者彙報的時候還沒完全看懂,後面再補充

相關文章