首先给出各个排序方式的性能比较:
排序方法的比较 | ||||||
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
平均情况 | 最好情况 | 最坏情况 | 辅助存储 | |||
插入排序 | 直接插入 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
希尔排序 | O(n1.3) | O(n) | O(n2) | O(1) | 不稳定 | |
选择排序 | 直接选择 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 | |
交换排序 | 冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(nlog2n) | O(n2) | O(nlog2n) | 不稳定 | |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 | |
基数排序 | O(d(r+n)) | O(d(rd+n)) | O(d(r+n)) | O(rd+n) | 稳定 | |
注:基数排序中,n代表关键字的个数,d代表长度,r代表关键字的基数 |
下面依次展开。
一、冒泡排序
1、原理
冒泡排序算法的运作如下:
a、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
b、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
c、针对所有的元素重复以上的步骤,除了最后一个。
d、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2、举例
初始序列为:17,3,25,14,20,9
第一轮排序过程:(当前正在比较的两个数用有色字体标出,红色代表这两个数发生了交换,蓝色代表没有交换)
17,3,25,14,20,9–>3,17,25,14,20,9–>3,17,25,14,20,9–>3,17,14,25,20,9–>3,17,14,20,25,9–>3,17,14,20,9,25
可以看到,第一轮排序进行了n-1次(n为数组长度),第一轮排序后,最大的一个数“冒”到了最后。
第二轮排序过程:
3,17,14,20,9,25–>3,17,14,20,9,25–>3,14,17,20,9,25–>3,14,17,20,9,25–>3,14,17,9,20,25
可以看到,第二轮排序进行了n-2次(n为数组长度),第二轮排序后,第二大的数“冒”到了最后第二位。
以此类推,第三轮排序后:3,14,9,17,20,25
第四轮排序后:3,9,14,17,20,25
第五轮排序后:3,9,14,17,20,25
总共需要n-1轮排序(n为数组长度)。
3、算法复杂度
若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值:C=n-1,M=0。
所以,冒泡排序最好的时间复杂度为O(n)。
若初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i 次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:C=n(n-1)/2,M=3n(n-1)/2,因此,冒泡排序的最坏时间复杂度为O(n2)。
综上,因此冒泡排序总的平均时间复杂度为O(n2)。
4、稳定度
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
5、示例代码
#include <iostream>
using namespace std;
template<typename T> //整数或浮点数皆可使用,若要使用物件时必须设定大于的运算子功能
void bubble_sort(T arr[], int len)
{
int i, j;
T temp;
for (i = 0; i < len – 1; i++)
for (j = 0; j < len – 1 – i; j++)
if (arr[j] > arr[j + 1])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
int main()
{
int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ‘ ‘;
cout << endl;
float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 };
len = (int) sizeof(arrf) / sizeof(*arrf);
bubble_sort(arrf, len);
for (int i = 0; i < len; i++)
cout << arrf[i] << ‘ ‘;
return 0;
}
二、选择排序
1、原理
选择排序的运作过程如下:
a)、比较初始两个元素大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置(下标)。
b)、向后遍历数组,如果当前的元素比当前k标记位置对应的元素要小,则用更新变量k等于当前元素在数组中的位置(下标)。
c)、以此比较直到数组末尾,此时我们找到了最小的那个数的下标,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值。
d)、然后从数组第二个元素的位置开始找到第二小的数,让它跟数组中第二个元素交换一下值,以此类推。
2、示例
初始序列为:17,3,25,14,20,9
第一轮排序过程:(蓝色字体表示当前最小下标k所指位置,红色字体表示交换两个元素位置)
17,3,25,14,20,9–>17,3,25,14,20,9–>17,3,25,14,20,9–>17,3,25,14,20,9–>17,3,25,14,20,9–>17,3,25,14,20,9–>3,17,25,14,20,9
第一轮进行了n-1次元素比较和1次元素交换,最终数组中最小的元素被放到了数组开头。
以此类推,第二轮排序后:3,9,25,14,20,17
第三轮排序后:3,9,14,25,20,17
第四轮排序后:3,9,14,17,20,25
第五轮排序后:3,9,14,17,20,25
总共需要n-1轮排序。
3、算法复杂度
选择排序的比较次数固定为O(n^2),但交换次数与数组初始序列有关。
当原数组有序时,交换0次;当原数组逆序时,交换n/2次;最坏情况交换n-1次。
所以选择排序的最坏、平均时间复杂度与冒泡排序是一个数量级的,都是O(n^2)。但由于交换次数比冒泡排序少,而且交换所需的CPU时间比比较所需的CPU时间多,因此n较小时,选择排序比冒泡排序快。
4、稳定性
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举个例子,序列5,8,5,2,9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
5、示例代码
#include<iostream>
#include<time.h>
#include<iomanip>
using namespace std;
const int N=10;
int main()
{
int a[N],i,j,temp,b;
srand(time(NULL));
for(i=0;i<N;i++)
a[i]=rand()%100;
for(i=0;i<N;i++)
cout<<setw(3)<<a[i];
cout<<endl;
for(i=0;i<N-1;i++)
{
temp=i;
for(j=i+1;j<N;j++)
{
if(a[temp]>a[j])
temp=j;
}
if(i!=temp)
{
b=a[temp];
a[temp]=a[i];
a[i]=b;}
}
for(i=0;i<N;i++)
cout<<setw(3)<<a[i];
cout<<endl;
}
三、直接插入排序
1、原理
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
a)、数组的第一个元素自己构成一个有序表,剩下的n-1个元素构成无序表。
b)、依次用无序表中的第一个元素与有序表中各个元素比较,将其插入到有序表中构成新的有序表,并将其从无序表中去除,剩余元素构成新的无序表。
c)、最终得到的有序表即为排序完成的数组,此时无序表中已无元素。
2、示例
初始序列为17,3,25,14,20,9
第一轮排序过程:(红色字体代表当前哨兵)
有序表A为17,无序表B为3,25,14,20,9,哨兵R为无序表中第一个元素3
将哨兵与有序表中各个元素比较,插入到表中某个位置构成新的有序表A‘为3,17,新的无序表B’为25,14,20,9,新的哨兵R’为25
第二轮排序后:
A”:3,17,25B”:14,20,9R”:14
以此类推,最终得到有序表:3,9,14,17,20,25即为排好的数组。
此时,无序表为空,哨兵指向数组外的下一个元素。
引入哨兵的两个用途:
a)、用于暂存无序表中第一个元素数据,放置有序表向后扩张造成的数据丢失。
b)、用来控制程序的结束,当发现哨兵越界时,说明排序过程已经结束。
3、算法复杂度
当文件的初始状态不同时,直接插入排序所耗费的时间是有很大差异的。
最好情况是数组初态为正序,此时每个乱序表中的第一个元素(即哨兵)都只需要和有序表中最后一个数比较即可找到插入的位
置,因此总共需要的比较次数为n-1次,而且需要的交换次数为0,因此,此时算法的时间复杂度为O(n)。
最坏情况是文件初态为反序,此时每个乱序表中的第一个元素都要依次和有序表中每个元素进行比较才能确定插入位置,因此总共需要的比较次数为:1+2+…+n-1,同样,需要的移动次数为1+2+…+n-1,因此,此时的时间复杂度为O(n2)。
算法的平均时间复杂度是O(n2)。
由于需要一个哨兵,因此算法的辅助空间复杂度是O(1),直接插入排序是一个就地排序。
4、稳定性
直接插入排序是稳定排序,即一旦发现哨兵所指元素和有序表中某个元素相等,我们将哨兵所指元素放到这个元素后面即可。
5、示例代码
#include<iostream>
using namespace std;
int main()
{
int a[]={98,76,109,34,67,190,80,12,14,89,1};
int k=sizeof(a)/sizeof(a[0]);
int j;
for(int i=1;i<k;i++)//循环从第2个元素开始
{
if(a[i]<a[i-1])
{
int temp=a[i];
for(j=i-1;j>=0 && a[j]>temp;j--)
{
a[j+1]=a[j];
}
a[j+1]=temp;//此处就是a[j+1]=temp;
}
}
for(int f=0;f<k;f++)
{
cout<<a[f]<<" ";
}
return 0;
}
四、快速排序
1、原理
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。
一趟快速排序的算法是:
a)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
b)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
c)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
d)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
e)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
注意:一趟快速排序不能得到最终结果,只是把所有小于key的值放在key的左边,所有大于key的值放在了key的右边。
要得到最终的有序数组,需要对key左右两边的无序数组再进行快速排序,直到数组不能分解(只含有一个数据),才得到正确的结果。
2、示例
初始序列为17,3,25,14,20,9
第一趟排序过程:(key为17,一开始i=0,j=5)
第一次扫描:从后往前(j–),找到第一个小于17的数是9,此时j=5,交换9和17的位置,得到序列:9,3,25,14,20,17
第二次扫描:从前往后(i++),找到第一个大于17的数是25,此时i=2,交换25和17的位置,得到序列:9,3,17,14,20,25
以此类似,交替扫描:
第三次扫描(j–):9,3,14,17,20,25,此时i=2,j=3
第四次扫描(i++):i++此时等于j了,循环终止。
因此,第一趟排序得到序列:9,3,14,17,20,25,可以看出,所有小于17的值都放到了17左边,所有大于17的值都放在了17右边。
然后,用同样的方法对17左右两边的无序数列进行快速排序,可得到最终的有序数列:3,9,14,17,20,25
3、算法复杂度
快速排序的复杂度和每次划分两个序列的相对大小有关。
最好情况:每次划分过程产生的两个区间大小都为n/2(如无序序列,越“无序”越好),这时快速排序法运行得很快了。有:
T(n)=2T(n/2)+θ(n),T(1)=θ(1)
解得:T(n)=θ(nlogn)
最快情况:每次划分过程产生的区间大小分别为1和n-1(如有序序列,无论逆序还是顺序,快速排序退化为冒泡排序),此时快速排序要进行很多次划分和比较。有:
T(n)=T(n-1)+T(1)+θ(n),T(1)=θ(1)
解得:T(n)=θ(n2)
快速排序的平均算法复杂度为θ(nlogn)。
算法的空间复杂度理论上是θ(1),但由于使用分治法,所有实际复杂度应该是θ(nlogn)。
注意:一般情况下,快速排序的性能总是最好的,因为其排列无序序列的性能非常好。
4、稳定性
快速排序是不稳定的,由于快速排序从后往前查找第一个小于key的元素,而一旦如果找到的这个元素前面有与之相等的其他元素,则必会打破稳定性,因此此时key元素可能会被调换到其他与之相等的元素之间。举例:
初始序列为:5,3,3,4,3,8,9,10,11
此时key为5,查找到第一个小于5的值为3,此时j为4,将j对应的3与key对应的5交换,会导致最后一个3反而放到最前面去了,稳定性被破坏。此时得到的序列为:3,3,3,4,5,8,9,10,11
5、示例代码
#include <iostream>
using namespace std;
void Qsort(int a[], int low, int high)
{
if(low >= high)
{
return;
}
int first = low;
int last = high;
int key = a[first];/*用字表的第一个记录作为枢轴*/
while(first < last)
{
while(first < last && a[last] >= key)
{
--last;
}
a[first] = a[last];/*将比第一个小的移到低端*/
while(first < last && a[first] <= key)
{
++first;
}
a[last] = a[first];
/*将比第一个大的移到高端*/
}
a[first] = key;/*枢轴记录到位*/
Qsort(a, low, first-1);
Qsort(a, first+1, high);
}
int main()
{
int a[] = {57, 68, 59, 52, 72, 28, 96, 33, 24};
Qsort(a, 0, sizeof(a) / sizeof(a[0]) - 1);
for(int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
cout << a[i] << "";
}
return 0;
}
五、归并排序
1、原理
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
如果每次都将两个子序列合并,则成为二元归并。以二元归并为例,归并排序包括以下几个步骤:
a)、将无序序列从中间划分为两个等长的子序列;
b)、将子序列依次划分下去,直到不能再划分为止(每个子序列只有两个元素 ),并将这两个元素按大小排序;
c)、将小的子序列两两进行归并操作,合成有序的稍大的序列,依次归并进行下去,直到合成整个序列。
注意到归并排序中最重要的就是归并操作,归并操作的过程如下:
a)、申请额外空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
b)、设定两个指针,最初位置分别为两个已经排序序列的起始位置;
c)、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
d)、重复步骤c直到某一指针超出序列尾;
e)、将另一序列剩下的所有元素直接复制到合并序列尾。
2、示例
初始序列为17,3,25,14,20,9
第一步:分割原始序列为两个子序列,分别为17,3,25和14,20,9;
第二步:继续分割子序列,形成更小的子序列,分别为17,3和25以及14,20和9;
第三步:每个子序列中只有两个元素了,将这两个元素按序排好,得到新的4个子序列为3,17和25以及14,20和9;
第四步:最小子序列的归并操作,合成两个大的序列3,17,25和9,14,20;
第五步:子序列的归并操作,合成最终的有序序列3,9,14,17,20,25。
归并操作步骤:(以上述第五步的子序列归并操作为例)
第一步:创建长度为6的临时数组存储中间变量;
第二步:初始时,两个指针分别指向两个子序列的第一个元素3和9,比较这两个元素,将较小的数(此处为3)放入临时数组中,并将指向3的指针后移一位。此时,两个序列为3,17,25和9,14,20,临时数组为3;
第二步:比较当前两个指针所指元素大小,此时为17和9,将较小的数9放入临时数组中,并将指向9的指针后移一位。此时,两个序列为3,17,25和9,14,20,临时数组为3,9;
依次类推,最终得到临时数组为3,9,14,17,20,25,即为归并操作后新的有序数列。
3、算法复杂度
归并排序的比较操作介于(nlogn)/2和nlogn-n+1之间,而赋值操作固定为2nlogn。因此,归并排序的时间复杂度固定为O(nlogn)。
由于归并操作需要创建临时数组,因此其空间复杂度为O(n)。
虽然归并排序比较占用内存,但却是一种高效率算法,其速度仅次于快速排序。
4、稳定性
由于归并排序中两个相同的数会被依次放入临时数组中,也就是说我们可以控制排在前面的数放在临时数组前面,而后面的数放在临时数组后面,因此,归并排序是一个稳定的排序方法。
5、示例代码
#include<iostream>
#include<ctime>
#include<cstring>
#include<cstdlib>
using namespace std;
/**将a开头的长为length的数组和b开头长为right的数组合并n为数组长度,用于最后一组*/
void Merge(int* data,int a,int b,int length,int n){
int right;
if(b+length-1>=n-1) right=n-b;
else right=length;
int* temp=new int[length+right];
int i=0,j=0;
while(i<=length-1&&j<=right-1){
if(data[a+i]<=data[b+j]){
temp[i+j]=data[a+i];i++;}
else{temp[i+j]=data[b+j];j++;}
}
if(j==right){//a中还有元素,且全都比b中的大,a[i]还未使用
memcpy(data+a+i+j,data+a+i,(length-i)*sizeof(int));
}
memcpy(data+a,temp,(i+j)*sizeof(int));
delete temp;
}
void MergeSort(int* data,int n){
int step=1;
while(step<n){
for(int i=0;i<=n-step-1;i+=2*step)
Merge(data,i,i+step,step,n);
//将i和i+step这两个有序序列进行合并
//序列长度为step
//当i以后的长度小于或者等于step时,退出
step*=2;//在按某一步长归并序列之后,步长加倍
}
}
int main(){
int n;
cin>>n;
int* data=new int[n];
if(!data) exit(1);
int k=n;
while(k--){
cin>>data[n-k-1];
}
clock_t s=clock();
MergeSort(data,n);
clock_t e=clock();
k=n;
while(k--){
cout<<data[n-k-1]<<' ';
}
cout<<endl;
cout<<"the algorithm used"<<e-s<<"miliseconds."<<endl;
delete data;
return 0;
}
六、基数排序(桶排序)
1、原理
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
桶排序主要使用于整数排序,如果需要对浮点数进行排序,需要清楚浮点数在机器中的存储方式,将浮点数转化成对应的整数后进行排序,完了再将排好序的整数转换成原始的浮点数。这一过程就比较繁琐了。
2、示例
初始序列为17,3,25,14,20,9(以LSD为例)
第一步:遍历序列,将个位相同的数放到相应的桶中,也就是17放到桶7中,3放到桶3中;
所得的各个桶包含元素:
桶0:20
桶1:
桶2:
桶3:3
桶4:14
桶5:25
桶6:
桶7:17
桶8:
桶9:9
按桶的顺序重新取出序列,得到20,3,14,25,17,9。
第二步:遍历序列,将十位相同的数依次放到对应的桶中,得到的桶状态:
桶0:3,9
桶1:14,17
桶2:20,25
桶3:
桶4:
桶5:
桶6:
桶7:
桶8:
桶9:
将桶中数字按序取出,即得到拍好的序列:3,9,14,17,20,25
注意到,原数列中最大数的十位为2,因此桶3~9均为空。
3、算法复杂度
设待排序列为n个记录,d个关键码,关键码的取值范围为r,则进行链式基数排序的时间复杂度为O(d(n+r)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(r),共进行d趟分配和收集。 此外,还需要2*r个指向队列的辅助空间,以及用于静态链表的n个指针,因此空间复杂度为O(n+r)。
4、稳定性
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
5、示例代码
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = newint[n];
int *count = newint[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete[]tmp;
delete[]count;
}
七、希尔(shell)排序
1、原理
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序实际上是一种分组插入方法,它是直接插入排序的改进。直接插入排序一次只能插入一个数据,但是当希尔排序的增量较大时,一次可以插入多个数据。
2、示例
初始序列为49,38,65,97,76,13,27,49,55,04。增量序列的取值依次为5,3,1。
第一步:增量为5时,原始序列分为5组,分别用不同颜色标出:49,38,65,97,76,13,27,49,55,04,对每组均进行直接插入排序,得到序列:13,27,49,55,04,49,38,65,97,76。
第二步:增量为3时,将第一步得到的序列分为3组,分别用不同颜色标出:13,27,49,55,04,49,38,65,97,76,对每组均进行直接插入排序,得到序列:13,04,49,38,27,49,55,65,97,76。
第三步:增量为1时,序列只要一组了,将其进行直接插入排序,可得最终的有序数列:04,13,27,38,49,55,65,76,97
3、算法复杂度
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。
有人通过大量的实验,给出了较好的结果:当n较大时,比较和移动的次数约在nl.25到n1.6之间,一般我们取平均时间复杂度O(n1.3)。另外,当步长取1的时候,希尔排序退化为直接插入排序,这时有最坏时间复杂度O(n2)和最好时间复杂度O(n)。
4、稳定性
由于我们将原始序列分成多组,因此可能会有相同的数字被分到不同的组,而在组内进行直接插入排序时,就可能将相同数字的原始顺序打乱。因此希尔排序是不稳定的排序方式。
5、示例代码
voidCSortTest::ShellSort(int*data,unsignedintlen)
{
if(len<=1||data==NULL)
return;
for(intdiv=len/2;div>=1;div/=2)
{
for(inti=div;i<len;i++)
{
for(intj=i;(data[j]<data[j-div])&&j>=0;j-=div)
{
swapInt(data+j,data+j-div);
}
}
}
}
八、堆排序
1、原理
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。
大根堆:二叉树中每个父节点比它的两个子节点都要大。
小根堆:二叉树中每个父节点比它的两个子节点都要小。
在数组的非降序排序中,需要使用的是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。我们每次都将堆顶的数取出,并重构剩余的数形成新的大根堆,当全部元素取出时,排序也就完成了。
具体过程:
a)、建堆,建堆是不断调整堆的过程,从len/2处开始调整,一直到第一个节点,此处len是堆中元素的个数。建堆的过程是线性的过程,从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2) 其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。
b)、调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,那边交互节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,因为是沿着深度方向进行调整的。
c)、堆排序:堆排序是利用上面的两个过程来进行的。首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。
2、算法复杂度
堆排序过程的时间复杂度固定是O(nlgn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn)。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序是就地排序,辅助空间为O(1)。
3、稳定性
我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序是不稳定的排序算法。
4、示例代码
//整理节点time:O(lgn)
template<typenameT>
void MinHeapify(T*arry,int size,int element)
{
int lchild=element*2+1,rchild=lchild+1;//左右子树
while(rchild<size)//子树均在范围内
{
if(arry[element]<=arry[lchild]&&arry[element]<=arry[rchild])//如果比左右子树都小,完成整理
{
return;
}
if(arry[lchild]<=arry[rchild])//如果左边最小
{
swap(arry[element],arry[lchild]);//把左面的提到上面
element=lchild;//循环时整理子树
}
else//否则右面最小
{
swap(arry[element],arry[rchild]);//同理
element=rchild;
}
lchild=element*2+1;
rchild=lchild+1;//重新计算子树位置
}
if(lchild<size&&arry[lchild]<arry[element])//只有左子树且子树小于自己
{
swap(arry[lchild],arry[element]);
}
return;
}
//堆排序time:O(nlgn)
template<typenameT>
void HeapSort(T*arry,int size)
{
int i;
for(i=size-1;i>=0;i--)//从子树开始整理树
{
MinHeapify(arry,size,i);
}
while(size>0)//拆除树
{
swap(arry[size-1],arry[0]);//将根(最小)与数组最末交换
size--;//树大小减小
MinHeapify(arry,size,0);//整理树
}
return;
}
九、影响排序效果的因素
因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
1、待排序的记录数目n;
2、记录(辅助空间)的大小(规模);
3、关键字的结构及其初始状态;
4、对稳定性的要求;
5、语言工具的条件;
6、存储结构;
7、时间和辅助空间复杂度等。
十、(转)排序方法的选择
1、若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则,因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
2、若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
3、若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序。