代码改变世界

归并排序(递归和非递归)和自然合并排序

  youxin  阅读(1512)  评论(0编辑  收藏  举报

  合并排序是一种分治法,实现上用了递归结构。过程是:先将待排序的元素分为两部分,一般是对等长度的两部分,称为左右L、R,先分别将L,R进行合并排序,然后将排序好的L、R合并在一起,则所有元素都有序。复杂度O(nlgn)。

复制代码
#include<iostream>
using namespace std;
void merge(int a[],int p,int q,int r)
{
    int n1=q-p+1;
    int n2=r-q;
    //create arrays L[1..n1+1] and R[1..n2+1]
    int *L=new int[n1+1]; //a[p,q]
    int *R=new int[n2+1]; //a[q+1,r]
    for(int i=0;i<n1;i++)
        L[i]=a[p+i-1];
    for(int j=0;j<n2;j++)
        R[j]=a[q+j];

    L[n1]=R[n2]=INT_MAX;

    int i,j;
    i=j=0;
    for(int k=p;k<=r;k++)
    {
        if(L[i]<=R[j])
            {
                a[k]=L[i];
                i++;
            }
        else 
        {
            a[k]=R[j];
            j++;
        }
    }
    
}



        
        
void mergeSort(int a[],int p,int r)
{
    int q;
    if(p<r)
    {
        q=(p+r)/2;
        mergeSort(a,p,q);
        mergeSort(a,q+1,r);
        merge(a,p,q,r);
    }
}
        

int main()
{
    int n;
    cin>>n;
    int *a=new int[n];
    for(int i=0;i<n;i++)
        cin>>a[i];
    mergeSort(a,0,n-1);
    for(int i=0;i<n;i++)
        cout<<a[i]<<ends;
}
复制代码

上面的merge程序要开辟2个数组,有点不好,一个更好的方案:

复制代码
void merge(int arr[],int first,int mid,int last)
{
 
    int *tmpArr=new int[last-first+1];
    int i=first,j=mid+1;
    int cur=0;
    while(i<=mid && j<=last)
    {
        if(arr[i]<arr[j])
        {
            tmpArr[cur++]=arr[i++];
        }
        else
        {
            tmpArr[cur++]=arr[j++];

            count += mid-i+1;//只增加这一句便可求逆序数
        }
    }
   while(i<=mid)
            tmpArr[cur++]=arr[i++];
   while(j<=last)
            tmpArr[cur++]=arr[j++];
   

    for(int k=0;k<cur;k++)
    {
        arr[first++]=tmpArr[k];
    }
    delete[] tmpArr;
    tmpArr=NULL;
}
复制代码

 

INT_MAX头文件是在<limits.h>中,这里不要也没问题。

C中int类型32位,范围是-2147483648到2147483647.

(1)最轻微的上溢是 INT_MAX + 1 :结果是 INT_MIN。 
(2)最严重的上溢是 INT_MAX + INT_MAX :结果是 -2。 
(3)最轻微的下溢是 INT_MIN - 1 :结果是 INT_MAX。 
(4)最严重的下溢是 INT_MIN +INT_MIN :结果是 0。

应付溢出的最佳方法就是防范于未然:充分了解数据的范围,选择恰当的变量类型。 
也可以考虑改用不需要你担心整数类型溢出的语言--Python语言.

 

void mergeSort(int a[],int p,int r)
{
    int q;
    if(p<r)
    {
        q=(p+r)/2;
        mergeSort(a,p,q);
        mergeSort(a,q+1,r);
        merge(a,p,q,r);
    }
}怎么理解?
其实,向这种递归调用里有2次递归调用的,我们可以只看一种,另一种一种,
我们另第一句
mergeSort(a,p,q); 为a
第二句
 mergeSort(a,p,q); 为b。
则调用的顺序大致可以看成下面的图:

开始调用时是(0,6),然后a(0,3);这时a(0,3)会不断调用自身,到a(0,1)和b[1,1]完成后运行merge(0,1,1).

我们可以想象单独调用(0,1)和(1,1)然后合并(0,1,1). a(0,3)只完成了a(0,1)(即:把a[0]和a[1]合并),还没有完成b(2,3),类似算出结果。

即把a[2]和a[3]合并。然后在merge(0,1,3).

其余类似。

可以参考:

http://www.ituring.com.cn/article/1327

 

对于算法mergeSort,还可以从多方面对他进行改进。例如,从分治策略的机制入手,容易消除算法中的递归。事实上,mergeSort的递归过程只是将待排序集合一分为2,直至待排序集合只剩下一个元素为止,然后不断合并2个已排好序的数组段。按此机制,可以首先将数组a中相邻元素两两配对,用合并算法将他们排序,构成n/2组长度为2的排好序的子数组段,然后再将他们排序成长度为4的排好序的子数组段,如此继续下去,直至整个数组排好序

复制代码
#include<iostream>
#include<limits.h>
using namespace std;

void mergePass(int x[],int y[],int s,int n);
void merge(int x[],int y[],int p,int q,int r);

void mergeSort(int a[],int n)
{
    int *b=new int[n];
    int s=1;
    while(s<n)
    {
        mergePass(a,b,s,n);//合并到数组b
        s=s*2;
        mergePass(b,a,s,n);//合并到数组a
        s=s*2;
    }
}
void mergePass(int x[],int y[],int s,int n)
{
    //合并大小为s的相邻子数组 到y[]
    int i=0;
    while(i<=n-2*s)
    {
        merge(x,y,i,i+s-1,i+2*s-1);
        i=i+2*s;
    }
    //剩下的元素少于2s
    if(i+s<n) merge(x,y,i,i+s-1,n-1);
    else for(int j=i;j<=n-1;j++) y[j]=x[j];

}
/*合并s[p:q]和s[q+1,r]到 d[p:r] */
void merge(int s[],int d[],int p,int q,int r)
{
    int i=p,j=q+1,k=p;
    while((i<=q) && (j<=r))
    {
        if(s[i]<=s[j]) d[k++]=s[i++];
        else  d[k++]=s[j++];
    }
    if(i>q) for(int m=j;m<=r;m++) d[k++]=s[m];
    else   for(int m=i;m<=p;m++)  d[k++]=s[m];
}

int main()
{
     
    cout<<"输入元素的个数";
    int n;
    cin>>n;
    int *a=new int[n];
    cout<<"输入"<<n<<"个元素"<<endl;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
 
    mergeSort(a,n);
    cout<<"mergeSort后"<<endl;
    for(int i=0;i<n;i++)
        cout<<a[i]<<ends;
    cout<<endl;
     
}
复制代码

非递归的迭代方法,避免了递归时深度为log2n的栈空间,空间只是用到申请归并临时用的TR数组,因此空间复杂度为O(n),并且避免递归也在时间性能上有一定的提升,应该说,使用归并排序时,尽量考虑用非递归方法

 

 上面的迭代方法没有下面的简洁:

复制代码
/**
     * 自定向上排序
     *
     * @param arr 待排序数组
     * @param n   数组的长度
     */
    public static void mergeSort(int[] arr, int n) {
        /*
         * 数组分2层循环 第一层是确定分组后每个组的长度,按照上面图的图示所知
         * size分别为1,2,4,8...
         * 当size=1 那么arr1.length=1 arr2.length=1 所以 1-1 排序 最终 “每” 2个元素有序
         * 当size=2 那么arr1.length=2 arr2.length=2 所以 2-2 排序 最终 “每” 4个元素有序
         * 当size=4 那么arr1.length=4 arr2.length=4 所以 4-4 排序 最终 “每” 8个元素有序
         * 所以这层循环 就是为了帮助我们创建符合要求变化的size大小
         * 最开始数组长度=1 也就是1个元素 他就是有序的 那么size = 2 * size这个算式就帮助我们迭代创建size分别为1,2,4,8...
         *
         */
        for (int size = 1; size <= n; size = 2 * size) {
            /*
             *i表示的是每个要合并分作的起始坐标也就是left 我们知道 left-right是通过mid分成arr1和arr2的
             * 也就是[l...mid]-[mid+1...r] 等价于[l...size-1]和[size...r]
             * 所以i的取值变化为i = i + 2 * size
             * 同时i不能越界 所以i<n
             */
            for (int i = 0; i + size < n; i = i + 2 * size) {
                /*
                 * 上面分析[l...mid]-[mid+1...r] 等价于[l...size-1]和[size...r]
                 * 所以 i等价l i + size - 1等价mid i + size + size - 1等价r
                 * 虽然i<n合法 但是i + size可能越界,
                 * 同时当i+size>=n说明 只有arr1 arr2为null,那么也就不归并了 他就是有序的
                 * 因为从上面得知,归并的前提是arr1和arr2是有序的
                 * 
                 * 
                 * i + size + size - 1相当于r 他可能越界 所以Math.min(i + size + size - 1, n - 1)
                 * 
                 */
                merge(arr, i, i + size - 1, Math.min(i + size + size - 1, n - 1));
            }
        }
    }
复制代码

 

 

如果不理解上面代码可以看图解:http://book.51cto.com/art/201108/287081.htm

扩展:自然和并排序

  自然合并排序是合并排序算法的一种改进。

 自然合并排序:对于初始给定的数组,通常存在多个长度大于1的已自然排好序的子数组段.例如,若数组a中元素为{4,8,3,7,1,5,6,2},则自然排好序的子数组段有{4,8},{3,7},{1,5,6},{2}.用一次对数组a的线性扫描就足以找出所有这些排好序的子数组段.然后将相邻的排好序的子数组段两两合并,构成更大的排好序的子数组段({3,4,7,8},{1,2,5,6}).继续合并相邻排好序的子数组段,直至整个数组已排好序。
复制代码
#include<iostream>
using namespace std;
#include<vector>

void merge(int a[],int p,int q,int r)
{
    //L1[p,q]大小为q-p+1
    //L2[q+1,r]大小为r-q;
    int n1=q-p+1;
    int n2=r-q;
    int *L1=new int[n1+1];
    int *L2=new int[n2+1];
    for(int i=0;i<n1;i++)
        L1[i]=a[p+i];
    for(int i=0;i<n2;i++)
        L2[i]=a[q+1+i];

    L1[n1]=INT_MAX;
    L2[n2]=INT_MAX;
    int i=0,j=0;
    for(int k=p;k<=r;k++)
    {
        if(L1[i]<L2[j])
        {
            a[k]=L1[i];
            i++;
        }
        else
        {
            a[k]=L2[j];
            j++;
        }
    }
    delete[] L1;
    delete[] L2;
}

void naturalMergeSort(int a[],int n)
{
         vector<int> v;
         v.push_back(0);                 //首作为分割点 
                        
       for(int i=0;i<n-1;i++)          //中间的分割点           
       if(a[i]>a[i+1]) v.push_back(i);
   
       v.push_back(n-1);               //尾分割点 
   
        for(int j=0;j<v.size();j++)     //输出测试分割点是否正确,可注释掉 
       cout<<v[j]<<endl;     
       cout<<v.size()<<"ok"<<endl;
   int s=1;
   
   for(int group=v.size()-1;group!=1;group=(group%2==0?group/2:group/2+1))
   {     
        int count=group/2;                  //合并次数  例如:5组合并需要两次,4组合并两次      
        //进行第一次合并 
        int p,q,r;

        p=0;q=s;r=2*s;
        if(r>v.size()-1) r=v.size()-1;
        merge(a,v[p],v[q],v[r]);
        
        //进行接下来的合并
        for(int j=1;j<count;j++)
        {
        p=r;q=p+s;r=q+s;
        if(r>v.size()-1) r=v.size()-1;
        merge(a, v[p]+1, v[q],v[r]);                       
        }     
        s+=s;        
   }   
}
 
int main()
{
    int n;
    cin>>n;
    int *a=new int[n];
    cout<<"输入"<<n<<"个元素"<<endl;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    naturalMergeSort(a,n);    
    for(int i=0;i<n;i++)
     cout<<a[i]<<endl;
     
    return 0;    
}
复制代码

总结:自然归并排序思想简单,但是实现的时候还有很多细节需要考虑,比如需要归并次数是分组次数除以二取下限。还有就是r的控制,每次都要保证在v.size()-1范围内不能超出。实现时候可能需要不断测试,最终成功。

参考网站:http://blog.163.com/guchonglin-6/blog/static/5752753120099247170200/

跟多;
http://www.cnblogs.com/liushang0419/archive/2011/09/19/2181476.html
http://dsqiu.iteye.com/blog/1707111


归并排序优化

 

 public static void mergeSort(int[] arr, int l, int r) {
        if (r-l<=15) {
           insertSort(arr,l,r);// 1 .
            return;
        } else {
            //找到中间边界mid 拆分2个数组[l...mid]和[mid+1...r]
            int mid = (r - l) / 2 + l;
            //左边继续拆分
            mergeSort(arr, l, mid);
            //右边边继续拆分
            mergeSort(arr, mid + 1, r);

            //一直拆分到l==r 说明只有一个元素 retuen
            //然后开始回溯合并排序
            if (arr[mid] > arr[mid + 1])//2.
                merge(arr, l, mid, r);
        }
    }
  • 当拆分到足够小的时候选择使用插入排序,原始是插入排序对相对有序的数组效率比较高,所以当数组越小的时候有序的几率就越大,所以使用插入排序
  • (arr[mid] <= arr[mid + 1]) 也就是所arr1中的最大的元素已经比arr2最小的元素还要小,那么arr1所有元素就小于等于arr2的所有元素,因为再归并中arr1和arr2都是有序的,那么此时[l...r]就是有序的,所以当(arr[mid] > arr[mid + 1])时我们才需要排序

 

 

《算法导论》2.3-7 判定是否存在两数字和为x  

问题描述:输入n个数,求给定数组中是否存在两个数,他们的和为x

输入数据:元素个数n,和x,数组a

输出数据:如果存在两个数则输出那两个数,否则输出“No Answer!”

思想:先归并排序后,在从两端计算:

复制代码
bool find(int a[],int len,int x,int &x1,int &x2)
{
    int i,j;
    for(i=0,j=len-1;i<len;)
    {
        int sum=a[i]+a[j];
        if(sum==x)
        {
            x1=a[i];
            x2=a[j];
           return true;
        }
        else
            if(sum>x)
            {
                j--;
            }
            else
            {
                i++;
            }
    }
    return false;
}

复制代码
mergeSort(a,0,n-1);
    int x1, x2;
    for(int i=0;i<n;i++)
        cout<<a[i]<<ends;
    if(find(a,n,15,x1,x2))
    {
        cout<<x1<<ends<<x2<<endl;
    }
    else
        cout<<"no find";
复制代码

复制代码



 

什么是逆序对.逆序对是判断一个数组有序程度的一个标示。一个完全有序的数组,逆序对个数=0
 1 2 3 4 5 6 8 7 一个逆序对   
 利用归并思想,当向上归并:
 
 2 3 6 8 | 1 4 5 7 
 
 2与1比较 1<2  那么1比左边2 3 6 8 都要小那么此时逆序对4 
 继续归并到
 1 2 3 当4<6时 4比左边6 8 小 逆序对未2  
 
 最终将计数相加
https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/


https://www.jianshu.com/p/d3b9ffdf1089
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示