【PTA-数据结构】进阶实验1-3.1 两个有序序列的中位数 (25 分)--两个数组的比较输出,分治策略

本文详细介绍了三种求解两个有序序列中位数的算法:二分归并排序、循环顺序比较和分治策略递归实现。针对每种算法,提供了具体的思想、代码实现和复杂度分析。特别是分治策略的算法,可以在O(log_2^N)的时间复杂度内找到中位数。

参考学习:进阶实验1-3.1 两个有序序列的中位数 (25分)
本文为优快云博主「5?li」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/wulila/article/details/108036788

题目

已知有两个等长的非降序序列S1, S2, 设计函数求S1与S2并集的中位数。有序序列A​0​​,A​1​​,⋯,A​N−1​​的中位数指A​(N−1)/2​​的值,即第⌊(N+1)/2⌋个数(A​0​​为第1个数)。
输入格式:

输入分三行。第一行给出序列的公共长度N(0<N≤100000),随后每行输入一个序列的信息,即N个非降序排列的整数。数字用空格间隔。

输出格式:

在一行中输出两个输入序列的并集序列的中位数。

算法0:合并到一个大数组,分治策略的二分归并排序 O ( 2 ∗ N + N l o g 2 N ) ) O(2*N+Nlog_2^N)) O(2N+Nlog2N))

没有实现,只是学习思想
输入:按照递增顺序排好序的数组 A [ p . . q ] A[p..q] A[p..q] A [ q + 1.. r ] A[q+1..r] A[q+1..r]
输出:按照递增顺序排好序的数组 A [ p . . r ] A[p..r] A[p..r]

void MergeSort(A,p,r)
{
    if(p<r)
        q=(p+r)/2
    MergeSort(A,p,q);
    MergeSort(A,q+1,r);
    Merge(A,p,q,r);
}

Merge()函数的具体实现

void Merge(int A[],int p,int q,int r)
{
    int x=q-p+1,y=r-q;
    int i,j,k;
    for(i=0;i<x;++i)//将A[p..q]复制到B[1..x]
        B[i]=A[i+p-1];
    for(j=0;j<y;++j)//将A[q+1..r]复制到C[1..y]
        C[j]=A[q+j];
    i=j=1;k=p;
    while(i<=x&&j<=y)
    {
        if(B[i]<=C[j])
        {
            A[k]=B[i];
            ++i;
        }
        else
        {
            A[k]=C[j];
            ++j;
        }
        ++k;
    }
    if(i>x)//B已是空数组
    {
        for(i=j;i<=y;++i)
            {A[k]=C[i];++k}
    }
    else//C已是空数组
    {
        for(j=i;j<=x;++j)
            {A[k]=B[j];++k}
    }
    
}

算法1:循环顺序数中位数 ( 2 ∗ n − 1 ) / 2 (2*n-1)/2 (2n1)/2个数,然后直接输出中位数

#include <iostream>
using namespace std;
class test
{
private:
    int n,i;
    int* a;
    int* b;
public:
    test(int nn)
    {
        n=nn;
        a=new int[n];
        b=new int[n];
        for(i=0;i<n;++i)
        {
            cin>>a[i];
        }
       for(i=0;i<n;++i)
        {
            cin>>b[i];
        }
    }
    void output()
    {
        int cnt=(2*n-1)/2;
        if(cnt>0)
        {
            int i,j;
            for(i=0,j=0; i<n,j<n;)//两个数组的比较输出
            {
                if(a[i]<b[j])
                {
                    i++;cnt--;
                    if (cnt==0)  break; 
                }
                else
                {
                    j++;cnt--;
                    if (cnt==0)  break; 
                }
            }
            if (a[i]<b[j]) cout<<a[i];
            else cout<<b[j];
        }
        else//特殊处理,输入n=1,直接比较输出
        {
            if(a[0]<b[0])
                cout<<a[0];
            else
                cout<<b[0];
        }
    }
};
int main()
{
    int N;
    cin>>N;
    test t(N);
    t.output();
    return 0;
}

算法2: O ( l o g 2 N ) O(log_2^N) O(log2N) 分治策略:找两个子序列的中位数,递归实现

