LeetCode困難刷題記錄——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.

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

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

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

 

分析:

題目要求log複雜度,加上兩個數組是有序的,很容易就想到二分查找的方法

第一,找到中位數的問題,其實就是找到排序爲第k位(k從0開始)的數的問題

第二,二分查找法的思路就是取mid——比較大小——縮小查找區間的過程,其他都好說,比較大小怎麼比?假設我們在nums1中查找第k位的數,兩個數組都是有序的,所以nums1[mid1]這個元素,在nums1中有mid1個元素比它小,那麼如果nums2中恰好有k-mid1個數比nums1[mid1]小,nums1[mid1]就是第k位數,如果nums2中比它小的數多於k-mid1個,nums1[mid1]就太大,反之太小

第三,如果第k位的數不在nums1中,需要在nums2裏再查找嗎?不用的,在nums1的查找過程中,lo1-1永遠指向比第k位數小的數,當查找結束後,就表示nums1中有lo1個數是比第k位數小的,顯而易見,第k位數就是nums2[k-lo1]

 

代碼:

/**
 * 尋找兩個有序數組的中位數
 * Median of Two Sorted Arrays
 * 
 * @author DongWei
 * @version 2019/5/31
 */
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        // 如果有空數組,直接返回結果
        if (m == 0) {
            if (n % 2 == 0) {
                return (nums2[n >> 1] + nums2[(n >> 1) - 1]) / 2.0;
            } else {
                return nums2[n >> 1];
            }
        }
        if (n == 0) {
            if (m % 2 == 0) {
                return (nums1[m >> 1] + nums1[(m >> 1) - 1]) / 2.0;
            } else {
                return nums1[m >> 1];
            }
        }

        // 開始在 nums1 中查找第 k 位數,即中位數,對於偶數長度的數組而言,查找較大的那個中位數
        int k = (m + n) >> 1;
        int lo1 = 0;
        int hi1 = Math.min(m - 1, k);
        while(true) {
            if (lo1 <= hi1) {
                // 二分查找
                int mid1 = (lo1 + hi1) >> 1;
                double res = binarySearch(mid1, k, nums1, nums2);
                if (res == Double.MAX_VALUE) {
                    hi1 = mid1 - 1;
                } else if (res == Double.MIN_VALUE) {
                    lo1 = mid1 + 1;
                } else {
                    return res;
                }
            } else {
                // 中位數在 nums2 中
                if ((n + m) % 2 == 0) {
                    // 長度爲偶數,根據不同情況計算較小的中位數
                    if (k <= lo1) {
                        // 較大中位數是 nums2[0],較小中位數必然是 num1 的最後一個元素
                        return (nums2[k - lo1] + nums1[lo1 - 1]) / 2.0;
                    } else if (lo1 == 0) {
                        // nums1 中沒有任何元素比較大中位數小,較小中位數必然在 nums2 中
                        return (nums2[k - lo1] + nums2[k - lo1 - 1]) / 2.0;
                    } else {
                        // 較小中位數可能是在 nums1 或 nums2 中,比較兩個數的大小確定
                        return (nums2[k - lo1] + Math.max(nums2[k - lo1 - 1], nums1[lo1 - 1])) / 2.0;
                    }
                } else {
                    // 長度爲奇數,直接返回結果
                    return nums2[k - lo1];
                }
            }

        }
    }
    
    /**
     * 判斷元素是不是中位數
     * 
     * @param v 待比較元素
     * @param need nums 中如果有 need 個元素比 v 小,說明 v 恰好是中位數
     * @param nums2 另一個數組
     * @return 判斷這個元素比中位數大還是小
     */
    private int compare(int v, int need, int[] nums2) {
        // 處理數組邊界情況
        int val1 = Integer.MIN_VALUE;
        int val2 = Integer.MAX_VALUE;
        if (need > 0 && need < nums2.length) {
            val1 = nums2[need - 1];
            val2 = nums2[need];
        } else if (need == 0) {
            val2 = nums2[need];
        } else if (need == nums2.length) {
            val1 = nums2[need - 1];
        } else {
            val1 = Integer.MAX_VALUE;
        }
        
        // 如果 nums2[need] 比 v 大,nums2[need - 1] 比 v 小,則 nums2 中恰好有 need 個元素比 v 小    
        if (val2 >= v && val1 <= v) {
            return 0;
        } else if (val2 < v) {
            return 1;
        } else {
            return -1;
        }
    }
    
    /**
     * 二分查找中位數
     * 
     * @param mid 二分查找中點
     * @param k 中位數是第 k 位元素
     * @param nums1 數組 1
     * @param nums2 數組 2
     * @return 如果 mid 元素是中位數,依照數組長度奇偶,計算中位數結果,否則用 Double.MAX_VALUE 和 Double.MIN_VALUE 表示大了還是小了
     */
    private double binarySearch(int mid, int k, int[] nums1, int[] nums2) {
        // nums2 中需要恰好有 k - mid 個元素比 nums1[mid] 小
        int need = k - mid;
        int cmp = compare(nums1[mid], need, nums2);
        if (cmp == 0) {
            // 計算中位數
            if ((nums1.length + nums2.length) % 2 == 0) {
                if (need == 0) {
                    // 較大中位數恰好是 nums1 的第 k 位數,nums2 中沒有比中位數小的元素
                    return (nums1[mid] + nums1[mid - 1]) / 2.0;
                } else if(mid == 0) {
                    // 較大中位數恰好是 nums1 的首元素,較小中位數必然在 nums2 中
                    return (nums1[mid] + nums2[need - 1]) / 2.0;
                } else {
                    // 較小中位數可能是在 nums1 或 nums2 中,比較兩個數的大小確定
                    return (Math.max(nums1[mid - 1], nums2[need - 1]) + nums1[mid]) / 2.0;
                }
            } else {
                return nums1[mid];
            }
        } else if (cmp == 1) {
            return Double.MAX_VALUE;
        } else {
            return Double.MIN_VALUE;
        }
    }
}

