寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组nums1和 nums2。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为 O(log (m+n)) 。

示例 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

Problem: 4. 寻找两个正序数组的中位数(复杂度O(log(n))解法)
思路

解题方法

第一步:裁剪

第二步:插入

第三步:异常处理

较长数组的裁剪后长度小于4呢?给定数组长度本来就为2或1呢?
其中一个空数组呢?
都是空数组呢?
思路
基于中位数的特点:两个升序数组合并排序后的数组的中位数,在两个数组分别取得的中位数的范围之间。通过这个性质,可以对于两数进行适当裁剪,将精简后较短的数组进行插入。

解题方法
举例说

数组1: 1, 2, 9, 10
数组2: 3, 4, 5, 6, 7
求两数组的中位数:

数组1: 5.5
数组2: 5
第一步:裁剪
由此可知,组合后中位数的范围是5-5.5。

由于数组2中位数 < 数组1中位数,因此可以对数组2的小数区(左边)和数组1的大数区(右边)进行裁剪。

怎么剪?

对于奇数数组(个数为奇数的数组),保留中间的那个数,中间往左(或往右,根据另一个数组的中位数大小而定)的所有数都可以剪掉,因为不影响中位数。

对于偶数数组,保留中间两个数,同理,中间两数往左(或往右)的数尽管剪。

然而,两个数组的裁剪量必须一致,否则组合后位置会失去平衡,因此,哪个数组可裁剪量小,就按那个量裁剪。

根据裁剪性质得出,数组1能够裁剪的量为1,数组2能够裁剪的量为2,取最小裁剪量为1。

我们就将数组1的右边大数区裁剪1个,数组2的左边小数区裁剪1个,得到

数组1: 1, 2, 9
数组2: 4, 5, 6, 7
数组1中位数: 2
数组2中位数: 5.5
中位数范围: 2 - 5.5
可以看到,裁剪后的两个数组依然遵循这个规律,因为其本质还是一个数组的拆分,以中位数为中心均匀裁剪,不影响组合后的中位数。

根据这个规律,我们可以继续裁剪。数组1可裁剪量为1,数组2可裁剪量为1,我们再将超出范围的一个数剪掉。裁剪后:

数组1: 2, 9
数组2: 4, 5, 6
中位数范围: 5 - 5.5
裁剪工作到这里就结束了。为什么?因为数组1已经达到了最小长度2。这个偶数数组实现了存储了中位数信息的最小单位,一旦再剪,中位数信息将丢失。此时将两个裁剪后的数组按序组合的数组中位数和原来两数组按序组合的中位数是一样的,都是5。

从上述过程中能看出,不管多长的数组,最终都能够以二分法裁剪为长度为2,储存中位数信息的偶数数组。这个步骤已经完成了时间复杂度的消耗,为O(log(n))。接下来的操作全部为O(1)。为什么?我接着讲。

第二步:插入
裁剪后两个数组有长有短(就算一样长也没关系)。其中至少有一个数组已经裁剪为2个数了。将这两个数插入到另一个长的数组,进行排序组合,就可以得到中位数。

很疑惑?时间复杂度不是为O(1)吗?怎么开始按序插入了,这不是又增加了O(m-n)的复杂度了吗?

很巧妙的是,这题只求中位数。因此,如果这个数超出了中间几个数的范围,那么插到左侧(或右侧)的任何位置都没关系,都不影响中位数的取值。听不懂?我举个例子:

裁剪后的两数组:
短的: 1, 999
长的: 2, 4, 6, 8, 10, 12, 14
将短的插入两头后: 1, 2, 4, 6, 8, 10, 12, 14, 999
往里推一点: 2, 1, 4, 6, 8, 10, 12, 999, 14
右边的再往里推一点: 2, 1, 4, 6, 8, 10, 999, 12, 14
发现没有?只要这两个数不挤到中间,中位数都是8,没什么影响,因为它们在中心元素4, 6, 8, 10的范围之外。只要他们按大小分别插在4的左边和10的右边,怎么插不影响。

这说明什么呢?说明只有插入数的大小在中心的几个数的范围内时才需要严格按顺序,其它大小的数随便插入。

中心几个数的半径是多大呢?按照插入的个数来定。设最中心的数索引为mid=size/2,那么我们插入两个,中心的范围就是[mid-2, mid+1]的这4个数。

插入数大小如果不在这四个数的范围内,根据大小(小于mid-2或大于mid+1),默认直接插到头部或尾部,在这四个数的范围内则按序插入,4毕竟是常数,按序插入也不会增加复杂度,该操作(参见brief_insert_ascend函数)消耗为O(2x6)=O(1)