设定两个序列的左右边界:
序列 S 1 、 S 2 , l 1 、 r 1 S1、S2,l1、r1 S1S2l1r1分别为序列 S 1 S1 S1的左右边界, l 2 、 r 2 l2、r2 l2r2分别为序列 S 2 S2 S2的左右边界。

  1. 分别求两个序列的中位数,特别的,对于偶序列,第二个序列 S 2 S2 S2的中位数为 S 2 [ ( l 2 + r 2 ) / 2 + 1 ] S2[(l2+r2)/2+1] S2[(l2+r2)/2+1],这样使子问题的数目也相等(使得序列 S 1 S1 S1左(右)边和 S 2 S2 S2右(左)边需要去除的数目个数相等)
  2. 比较两个中位数,如果相等,则为所求。
    否则:
  3. 对较大的中位数所在的序列,去掉其后的部分,对于较小的中位数序列,去掉比这个中位数小的部分。将剩下的等长序列,继续递归调用。
  4. 注意:递归基1:序列S1和S2均各只剩1个数字,比较两个数字的大小,输出小的那个数为中位数。
    递归基2:序列 S 1 S1 S1 S 2 S2 S2均各剩下2个数字,先比较 S 1 [ l 1 ] S1[l1] S1[l1] S 2 [ l 2 ] S2[l2] S2[l2]:若 S 1 [ l 1 ] < S 2 [ l 2 ] S1[l1]<S2[l2] S1[l1]<S2[l2],则中位数是 S 1 [ r 1 ] S1[r1] S1[r1] S 2 [ l 2 ] S2[l2] S2[l2]中的小者;若 S 1 [ l 1 ] > S 2 [ l 2 ] S1[l1]>S2[l2] S1[l1]>S2[l2],则中位数是 S 1 [ l 1 ] S1[l1] S1[l1] S 2 [ r 2 ] S2[r2] S2[r2]中的小者。

注意问题:return MidTerm(a,b,mid1,r1,l1,mid2);return 调用的函数中,数组不加[]
注意调用函数,是从0到n-1
另外不能使用类实现,在外部main()函数递归调用,不能使用类内私有变量。

#include <iostream>
using namespace std;

