首先给出各个排序方式的性能比较:
排序方法的比较 | ||||||
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
平均情况 | 最好情况 | 最坏情况 | 辅助存储 | |||
插入排序 | 直接插入 | 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;
}