zoukankan      html  css  js  c++  java
  • 归并排序(递归和非递归)和自然合并排序

      合并排序是一种分治法,实现上用了递归结构。过程是:先将待排序的元素分为两部分,一般是对等长度的两部分,称为左右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
  • 相关阅读:
    33、springboot——springboot整合RabbitMQ(1)②
    33、springboot——消息之JMS,AMQP简介①
    32、springboot——缓存之整合Redis
    Redis学习——2、RDB的简单相关配置及测试
    Redis学习——1、CentOS7中安装redis以及一些基本配置
    CentOS7查看及开放端口
    moment
    flex兼容性
    caniuse使用
    混合整数线性规划,图的最大流,图的匹配,求解
  • 原文地址:https://www.cnblogs.com/youxin/p/2476266.html
Copyright © 2011-2022 走看看