這個代碼跑起來速度10ms~14ms不等,一般是12ms左右

覺得自己這個算法寫起來十分冗長, 不優雅,參考了別人的算法,用的另一種二分查找思路,不是在nums1和nums2裏查找,而是在k上進行二分查找

第一,查找第k位數,數組中有k個元素比它小,這k個元素分佈在nums1和nums2中,必然有一個數組中比中位數小的元素個數較大,且數量大於等於k/2個,相當於每次二分查找我們可以確定k/2個比中位數小的數字在哪。例如下圖中,一共9個元素,中位數是第4位元素5,在數組2中有3個元素比5小,超過了4/2,確定了4/2個元素比中位數小,下次查找就可以排除灰色的範圍

第二,怎麼判斷比中位數小的元素在哪個數組裏分佈的數量更多?比較一下兩個數組的第k/2個元素,比較小的元素所在數組,必然有k/2個元素比中位數小

代碼如下,比我的快1~2ms,簡潔

/**
 * 尋找兩個有序數組的中位數
 * Median of Two Sorted Arrays
 *
 * @author DongWei
 * @date 2019/5/31
 */
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;

        int target = ((m + n) >> 1) + 1;
        if ((m + n) % 2 == 0) {
            return (getKth(target, 0, 0, nums1, nums2) + getKth(target - 1, 0, 0, nums1, nums2)) / 2.0;
        } else {
            return getKth(target, 0, 0, nums1, nums2);
        }
    }

    /**
     * 查找第 k 位數
     *
     * @param k 查找第 k 位數
     * @param lo1 nums1 的查找起始範圍
     * @param lo2 nums2 的查找起始範圍
     * @param nums1 數組 1
     * @param nums2 數組 2
     * @return 第 k 位數
     */
    private double getKth(int k, int lo1, int lo2, int[] nums1, int[] nums2) {
        // 如果一個數組的查找起始範圍超過了下標範圍,說明第 k 位數在另一個數組裏
        if (lo1 >= nums1.length) return nums2[lo2 + k - 1];
        if (lo2 >= nums2.length) return nums1[lo1 + k - 1];

        // 查找第 1 位數,比較大小返回
        if (k == 1) {
            return Math.min(nums1[lo1], nums2[lo2]);
        }
        // 判斷哪個數組中有 k / 2 個元素小於中位數
        // 如果數組的剩餘長度小於 k / 2,用 Integer.MAX_VALUE 處理,比較大小後判定結果一定是另一個數組
        int value1 = Integer.MAX_VALUE;
        int value2 = Integer.MAX_VALUE;
        if (lo1 + (k >> 1) <= nums1.length) value1 = nums1[lo1 + (k >> 1) - 1];
        if (lo2 + (k >> 1) <= nums2.length) value2 = nums2[lo2 + (k >> 1) - 1];
        // 比較一下兩個數組的第 k/2 個元素,比較小的元素所在數組,必然有 k/2 個元素比中位數小
        if (value1 < value2) {
            return getKth(k - (k >> 1), lo1 + (k >> 1), lo2, nums1, nums2);
        } else {
            return getKth(k - (k >> 1), lo1, lo2 + (k >> 1), nums1, nums2);
        }
    }
}

運行速度9ms~13ms

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