您的位置:首页 > 编程语言 > C语言/C++

[Leetcode 4] Median of Two Sorted Arrays

2016-07-31 22:52 393 查看
原题见leetcode 4。这题分简单版(长度相同)、复杂版(长度不同)。leetcode原题是复杂版,这里都会介绍。

求相同长度两个有序数组的中位数

基本思想是二分查找,分别找到两个数组的中位数(O(1)),然后根据它们之间的关系来决定丢弃数组的哪一半。每次丢掉一半,直到找到中位数,或者其中一个数组长度为2(base case,这种情况下需要计算两个长度都为2的数组的中位数,时间复杂度O(1))。总的时间复杂度为log(n)。

设数组A1、A2的中位数分别为m1、m2,合并后的数组A的中位数为m。考虑三种情况:

1. m1 = m2

(1)n为奇数。则m=(m1+m2)/2=m1。

(2)n为偶数(这里采用的中位数定义是中间两位数的算术平均值)。假设A1中间两位是a1, a2,A2中间两位是a3, a4,那么合并后A的中间四位只可能是a1, a3, a4, a2或者a3, a1, a2, a4。否则,可以反证m1不可能等于m2。所以,m=m1。

2. m1 < m2

(1)n为奇数。m1左侧的元素与m2右侧的元素都不可能是中位数。所谓中位数,就是靠近数组中间最近的元素。因为m1<m2,那么这些元素靠近中心最近的结构就是(m1左侧元素), m1, m2, (m2右侧元素)。即使在这种情况下,括号中的元素也绝不可能成为中位数。而且,当m1与m2距离越来越远时,括号中的元素也与中心越来越远。分析下来,这种情况下,中位数只可能出现在子数组A1[m1,
..]与A2[.., m2]中。可以用递归解决。注意,需要包含m1与m2,因为它们可能是中位数。还需要注意的是,这一定是non-trivial division,即划分后的数组一定比原数组小。因为一定会至少抛弃一个元素,考虑长度为3的数组。所以,递归的base class不需要考虑n=3的情况,因为3一定会划分成长度为1和2的子数组(并且丢弃1,选择2)。那么base class需要考虑长度为2的数组吗?见下。

(2)n为偶数。类似(1)的分析,A1[.., m1)与A2(m2, ..]中的元素都不可能是中位数。而且a1, a2, a3, a4中的任何数都可能是中位数,比如A中的顺序可能为:……a1, a2, a3, a4……,……a1, a3, a2, a4……,……a3, a1, a2,
a4……,……a1, a3, a4, a2……等。所以,中位数只可能出现在子数组A1[m1, ..]与A2[.., m2]中。如果n=4,则会分成长度分别为1、3的数组,然后在两个长度为3的数组上递归。如果n=2,因为中位数包含了全部的两个元素,如果按照上面的规则,会继续在长度为2的两个数组上递归下去,无限循环!这就是trivial division,因为可能出现一种情况导致递归时并没有把问题规模减小(哪怕是一个元素)。所以,当n=2时,不能继续递归下去,必须把它作为base case来处理。长度为2的两个有序数组[a1,
a2], [a3, a4]的中位数是:(max(a1, a3) + min(a2, a4)) / 2。

3. m1 > m2

类似2的分析,只是把m1与m2交换一下。

代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
#include <cassert>

using namespace std;

class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
{
return getMedian(nums1, 0, nums1.size() - 1, nums2, 0, nums2.size() - 1);
}

private:
double getMedian(vector<int>& nums1, int s1, int t1, vector<int>& nums2, int s2, int t2)
{
// assure equal size
assert(t1 - s1 == t2 - s2);

int n = t1 - s1 + 1;

// error handling
if (n <= 0)
return -1;

// check original input array size
if (n == 1)
return (nums1[s1] + nums2[s2]) / 2;

// base case
if (n == 2)
{
return (max(nums1[s1], nums2[s2]) + min(nums1[t1], nums2[t2])) / 2;
}

double m1 = median(nums1, s1, t1);
double m2 = median(nums2, s2, t2);

if (m1 == m2)
return m1;

// recursion
if (m1 < m2)
{
if (n % 2 == 0)
return getMedian(nums1, s1 + n/2-1, t1, nums2, s2, s2 + n/2);
else
return getMedian(nums1, s1 + n/2, t1, nums2, s2, s2 + n/2);
}
else
{
if (n % 2 == 0)
return getMedian(nums1, s1, s1 + n/2, nums2, s2 + n/2-1, t2);
else
return getMedian(nums1, s1, s1 + n/2, nums2, s2 + n/2, t2);
}
}

// return median of a single non-empty sorted array
double median(vector<int>& nums, int s, int t)
{
int n = t - s + 1;
if (n % 2 == 0)
{
return (nums[s + n/2 - 1] + nums[s + n/2]) / 2;
}
else
{
return nums[s + n/2];
}
}
};

在递归函数getMedian()中,虽然根据前面的分析,不会出现n=1的base case(不会通过递归访问到长度为1的数组),但加上n=1的原因是考虑到原始输入数组的大小。还有种方法是放在包装函数findMedianSortedArrays()中判断,好处是不用每次递归都判断一次。

求不同长度两个有序数组的中位数

基本思路还是一样,区别是数组长度不一样,所以可能最后一个数组很短,另一个数组很长,需要更新几种情况。

1. 短数组长度为0。返回长数组的中位数。

2. 短数组长度为1,A1  = {a1}

(1)长数组长度也为1

(2)长数组长度为2,A2 = {a2, a3}。m = median(a1, a2, a3)

(3)长数组长度为大于2的奇数,A2 = {……, a2, a3, a4, ……}(a3是中位数)

o 如果a1落在[a1, a2]或[a2, a3]之间,那么m = (a1 + a3) / 2

o 如果a1落在[……, a2)之间,m = (a2 + a3) / 2

o 如果a1落在(a4, ……]之间,m = (a3 + a4) / 2

a3在每种情况下都出现,区别是其他三个元素。不难得出:m = (a3 + median(a1, a2, a4)) / 2

(4)长数组长度为大于2的偶数,A2 = {……,
a2, a3, a4, a5, ……}((a3, a4)/2是中位数)

 
类似(3)可得,m = median(a1, a3, a4)

回顾下(2),发现其实是(4)的特殊情况,所以可以和(4)合并。

3. 短数组长度为2,A1 = {a1,
a2}

(1)长数组长度也为2,A2
= {a3, a4}。m = median(max(a1, a3), min(a2, a4))

(2)长数组长度为大于2的奇数,A2
= {……, a3, a4, a5, ……}(a4是中位数)

一共7种情况,可以规约成m
= median(max(a1, a3), a4, min(a2, a5))。如果得不出这公式,写出7种情况也行。

(3)长数组长度为大于2的偶数,A2
= {……, a3, a4, a5, a6, ……}((a4,
a5)/2是中位数)

一共15种情况,根据(2)的公式推测m
= median(max(a1, a3), a4, a5, min(a2, a6)),可以简单证明该公式的正确性。如果得不出该公式也没关系,大不了求一下长度为6的数组的中位数,也是O(1)。

这里我们假设知道哪个是短数组,所以一个小技巧就是在外层的包装函数中先检测好数组长度。在调用递归函数时,保证传入的参数顺序,一定先是短数组。这样,递归函数就可以避免不必要的if-else。下面是代码:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  algorithm leetcode c++