算法-寻找两个正序数组的中位数

问题概述


题目:4.寻找两个正序数组的中位数(LeetCode)

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

输入:
给定一个长度nums1Size为m的正序数组nums1和一个长度nums2Size为n的正序数组nums2
第一行输入m,n
第二行m个数组元素
第三行m个数组元素

约束条件:
nums1Size == m,
nums2Size == n
0 <= m <= 1000,
0 <= n <= 1000,
-106 <= nums1[i], nums2[j] <= 106

输出:
中位数值

样例输入:
2 2
1 2
3 4
样例输出:
2.5


1. 问题分析


1.1 算法核心

本题较为困难,有很多种解法,但是要满足题设要求时间复杂度O(log(m+n))是有难度的,普通解法达不到这样的时间复杂度,而复杂解法则需要一定的数学知识,最重要的就是有关中位数的定义及性质。

中位数的定义及性质:中位数是统计学中的专有名词,代表一个样本、种群或概率分布的一个数值,其可将数值集合划分为上下相等的上下两部分。


1.2 算法分析

本题的算法并不唯一,主要讨论四种算法,
第一种是最简单粗暴的合并法,将两个正序数组合并为一个正序数组后直接查找中位数即可。
第二种是查找法,不合并给定的正序数组,而是在循环中遍历数组的元素,直到我们找到中位数,比如两个正序数组共有9个数就要遍历到第5小的数时返回中位数结果。
第三种是二分递归法,这种算法是基于了二分查找第k小数的思想来查找中位数的,这是因为本题的查找中位数本质上就是查找第k小数的一种特殊情况,比如对于奇数k而言,实际上我们就是查找第(k+1)/2小的数。
第四种就是数组切分法,这种方法是建立在数论中对数字的研究分析,对数字本身的定义以及性质的应用。中位数是统计学中的专有名词,代表一个样本、种群或概率分布的一个数值,其可将数值集合划分为上下相等的上下两部分。
算法复杂度如下表1-1所示:
算法表
从表上就可以看出,合并和查找法并不满足问题的约束条件的时间复杂度,而二分查找第k小数的算法也比较简单,因此就不在本文再进行赘述,本文着重描述最后一种算法数组切分法,且给出算法实现。


2. 数组切分法

2.1 算法概述

数组切分的意思是将nums1和nums2两个正序数组分别在第i(0 <= i <= m)和第j(0 <= j <= n)的位置进行切分,把i和j左边的部分组合成左半部分,右边的组合成右半部分。

1.当nums1Size和nums2Size(两个正序数组总长度)加起来是偶数时,如果满足以下条件:
(1) 左半部分的长度等于右半部分,
即j = (m + n) / 2 – i
(2) 左半部分的最大值小于等于右半部分的最小值,
即max(nums1[i-1], nums2[j-1]) <= min(nums1[i],nums2[j])
那么中位数就可以表示如下:
(左半部分最大值+右半部分最小值)/ 2,
即(max(nums1[i-1], nums2[j-1]) + min(nums1[i],nums2[j])) / 2

2.当两个正序数组总长度加起来是奇数时,如果满足以下条件:
(1) 左半部分的长度比右半部分大1,
即j = (m + n + 1) / 2 – i
(2) 左半部分的最大值小于等于右半部分的最小值,
即max(nums1[i-1], nums2[j-1]) <= min(nums1[i],nums2[j])
那么中位数就可以表示如下:
左半部分最大值,也就是左半部分比右半部分多出来的那一个数,
即max(nums1[i-1],nums2[j-1])

以上所述的两种情况就是根据中位数的性质得出来的算法分析要点,但在计算机程序设计中,因为int型数据会截断小数数值,因此实际程序设计中我们可以将两种情况的第一个条件合并为一个通用条件:
j = (m + n + 1) / 2 – i
这是因为如果是偶数,由于是int型数据所以加1后也不影响j的最终值。
但是需要注意的一个点是由于0 <= i <= m,为了保证0 <= j <= n,我们在程序设计时还必须要保证m <= n,因此要注意第一个数组的长度一定要小于等于第二个数组的长度。
除此之外还要注意就是第二个条件中左半部分的最大值要小于等于右半部分的最小值,但两个数组本来就是有序的数组,因此nums1[i-1]一定是小于等于nums1[i]的,实际上比较的过程中只用考虑nums1[i-1]是否小于等于nums2[j]即可,同理nums2[j-1]也只需要与nums1[i]比较。

2.2 深入分析

在上一小节分析中我们知道了数组切分法需要满足以下两个条件:
(1) 不论数组总长度为偶数还是奇数,左右两半部分都要满足
j = (m + n + 1) / 2 – i
(2) 左半部分的最大值小于等于右半部分的最小值,
即max(nums1[i-1], nums2[j-1]) <= min(nums1[i],nums2[j])
当满足这两个条件时,根据数组总长度为偶数还是奇数分别给出中位数的值:
(1) 如果总长为偶数,那么中位数为,
(max(nums1[i-1], nums2[j-1]) + min(nums1[i],nums2[j])) / 2
(2) 如果总长为奇数,那么中位数为,
max(nums1[i-1], nums2[j-1]) <= min(nums1[i],nums2[j])

