zoukankan      html  css  js  c++  java
  • 排序算法(2)-归并,快速

    上节分析了O(n^2)的算法,这节就分析O(nlgn)的算法-归并,快速和堆排序。

    一:综述

        O(nlgn) 的算法可以分为两大类,两者所用的技术差别较大。归并和快速排序采用的是分治策略,这两者相当于一个对称的过程,一个是自顶向上合并子问题,另一个则自上向下分解子问题。而堆排序利用堆这一数据结构元素间的特殊关系来排序一个序列,另外采用二叉树的方式组织数据使其效率大大提高。

    二:分治策略排序算法

    1.为什么使用分治?

        在上节算法的分析中,不管是冒泡。选择还是插入都不适用于大规模的数据,因为数据一大,数据间比较,移动的次数也增大,这导致运行时间大大增加。自然而然就想到如何把大的问题分解成小问题,如果小问题计算代价小且和原问题相似,同时合并的代价也小。那用分治策略是优于直接对大问题进行处理的。而在排序中,一个大数据不断的分解这个处理过程代价小,直到只剩一个元素的时候,问题规模最小只要简单处理,合并过程虽然需要较多处理,但代价也小。所以将分治用于排序主要是解决三个处理过程,怎么将大的数据分成小数据?怎么对小数据进行处理?怎么将处理结果合并成最后的结果?

    2.如何实现归并排序

        归并排序:

        怎么分解?归并排序是将n规模的序列分成n/2规模(n为偶数情况,奇数也类似),n/2分为n/4,......。直到序列长度为1,这个小问题直接求解,1长度的序列就已经排好序了。怎么合并?归并排序的合并过程需要较多处理,我们就专门分析一下这个过程。依照算法导论,我们也利用牌堆进行分析,假设只有两张牌,比较,小的放上面,大的放下面。如果是两堆牌呢?这两堆牌是通过前面合并得到,它们各自都已经排好序了。同样的比较牌堆的第一张,小的取出来放到另一堆去,然后再比较两牌堆顶的牌继续不断的把小的取出。当某堆牌取空时,将另一堆剩下的直接放到第三堆的底下,因为它们都排好序了,所以剩下的肯定比第三堆的都大。这个过程最多执行n次,比如当两堆牌刚好牌数一样,并且大小都是交替的。

    #include <iostream>
    const int len=10;
    using namespace std;
    void merge(int *a,int p,int q,int r)
    {
        int n1=q-p+1,n2=r-q;
        int *L=new int[n1]();
        int *R=new int[n2]();
        for (int i=0;i<n1;i++)
        {
            L[i]=a[p+i];
        }
        for (int j=0;j<n2;j++)
        {
            R[j]=a[q+j+1];
        }
        int i=0,j=0,k=p;
        while (i<n1&&j<n2)
        {
            if (L[i]<=R[j])
            {
                a[k++]=L[i++];
            } 
            else 
            {
                a[k++]=R[j++];
            }
        }
        while (i<n1)
        {
            a[k++]=L[i++];
        }
        while (j<n2)
        {
            a[k++]=R[j++];
        }
        
        delete [] R;
        delete [] L;
    }
    int main() 
    {  
        int a[len]={2,4,5,7,10,1,2,3,6,9};
         merge(a,0,4,9);
        for(int i=0;i<len;i++) cout<<a[i]<<" ";
        cout<<endl;
    }

              merge代码分析,传入的是指向数组的指针,和(p,q),(q+1,r)间的两个已经排好序的序列如,主程序所举的例子2,4,5,7,101,2,3,6,9。在子函数里,先新建两个动态数组来存放这两个序列。然后如果任一序列未到底时,就比较,并赋值给a。一个序列到底了,就将另一个未到底的直接复制到a下。

        综上我们完成了合并的过程,分解呢?分解就是要确定q的值,一般我们将q设置为image ,它能够将A[p,r]分成n/2向下和向上取整个元素。(把p,r分别为偶数,奇数情况考虑下就可以证明)。如何一直分解直到只剩一个元素时排序并返回呢?这就要利用递归的方法了,函数不断的调用自身,分解成越来越小的子问题。

    #include <iostream>
    const int len=5;
    using namespace std;
    void merge(int *a,int p,int q,int r)
    
    {
        int n1=q-p+1,n2=r-q;
        int *L=new int[n1]();
        int *R=new int[n2]();
        for (int i=0;i<n1;i++)
        {
            L[i]=a[p+i];
        }
        for (int j=0;j<n2;j++)
        {
            R[j]=a[q+j+1];
        }
        int i=0,j=0,k=p;
        while (i<n1&&j<n2)
        {
            if (L[i]<=R[j])
            {
                a[k++]=L[i++];
            } 
            else 
            {
                a[k++]=R[j++];
        
            }
        }
        while (i<n1)
        {
            a[k++]=L[i++];
        }
        while (j<n2)
        {
            a[k++]=R[j++];
        }
        
        delete [] R;
        delete [] L;
    }
    void mergesort(int *a,int p,int r)
    {   
        
        if (p<r)
        {
            int q=(p+r)/2;
            mergesort(a,p,q);
            mergesort(a,q+1,r);
            merge(a,p,q,r);
        }
    
    
    }
    int main()
    {  
        int a[len]={25,15,4,30,7};
        mergesort(a,0,4);
      
        for(int i=0;i<len;i++) cout<<a[i]<<" ";
        cout<<endl;
    } //main end

        程序分析:

    image

        上图分析了程序的递归调用流程,我们就根据上图来分析,首先调用mergesort(a,0,4);,0<4,所以计算q=2.调用mergesort(a,0,2);0<2,q=1,调用mergesort(a,0,1);0<1,q=0,调用mergesort(a,0,0);0==0,递归返回,执行mergesort(a,p,q)的下一条语句mergesort(a,q+1,r)这时q=0,r=1。调用mergesort(a,1,1),1==1,调用返回,执行mergesort(a,q+1,r)的下一条语句merge(a,p,q,r)。此时p=0,q=0,r=1.调用merge(a,0,0,1)。这时返回至mergesort(a,0,2),q=1,调用mergesort(a,q+1,r),即mergesort(a,2,2),返回,调用merge(a,0,1,2)。于是左边这一半排序完成,开始右边的排序,其中q=2,r=4,调用mergesort(a,q+1,r),即mergesort(a,3,4)。同样的在这一半中左边调用mergesort(a,p,q),右边调用mergesort(a,q+1,r),merge,merge后就对数组排好序了。

      递归的过程确实容易搞混淆的,在程序里是顺序的,而不是并行的两个排序过程。另外变量间的变化也是容易弄错的。

    复杂度分析:

    MERGE函数的复杂度为c •n (n为元素个数, c为移动一个元素, 比较一次所花费的工作量)。T(n)= 0, n=1 (只一个元素的数组无需排序),T(n)= 2 T(n/2) + c • n = , n>1。将T(n)展开,就的下图。

    image

    3.如何实现快速排序

       如果说归并是从底不断排序好,不断合并,到顶时使序列最终成为有序,那么快速,就是从顶一直向下使序列不断有序,当到底时,序列也就成为有序了的。其中有序是确定一个值Aq把序列分为两个部分,一部分的所有值都比Aq大,另一部分都比Aq小。

    数组A[p…r]被划分为两个(可能空)子数组A[p…q-1]和A[p+1..r],使得A[p…q-1]中每个元素都小于或等于A[q],A[q+1..r]中的元素大于等于A[q]。下标q在这个划分过程中进行计算;这个数组的划分过程很重要,通过交换来维护两个子区域的性质。

    #include <iostream>
    const int len=8;
    using namespace std;
    int partition(int *a,int p,int r)
    {
        int x=a[r],i=p-1,temp=0;
        for (int j=p;j<r-1;j++)
        {
            if (a[j]<x)
            {
                i++;
                temp=a[i];
                a[i]=a[j];
                a[j]=temp;
            }
        }
        temp=a[i+1];
        a[i+1]=a[r];
        a[r]=temp;
        return i+1;
        
    }
    int main()
    {  
        int p,a[len]={2,8,7,1,3,5,6,4};
        p=partition(a,0,7);
    
        for(int i=0;i<len;i++) cout<<a[i]<<" ";
        cout<<endl;
    } //main end

    image image

        如上图和程序,首先初始化x称为主元为最后一个元素a[r],i为序列开始之前的一个值,然后j从p开始的r-1,判断其是否小于等于主元,若满足则i加1,并交换a[i]和a[j]。这是为了满足当元素属于下面各个范围时,符合一定性质 1.p<=k<=i,a[k]<=x; 2. i+1<=k<=j-1,a[k]>x;  3.k=r,a[k]=x;初始时,p和i间,i+1和j-1间没有元素,成立,进入循环,假设第一个元素大于主元,j加1,什么都不做,符合上面的性质。如第一个元素小于主元,i加1,交换a[i]和a[j],先扩大i的范围,再将小于主元的元素交换进来。

      整个快速排序的过程,也类似于归并排序,分解,解决,合并。分解就是确定主元在序列中位置,解决即递归用于两个数组,合并这无需,因为递归到最后,数组都排好序了。

    #include <iostream>
    const int len=8;
    using namespace std;
    int partition(int *a,int p,int r)
    {
        int x=a[r],i=p-1,temp=0;
        for (int j=p;j<=r-1;j++)
        {
            if (a[j]<x)
            {
                i++;
                temp=a[i];
                a[i]=a[j];
                a[j]=temp;
            }
        }
        temp=a[i+1];
        a[i+1]=a[r];
        a[r]=temp;
        return i+1;
        
    }
    void quicksort(int *a,int p,int r)
    {   
        int q;
        if (p<r)
        {
            q=partition(a,p,r);
            quicksort(a,p,q-1);
            quicksort(a,q+1,r);
    
        }
    
    
    }
    int main()
    {  
        int a[len]={2,8,7,1,3,5,6,4};
        quicksort(a,0,7);
        for(int i=0;i<len;i++) cout<<a[i]<<" ";
        cout<<endl;
    } //main end

    算法分析(略)

  • 相关阅读:
    截取UIImagePickerController的拍照事件
    Xcode报错:run custom shell script '[cp] copy pods resource
    XCode报错:Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang failed with exit code 1
    Mac环境下实现alias的重命名命令(永久生效)
    Swift 3.0在集合类数据结构上的一些新变化
    iOS几种简单有效的数组排序方法
    二分法查找、快速排序思想与实现
    iOS10 相册权限
    ios应用版本号设置规则
    iOS白名单设置
  • 原文地址:https://www.cnblogs.com/dawnminghuang/p/3860841.html
Copyright © 2011-2022 走看看