来源:http://blog.csdn.net/theprinceofelf/article/details/6672677
前奏:引入一个简单的操作函数,交换swap,功能是交换传入的两个值,这个简单的操作可以方便后面的程序编码:
- inline void swap(int &a,int &b)
- {
- a = a^b;
- b = a^b;
- a = a^b;
- };
上面的 ^ 是 异或 操作,这个交换实现是一种不使用中间变量进行交换的hack code,娱乐性质和实用性质都有一点儿。
1.冒泡排序
- void bubblesort(int *arr,int n)
- {
- for( int i=0; i<n; ++i)
- {
- for(int j=0; j<n-1-i; ++j)
- {
- if( arr[j] > arr[j+1] )
- swap(arr[j],arr[j+1]);
- }
- }
- }
冒泡排序应该是大家比较熟悉的,其特点如下:
(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.直接插入排序
- void insertsort(int *arr,int n)
- {
- for(int i=1; i<n; ++i)
- {
- int temp = arr[i];
- int j = i-1;
- for(; j>=0 && temp<arr[j] ; --j)
- { arr[j+1] = arr[j]; }
- arr[j+1] = temp;
- }
- }
直接插入排序,是一种十分有用的简单排序算法,由于其一些优秀的特性,在高级排序中往往会混合 直接插入排序,那么我们就来详细看看,直接插入排序的特点:
(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 ] 于是整个序列都很快有序了。
- void shellsort(int *arr,int n)
- {
- const int dltalen = 9;
- /*The best known sequence according to research by Marcin Ciura is
- 1, 4, 10, 23, 57, 132, 301, 701, 1750.*/
- int dlta[dltalen] = {1750,701,301,132,57,23,10,4,1};
- int temp;
- for(int t=0; t<dltalen; ++t)
- {
- int dk = dlta[t];
- for( int i=dk; i<n; ++i)
- {
- temp = arr[i]; /*临时存放*/
- int j = i-dk;
- for( ; j>=0 && temp<arr[j]; j-=dk)/*移动位置*/
- { arr[j+dk] = arr[j]; }
- arr[j+dk] = temp;/*插入*/
- }
- }
- }
希尔排序最有趣的地方在于她的步长序列选择上,步长序列选择的好坏直接决定了算法的效率,这也是为什么希尔排序效率是一个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)的空间复杂度而被人们记住的。在数据量巨大的情况下,堆排序的效率在慢慢接近快速排序。下面先看正统的堆排序实现:
- void heapAdjust( int *heap, int low, int high )
- {
- int temp = heap[low];
- for( int i=low*2+1; i<=high; i*=2)
- { /************************/
- if( i<high && heap[i]<=heap[i+1] )/* A点 */
- { ++i; } /* */
- if( heap[i] <= temp ) /* B点 */
- { break; } /*如果是建立小顶堆,只需*/
- heap[low] = heap[i] ; /*将A和B的<=改为>=即可 */
- low = i; /************************/
- }
- heap[low] = temp;
- }
- void heapSort( int *heap, int size )
- {
- for(int i=size/2-1; i>=0; --i )
- { heapAdjust(heap,i,size-1);}
- for(int i=size-1 ; i>0 ; --i )
- {
- swap( heap[0], heap[i] );
- heapAdjust(heap,0,i-1);
- }
- }
上面的堆排序是正统直观的实现方式,当然里面已经包含了一些精巧的特点,例如A点和B点的 <= 如果是换成< 也是可以工作的,但效率会低一些。B点的 <= 其实比较好理解,就是在 = 的情况下,减少一次不必要的赋值;但A点的 <= 中的 = 将直接影响堆调整时元素下降的速度。所以这个正统的版本其实已经是蛮经得起推敲的一个写法了。下面给出的则是一个更加优化的版本:
- void heapadjust( int *heap, int low, int high )
- {
- int temp = heap[low];
- for( int i=(low<<1)+1; i<=high; i=(i<<1))
- { /************************/
- if( i<high && heap[i]<heap[i+1] ) /* A点 */
- { swap(heap[i],heap[i+1]); } /* */
- if( heap[i] <= temp ) /* B点 */
- { break; } /*如果建立小顶堆,只需将*/
- heap[low] = heap[i] ; /*A点<改为>,B点<=改为>= */
- low = i; /************************/
- }
- heap[low] = temp;
- }
- void heapsort( int *heap, int size )
- {
- for(int i=(size>>1)-1; i>=0; --i )
- { heapadjust(heap,i,size-1);}
- for(int i=size-1 ; i>0 ; --i )
- {
- swap( heap[0], heap[i] );
- heapadjust(heap,0,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.快速排序
快速排序是实践工作中,最常用的一种排序算法了,被普遍认为是一般情况下的最高效算法。
- void qsort_op(int *arr,int n)
- {
- if( n > 1 )
- {
- int low = 0;
- int high = n-1;
- int pivot = arr[low];/* 取第1个数作为中轴,进行划分 */
- while( low < high )
- {
- while( low < high && arr[high] >= pivot )
- --high;
- arr[low] = arr[high];
- while( low < high && arr[low] <= pivot )
- ++low;
- arr[high] = arr[low];
- }
- arr[low] = pivot;
- qsort_op(arr,low);
- qsort_op(arr+low+1,n-low-1);
- }
- }
上述的代码,是我能够写出的最简洁的快速排序实现了,使用的是严蔚敏老师的《数据结构》的快速排序实现思量,没有采用《算法导论》的实现思路,因为综合比较后,发现严蔚敏老师的思路能显著减少移动次数,是一种更好的实现思路,但本质上两者是共通的。通常情况下的快速排序一般都是递归版本的。而关于快速排序,其实还可以有非递归的实现方式。因为本质上,大部分的递归都可以用栈来模拟。下面给出快速排序的非递归实现版本:
- void xp_sort(int *arr,int size)
- {
- int begin = 0;
- int end = size -1;
- if( begin < end )
- {
- const int stack_deepth = 65536;
- int stack[stack_deepth];
- int top = 0;
- stack[top++] = begin;
- stack[top++] = end;
- while( top != 0 )
- {
- int end_temp = stack[--top];
- int begin_temp = stack[--top];
- int high = end_temp;
- int low = begin_temp;
- if( low < high )
- {
- int pivot = arr[low];
- while( low < high )
- {
- while( low<high && pivot <= arr[high] )
- { --high;}
- arr[low] = arr[high];
- while( low<high && pivot >= arr[low] )
- { ++low; }
- arr[high] = arr[low];
- }
- arr[low] = pivot;
- stack[top++] = begin_temp;
- stack[top++] = low-1;
- stack[top++] = low+1;
- stack[top++] = end_temp;
- }
- }
- }
- }
快速排序这么受欢迎的究竟是为什么呢?因为快速排序在通常意义下确实是最快的,请不要带之一。人们总是乐于找到最快的算法,人们也常常好奇于第一名是谁,大多数人都对第二名不感兴趣,冠军总是荣耀的,亚军总是默默无闻的。这也是为什么人们通常喜欢讨论“快速排序vs堆排序vs归并排序vs希尔排序”的原因。而他们间的较量结果大致是这样的:中小规模下,希尔排序可能更快(注意可能),大规模下快速排序最快但可能发生最坏情况n²,归并排序可以多线程而且是唯一稳定的高效排序,堆排序可以保证最坏情况下都是nlogn,对了弱弱地提一句,快速排序的最坏情况可能并不是n²的时间复杂度,而是"error:stack over flow",一个明明应该正常工作的排序,在某个时候就莫名地溢栈了,这让人情何以堪啊!
综合上述,得到的各种情况下的最优排序分别是:
(1)序列完全有序,或者序列只有尾部部分无序,且无序数据都是比较大的值时,【直接插入排序】最佳(哪怕数据量巨大,这种情形下也比其他任何算法快)
(2)数据基本有序,只有少量的无序数据零散分布在序列中时,【希尔排序】最佳
(3)数据基本逆序,或者完全逆序时,【希尔排序】最佳(哪怕是数据量巨大,希尔排序处理逆序数列,始终是最好的,当然三数取中优化的快速排序也工作良好)
(4)数据包含大量重复值,【希尔排序】最佳(来自实验测试,直接插入排序也表现得很好)
(5)数据量比较大或者巨大,单线程排序,且较小几率出现基本有序和基本逆序时,【快速排序】最佳
(6)数据量巨大,单线程排序,且需要保证最坏情形下也工作良好,【堆排序】最佳
(7)数据量巨大,可多线程排序,不在乎空间复杂度时,【归并排序】最佳