自学内容网 自学内容网

大一计算机的自学总结:归并排序及归并分治

前言

之前写过最基础的三种排序算法大一计算机的自学总结:初见排序,但时间复杂度都是O(n^2),运行起来会很慢,而归并排序的时间复杂度是O(n*logn),会比之前三种排序算法快很多。

一、归并排序

原理就是每次都将数组分为左半部分和右半部分,使它们各自有序,再合并起来,让整体有序。

1.递归版

根据原理不难看出,如果用递归的话过程会非常简单。

#include<bits/stdc++.h>
using namespace std;

//全局变量
#define n 10
int arr[n]; 

//左跨右
void merge(int l,int m,int r)
{
int i=l; 
int a=l;
int b=m+1;
int help[n];//辅助数组 

while(a<=m&&b<=r)
{
help[i++]=arr[a]<=arr[b]?arr[a++]:arr[b++];
}

while(a<=m)
{
help[i++]=arr[a++];
}
while(b<=r)
{
help[i++]=arr[b++];
}

for(i=l;i<=r;i++)
{
arr[i]=help[i];
}
} 

//归并排序 ——递归版 
void mergeSort(int l,int r)
{
if(l==r)
{
return ;
}

int m=(l+r)/2;
mergeSort(l,m);
mergeSort(m+1,r);
merge(l,m,r);
}

int main()
{
for(int i=0;i<n;i++)
{
cin>>arr[i];
}

//归并排序 
mergeSort(0,n-1);

cout<<"Sorted:"<<endl;
for(int i=0;i<n;i++)
{
cout<<arr[i]<<" ";
}

return 0;
}

排序函数没什么好说的,就是一个递归函数,重点是merge函数。

merge函数的作用是让两部分整体有序,考虑到直接在原数组上操作很麻烦,所以这里借助一个辅助数组,先将两部分的数从大到小填入辅助数组,再用辅助数组覆盖原数组即可。

将两部分的数从大到小填入,思路是,借助两个指针a和b,分别控制左半部分和右半部分的数,还有指针i,控制填入help数组的位置。当a和b都在各自范围内时,根据各自位置数的大小填入辅助数组。之后,将两部分的剩余部分填入辅助数组即可。

2.非递归版

在有些题目中,可能要求额外的空间复杂度为O(1),此时就不能使用递归的方法了。

#include<bits/stdc++.h>
using namespace std;

//全局变量
#define n 10
int arr[n];

//左跨右
void merge(int l,int m,int r)
{
int i=l; 
int a=l;
int b=m+1;
int help[n];//辅助数组 

while(a<=m&&b<=r)
{
help[i++]=arr[a]<=arr[b]?arr[a++]:arr[b++];
}

while(a<=m)
{
help[i++]=arr[a++];
}
while(b<=r)
{
help[i++]=arr[b++];
}

for(i=l;i<=r;i++)
{
arr[i]=help[i];
}
} 

//归并排序——非递归版
void mergeSort(void)
{
for(int l,m,r,step=1;step<n;step*=2)
{
l=0;
while(l<n)
{
m=l+step-1;
if(m+1>n-1)
{
break;
}
r=(m+step>n-1)?(n-1):(m+step);
merge(l,m,r);
l=r+1;
}
}
} 

int main()
{
for(int i=0;i<n;i++)
{
cin>>arr[i];
}

//归并排序 
mergeSort();

cout<<"Sorted:"<<endl;
for(int i=0;i<n;i++)
{
cout<<arr[i]<<" ";
}

return 0;
}

观察递归的过程,可以发现,每次都是从中间分割直到只剩一个数,那么考虑设置步长step变量来控制每次merge的两部分大小。

merge函数跟递归版一样,重点是排序函数的变化。

初始化step=1,每次都乘以二,表示递归中分割的过程。之后使l=0,只要l没到最后,此时可以借助step来计算m的位置,若m+1越界,则表示只剩下一部分,所以break;否则计算r的大小,此时注意当剩余部分不够一个step时让r=n-1。之后merge两部分,让l=r+1往下跳即可。

二、归并分治

归并排序的思想,即分开求解再合并。

1.条件

(1)答案可以分为左侧答案+右侧答案+左跨右的答案

(2)在计算左跨右的答案时,若左右各有序,则可简便计算

2.例题——计算数组的小和

#include <iostream>
using namespace std;

typedef long long ll;

ll merge(int arr[],int n,int l,int m,int r)
{
    ll ans=0;
    for(ll i=l,j=m+1,sum=0;j<=r;j++)
    {
        while(i<=m&&arr[i]<=arr[j])
        {
            sum+=arr[i++];
        }
        ans+=sum;
    }
    
    int i=l;
    int a=l;
    int b=m+1;
    int help[n];
    while (a<=m&&b<=r)
    {
        help[i++]=arr[a]<=arr[b]?arr[a++]:arr[b++];
    }
    while(a<=m)
    {
        help[i++]=arr[a++];
    }
    while(b<=r)
    {
        help[i++]=arr[b++];
    }
    for(i=l;i<=r;i++)
    {
        arr[i]=help[i];
    }

    return ans;
}

ll smallSum(int arr[],int n,int l,int r)
{
    if(l==r)
    {
        return 0;
    }
    int m=(l+r)/2;
    return smallSum(arr,n,l, m)+smallSum(arr,n,m+1,r)+merge(arr,n,l,m,r);
}

int main() 
{
    int n;
    cin>>n;
    int arr[n];
    for(int i=0;i<n;i++)
    {
        cin>>arr[i];
    }

    cout<<smallSum(arr,n,0,n-1);
    return 0;
}

 由上述第一个条件分析可以得出,求数组中每个数的小和,答案可以分为左半部分数的小和、右半部分数的小和以及左跨右部分的小和三者之和。

求小和的函数就是一个递归函数,重点是merge函数。

merge函数负责返回左跨右部分的小和,即右侧每一个数在左侧的小和。此时考虑上述第二个条件:左右各有序可简便运算。这是肯定的,若左右各有序,只需要遍历一遍即可解决。

解决方法如下:初始化i和j两指针分别控制左侧和右侧,当i始终在左侧,并且i位置的数比j位置的数小时,将数累加到sum里。若不小于j位置的数,因为左右各有序,则说明i位置右侧没有比j位置数更小的数,跳出循环,将sum累加到ans里。之后,将j移动到下一个位置, 此时,由于左右各自有序,所以不需要从头遍历左侧部分,只需要从i位置继续累加即可。

最后加上归并排序的部分使整体有序即可。

总结

归并排序的时间复杂度非常优秀,并且其归并分治的思想更是非常非常重要!!

END


原文地址:https://blog.csdn.net/2402_89326161/article/details/145244245

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!