排序算法

来源:互联网 发布:java怎么求最小公倍数 编辑:程序博客网 时间:2024/05/16 02:15

来源:http://blog.csdn.net/theprinceofelf/article/details/6672677




前奏:引入一个简单的操作函数,交换swap,功能是交换传入的两个值,这个简单的操作可以方便后面的程序编码:

[cpp] view plaincopy
  1. inline void swap(int &a,int &b)  
  2. {  
  3.     a = a^b;  
  4.     b = a^b;  
  5.     a = a^b;      
  6. };  

    上面的 ^ 是 异或 操作,这个交换实现是一种不使用中间变量进行交换的hack code,娱乐性质和实用性质都有一点儿。

1.冒泡排序

[cpp] view plaincopy
  1. void bubblesort(int *arr,int n)  
  2. {  
  3.     forint i=0; i<n; ++i)  
  4.     {  
  5.         for(int j=0; j<n-1-i; ++j)  
  6.         {  
  7.             if( arr[j] > arr[j+1] )  
  8.                 swap(arr[j],arr[j+1]);  
  9.         }     
  10.     }  
  11. }  

冒泡排序应该是大家比较熟悉的,其特点如下:

   (1)稳定的,即如果有 [...,5,5...] 这样的序列,排完序后,这两个5的顺序一定不会改变,这在一般情况下是没有意义的,但当 5 这个节点不仅仅是一个数值,是一个结构体或者类实例,而结构体有附带数据的时候,这两个节点的顺序往往是有意义的,如果能够稳定有时候是关键的。因为如果不稳定则可能破坏附带数据的顺序结构。

    (2)比较次数恒定,总是比较 n²/2 次,哪怕数据已经完全有序了

    (3)最好的情况下,一个数据也不用移动,冒泡排序的最好情况是: 【数据已经有序】

    (4)最坏的情况下,移动 n²/2 次数据,冒泡排序的最坏情况是:【数据是逆序的】。

      需要说明的是的 n²/2  这个结果是:1+2+3+...+n-1 = n(n-1)/2,等差数列求和得到。

2.简单选择排序

3.直接插入排序

[cpp] view plaincopy
  1. void insertsort(int *arr,int n)  
  2. {  
  3.     for(int i=1; i<n; ++i)  
  4.     {  
  5.         int temp = arr[i];  
  6.         int j = i-1;  
  7.         for(; j>=0 && temp<arr[j] ; --j)  
  8.         {   arr[j+1] = arr[j];  }  
  9.         arr[j+1] = temp;  
  10.     }     
  11. }  

直接插入排序,是一种十分有用的简单排序算法,由于其一些优秀的特性,在高级排序中往往会混合 直接插入排序,那么我们就来详细看看,直接插入排序的特点:

(1)稳定的,这点不多做解释,参见冒泡排序的说明

(2)最好情况下,只做 n-1 次比较,不做任何移动,比如 [ 1, 2, 3, 4, 5 ] 这个序列,算法a.检查2 能否插入1 前==>不能;b.检查3能否插入到2前==>不能;...以此类推,只需做完 n-1 次比较就完成排序,0次移动数据操作。直接插入排序的最好情况是【数据完全有序】

(3)最坏情况下,做 n²/2 次比较,做 n²/2 次移动数据操作,比如 [ 5, 4, 3, 2, 1 ]这个序列,4需要插入到5前,3需要插入到4,5前,...1需要插入到2,3,4,5前,同样由等差数列求和公式,可得比较次数和移动次数都是n(n-1)/2,简记为n²/2。直接插入排序的最好情况是【数据完全逆序】

(4)有人说直接插入排序是在序列越有序表现越好,数据越逆序表现越差,其实这种说法是错误的。举个例子说明,序列a [ 6,1,2,3,4,0 ] ,数据其实已经基本有序,只是0,6的位置不对,简单0,6交换即可得到正确序列,但插入排序会把 1,2,3,4以此插入到6前,在把0插入到1,2,3,4,6前,几乎有2n次移动操作。可见直接插入排序要想达到高效率,要求的有序不是基本有序,而前半部分完全有序,只有尾部有部分数据无序,例如 [ 0,1,2,3,4,5,5,6,7,8,9,........,107,99,96,101] 对这样一个只有尾部有部分数据无序,且尾部数据不会干扰到序列首部的 [0,1,2,3,4....] 的位置时,直接插入排序 是其他任何算法都无法匹敌的。

4.希尔排序

   这是一个神奇的排序,shell排序起初的设计目的就是改进直接插入排序(另外有一种二分插入排序也是对直接插入排序的改进),因为直接插入排序在诸如 [ 6,1,2,3,4,5,0 ] 这样的基本有序数列上表现不佳,人们设想是不是可以让插入的步长更大一些,比如步长为3,则相当于将序列分组为  [ 6,3 ,0 ]  [1,4 ] [ 2,5 ]这样三个子序列进行插入排序,这样[ 6,3,0 ] 一组可以很快地变换到 [ 0,3,6 ] 于是整个序列都很快有序了。