接下来深入讨论算法实际设计中遇到的不同情况,先考虑一般情况,就是i和j划分的位置不在数组的头也不在数组的尾,也即考虑i != 0 && i != m以及j != 0 && j != n的情况。共有两种情况如下:

  1. 如果nums1[i-1] > nums2[j],那么就需要i往左移(i减小),j往右移(j增大)
  2. 如果nums2[j-1] > nums1[i],那么就需要i往右移(i增大),j往左移(j减小)

除此之外还要考虑边界条件,就是如果i的切分位置在数组的头部位置,就是i=0时,以及i的切分位置在数组的尾部位置,就是i=m的两种边界条件,类似的第二个数组的边界条件为j=0以及j=n。

这四种情况无非就是两个数组是否在头或者尾进行切分的特殊情况,在程序设计中,可以利用三目运算符判断当前切分的位置是否是头或者是尾,如果是nums1[0]的时候,能发现nums1全部切分到右半部分了,因此左半部分的最大值就是nums2[j-1],在进行第二个条件判别时因为nums1[i-1]没有值(已经越界了)但我们为了设计简洁又需要一个值来进行比较,就在这种情况下将nums1[i-1](并不是直接访问nums1[i-1]的值,i-1这时候是越界的,程序设计时应该是将nums1[i-1]这个值赋值给一个中间变量参与比较)的中间变量(称为iLeft)值设置为C库中的int型最小值INT_MIN,这样第二个条件就不会因为越界问题而导致程序出错,同理可知j=0时也是同样的处理。
同理换位思考,如果是nums2的数组全部切分到左半部分了,也就是j=n的情况,要给nums2的中间变量(称为jRight)赋值为nums2[j]显然越界了,因此同样的将jRight设置为一个特殊值int型的最大值INT_MAX,同理可知i==m时也是同样的处理。
综上所述,实现原理已经陈述完整,接下来是代码实现。

3. 解决方案

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <string>
#include <istream>
#include <cmath>
using namespace std;

/*
算法问题:寻找两个正序数组的中位数
问题描述:
	给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
    请你找出并返回这两个正序数组的中位数 。
    算法的时间复杂度应该为 O(log (m+n)) 。
输入:
    第一行输入m,n
	第二行m个数组元素
	第三行m个数组元素
输出:
	第四行输出中位数
样例输入:
    2 2
    1 2
    3 4
样例输出:
	2.5
*/

double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size);

int main(void)
{
    int nums1[] = {1, 3};
    int nums2[] = {0, 2, 7, 8};
    int nums1Size = 2;
    int nums2Size = 4;
    double result = 0;

    result = findMedianSortedArrays(nums1, nums1Size, nums2, nums2Size);
    cout << result << endl;

    return 0;
}

double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size)
{
    if(nums1Size > nums2Size)
        return findMedianSortedArrays(nums2, nums2Size, nums1, nums1Size);

    int len_short = nums1Size;
    int len_long = nums2Size;
    int left = 0;
    int right = len_short;
    int median1 = 0;
    int median2 = 0;

    while(left <= right)
    {
        int i = (left + right) / 2;
        int j = (len_short + len_long + 1) / 2 - i;

        int iLeft = i ? nums1[i - 1] : INT_MIN;
        int iRight = (i == len_short ? INT_MAX : nums1[i]);
        int jLeft = j ? nums2[j - 1] : INT_MIN;
        int jRight = (j == len_long ? INT_MAX : nums2[j]);

        if(iLeft < jRight)
        {
            median1 = fmax(iLeft, jLeft);
            median2 = fmin(iRight, jRight);
            left = i + 1;
        }
        else
            right  =  i - 1;
    }

    return (len_short + len_long) % 2 ? median1 : (median1 + median2) / 2.0;
}

4. 资源分享

旗木白哉のGitHub:
https://github.com/YangMengHeng
GitHub/Gitee源代码下载地址:
GitHub
https://github.com/YangMengHeng/myCode/tree/master/VSCode
https://github.com/YangMengHeng/myCode/tree/master/Java
Gitee
https://gitee.com/QMBZ/myCode/tree/master/VSCode
https://gitee.com/QMBZ/myCode/tree/master/Java
旗木白哉のblog源代码下载地址:
https://github.com/YangMengHeng/myCode/tree/master/%E6%97%97%E6%9C%A8%E7%99%BD%E5%93%89%E3%81%AEblog
https://gitee.com/QMBZ/myCode/tree/master/%E6%97%97%E6%9C%A8%E7%99%BD%E5%93%89%E3%81%AEblog


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值