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

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