[cpp] view plaincopy
  1. void shellsort(int *arr,int n)  
  2. {  
  3.     const int dltalen = 9;  
  4.     /*The best known sequence according to research by Marcin Ciura is 
  5.       1, 4, 10, 23, 57, 132, 301, 701, 1750.*/  
  6.     int dlta[dltalen] = {1750,701,301,132,57,23,10,4,1};  
  7.     int temp;  
  8.   
  9.     for(int t=0; t<dltalen; ++t)  
  10.     {  
  11.         int dk = dlta[t];  
  12.         forint i=dk; i<n; ++i)  
  13.         {     
  14.             temp = arr[i]; /*临时存放*/  
  15.   
  16.             int j = i-dk;  
  17.             for( ; j>=0 && temp<arr[j]; j-=dk)/*移动位置*/  
  18.             {   arr[j+dk] = arr[j]; }  
  19.   
  20.             arr[j+dk] = temp;/*插入*/                       
  21.         }  
  22.     }  
  23. }  

希尔排序最有趣的地方在于她的步长序列选择上,步长序列选择的好坏直接决定了算法的效率,这也是为什么希尔排序效率是一个n²/2 ~nlog²n的原因,纠正一下传说来自《大话数据结构》的表中将希尔排序记作了n²/2 ~nlogn,这是不对的,目前的理论研究证明的希尔排序最好效率是nlog²n,这个logn上的平方是不能少的,差距很大的。上面的希尔排序中使用一个特殊的序列,是Marcin Ciura发布的研究报告中得到的目前已知最好序列,在使用这个特别的步长序列时,希尔排序的效率是nlog²n。论文的原文在:http://oeis.org/A102549,大家可以详细研究一下。那么希尔排序有哪些特点呢?

(1)希尔排序是不稳定的

(2)希尔排序特别适合于,大部分数据基本有序,只有少量数据无序的情况下,如 [ 6,1,2,3,4,5,0 ] 希尔排序能迅速定位到无序数据,从而迅速完成排序

(3)希尔排序的步长序列,无论如何选择最后一个必须是1,因为希尔排序的最后一步本质上就是直接插入排序,只是通过前面的步长排序,将序列尽量调整到直接插入排序的最高效状态。

(4)研究表明优良的步长序列选择下,在中小规模数据排序时,希尔排序是可以快过快速排序的。因为希尔排序的最佳步长下效率是 n*logn*logn*a(非常小常数因子) ,而快速排序的效率是 n*logn*b(小常数因子),在 n 小于一定规模时,logn*a 是可能小于b的,比如 a=0.25,b=4,n = 65535;此时logn*a<4 ,b=4;当然我一直没有看到希尔排序的确切常数因子报告,倒是隐约记得在什么地方看到快速排序的常数因子是4,但无法确定,如果谁知道快速排序的确切常数因子,麻烦告知。

5.堆排序

    堆排序是由于其最坏情况下nlogn时间复杂度,以及o(1)的空间复杂度而被人们记住的。在数据量巨大的情况下,堆排序的效率在慢慢接近快速排序。下面先看正统的堆排序实现:

[cpp] view plaincopy
  1. void heapAdjust( int *heap, int low, int high )  
  2. {  
  3.     int temp = heap[low];  
  4.     forint i=low*2+1; i<=high; i*=2)  
  5.     {                                     /************************/  
  6.         if( i<high && heap[i]<=heap[i+1] )/*         A点          */  
  7.         {   ++i;        }                 /*                      */  
  8.         if( heap[i] <= temp )             /*         B点          */  
  9.         {   break;      }                 /*如果是建立小顶堆,只需*/  
  10.         heap[low] = heap[i] ;             /*将A和B的<=改为>=即可  */  
  11.         low = i;                          /************************/  
  12.     }  
  13.     heap[low] = temp;  
  14. }  
  15. void heapSort( int *heap, int size )  
  16. {  
  17.     for(int i=size/2-1; i>=0; --i )  
  18.     {   heapAdjust(heap,i,size-1);}  
  19.     for(int i=size-1  ; i>0 ; --i )  
  20.     {  
  21.         swap( heap[0], heap[i] );  
  22.         heapAdjust(heap,0,i-1);  
  23.     }  
  24. }  
    代码由两部分组成,heapAdjust的调整 [ low , high ] 区间内的元素满足堆性质,代码正确工作的前提条件是只有heap[low]一个元素是不满足堆性质。heapSort 第一个for 循环是将 [ 0 ,size-1 ] 区间内的元素建成一个“大顶堆”。很多人不明白为什么建堆的时候是从 size/2-1 开始,到0结束,而显然这个时候 [ size/2, size ] 这接近一半的区间都是不满足堆性质的。这是因为这一半的区间在开始的时候不需要满足堆性质,因为你如果把整个堆看做一颗二叉树,那么这一半的区间就一定是树叶,树叶之间不需要满足特定的性质,而重要的是树叶和上层的树枝之间满足堆性质即可,这就是为什么heapAdjust是在 [ size/2-1, 0 ] 这个区间进行的原因。heapSort 的第二个for 循环是每次从堆顶取出元素,然后重新调整堆。整个排序就是在不断地从堆顶取元素,不断地重新调整堆。

    上面的堆排序是正统直观的实现方式,当然里面已经包含了一些精巧的特点,例如A点和B点的 <= 如果是换成< 也是可以工作的,但效率会低一些。B点的 <= 其实比较好理解,就是在 = 的情况下,减少一次不必要的赋值;但A点的 <= 中的 = 将直接影响堆调整时元素下降的速度。所以这个正统的版本其实已经是蛮经得起推敲的一个写法了。下面给出的则是一个更加优化的版本:

