YSMull
<-- algorithm



原题链接

思路

根据中位数的性质,可以把原问题转化为找到一个划分,他把两个数组分别切成左右两个部分,满足性质:

  1. 左部的元素个数要么跟右部元素个数相同,要么左部多一个元素
  2. 左部分最大元素小于等于右部分最小元素

如果两个数组的长度之和是奇数,那么左部分的元素个数会比右部分的元素个数多一个,此时中位数为左部分的最大值;否则是做部分的最大值和右部分的最小值的平均数。

定义问题

上面只是描述了思想,下面我们对这个抽象的思想进行形式化的定义。

我们定义 数组 A 的第 i 个划分 为:从数组 A 的下标 i 之前的空隙处划一刀,其中 $i \in [0, \text{len})$

我们要找到的划分是满足性质 A[i-1] <= B[j] && B[j-1] <= A[i] 的划分,但是这个性质只适合遍历搜索使用,满足这个性质的划分可能有多个。 例如:

A: 12221
B: 12221

对于数组A,我们列出所有的划分,满足性质的是中间的三个划分 [F,T,T,T,F]。

可以看到,这个性质不是一个可以用于二分求解的性质,因为它不单调。因此,我们需要把该性质转化为一个可以方便进行二分查找的性质。

转化问题

通过观察上图的数组 A,如果位置 i 是 第一个 满足 A[i] >= B[j-1](意味着 A[i-1] < B[j]),那么位置 i 正是符合要求的划分。

因此,我们定义可二分搜索的性质为:满足 A[i] >= B[j-1] 的划分。

你可以看到,我们事实上通过 减弱命题 的方式,把一个 [F,T,T,T,F] 的暴力搜索问题,变成了 [F,T,T,T,T] 的二分搜索问题。

【注意】:如果最后没有找到满足这个性质的划分,那么也产生了一个划分,数组 A 全部都被划分到了左半部分,此时,左半部分的最大值也是小于右半部分的最小值的,所以这个划分必然是合法的。

代码实现

为了减少一些心智负担,使我们即便过了很久之后,也能把这个算法一次写对。我们先对两个数组进行交换,减少二分区间的起始坐标的计算。 然后就是边界情况处理了,不再赘述。
该实现的二分搜索部分,是一个标准的左闭右开False/True二分模板,不需要过脑袋直接写即可。

use std::mem::swap;
use std::cmp::{max, min};

impl Solution {
    pub fn find_median_sorted_arrays(mut nums1: Vec<i32>, mut nums2: Vec<i32>) -> f64 {
        if nums1.len() > nums2.len() {
            swap(&mut nums1, &mut nums2);
        }
        return Solution::find_median_sorted_arrays_0(&nums1, &nums2);
    }
    
    fn check(a: &Vec<i32>, b: &Vec<i32>, total_left: &usize, mid: usize) -> bool {
        let j = total_left - mid;
        return a[mid] >= b[j - 1];
    }

    fn find_median_sorted_arrays_0(a: &Vec<i32>, b: &Vec<i32>) -> f64 {
        let total_left = (a.len() + b.len() + 1) / 2;
        
        // 无脑的标准左闭右开二分,返回满足性质的第一个位置
        let mut l = 0;
        let mut r = a.len();
        while l < r {
            let mid = l + (r - l) / 2;
            if Solution::check(&a, &b, &total_left, mid) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }

        let i = l;
        let j = total_left - i;
        // 边界处理
        let a_left_max = if i == 0 { i32::MIN } else { a[i - 1] };
        let a_right_min = if i == a.len() { i32::MAX } else { a[i] };
        let b_left_max = if j == 0 { i32::MIN } else { b[j - 1] };
        let b_right_min = if j == b.len() { i32::MAX } else { b[j] };
        if (a.len() + b.len()) % 2 == 0 {
            return (max(a_left_max, b_left_max) + min(a_right_min, b_right_min)) as f64 / 2.0;
        } else {
            return max(a_left_max, b_left_max) as f64;
        }
    }
}

转化为 find_kth 问题

用同样的思路,可以实现 find_kth,寻找两个有序数组归并后第 k 个数。

划分的时候,保证左半部分总是有k个元素,并且左半部分的元素小于右半部分的元素。

不过要注意的是,二分的时候不一定能从较短的数组的第0个元素开始切。

use std::mem::swap;
use std::cmp::{max};

impl Solution {
    pub fn find_median_sorted_arrays(mut nums1: Vec<i32>, mut nums2: Vec<i32>) -> f64 {
        if nums1.len() > nums2.len() {
            swap(&mut nums1, &mut nums2);
        }
        let total = nums1.len() + nums2.len();
        if total % 2 == 0 {
            let l = Solution::find_kth(&nums1, &nums2, total / 2);
            let r = Solution::find_kth(&nums1, &nums2, total / 2 + 1);
            return (l + r) as f64 / 2.0;
        } else {
            return Solution::find_kth(&nums1, &nums2, total / 2 + 1) as f64;
        }
    }

    fn check(short_nums: &Vec<i32>, long_nums: &Vec<i32>, k: &usize, mid: usize) -> bool {
        let j = k - mid;
        return short_nums[mid] >= long_nums[j - 1];
    }

    fn find_kth(a: &Vec<i32>, b: &Vec<i32>, k: usize) -> i32 {
        // 起始位置计算
        let mut l = if k > b.len() { k - b.len() } else { 0 };
        let mut r = a.len();
        // 标准二分模板
        while l < r {
            let mid = l + (r - l) / 2;
            if Solution::check(&a, &b, &k, mid) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        let i = l;
        let j = k - i;
        // 边界处理
        let a_left_max = if i == 0 { i32::MIN } else { a[i - 1] };
        let b_left_max = if j == 0 { i32::MIN } else { b[j - 1] };
        return max(a_left_max, b_left_max);
    }
}