第三步:异常处理
较长数组的裁剪后长度小于4呢?给定数组长度本来就为2或1呢?此时,使用纯粹的按序插入(insert_ascend函数),量不大所以不影响复杂度。其中一个空数组呢?直接返回另一个非空数组的中位数。都是空数组呢?测试用例没有,大概用不上。

#include <algorithm>
#include <vector>

class Solution {
  public:
    double findMedianSortedArrays(std::vector<int>& nums1,
                                  std::vector<int>& nums2) {
        if (nums1.empty()) {
            return get_median_number(nums2, 0, nums2.size());
        } else if (nums2.empty()) {
            return get_median_number(nums1, 0, nums1.size());
        }
        double result = 0.0;
        int begin1 = 0, end1 = nums1.size(), begin2 = 0, end2 = nums2.size();
        // trim extra number until one of them reduce to 2 numbers
        // further reduce will interrupt median number
        // this loop has O(log(n)) complexity, n is the smaller one
        while (begin1 < end1 - 2 && begin2 < end2 - 2) {
            double mid_num1 = get_median_number(nums1, begin1, end1);
            double mid_num2 = get_median_number(nums2, begin2, end2);
            int trim1 = get_trim_length(begin1, end1);
            int trim2 = get_trim_length(begin2, end2);
            // trim the same size of both list to keep balance
            int trim_len = std::min(trim1, trim2);
            // trim the left side(of median point) of smaller one(judge by
            // median value) and the right side of bigger one because they
            // doesn't affect median number
            if (mid_num1 < mid_num2) {
                begin1 += trim_len;
                end2 -= trim_len;
            } else {
                end1 -= trim_len;
                begin2 += trim_len;
            }
        }
        // create 2 vector slice based on index
        std::vector<int> v1(nums1.begin() + begin1, nums1.begin() + end1),
            v2(nums2.begin() + begin2, nums2.begin() + end2);
        // first one is trimmed below 2
        if (begin1 >= end1 - 2) {
            if (v2.size() < 4) {
                for (auto& i : v1) {
                    insert_ascend(v2, i);
                }
            }
            // if second one is bigger than 4, brief_insert_ascend has O(1)
            // complexity
            // this function use direct insert because it doesn't
            // affect median number
            else {
                for (auto& i : v1) {
                    brief_insert_ascend(v2, i);
                }
            }
            return get_median_number(v2, 0, v2.size());
        }
        // second one is trimmed below 2
        else if (begin2 >= end2 - 2) {
            if (v1.size() < 4) {
                for (auto& i : v2) {
                    insert_ascend(v1, i);
                }
            }
            // if first one is bigger than 4, this function has O(1) complexity
            else {
                for (auto& i : v2) {
                    brief_insert_ascend(v1, i);
                }
            }
            return get_median_number(v1, 0, v1.size());
        }
        // preserve a result in case it fails
        return result;
        // In summary, this solution has O(log(n)) complexity when size becomes
        // big
    }
    double get_median_number(const std::vector<int>& nums, int begin, int end) {
        int mid = (end - begin) / 2;
        if ((end - begin) % 2 == 0) {
            return double(nums[begin + mid] + nums[begin + mid - 1]) / 2.0;
        } else {
            return double(nums[begin + mid]);
        }
    }
    int get_trim_length(int begin, int end) {
        if ((end - begin) % 2 == 0) {
            return (end - begin) / 2 - 1;
        } else {
            return (end - begin) / 2;
        }
    }
    void insert_ascend(std::vector<int>& nums, int val) {
        if (val < nums[0]) {
            nums.insert(nums.begin(), val);
            return;
        }
        if (val >= nums.back()) {
            nums.insert(nums.end(), val);
            return;
        }
        for (int i = 0; i < nums.size() - 1; ++i) {
            if (nums[i] <= val && val < nums[i + 1]) {
                nums.insert(nums.begin() + i + 1, val);
                return;
            }
        }
    }
    void brief_insert_ascend(std::vector<int>& nums, int val) {
        int mid = nums.size() / 2;
        if (val < nums[mid - 2]) {
            nums.insert(nums.begin(), val);
            return;
        }
        if (val >= nums[mid + 1]) {
            nums.insert(nums.end(), val);
            return;
        }
        for (int i = mid - 2; i < mid + 1; ++i) {
            if (nums[i] <= val && val < nums[i + 1]) {
                nums.insert(nums.begin() + i + 1, val);
                return;
            }
        }
    }
};

复杂度
时间复杂度:
O(log(n)), 其中n为较短的数组

空间复杂度:O(m-n+log(n))

  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wanderer001

ROIAlign原理

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值