排序算法分析

原文:http://blog.csdn.net/wangxiaojun911/article/details/4581621程序员

排序(Sorting)是算法应用中最重要的工具。据统计,计算机运行时间的四分之一都耗费在了排序上。判断排序算法优劣的方法是进行时间和空间效率分析。分析时间效率的三个重要记号是O、Ω和Θ。

O记号起源于P.Bachmann在1892年发表的一篇数论文章。O在一个常数因子内给出某函数的一个上界。对一个函数g(n), O(g(n))表示一个函数集合。O(g(n)) = {f(n):存在正常数c和n0,使对于全部的n>=n0,有0<=f(n)<=cg(n)}。

在使用O记号进行效率统计以前,先假定分析的机器模型。咱们使用单处理器,随机存取器(random-access machine, RAM)。即指令一条一条执行,没有并发操做。进一步假定每次运算只消耗一条指令。把每条指令记为1次(time)。好比:
int n = 10; //1 time
for(int i=0;i < n;i++)//n times
{
...//c条语句

该算法总的运行时间就是每一条语句执行次数之和1+c*n。一般,算法运行时间(或称时间复杂度)不只与数据规模n相关,而且与数据的输入形式相关。例如,在一些排序算法中,若输入的数组已经排好序了,那么世纪运行时间会大大减小(或增长)。因此,咱们在不可以明确输入数据状态的状况下,定义算法的“最坏状况”和“最好状况”。在实际分析中,通常考察最坏状况的运行时间,即对于规模为n的状况下,算法的最长运行时间。这是由于最坏状况在应用当中频繁出现,并且即便考察一个随机的数据输入,它的结果与最坏状况同样差(好比都是输入规模的二次函数)。算法

很明显,用来表示上界的O记号能够很天然的描述这种最坏状况。设算法最坏运行时间为f(n),能够找到一个简单的函数g(n)使f(n)能够用O(g(n))来描述。一般咱们说“运行时间是O(n^2)”便表示对于f(n),无论n为什么值,也无论具体是怎样输入,它都有最坏运行时间O(n^2)。一般选取的g(n)有1, n, n*lgn, n^2 和 n^3, 它们在O记号下的时间复杂度依次递增。
(注1:咱们在这里说的lgn,是以2为底的对数)
(注2:一般,指数函数比多项式函数增加快,多项式又比对数增加快。对于对数来讲,无论底数是多少,其渐进线都是同样的。Exponential functions grow faster than polynomial functions, which grow faster than polylogarithmic functions)api

相似,Ω记号给出了函数的渐进下界。即对一个函数g(n), Ω(g(n))= {f(n):存在正常数c和n0,使对于全部的n>=n0,有0<=cg(n)<=f(n)}。Θ记号则最强,它同时表示上下界。Θ由Knuth提出,是最准确的记号。可是,许多人至今仍然偏心使用O记号。数组

注意,咱们假定g(n)是渐进非负函数,以上记号才能够成立。
下面分析一些具体的排序算法。

1. 插入排序
插入排序如同打牌,一次从桌上摸起一张牌,并将它插入手中牌中的正确位置上。此算法在排序过程当中将牌分红了2个部分:还在桌上的牌(未排序)和手中的牌(有序)。排序当中有一个比较过程以及在插入后将插入项以后的牌向后移动的过程。在最坏状况下,原始牌是逆序排列的,那么每插入一张牌,全部手中的牌都要向后移动一格。假设有n张牌,那么移动的次数就是1+2+3+4+...+n = n*(n-1)/2次。即此算法是O(n^2)的。
程序实现:并发

[c-sharp]  view plain copy
 
  1. void insertionSort(int input[])  
  2. {  
  3.   int tmp,i;  
  4.   for(int j=1;j < MAX;j++)  
  5.   {  
  6.     tmp=input[j];  
  7.     i=j-1;  
  8.     while(i>=0 && tmp < input[i])  
  9.     {  
  10.          input[i+1]=input[i];  
  11.          i--;  
  12.     }  
  13.     input[i+1]=tmp;  
  14.   }  
  15. }  



2.冒泡排序
冒泡排序也许是实现最简单的排序算法。时间复杂度也十分容易计算,也是O(n^2),并且当输入数据自己有序的时候效率也不会提升!它惟一的用途大概就是用来测试一个程序员是否是弱智。dom

[cpp]  view plain copy
 
  1. void bubble(int *input)  
  2. {  
  3.   for(int i=0;i < (MAX-1);i++)  
  4.     for(int j=0;j<(MAX-1-i);j++)  
  5.       if(input[j]>input[j+1])  
  6.         swap(input[j],input[j+1]);  
  7. }  



3.快速排序
快速排序是一种递归算法:将原问题分红若干个规模小可是结构类似的问题,递归地解决这些问题,再合并结果,就获得了原问题的解。快速排序首先在输入数据中选择一个元素做为“主元”,而后依据主元把数据分红2个部分,前一个部分的每一个元素都比主元小,后一个部分都比主元大。而后分别在两个部分快速排序,直到不能再分为止。这样整个数据就有序了。函数

[cpp]  view plain copy
 
  1. void qsort(int input[],int start,int end)  
  2. {  
  3.   int i,j;   
  4.   if(start < end)  
  5.   {  
  6.     i=start;j=end+1;   
  7.     while(1){  
  8.       do i++;   
  9.       while(!(input[i]>=input[start]||i==end));  
  10.       do j--;   
  11.       while(!(input[j]<=input[start]||j==start));   
  12.       if(i < j)  
  13.         swap(input[i],input[j]);  
  14.       else  
  15.         break;   
  16.     }  
  17.     swap(input[start],input[j]);  
  18.     qsort(input,start,j-1);  
  19.     qsort(input,j+1,end);  
  20.   }  
  21. }  

 

另外一个版本的快速排序:工具

 

[cpp]  view plain copy
 
  1. int Partition(int input[],int start,int end)  
  2. {  
  3.      int x = input[end];  
  4.      int i = start - 1;  
  5.      for(int j = start; j < end; j++)  
  6.      {  
  7.          if( input[j] <= x)  
  8.          {  
  9.              i++;  
  10.              swap(input[i],input[j]);  
  11.          }   
  12.      }  
  13.      swap(input[i+1],input[end]);  
  14.      return i+1;  
  15. }    
  16. void qsort(int input[],int start,int end)  
  17. {  
  18.      if(start < end)   
  19.      {  
  20.          int q = Partition(input, start, end);  
  21.          qsort(input, start, q-1);  
  22.          qsort(input, q+1, end);       
  23.      }   
  24. }  


考察快速排序的最坏效率,要考虑两种极端状况。1、假如每次划分的两个部分的元素相等,那么总递归时间T(n)为2*T(n/2)+n。其中后面的n为划分的开销。可证实,T(n)是O(n*logn)的(事实上,这个解是先猜想,再靠数学概括法加以证实的)。2、假如划分严重不对称,即分红一边是1个元素,一边是n-1个元素。那么有T(n)=T(1)+T(n-1)+n,实际上与插入排序同样,是一个算术级数!也就是说,在这种状况下,快速排序蜕变成了插入排序,因此时间复杂度为O(n^2)。
快速排序最神奇的地方在于,对于随机输入的数据,它可以自动调整到好的划分上去,其运行时间与最佳时间很是类似。例如,产生一个99:1的划分。看似很是的不平衡吧。可是能够证实,它的运行时间也是O(n*lgn)!
总的来讲,虽然算法的时间是O(n^2),可是快速排序在绝大多数状况下都能达到O(n*logn)的效率,使之成为了居家旅行,杀人越货当中不可或缺的排序利器。

4.合并排序
前面提到,既然把数据平均划分红两个部分分别排序,就能够达到很好的效率O(n*lgn),那么,是否是存在这样一种算法呢?合并算法是这样一种算法:将n个元素分红各含n/2个元素的子序列,而后对两个子序列分别排序。测试

[cpp]  view plain copy
 
  1. void mergeSort(int *input,int start,int end)  
  2. {  
  3.   int mid;  
  4.   if(start < end)  
  5.   {  
  6.   mid=(start+end)/2;  
  7.   mergeSort(input,start,mid);  
  8.   mergeSort(input,mid+1,end);  
  9.   merge(input,start,mid,end);  
  10.   }  
  11. }  
  12.   
  13. void merge(int *input,int start,int mid, int end) //辅助函数  
  14. {  
  15.   int n1=mid-start+1;   
  16.   int n2=end-mid;  
  17.   
  18.   int *L=new int[n1+1];   
  19.   int *R=new int[n2+1];  
  20.   for(int i=0;i < n1;i++)  
  21.     L[i]=input[start+i];  
  22.   for(int i=0;i < n2;i++)  
  23.     R[i]=input[mid+1+i];  
  24.   L[n1]=INF;  
  25.   R[n2]=INF;  
  26.   
  27.   int i=0,j=0;   
  28.   for(int k=start;k<=end;k++)  
  29.   {  
  30.     if(L[i]<=R[j])  
  31.       input[k]=L[i++];  
  32.     else  
  33.       input[k]=R[j++];  
  34.   }  
  35.   
  36.   delete L,R;  
  37. }  

 

能够看出时间复杂度为T(n)=2*T(n/2)+n,也就是O(n*lgn)!但须要指出的是,合并排序算法必须创建辅助数组L和R,它们的规模如同n同样线性增加。即它不是一个原地(in place)排序算法,在虚拟环境中不可以很好的工做。

5.堆排序
堆能够被视为一棵彻底二叉树(除去最后一层之外就是一个满二叉树,最后一层结点从左到右开始填)。咱们在堆排序中使用“最大堆”(MAX-HEAP),即每一个结点的值都比它的父结点要小。固然也有“最小堆”,原理都是同样的。
咱们把须要排序的数组看做一个堆,堆的每一个结点和数组中放该结点的那个元素对应。注意,堆的长度heap-size与数组长度length不尽相同,前者可能小于后者,任何在heap-size以后的元素都不属于相应的堆。
堆具备彻底二叉树的一些有趣性质:
设n0为度为0的结点总数(叶子结点);
n1为度为1的结点总数(彻底二叉树中只有一个,或者没有);
n2为度为2的结点总数。
如今咱们来求解叶子数量n0。
有n0+n1+n2=n---(1)
0*n0+1*n1+2*n2=n-1----(2)
消去n2,有n0=(n-n1+1)/2----(3)
根据式(3), 既然n1要么为1,要么为0,那么叶子节点要么等于n/2,要么等于(n+1)/2。这意味着在整个堆中,有一半(当n1=1),或者略少于一半(当n1=0)的结点是有子结点的。且这些结点都集中在数组的低位部分。
如下函数对于制定的输入input,使根为i的子树成为最大堆。这是堆排序中最重要的操做,称为堆的保持。此算法将遍历根为1的全部子树。在最坏的状况下,子树的底层刚好半满,这时,须要遍历的结点数为:
n * (0+1+2+...+2^(n-1)) / (1+2+3+...+2^(n-1)+2^(n-1)) 
= n * (2^n - 1) / (2^n + 2^(n-1)-1) 
< n * 2/3.
即全部结点的三分之二。计算运行时间:T(n) <= T(2/3*n)+ Θ(1).可证实T(n)=O(lgn).ui

[cpp]  view plain copy
 
  1. void MaxHeapify(int *input,int i)  
  2. {  
  3.   int largest;  
  4.   int l = i*2;  
  5.   int r = i*2+1;  
  6.   if( l <= HeapSize && input[l] > input[i])  
  7.     largest = l;  
  8.   else  
  9.     largest = i;  
  10.   if( r <= HeapSize && input[r] > input[largest])  
  11.     largest = r;  
  12.   if(largest != i)  
  13.   {  
  14.     swap(input[i],input[largest]);  
  15.     MaxHeapify(input, largest);   
  16.   }  
  17. }   

 

针对一个数组创建堆。注意只要对有子结点的结点进行堆的保持就能够了。能够证实创建堆是O(n)的。

 

[cpp]  view plain copy
 
  1. int HeapSize = 0;  
  2. void BuildMaxHeap(int *input)  
  3. {  
  4.   HeapSize = MAX-1;  
  5.   for(int i = MAX/2; i >= 0; i--)  
  6.     MaxHeapify(input,i);  
  7. }   

 

如下是堆排序的调用算法。此过程调用了O(lgn)的堆保持函数并循环n-1次。即获得了O(n*lgn)的时间复杂度。

[cpp]  view plain copy
 
  1. void HeapSort(int *input)  
  2. {  
  3.   BuildMaxHeap(input);  
  4.   for(int i = MAX-1; i > 0; i--) // no need to process when i = 0  
  5.   {  
  6.     swap(input[0],input[i]);  
  7.     HeapSize--;  
  8.     MaxHeapify(input,0);  
  9.     }  
  10. }  

堆排序总能达到O(n*lgn)的运行效率,就像合并排序同样。堆排序又是一种原地排序算法,就像快速排序和插入排序同样。所以,堆排序结合了以上几种排序的优势。可是,堆排序的实现不如快速排序的紧凑和简单。有资料代表,快速排序在实际应用中优于堆排序。参考文献:Thomas H. Cormen, Charles E. Leiserson, Introduction to Algorithms, 2ed

相关文章
相关标签/搜索