给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m n))
。
示例 1:
代码语言:javascript复制输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
代码语言:javascript复制输入: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呢? 其中一个空数组呢? 都是空数组呢?(手动滑稽) 复杂度
Code
结语(吐槽)
思路 基于中位数的特点:两个升序数组合并排序后的数组的中位数,在两个数组分别取得的中位数的范围之间。通过这个性质,可以对于两数进行适当裁剪,将精简后较短的数组进行插入。
解题方法 举例说
数组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函数),量不大所以不影响复杂度。
其中一个空数组呢? 直接返回另一个非空数组的中位数。
都是空数组呢?(手动滑稽) 测试用例没有,大概用不上。
复杂度 时间复杂度: O(log(n)), 其中n为较短的数组
空间复杂度: O(m-n log(n))
代码语言:javascript复制#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;
}
}
}
};