[cpp] view plaincopy
  1. void heapadjust( int *heap, int low, int high )  
  2. {  
  3.     int temp = heap[low];  
  4.     forint i=(low<<1)+1; i<=high; i=(i<<1))  
  5.     {                                     /************************/  
  6.         if( i<high && heap[i]<heap[i+1] ) /*         A点          */  
  7.         {   swap(heap[i],heap[i+1]); }    /*                      */  
  8.         if( heap[i] <= temp )             /*         B点          */  
  9.         {   break;      }                 /*如果建立小顶堆,只需将*/  
  10.         heap[low] = heap[i] ;             /*A点<改为>,B点<=改为>= */  
  11.         low = i;                          /************************/  
  12.     }  
  13.     heap[low] = temp;  
  14. }  
  15. void heapsort( int *heap, int size )  
  16. {  
  17.     for(int i=(size>>1)-1; i>=0; --i )  
  18.     {   heapadjust(heap,i,size-1);}  
  19.     for(int i=size-1  ; i>0 ; --i )  
  20.     {  
  21.         swap( heap[0], heap[i] );  
  22.         heapadjust(heap,0,i-1);  
  23.     }  
  24. }  
    这个版本的堆排序主要优化了两点,所有乘2、除2操作都优化成了位移,另外在heapadjust函数中,if( i<high && heap[i]<heap[i+1] 时,不是++i 操作,而是使用的是swap交换heap[i]和heap[i+1],这里为什么使用交换,以及为什么 <= 换成了 < 是可以推敲的。那么现在我们总的来看一下堆排序的特征:

(1)堆排序是不稳定的

(2)堆排序在最坏的情形下都能保证nlogn的时间复杂度,这是因为对于深度为k的堆,调整算法(heapadjust)至多比较2(k-1)次,建立n个元素,深度为h的堆时,总共比较次数不超过4n;另外,n个结点的完全二叉树深度为 [logn]+1,在抽取堆顶,重新调整过程中调用调整算法n-1次,求和的值小于2n[ logn ];总共的比较次数一定小于4n+2n[logn]

(3)堆排序在任何时候都表现出出色的稳定性,这种稳定大概可以这样解释:当遇到基本有序、基本逆序的序列时,堆排序和插入排序、希尔排序表现得接近;当遇到大规模数据的时候,堆排序表现得和快速排序接近。也就是说:在任何某种情形下,堆排序都基本不是表现最好的,但一定和表现最好的算法差距不大,相应地一定远远好于这种情形下表现较差的算法,没有任何序列能够使堆排序进入所谓特别糟糕的情形。堆排序永远是那么稳定,按照你所期望性能运行,永远不是最好,永远都接近最好的算法;如果非要用句话形容堆排序就是:【万年老二】

(4)堆排序的建堆策略是影响性能的一项重要因素,举例说明:你可以使用建立“小顶堆”来完成“降序”排序,你也可以使用建立“大顶堆”来完成“升序”排序;但这两种策略都是极其低效的;你相当于你建立了一个基本逆序的序列,你最后要得到一个顺序的序列。正确的策略应该是“小顶堆”来完成“升序”,“大顶堆”来完成“降序”,注意这种策略的代码不易编写,我上面给出示范代码是“大顶堆”完成“升序”的排序方式,而大部分的教材也采用的是这种较易实现的方式。后续可能补充"大顶堆"完成“降序”的算法。

6.归并排序

    还在优化代码,我想写出尽量 简单 可读 高效 的代码给大家分享。分析也相应延后。

7.快速排序

   快速排序是实践工作中,最常用的一种排序算法了,被普遍认为是一般情况下的最高效算法。

[cpp] view plaincopy
  1. void qsort_op(int *arr,int n)  
  2. {  
  3.     if( n > 1 )  
  4.     {  
  5.         int low    = 0;  
  6.         int high   = n-1;  
  7.         int pivot  = arr[low];/* 取第1个数作为中轴,进行划分 */  
  8.         while( low < high )  
  9.         {  
  10.             while( low < high && arr[high] >= pivot )  
  11.                 --high;  
  12.             arr[low] = arr[high];  
  13.   
  14.             while( low < high && arr[low] <= pivot )  
  15.                 ++low;  
  16.             arr[high] = arr[low];     
  17.         }  
  18.         arr[low] = pivot;  
  19.         qsort_op(arr,low);        
  20.         qsort_op(arr+low+1,n-low-1);  
  21.     }  
  22. }  

    上述的代码,是我能够写出的最简洁的快速排序实现了,使用的是严蔚敏老师的《数据结构》的快速排序实现思量,没有采用《算法导论》的实现思路,因为综合比较后,发现严蔚敏老师的思路能显著减少移动次数,是一种更好的实现思路,但本质上两者是共通的。通常情况下的快速排序一般都是递归版本的。而关于快速排序,其实还可以有非递归的实现方式。因为本质上,大部分的递归都可以用栈来模拟。下面给出快速排序的非递归实现版本:

[cpp] view plaincopy
  1. void xp_sort(int *arr,int size)  
  2. {  
  3.     int begin = 0;  
  4.     int end = size -1;  
  5.   
  6.     if( begin < end )  
  7.     {  
  8.         const int stack_deepth = 65536;  
  9.         int stack[stack_deepth];  
  10.         int top = 0;  
  11.         stack[top++] = begin;  
  12.         stack[top++] = end;  
  13.   
  14.         while( top != 0 )  
  15.         {  
  16.             int end_temp   = stack[--top];  
  17.             int begin_temp = stack[--top];  
  18.   
  19.             int high = end_temp;  
  20.             int low  = begin_temp;  
  21.   
  22.             if( low < high )  
  23.             {  
  24.                 int pivot = arr[low];  
  25.                 while( low < high )  
  26.                 {  
  27.                     while( low<high && pivot <= arr[high] )  
  28.                     { --high;}  
  29.                     arr[low] = arr[high];  
  30.                     while( low<high && pivot >= arr[low]  )  
  31.                     { ++low; }  
  32.                     arr[high] = arr[low];  
  33.                 }  
  34.                 arr[low] = pivot;  
  35.   
  36.                 stack[top++] = begin_temp;  
  37.                 stack[top++] = low-1;  
  38.                 stack[top++] = low+1;  
  39.                 stack[top++] = end_temp;  
  40.             }  
  41.         }     
  42.     }  
  43. }  

   快速排序这么受欢迎的究竟是为什么呢?因为快速排序在通常意义下确实是最快的,请不要带之一。人们总是乐于找到最快的算法,人们也常常好奇于第一名是谁,大多数人都对第二名不感兴趣,冠军总是荣耀的,亚军总是默默无闻的。这也是为什么人们通常喜欢讨论“快速排序vs堆排序vs归并排序vs希尔排序”的原因。而他们间的较量结果大致是这样的:中小规模下,希尔排序可能更快(注意可能),大规模下快速排序最快但可能发生最坏情况n²,归并排序可以多线程而且是唯一稳定的高效排序,堆排序可以保证最坏情况下都是nlogn,对了弱弱地提一句,快速排序的最坏情况可能并不是n²的时间复杂度,而是"error:stack over flow",一个明明应该正常工作的排序,在某个时候就莫名地溢栈了,这让人情何以堪啊!

综合上述,得到的各种情况下的最优排序分别是:

(1)序列完全有序,或者序列只有尾部部分无序,且无序数据都是比较大的值时,【直接插入排序】最佳(哪怕数据量巨大,这种情形下也比其他任何算法快)

(2)数据基本有序,只有少量的无序数据零散分布在序列中时,【希尔排序】最佳

(3)数据基本逆序,或者完全逆序时,【希尔排序】最佳(哪怕是数据量巨大,希尔排序处理逆序数列,始终是最好的,当然三数取中优化的快速排序也工作良好)

(4)数据包含大量重复值,【希尔排序】最佳(来自实验测试,直接插入排序也表现得很好)

(5)数据量比较大或者巨大,单线程排序,且较小几率出现基本有序和基本逆序时,【快速排序】最佳

(6)数据量巨大,单线程排序,且需要保证最坏情形下也工作良好,【堆排序】最佳

(7)数据量巨大,可多线程排序,不在乎空间复杂度时,【归并排序】最佳




0 0
原创粉丝点击