排序算法的好坏直接影响程序的执行速度和辅助存储空间的占有量。
此处将详细分析常见的各种排序算法,并从时间复杂度、空间复杂度、适用情况等多方面进行综合比较。
1. 选择排序:
基本原理:
①对于给定的一组记录,经过第一轮比较后得到最小的记录,然后将该记录与第一个记录的位置进行交换;
②接着对不包括第一个记录以外的其他记录进行第二轮比较,得到最小的记录并与第二个记录进行位置交换;
③重复该过程,直到进行比较的记录只有一个为止。
以数组{38,65,97,76,13,27,49}为例,选择排序具体步骤如下:
第一趟排序后:13 [ 65 97 76 38 27 49 ]
第二趟排序后:13 27 [ 97 76 38 65 49 ]
第三趟排序后:13 27 38 [ 76 97 65 49 ]
第四趟排序后:13 27 38 49 [ 97 65 76 ]
第五趟排序后:13 27 38 49 65 [ 97 76 ]
第六趟排序后:13 27 38 49 65 76 [ 97 ]
最后排序结果:13 27 38 49 65 76 97
示例程序如下:
#include <stdio.h>
void SelectSort(int *a,int n) //选择排序函数
{
int i;
int j;
int temp = 0;
int flag = 0;
for(i=0;i<n-1;i++)
{
temp=a[i];
for(j=i+1;j<n;j++)
{
if(a[j]<temp)
{
temp = a[j];
flag = j;
}
}
if(flag != i)
{
a[flag] = a[i];
a[i] = temp;
}
}
}
int main()
{
int i = 0;
int a[] = {5,4,9,8,7,6,0,1,3,2};
int len = sizeof(a)/sizeof(a[0]);
SelectSort(a,len);
for(i=0;i<len;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
程序输出结果为: 0 1 2 3 4 5 6 7 8 9
看出,选择排序的特点是交换移动数据次数相当少,节约了响应时间。无论是最好情况,还是最差情况,其比较次数都是一样的,第 i 趟排序需要进行 n-i 次。而对于交换次数而言,最好的情况是有序,需交换0次;最差的情况是逆序,需交换n-1次,基于最终的排序时间是比较与交换的次数总和,所以,总的时间复杂度为 O(n^2) 。
2. 插入排序
基本原理:
①对于给定的一组记录,初始时假设第一个记录自成一个有序序列,其余的记录为无序序列;
②接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到其之前的有序序列中;
③直到最后一个记录插入到有序序列中为止。
以数组{38,65,97,76,13,27,49}为例,直接插入排序具体步骤如下:
第一步插入38以后:[ 38 ] 65 97 76 13 27 49
第二步插入65以后:[ 38 65 ] 97 76 13 27 49
第三步插入97以后:[ 38 65 97 ] 76 13 27 49
第四步插入76以后:[ 38 65 76 97 ] 13 27 49
第五步插入13以后:[ 13 38 65 76 97 ] 27 49
第六步插入27以后:[ 13 27 38 65 76 97 ] 49
第七步插入49以后:[ 13 27 38 49 65 76 97 ]
示例程序如下:
#include <stdio.h>
void InsertSort(int par_array[],int array_size) //直接插入排序函数
{
int i,j;
int temp;
for(i=1;i<array_size;i++)
{
temp = par_array[i];
for(j=i-1;j>=0;j--)
{
if(temp<par_array[j])
{
par_array[j+1] = par_array[j];
}
else
break;
}
par_array[j+1] = temp;
}
}
int main()
{
int i = 0;
int a[] = {5,4,9,8,7,6,0,1,3,2};
int len = sizeof(a)/sizeof(a[0]);
InsertSort(a,len);
for(i=0;i<len;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
程序输出结果为: 0 1 2 3 4 5 6 7 8 9
3. 冒泡排序
基本原理:单向冒泡排序,假设由小到大排序
①对于给定的n个记录,从第一个记录开始依次对相邻的两个记录进行比较,当前面的记录大于后面的记录时, 交换其位置,进行一轮比较和换位后,n个记录中的最大记录将位于第n位;
②然后对前(n-1)个记录进行第二轮比较;
③重复该过程直到进行比较的记录只剩下一个时为止。
以数组{36,25,48,12,25,65,43,57}为例,冒泡排序具体步骤如下:
一趟排序的过程如下:
R[1] 25 36 48 12 25 65 43 57
R[2] 25 36 48 12 25 65 43 57
R[3] 25 36 12 48 25 65 43 57
R[4] 25 36 12 25 48 65 43 57
R[5] 25 36 12 25 48 65 43 57
R[6] 25 36 12 25 48 43 65 57
R[7] 25 36 12 25 48 43 57 65
一趟排序后结果:25 36 12 25 48 43 57 65
经过多趟排序后的结果如下:
初始状态:[ 36 25 48 12 25 65 43 57 ]
1 趟排序:[ 25 36 12 25 48 43 57 65 ]
2趟排序: [ 25 12 25 36 43 48 ] 57 65
3趟排序: [ 12 25 25 36 43 ] 48 57 65
4趟排序: [ 12 25 25 36 ] 43 48 57 65
5趟排序: [ 12 25 25 ] 36 43 48 57 65
6趟排序: [ 12 25 ] 25 36 43 48 57 65
7趟排序: [ 12 ] 25 25 36 43 48 57 65
示例程序如下:
#include <stdio.h>
void Swap(int& a, int& b) //交换a,b
{
int temp;
temp = a;
a = b;
b = temp;
}
void BubbleSort(int array[],int len) //单向冒泡排序函数
{
int i,j;
for(i=0;i<len-1;++i)
{
for(j=len-1;j>i;--j)
{
if(array[j]<array[j-1])
Swap(array[j],array[j-1]);
}
}
}
int main()
{
int i = 0;
int a[] = {5,4,9,8,7,6,0,1,3,2};
int len = sizeof(a)/sizeof(a[0]);
BubbleSort(a,len);
for(i=0;i<len;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
程序输出结果: 0 1 2 3 4 5 6 7 8 9
-->引申部分:双向冒泡排序
基本思想:
首先将第一个记录的关键字与第二个记录的关键字进行比较,若为“逆序”(即L.r[1].key > L.r[2].key),则将两 个记录交换;然后比较第二个记录和第三个记录的关键字。 依次类推,直至第n-1个记录的关键字和第n个记录的 关键字比较过为止。这是第一趟冒泡排序,其结果是使得关键字最大的记录被安置到最后一个记录为止。
第一趟排序之后进行第二趟冒泡排序,将第n-2个记录的关键字和第n-1个记录的关键字进行比较,若为逆序 (即L.r[n-1].key > L.r[n-2].key),则将两个记录交换,然后比较第n-3个记录和第n-2个记录的关键字。 依次类推, 直至第1个记录的关键字和第2个记录的关键字比较过为止,其结果是使得关键字最小的记录被安置到第一个位置 上。
再对其余的n-2个记录进行上述同样的操作,其结果是使关键字次大的记录被安置到第n-1个记录的位置,使得 关键字次小的记录被安置到第2个记录的位置。
一般的,第 i 趟冒泡排序为:若 i 为奇数,则从L.r[i/2+1]~L.r[n-i/2]依次比较相邻两个记录的关键字,并在“逆 序”时交换相邻记录,其结果是这n-i+1个记录中关键字最大的记录被交换到第n-i/2的位置上;若i为偶数,则从L.r[n-i/2]~L.r[i/2]依次比较相邻两个记录的关键字,并在“逆序”时交换相邻记录,其结果是这n-i+1个记录中关键字最小的记录被交换到第i/2的位置上。整个排序过程需要进行K(1≤K<n)趟冒泡排序,同样判别冒泡排序结束的条件是“在一趟排序过程中没有进行过交换记录的操作”。
示例程序如下:
#include <stdio.h>
void Swap(int& a, int& b) //交换a,b
{
int temp;
temp = a;
a = b;
b = temp;
}
void Bubble2Sort(int array[],int len) //双向冒泡排序函数
{
int left = 1;
int right = len-1;
int t;
do
{
//正向的部分
for(int i=right;i>=left;i--)
{
if(array[i]<array[i-1])
{
Swap(array[i],array[i-1]);
t=i;
}
}
left = t+1;
//反向的部分
for(i=left;i<right+1;i++)
{
if(array[i]<array[i-1])
{
Swap(array[i],array[i-1]);
t=i;
}
}
right = t-1;
}while(left<=right);
}
int main()
{
int i = 0;
int a[] = {5,4,9,8,7,6,0,1,3,2};
int len = sizeof(a)/sizeof(a[0]);
Bubble2Sort(a,len);
for(i=0;i<len;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
程序输出结果: 0 1 2 3 4 5 6 7 8 9
4.归并排序
归并排序是利用递归与分治的技术将数据序列划分成为越来越小的半子表,再对半子表排序,最后再用递归步骤将排好序的半子表合并为越来越大的有序序列。“归”即递归的将数组折半的分离为单个数组。如,数组[2,6,1,0]会先折半,分为[2,6]和[1,0]两个子数组,然后再折半将数组分离,分为[2],[6],[1],[0]。 “并”即将分开的数组按照从小到大或从大到小的顺序在放在一个数组中。[2],[6]合并到一个数组中为[2,6],[1],[0]合并到一个数组中为[0,1],然后再将[2,6]和[0,1]合并到一个数组中即为[0,1,2,6]。
基本原理:
对于给定的一组记录(假设共有n个记录),首先将每两个相邻的长度为1的子序列进行归并,得到n/2(向上取整)个长度为2或1的有序子序列,再将其两两归并,反复执行此过程,直到得到一个有序序列为止。即关键两步:第一步,划分子表;第二步,合并半子表。
以数组{49,38,65,97,76,13,27}为例,归并排序过程如下:
初始关键字:[49] [38] [65] [97] [76] [13] [27]
一趟归并后:[38 49] [65 97] [13 76 ] [ 27 ]
二趟归并后: [38 49 65 97] [ 13 27 76 ]
三趟归并后: [ 13 27 38 49 65 76 97 ]
示例程序如下:
#include <stdio.h>
void Merge(int array[],int p,int q,int r) //合并函数
{
int i,j,k,n1,n2;
n1 = q-p+1;
n2 = r-q;
int* L = new int[n1];
int* R = new int[n2];
for(i=0,k=p;i<n1;i++,k++)
L[i] = array[k];
for(i=0,k=q+1;i<n2;i++,k++)
R[i] = array[k];
for(k=p,i=0,j=0;i<n1&&j<n2;k++)
{
if(L[i]>R[j])
{
array[k] = L[i];
i++;
}
else
{
array[k] = R[j];
j++;
}
}
if(i < n1)
{
for(j=i;j<n1;j++,k++)
array[k] = L[j];
}
if(j < n2)
{
for(i=j;i<n2;i++,k++)
array[k] = R[i];
}
}
void MergeSort(int array[],int p,int r) //递归函数 ---归并排序函数
{
if(p < r)
{
int q = (p+r)/2;
MergeSort(array,p,q);
MergeSort(array,q+1,r);
Merge(array,p,q,r);
}
}
int main()
{
int i = 0;
int a[] = {5,4,9,8,7,6,0,1,3,2};
int len = sizeof(a)/sizeof(a[0]);
MergeSort(a,0,len-1);
for(i=0;i<len;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
程序输出结果:9 8 7 6 5 4 3 2 1 0
5.快速排序
高效的排序算法,采用“分而治之”的思想,把大的拆分为小的,小的再拆分更小的。
基本原理:
对于一组给定的记录,通过一趟排序后,将原序列分为两部分,其中前部分的所有记录均比后部分的所有记录小,然后再依次对前后两部分记录进行快速排序, 递归该过程,直到序列中的所有记录均有序为止。
具体算法步骤如下:
(1)分解:将输入的序列array[m,...,n]划分成两个非空子序列array[m,...,k]和array[k+1,...,n],使array[m,...,k]中的任一元素的值不大于array[k+1,...,n]中任一元素的值。
(2)递归求解:通过递归调用快速排序算法分别对array[m,...,k]和array[k+1,...,n]进行排序。
(3)合并:由于对分解出的两个子序列的排序是就地进行的,所以在array[m,...,k]和array[k+1,...,n]都排好序后,不需要执行任何计算array[m,...,n]就已排好序。
以数组{49,38,65,97,76,13,27,49}为例。
第一趟排序过程如下:
初始化关键字 [ 49 38 65 97 76 13 27 49 ]
第一次交换后: [ 27 38 65 97 76 13 49 49 ]
第二次交换后: [ 27 38 49 97 76 13 65 49 ]
j 向左扫描,位置不变,第三次交换后:[ 27 38 13 97 76 49 65 49 ]
i 向右扫描,位置不变,第四次交换后: [ 27 38 13 49 76 97 65 49 ]
j 向左扫描 [ 27 38 13 49 76 97 65 49]
整个排序过程如下:
初始化关键字 [ 49 38 65 97 76 13 27 49 ]
一趟排序后: [ 27 38 13 ] 49 [ 76 97 65 49 ]
二趟排序后: [13] 27 [38] 49 [49 65] 76 [97]
三趟排序后: 13 27 38 49 49 [65] 76 97
最后的排序结果: 13 27 38 49 49 65 76 97
示例程序如下:
#include <stdio.h>
void Sort(int array[],int low,int high)
{
int i,j;
int index;
if(low>=high) return;
i = low;
j = high;
index = array[i];
while(i<j)
{
while(i<j&&array[j]>=index)
j--;
if(i<j)
array[i++] = array[j];
while(i<j&&array[i]<index)
i++;
if(i<j)
array[j--] = array[i];
}
array[i] = index;
Sort(array,low,i-1);
Sort(array,i+1,high);
}
void QuickSort(int array[],int len)
{
Sort(array,0,len-1);
}
int main()
{
int i = 0;
int a[] = {5,4,9,8,7,6,0,1,3,2};
int len = sizeof(a)/sizeof(a[0]);
QuickSort(a,len);
for(i=0;i<len;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
程序输出结果: 0 1 2 3 4 5 6 7 8 9
当初始的序列整体或局部有序时,快速排序的性能会下降,退化为冒泡排序。
快速排序的相关特点:
(1)最坏时间复杂度:最坏情况是选择的基准关键字是待排序的所有记录中最小或最大的。总共比较次数为n(n-1)/2,时间复杂度为O(n^2)。
(2)最好时间复杂度:最好情况是选择的基准关键字为待排序的记录中的中间值。总共比较次数为nlogn,时间复杂度为O(nlogn)。
(3)平均时间复杂度:O(nlogn)
(4)空间复杂度:O(logn)
(5)基准关键字的选取:首尾中间位置三者取中,或,取左右之间的一个随机数。
快速排序与归并排序的原理都是基于分治思想,即首先把待排序的元素分为两组,然后分别对这两组排序,最后把两组结果合并起来。不同点在于,进行的分组策略不同,后面的合并策略也不同。归并排序的分组策略是假设待排序的元素存放在数组中,那么其把数组前面一半元素作为一组,后面一半元素作为另一组。而快速排序则是根据元素的值来分组,即大于某个值的元素放在一组,而小于的放在另一组。而对于合并策略,快速排序已经根据元素大小分组了,只需把两个组合并即可,归并排序则需要对两个有序的数组根据大小合并。
总结:
排序方法 时间复杂度 适合情况
简单选择排序: O(n^2) n小
直接插入排序: O(n^2) 初始序列整体或局部有序
冒泡排序: O(n^2) n小
归并排序: O(nlogn) n大
快速排序: O(n^2) n大