int MidTerm(int a[],int b[],int l1,int r1,int l2,int r2)
{
    if(l1==r1)//递归基1
    {
        if(a[l1]<b[l2])
            return a[l1];
        else return b[l2];
    }
    else if(l1+1==r1)//递归基2
    {
        if(a[l1]<b[l2])
        {
            if(a[r1]<b[l2]) return a[r1];
            else return b[l2];
        }
        else 
        {
            if(b[r2]<a[l1]) return b[r2];
            else return a[l1];
        }
    }
    else
    {
        int mid1=(l1+r1)/2;
        int mid2=(l2+r2+1)/2;
        if(a[mid1]==b[mid2])
        {
            return a[mid1];
        }
        else
        {
            if(a[mid1]<b[mid2])
                return MidTerm(a,b,mid1,r1,l2,mid2);//递归调用
            else
                return MidTerm(a,b,l1,mid1,mid2,r2);//递归调用
        }
    }
}
int main()
{
    int n,i;
    cin>>n;
    int* a;
    int* b;
    a=new int[n];
    b=new int[n];
    for(i=0;i<n;++i)
    {
        cin>>a[i];
    }
   for(i=0;i<n;++i)
    {
        cin>>b[i];
    }
    cout<<MidTerm(a,b,0,n-1,0,n-1);
    return 0;
}
<think>好的,我现在要解决的问题是如何找到两个已排序数组合并后的中位数。首先,我需要明确中位数的定义。对于一个有序数组来说,如果长度是奇数,中位数就是中间的那个数;如果是偶数,就是中间两个数的平均值。那对于两个数组合并后的情况,应该也是同样的逻辑,但合并后的数组可能很大,直接合并的话时间复杂度会很高,特别是当数组很大时,这样的方法效率太低。 我记得用户提到过最有效的方法是使用二查找,这可能涉及到分治或者类似的策略。让我先想想直接合并的方法。假设数组nums1的长度是m,nums2的长度是n,合并后的数组长度是m+n。如果我能找到第k小的元素,那么当m+n是奇数时,k就是(m+n)//2 +1,偶数的话可能需要找到第k和k+1的元素取平均。但直接合并需要O(m+n)的时间,对于大数组来说可能不够高效。 那如何用更高效的方法呢?比如二查找。这里可能需要比较两个数组中位数,然后排除不可能的部。例如,比较两个数组中位数,假设nums1中位数是mid1,nums2的是mid2。如果mid1 <= mid2,那么nums1的前半部可能可以排除,反之亦然。这样每次可以排除掉一部元素,从而减少需要处理的数据量。这种方法的时间复杂度应该是对数级别的,比如O(log(min(m,n)))。 不过具体的实现细节可能比较复杂。比如,如何确定每次排除多少个元素?如何确保在数组长度不同的情况下正确处理边界条件?例如,当其中一个数组的所有元素都被排除后,如何处理剩下的数组? 另外,需要处理奇偶长度的问题,以及如何找到第k小的元素。可能需要递归或循环地调整k的值,每次排除一部元素,直到k减到1或者某个基本情况。 例如,假设要找两个数组中第k小的元素,可以比较两个数组中第k/2个元素的大小。如果A[k/2-1] < B[k/2-1],那么可以排除A的前k/2个元素,因为它们不可能是第k小的元素。然后将问题转化为在剩下的元素中找第k -k/2小的元素。这样的过程需要处理当某个数组长度不足k/2的情况,这时候可能需要取最后一个元素进行比较,然后排除可能的元素。 举个例子,假设A的长度是m,B的长度是n,且m <=n。要找第k小的元素。比较A[i]和B[j],其中i = min(k//2, m),j = k -i。如果A[i-1] <= B[j-1],则排除A的前i个元素,k -=i,否则排除B的前j个元素,k -=j。这样不断缩小范围,直到k=1或者其中一个数组被排除完。 这样的算法需要处理很多边界情况,比如当其中一个数组为空时,直接返回另一个数组的第k个元素。或者当k=1时,返回两个数组当前剩余元素中的较小者。 那在具体实现时,可能需要编写一个辅助函数来递归或迭代地处理这个问题。此外,需要考虑奇偶合并后的长度来确定需要找的是第几个元素。 例如,合并后的总长度是m+n,如果总长度是奇数,中位数是第(m+n+1)//2小的元素;如果是偶数,则是第(m+n)//2和第(m+n)//2 +1两个元素的平均值。 所以整个算法的步骤可能如下: 1. 确定总长度是奇数还是偶数,决定要找的位置。 2. 使用二查找的方法找到相应的第k小元素。 3. 处理各种边界情况,如某个数组为空,或者k超出某个数组长度的情况。 现在,我需要考虑如何将这个思路转化为具体的代码,比如Python实现。可能需要处理索引的问题,避免越界,同时正确排除元素。 举个例子,假设nums1 = [1,3], nums2 = [2]。合并后的数组是[1,2,3],中位数是2。使用二的方法,k=2(因为总长度3,奇数是第2个元素?可能需要再确认索引的计算)。 或者另一个例子,nums1 = [1,2], nums2 = [3,4],合并后的中位数(2+3)/2=2.5。这时需要找到第2和第3个元素。 可能需要注意,当总长度是偶数时,需要两次调用找第k和k+1的函数,或者是否有更高效的方式? 不过在实际中,可能可以将两种情况合并处理,比如不管奇偶都计算两个位置,然后取平均。如果总长度是奇数,两次调用的结果可能相同,或者只需要计算一次。 然后,关于代码实现,可能需要写一个函数来找到第k小的元素。例如: def find_kth(nums1, nums2, k): # 处理边界情况,其中一个数组为空的情况 if len(nums1) == 0: return nums2[k-1] if len(nums2) == 0: return nums1[k-1] # 其他情况,比较i和j的元素 # 确保i + j =k,这里i取min(k//2, len(nums1)), j =k -i 这样的函数需要处理递归或迭代的情况。或者,可以用循环来不断缩短数组的搜索范围。 不过,这样的递归可能导致栈溢出,如果数组很大,但log级别应该没问题。 另一个问题是如何处理当k比两个数组的长度之和还大,但根据问题设定,中位数的位置不会超过总长度,所以调用时应该没问题。 现在,我需要验证这个思路是否正确。例如,测试一些例子: 测试案例1: nums1 = [1,3], nums2 = [2], 总长度3中位数是2。 调用函数找第2小的元素: 初始k=2. 比较i=1(k//2=1), nums1[0] =1, nums2[0] =2。因为1<=2,排除nums1的前1个元素,剩下的nums1是[3],k=2-1=1。现在找第1小的元素,比较nums1[0]和nums2[0],取较小的,即3和2中的2,所以最终得到2,正确。 测试案例2: nums1 = [1,2], nums2 = [3,4], 总长度4,需要找第2和第3小的元素。 找第2小的元素:k=2. 比较i=1,nums1[0]=1,nums2[0]=3,排除nums1的前1个元素,k=1。剩下nums1=[2], nums2=[3,4]。找第1小的元素是2和3中的2,所以第2小的是2。然后找第3小的元素,k=3比较i=1,nums1的i=1(可能超出长度?或者需要处理i不超过nums1的长度)。此时nums1的长度是1,所以i=1,但nums1的索引是0到0。所以可能这里需要调整。或者,原数组已经被缩短,所以在这种情况下,i可能取min(k//2, len(nums1))。 假设现在要找第3小的元素,初始两个数组是[1,2]和[3,4]。总长度是4,中位数是第2和第3的平均,即2.5。当找第3小的元素,k=3比较i =3//2=1,所以i=1,nums1[0]=1, nums2[3-1-1=1?或者 j =k -i=3-1=2,但nums2的长度是2,所以nums2的索引是1。此时nums2[j-1] = nums2[1]=4。比较1 <=4,所以排除nums1的前i=1个元素,此时剩下的nums1是[2],k=3-1=2。然后现在找第2小的元素,在两个数组[2]和[3,4]中。此时i=2//2=1,nums1的长度是1,所以i=1,j=2-1=1。nums1[0]=2,nums2[0]=3比较2 <=3,所以排除nums1的前1个元素,此时nums1为空,剩下的k=2-1=1。此时返回nums2的k-1=0索引,即3。所以第3小的元素是3,而第2小的是2,所以平均是2.5,正确。 看起来这样的逻辑是正确的。那在代码实现时,如何处理这些情况? 可能的Python代码大致结构如下: def findMedianSortedArrays(nums1, nums2): m, n = len(nums1), len(nums2) total = m + n if total % 2 ==1: return find_kth(nums1, nums2, total//2 +1) else: return (find_kth(nums1, nums2, total//2) + find_kth(nums1, nums2, total//2 +1))/2 def find_kth(a, b, k): if len(a) > len(b): a, b = b, a # 保证a是较短的数组 if not a: return b[k-1] if k ==1: return min(a[0], b[0]) # 取i和j,其中i +j =k i = min(k//2, len(a)) j = k -i if a[i-1] <= b[j-1]: # 排除a的前i个元素 return find_kth(a[i:], b, k -i) else: # 排除b的前j个元素 return find_kth(a, b[j:], k -j) 这样,每次递归调用时,较短的数组会被优先处理,可能更快缩小问题规模。这里可能需要注意数组的切片是否正确,以及递归终止条件。 例如,当a为空时,直接返回b的第k-1个元素。当k=1时,返回两个数组首元素的较小者。 测试这个函数是否正确: 测试案例1: nums1 = [1,3], nums2 = [2] findMedianSortedArrays([1,3], [2])调用时,总长度3,奇数,返回find_kth([1,3], [2], 2). 在find_kth函数中,a是[1,3],b是[2]. len(a)=2, len(b)=1. 进入函数后,i = min(2//2=1, 2)1. j=2-1=1. 比较a[0]=1和b[0]=2。因为1<=2,所以排除a的前1个元素,即a变为[3],k=2-1=1。再次调用find_kth([3], [2], 1)。此时k=1,返回min(3,2)=2。正确。 测试案例2: nums1 = [1,2], nums2 = [3,4] 总长度4,偶数,需要找第2和第3个元素。find_kth两次调用: 第一次k=2,第二次k=3. 当k=2时,i=1比较a[0]=1和b[1-1]=b[0]=31<=3,排除a的前1个元素,k=1。调用find_kth([2], [3,4],1) →min(2,3)=2。 当k=3时,i=3//2=1,j=3-1=2。比较a[0]=1和b[1]=4。1<=4,排除a的前1,k=2。此时a为空,所以返回b[2-1]=b[1]=4?这不对啊,因为合并后的数组是[1,2,3,4],第3小的元素应该是3。这时候哪里出错了? 哦,看来我的算法可能有问题。当处理k=3时,原数组是nums1=[1,2], nums2=[3,4]. 在find_kth的第一次调用中,a是[1,2], b是[3,4]. i=3//2=1,j=3-1=2。比较a[0]=1和b[1]=4。因为1<=4,所以排除a的前1个元素,即a变为[2],k=3-1=2。然后递归调用find_kth([2], [3,4], 2). 在第二次调用中,a是[2], b是[3,4]. i=2//2=1,j=2-1=1比较a[0]=2和b[0]=3。2 <=3,所以排除a的前1个元素,此时a变为空,k=2-1=1。现在调用find_kth([], [3,4],1),返回b[0]=3。所以最终结果是3,正确。这样,两次调用的结果别是2和3,平均为2.5,正确。 所以,之前的例子中的错误可能是我在析过程中的误解,但实际代码是正确的。 这说明这个算法是正确的。那这样的实现应该能够解决问题,时间复杂度是O(log(min(m,n))),因为每次递归调用都会减少问题规模,且每次处理的数组是较短的数组,所以时间效率较高。 总结来说,解决这个问题的关键在于利用二查找的思想,每次排除掉不可能的部,从而快速缩小搜索范围。需要注意的是处理各种边界条件,如数组为空、k=1等情况,以及正确计算索引以避免越界错误。</think>### 两个已排序数组的合并中位数算法 要高效求解两个已排序数组的合并中位数,可采用基于二查找的分治策略,时间复杂度为$O(\log(\min(m, n)))$,避免直接合并数组的低效操作[^1]。 #### 算法步骤 1. **确定中位数位置**: - 若合并后总长度$m+n$为奇数,中位数为第$\frac{m+n+1}{2}$小的元素。 - 若为偶数,则为第$\frac{m+n}{2}$和$\frac{m+n}{2}+1$小元素的平均值。 2. **二查找核心思想**: - 每次比较两个数组的第$\frac{k}{2}$个元素,排除较小元素的前半部,缩小问题规模[^1]。 - 递归调整$k$的值,直到$k=1$或某数组为空。 #### Python实现 ```python def findMedianSortedArrays(nums1, nums2): def find_kth(a, b, k): if len(a) > len(b): a, b = b, a # 确保a是较短的数组 if not a: return b[k-1] if k == 1: return min(a[0], b[0]) # 计算割点 i = min(k // 2, len(a)) j = k - i if a[i-1] <= b[j-1]: return find_kth(a[i:], b, k - i) else: return find_kth(a, b[j:], k - j) total = len(nums1) + len(nums2) if total % 2 == 1: return find_kth(nums1, nums2, total // 2 + 1) else: left = find_kth(nums1, nums2, total // 2) right = find_kth(nums1, nums2, total // 2 + 1) return (left + right) / 2 ``` #### 示例- **输入**: $nums1 = [1, 3]$, $nums2 = [2]$ **输出**: $2.0$ **解析**: 合并后数组为$[1, 2, 3]$,中位数是第2小的元素$2$[^2]。 - **输入**: $nums1 = [1, 2]$, $nums2 = [3, 4]$ **输出**: $2.5$ **解析**: 合并后数组为$[1, 2, 3, 4]$,中位数为$(2 + 3) / 2 = 2.5$[^2]。 #### 关键点 - **边界处理**:当数组为空或$k=1$时直接返回结果。 - **复杂度控制**:每次递归排除约一半数据,时间复杂度为$O(\log(\min(m, n)))$。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值