排序算法(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

 

 

 

 

 

算法分析(略)

posted @ 2014-07-22 16:26  dawnminghuang  阅读(511)  评论(0编辑  收藏  举报