第六章:排序
归纳起来,常见的排序算法分为如下5类:
1)插入排序:普通插入排序,shell排序等;
2)选择排序:普通选择排序,堆排序;
3)交换排序:冒泡法,快速排序;
4)归并排序;
5)基数排序。
下面,就来实现各个排序算法。要掌握这些算法,大家首先要理解各个算法的具体执行原理和过程,然后应该把这些过程用C语言表达出来。
其中归并排序与基数排序留给大家自学。
6.1插入排序
基本插入排序的实现是这样的:每次将一个待排序的数据元素,插入到前面已经排好序的数列中的适当位置,使数列依然有序;直到待排序数据元素全部插入完为止。
假如待排数据为:9,6,2,3,10,那么每趟插入排序之后(从小到大),数据的变化情况为:
6,9,2,3,10
2,6,9,3,10
2,3,6,9,10
2,3,6,9,10
基本插入排序的时间复杂度为O(n^2),是一种稳定排序算法。所谓的稳定排序是指待排序的记录序列中可能存在两个或两个以上关键字相等的记录。排序前的序列中Ri领先于Rj(即i<j).若在排序后的序列中Ri仍然领先于Rj,则称所用的方法是稳定的。比如下面的数据:
3,6,1a,2,1b,8,4,7
其中1a和1b的值都是1,为了区别,1a在1b的前面。如果排序之后,1a依然在1b的前面,那么就是稳定排序,否则就是非稳定排序。
简单插入排序的算法实现:
void insert_sort(int a[], int n)
{
int i, j, tmp;
for (i = 1; i < n; i++)
{
tmp = a[i];
j = i - 1;
while (tmp < a[j])
{
a[j+1] = a[j];
j--;
}
a[j+1] = tmp;
}
}
6.1.1希尔排序
希尔排序是一种经过改进的插入排序算法。基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。具体做法:首先确定一组增量d0,d1,d2,d3,...,dt-1()其中n>d0>d1>...>dt-1=1),对于i =0,1,2,...,t-1,依次进行下面的各趟处理:根据当前增量di将n个元素分成di个组,每组中元素的下标相隔为di;再对各组中元素进行直接插入排序。
下面是一个希尔排序的实现算法:
void shellsort(int a[], int n)
{
int i, j, gap, tmp;
gap = n/2;
while (gap > 0) {
for (i = gap + 1; i <= n; i++)
{
j = i - gap;
while (j > 0)
{
if (a[j] > a[j+gap])
{
tmp = a[j];
a[j] = a[j+gap];
a[j+gap] = a[j];
}
else
{
j = 0;
}
}
}
gap /= 2;
}
}
希尔排序的复杂度为:O(n1.25),它不是稳定排序。
6.2选择排序
选择排序的思想是:每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
假如待排数据为:9,6,2,3,10,那么每趟选择排序之后(从小到大),数据的变化情况为:
2,6,9,3,10
2,3,9,6,10
2,3,6,9,10
2,3,6,9,10
简单选择排序的时间复杂度为O(n^2),它是不稳定排序。
void selectsort(int a[], int n)
{
int i, j, k, tmp;
for (i = 0; i < n-1; i++)
{
k = i;
for (j = i + 1; j < n; j++)
{
if (a[j] < a[k])
k = j;
}
if (k != i)
{
tmp = a[i];
a[i] = a[k];
a[k] = tmp;
}
}
}
6.2.1堆排序
堆排序是一树形选择排序,在排序过程中,将R[1..N]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择最小的元素。
void sfilter(int a[], int l, int m)
{
int i, j x;
i = l;
j = 2 * i;
x = a[i];
while ( j <= m)
{
if (j < m && a[j] < a[j+1])
j++;
if (x < a[j])
{
a[i] = a[j];
i = j;
j = 2 * i;
}
else
{
j = m + 1;
}
}
a[i] = x;
}
void heapsort(int a[], int n)
{
int i, w;
for (i = n/2; i >= 1; i--)
sfilter(a, i, n);
for (i = n; i >= 2; i--)
{
w = a[i];
a[i] = a[1];
a[1] = w;
sfilter(a, 1, i - 1);
}
}
堆排序的时间复杂度为O(nlogn),它是不稳定排序。
6.3交换排序
交换排序的基本思想是:两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。
6.3.1冒泡排序
首先来看著名的交换排序算法:冒泡法。冒泡法的排序思想是:从第n个元素(a[n-1])开始,扫描数组,比较相邻两个元素,如果次序相反则交换。如此反复,直到没有任何两个违反相反原则的元素
假如待排数据为:9,6,2,3,10,那么每趟冒泡排序之后(从小到大),数据的变化情况为:
2,9,6,3,10
2,3,9,6,10
2,3,6,9,10
2,3,6,9,10
冒泡排序的算法如下:
void bubble_sort(int a[], int n)
{
int i, j, tmp;
for (i = 0; i < n-1; i++)
{
for (j = n-1; j >= i+1; j--)
{
if (a[j] < a[j-1])
{
tmp = a[j];
a[j] = a[j-1];
a[j-1] = tmp;
}
}
}
}
冒泡排序的时间复杂度为O(n^2),它是稳定排序。
6.3.2快速排序
快速排序的思想是:在当前无序区R[1..H]中任取一个数据元素作为比较的“基准”(不妨记为X,R[1]),用此基准将当前无序区划分为左右两个较小的无序区:R[1..I-1]和R[I+1..H],且左边的无序子区中数据元素均小于等于基准元素,右边的无序子区中数据元素均大于等于基准元素,而基准X则位于最终排序的位置上,即R[1..I-1]≤X≤R[I+ 1..H](1≤I≤H),当R[1..I-1]和R[I+1..H]均非空时,分别对它们进行上述的划分过程,直至所有无序子区中的数据元素均已排序为止。
找一个数X(比如头一个元素)做基准,右边比X小的移动到左边,左边比X大的移动到右边,最后空出的位置就是X自己的位置
快速排序的时间复杂度为O(nlogn),最坏情况为O(n^2)。对于大的、乱数序列一般相信是最快的已知排序。待排记录序列按关键字顺序有序时,直接插入排序和起泡排序能达到O(n)的时间复杂度,而对于快速排序而言,这是最不好的情况。对于很小的数组(如N<=20),快速排序不如插入排序好。
void quickSort(int a[],int left,int right)
{
int i,j,temp;
i=left;
j=right;
temp=a[left];
if(left>right)
return;
while(i<j)/*找到最终位置*/
{
while(a[j]>=temp && j>i)
j--;
if(j>i)
a[i++]=a[j];
while(a[i]<=temp && j>i)
i++;
if(j>i)
a[j--]=a[i];
}
a[i]=temp;
quickSort(a,left,i-1);/*递归左边*/
quickSort(a,i+1,right);/*递归右边*/
}
void qsort(int a[], int n)
{
quickSort(a, 0, n-1);
}
6.4排序复杂度比较
下标列出了各种排序算法的时间复杂度和空间复杂度: