1.基本概念
1.1 稳定排序(stable sort)和非稳定排序
稳定排序是所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,。反之,就是非稳定的排序。比如:一组数排序前是a1,a2,a3,a4,a5,其中a2=a4,经过某种排序后为a1,a2,a4,a3,a5,则我们说这种排序是稳定的,因为a2排序前在a4的前面,排序后它还是在a4的前面。假如变成a1,a4,a2,a3,a5就不是稳定的了。
1.2 内排序( internal sorting )和外排序( external sorting)
在排序过程中,所有需要排序的数都在内存,并在内存中调整它们的存储顺序,称为内排序;在排序过程中,只有部分数被调入内存,并借助内存调整数在外存中的存放顺序排序方法称为外排序。
1.3 算法的时间复杂度和空间复杂度
所谓算法的时间复杂度,是指执行算法所需要的计算工作量。 一个算法的空间复杂度,一般是指执行这个算法所需要的内存空间。
2.几种常见算法
2.1 冒泡排序(Bubble Sort)
冒泡排序方法是最简单的排序方法。这种方法的基本思想是,将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮。在冒泡排序算法中我们要对这个“气泡”序列处理若干遍。所谓一遍处理,就是自底向上检查一遍这个序列,并时刻注意两个相邻的元素的顺序是否正确。如果发现两个相邻元素的顺序不对,即“轻”的元素在下面,就交换它们的位置。显然,处理一遍之后,“最轻”的元素就浮到了最高位置;处理二遍之后,“次轻”的元素就浮到了次高位置。在作第二遍处理时,由于最高位置上的元素已是“最轻”元素,所以不必检查。一般地,第i遍处理时,不必检查第i高位置以上的元素,因为经过前面i-1遍的处理,它们已正确地排好序。
冒泡排序是稳定的,算法时间复杂度是O(n ^2)。
代码如下:
void swap(int &a, int &b){
int temp;
temp=a;
a=b;
b=temp;
}
void BubbleSort(int r[], int n)
{
int exchange=n;
while(exchange)
{
int bound=exchange;
exchange=0;
for(int j=1;j<bound;j++)
if(r[j]>r[j+1])
{swap(r[j],r[j+1]);
exchange=j;
}
}
}
2.2 选择排序(Selection Sort)
选择排序的基本思想是对待排序的记录序列进行n-1遍的处理,第i遍处理是将L[i..n]中最小者与L[i]交换位置。这样,经过i遍处理之后,前i个记录的位置已经是正确的了。
选择排序是不稳定的,算法复杂度是O(n ^2 )。
代码如下:
void SelectSort(int r[], int n)
{
for(int i=1;i<n;i++)
{
int index;
for(int j=i+1;j<=n;j++){
if(r[j]<r[index])index=j;
}
if(index!=i)swap(r[i],r[index]);
}
}
2.3 插入排序 (Insertion Sort)
插入排序的基本思想是,经过i-1遍处理后,L[1..i-1]己排好序。第i遍处理仅将L[i]插入L[1..i-1]的适当位置,使得L[1..i] 又是排好序的序列。要达到这个目的,我们可以用顺序比较的方法。首先比较L[i]和L[i-1],如果L[i-1]≤ L[i],则L[1..i]已排好序,第i遍处理就结束了;否则交换L[i]与L[i-1]的位置,继续比较L[i-1]和L[i-2],直到找到某一个位置j(1≤j≤i-1),使得L[j] ≤L[j+1]时为止。图1演示了对4个元素进行插入排序的过程,共需要(a),(b),(c)三次插入。
直接插入排序是稳定的,算法时间复杂度是O(n ^2) 。
代码如下:
void InsertSort(int r[], int n)
{
for(int i=1;i<=n;i++)
{
r[0]=r[i];
for(int j=i-1;r[0]<r[j];j--){
r[j+1]=r[j];
}
r[j+1]=r[0];
}
}
2.4 堆排序
堆排序是一种树形选择排序,在排序过程中,将A[n]看成是完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择最小的元素。
//筛选法调整堆的算法
void Sift(int r[],int k, int m)
{
int i=k,j=2*i; //置i为要筛的结点,j为i的左孩子
while(j<=m) //筛选还没有进行到叶子
{
if(j<m&&r[j]<r[j+1])j++; //比较i的左右孩子,j为较大者
if(r[i]>r[j])break; //根结点已经大于左右孩子中的较大者
else{
swap(r[i],r[j]); //将根结点与结点j交换
i=j;j=2*i; //被筛结点位于原来结点j的位置
}
}
}
//堆排序算法
void HeapSort(int r[], int n){
for(int i=n/2;i>=1;i--){
Sift(r,i,n);
}
for(int i=1;i<n;i++){
swap(r[1],r[n-i+1]);
Sift(r,1,n-1);
}
}
堆排序是不稳定的,算法时间复杂度O(nlog n)。
2.5 归并排序
设有两个有序(升序)序列存储在同一数组中相邻的位置上,不妨设为A[l..m],A[m+1..h],将它们归并为一个有序数列,并存储在A[l..h]。
其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlog2n)。
//一次归并算法Merge
void Merge(int r[],int r1[],int s, int m, int t)
{
int i=s,j=m+1,k=s;
while(i<=m&&j<=t){
if(r[i]<=r[j])r1[k++]=r[i+1]; //取r[i]和r[j]中较小者放入r1[k]
else r1[k++]=r[j++];
}
if(i<=m)while(i<=m) //若第一个子序列没处理完,则进行收尾处理
r1[k++]=r[i++];
else while(j<=t) //若第二个子序列没处理完,则进行收尾处理
r1[k++]=r[j++];
}
//一趟归并排序算法MergePass
void MergePass(int r[], int r1[],int n, int h)
{
int i=1;
while(i<=n-2h+1) //待归并记录至少有两个长度为h的子序列
{
Merge(r,r1,i,i+h-1,i+2*h-1);
i+=2*h;
}
if(i<n-h+1)Merge(r,r1,i,i+h-1,n); //待归并序列中有一个长度小于h
else for(int k=i;k<=n;k++) //待归并序列中只剩一个子序列
r1[k]=r[k];
}
//归并排序非递归算法MergeSort1
void MergeSort1(int r[], int r1[], int n)
{
int h=1;
while(h<n)
{
MergePass(r,r1,n,h);
h=2*h;
MergePass(r1,r,n,h);
h=2*h;
}
}
//归并排序的递归算法MergeSort2
void MergeSort2(int r[],int r1[], int s, int t){
if(s==t)r1[s]=r[s];
else{
m=(s+t)/2;
MergeSort2(r,r1,s,m); //归并排序前半个子序列
MergeSort2(r,r1,m+1,t); //归并排序后半个子序列
Merge(r1,r,s,m,t); //将两个已排序的子序列归并
}
}
2.6 快速排序
快速排序是对冒泡排序的一种本质改进。它的基本思想是通过一趟扫描后,使得排序序列的长度能大幅度地减少。在冒泡排序中,一次扫描只能确保最大数值的数移到正确位置,而待排序序列的长度可能只减少1。快速排序通过一趟扫描,就能确保某个数(以它为基准点吧)的左边各数都比它小,右边各数都比它大。然后又用同样的方法处理它左右两边的数,直到基准点的左右只有一个元素为止。
快速排序是不稳定的,最理想情况算法时间复杂度O(nlog2n),最坏O(n ^2)。
代码如下:
void swap(int &a, int &b){
int temp;
temp=a;
a=b;
b=temp;
}
int partition(int r[],int first, int end)
{
int i=first, j=end;
while(i<j)
{
while(i<j&&r[i]<=r[j])j--;
if(i<j){
swap(r[i],r[j]);
i++;
}
while(i<j&&r[i]<=r[j])i++;
if(i<j){
swap(r[i],r[j]);
j--;
}
}
return i;
}
void Quick(int r[], int first, int end){
if(first<end){
int pivot=partition(r,first,end);
Quick(r, first, pivot-1);
Quick(r,pivot+1,end);
}
}
2.7 希尔排序
在直接插入排序算法中,每次插入一个数,使有序序列只增加1个节点,并且对插入下一个数没有提供任何帮助。如果比较相隔较远距离(称为 增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。
希尔排序是不稳定的,其时间复杂度为O(n ^2)。
代码如下:
void ShellSort(int r[], int n)
{
for(int d=n/2;d>=1;d=d/2)
{
for(int i=d+1;i<=n;i++){
r[0]=r[i];
for(int j=i-d;j>0&&r[0]<r[j];j=j-d)
{r[j+d]=r[j];}
r[j+d]=r[0];
}
}
}
表一 各种排序比较
该表中希尔排序的时间复杂度不对。
按平均时间将排序分为以下4类:
1 平方阶(O(n^2))排序: 一般称为简单飘絮,例如直接插入。直接选择和冒泡排序。
2 线性对数阶(O(nlgn)排序: 如快排,堆和归并排序。
3 O(n^(1+&))阶排序:&是介于0 和1之间的常数,即0<&<1, 如希尔排序。
4 线性阶(O(n))排序:如桶、箱和基数排序。
简单排序中直接插入排序最好,快熟排序最快。当文件为正序时,直接插入排序和冒泡排序均最佳。
一 影响排序效果的因素如下:
1 待排序的记录数目n
2 记录的大小(规模)
3 关键字的结构及其初始状态
4 对稳定性的要求
5 语言工具的选择
6 存储结构
7 时间和空间复杂度等
二 不同条件下排序方法的选择
(1) 若n较小(n<=50),可以采用 直接插入 或者 直接选择排序。
(2) 若文件初始状态基本有序(指正序),则应直接插入排序、冒泡排序或随机的快速排序为宜
(3) 若n较大, 则应采用时间复杂度为O(nlgn)的排序方法(快速排序、堆排序或归并排序)
快速排序被认为是目前基于比较的内部排序中最好的方法。 当待排序的关键字随机分布时, 快速排序的平均时间最短。
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况,这两种排序都是不稳定的。
时间复杂度:
常见的渐进时间 复杂度有:
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n).
时间复杂度都不带常数或者常数系数的,所以不存在O(2n)这样的时间复杂度。
空间复杂度:
算法原地工作是指算法所需辅助空间是常量,即O(1).