排序作为一个如此经典与常见的问题,排序算法自然也是各种各样、各具特点。
本文主要讨论三个时间复杂度为O(n2)的简单排序算法,即插入排序、选择排序和冒泡排序。
在Wiki了排序算法之后,瞬间感觉世界之大排序算法之多。Wiki链接。
简单排序算法 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 是否原址 |
---|---|---|---|---|
插入排序 | O(n) | O(n2) | O(n2) | 是 |
希尔排序 | O(nlogn) | O(nlog2n) | O(nlog2n) or O(n5/4) | 是 |
选择排序 | O(n2) | O(n2) | O(n2) | 是 |
冒泡排序 | O(n) | O(n2) | O(n2) | 是 |
地精排序 | O(n) | O(n2) | O(n2) | 是 |
上图截自Wikipedia,足可见排序算法是如此得花式繁多。但这里只介绍常用的三种简单排序及其改进版本。
排序分为内排序和外排序,内排序为在内存中进行地排序,外排序主要是外存储器中大文件的排序。
内排序按照排序方式可以分为基于比较的排序,和不基于比较的排序。其中基于比较的排序算法有插入排序,交换排序,选择排序,归并排序等。不基于比较的排序有基数排序,桶排序等。以下算法均以升序排列为例。
如果一个排序算法在排序过程中只使用了常数个存储空间(与问题规模无关),则称该排序算法为原址的。后面会介绍一些非原址的排序算法,如归并排序。(next time)
首先定义一个交换元素的函数供下面的函数调用:
void swap(int &x,int &y)
{
int temp = x;
x = y;
y = temp;
}
插入排序(Insertion Sort)
直接插入排序
在生活中我们也许经常用到插入排序,例如玩扑克牌时按照大小整理牌序的过程。
思路(步骤):
1. 对第一个元素不做任何处理,作为初始时的已排序列
2. 从第二个元素开始,在已排序的序列中查找该元素其应该位于的位置
3. 在查找过程中,将大元素依次往后移,直到遇到合适位置
4. 将该元素插入该位置,已排序列元素个数加1
5. 重复2,3,4直到最后一个元素
当已经排好序时,最好时间复杂度为O(n)。逆序时,最坏时间复杂度为O(n2)。平均时间复杂度也是O(n2)。
伪代码如下:
Insertion_Sort(A)
for i=2 to A.length
key = A[i]
j = i-1
while j>0 and A[j]>key
A[j+1] = A[j]
j = j-1
A[j+1] = key
其中A表示一个数组,”.”号表示调用属性,比如A.length表示数组A的长度,伪代码中数组下标从1开始,即为数组元素的逻辑位序。采用缩进表示语句间的层次等特点。
伪代码表示算法的优点: 简单易读,屏蔽了高级语言的细节,对于由程序设计语言基础的人来说根据伪代码写出对应的代码会非常容易。当遇到复杂问题时先写出伪代码也许会是解决问题的一个好途径。
采用C++/C实现上述伪代码:
void Insertion_Sort(int A[],int n)
{
int i,j,key;
for(i=1;i<n;i++)
{
key = A[i];
j = i-1;
while(j>=0 && A[j]>key)
{
A[j+1] = A[j];
j --;
}
A[j+1] = key;
}
}
排序过程的动图(以下均来自Wikipedia)演示如下:
折半插入排序(Binary Insertion Sort)
可以看到上述插入排序中查找和移动是结合在一起的,并且在插入第i个元素时,前i-1个元素时有序的。那么可以使用有序表的二分查找代替逐项查找,并且先查找再移动。那么就可以改进其中查找过程的时间复杂度。但由于并没有改进移动过程。所以总体时间复杂度并不会产生变化,仍为O(n2)。
伪代码如下:
Bin_Insertion_Sort(A)
for i=2 to A.length
if A[i-1] > A[i]
key = A[i]
low = 1
high = i-1
while low <= high
mid = (high+low)/2
if key > A[mid] //插入点在左半区
high = mid-1
else //插入点在于右半区
low = mid+1
while j=i-1 downto high+1 //元素集中后移
A[j+1] = A[j]
A[high+1] = key //插入
C++实现如下:
void Bin_Insertion_Sort(int A[],int n) //升序
{
int i,j,low,high,mid;
int key;
for(i=1;i<n;i++)
{
if(A[i-1] > A[i]) //反序时
{
key = A[i];
low = 0;
high = i-1;
while(low <= high)
{
mid = (low+high)/2;
if(key < A[mid])
high = mid-1; //插入点前半区
else
low = mid+1; //插入点后半区
}
for(j=i-1;j>=high+1;j--) //元素集中后移
A[j+1] = A[j];
A[high+1] = key; //插入
}
}
}
希尔排序(Shell Sort)
希尔排序也是一种插入排序,是简单插入排序经过改进后的一个高效版本,也称缩小增量排序。同时希尔排序也是冲破O(n2)的第一批算法之一。
思路(步骤):
1. d = n/2
2. 将序列分为d个分组,在每个分组内进行直接插入排序
3. d=d/2,重复2,直到d=1
分析:对于一趟排序过程,A[1…n]被分为d个子序列。即A[1],A[1+d],A[1+2d]…、A[2],A[2+d],A[2+2d]…等d个序列。一趟排序完成之后,每个子序列内部有序,再将d缩小为d/2,重复这一过程。直到d=1时,会对整个数组进行一次直接插入排序。所以最后得到的结果一定是正确的。
时间复杂度:最好为O(nlogn),平均O(n1.25),证明过程复杂,这里不做证明。可参考ShellSort。
伪代码表示如下:
Shell_Sort(A)
d = A.length/2
while d>0
for i=d+1 to n //从第一个序列的第二个元素开始
temp = A[i]
j = i-d
while j>0 and temp < A[j] //对每一个序列进行插入排序
A[j+d] =A[j]
j = j-d
A[j+d] = temp
d = d/2
C++实现如下:
void Shell_Sort(int A[],int n)
{
int i,j,d,temp;
d = n/2;
while(d > 0)
{
for(i=d;i<n;i++)
{
temp = A[i];
j = i-d;
while(j>=0 && temp<A[j])
{
A[j+d] = A[j];
j = j-d;
}
A[j+d] = temp;
}
d = d/2;
}
}
动图演示:
选择排序(Selection Sort)
思路(步骤):
1. 在所有n个元素中选出最小的元素排在第一个位置
2. 选出次小元素排在下一位置
3. 重复2直到排序完成
因为在排序过程的比较次数与n2成正比,仅仅是交换次数从0到n-1的变化。所以选择排序的最好、最坏和平均时间复杂度都是O(n2).
伪代码如下:
Selection_Sort(A)
for i=1 to A.length-1
min = A[i]
for j=i+1 to A.length
if(A[j] < min)
min = A[j]
k = j
exchange A[j] with A[k]
C++实现如下:
void Selection_Sort(int A[],int n)
{
int i,j,k,min;
for(i=0;i<n;i++)
{
min = A[i];
for(j=i+1;j<n;j++)
{
if(A[j]<min)
{
min = A[j];
k = j;
}
}
if(min != A[i])
swap(A[i],A[k]);
}
}
动图演示:
冒泡排序(Bubble Sort)
冒泡排序为交换排序中的最简单的一种。交换排序主要有冒泡排序和快速排序。(快速排序next time)
基础版本
思路(步骤):
1. 将序列分为(全局)有序区和待排无序区,初始时所有元素位于待排无序区中
2. 从待排无序区末尾开始将元素与前一元素比较,若小则交换
3. 一轮循环后,无序区最小元素移到最前方并入有序区中,无序区缩小
4. 重复2,3直到排序完成
最好时间复杂度在已排好的序列时取到为O(n),平均和最坏时间复杂度均为O(n2)
伪代码如下:
Bubble_Sort(A)
for i=1 to A.length-1
for j=A.length downto i+1
if(A[j] < A[j-1])
exchange A[j] with A[j-1]
C++实现如下:
void Bubble_Sort(int A[],int n)
{
for(int i=0;i<n-1;i++)
{
for(int j=n-1;j>i;j--)
{
if(A[j] < A[j-1])
swap(A[j],A[j-1]);
}
}
}
动图演示(图中为从后往前):
稍加改进
改进思路:若在冒泡过程中那一轮循环结束后已经排好了序(不再冒泡/交换),则直接结束。增加一个表示本轮循环是否交换的变量即可。
C++实现:
void Bubble_Sort(int A[],int n)
{
bool exchange;
for(int i=0;i<n-1;i++)
{
exchange = false;
for(int j=n-1;j>i;j--)
{
if(A[j] < A[j-1])
{
swap(A[j],A[j-1]);
exchange = true;
}
}
if(!exchange)
return;
}
}
地精排序(Gnome Sort)
地精排序号称最简单的排序。交换排序的一种。又称Stupid Sort。只使用一层循环。Wiki链接
思路:默认情况下前进冒泡,一旦遇到冒泡的情况就往回冒,直到把这个数放好为止。
分析:就排序过程而言,与插入排序基本一致,唯一区别就是地精排序通过交换元素实现元素的向后移动,而插入排序是借助了一个额外的存储空间使元素往后移动。所以说插入排运行时间略优于地精排序。
其时间复杂度同样是最好为O(n),平均和最坏均为O(n2)。
步骤:
1. 初始时,i为0,则直接自加1
2. 然后将A[i]与A[i-1]比较,发生交换则i自减1,没发生交换则自加1
3. 重复2直到排序完成
伪代码如下:
Gnome Sort(A)
i=1
while(i<=A.length)
if i==1 or A[i]>=A[i-1]
i++
else
exchange A[i] with A[i-1]
i--
C++实现如下:
void Gnome_Sort(int A[],int n)
{
int i=0;
while(i<n)
{
if(i==0 || A[i]>=A[i-1])//升序
{
i ++;
}
else
{
swap(A[i],A[i-1]);
i --;
}
}
}
其中可改进的地方还很多,这里不做实现。
动图演示: