内排序与外排序:根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。这里我们主要就介绍内排序的多种方法。
对于内排序来说,排序算法的性能主要是受3个方面影响:时间性能、辅助空间、算法的复杂性。根据排序过程中借助的主要操作,我们把内排序分为:插入排序、交换排序、选择排序和归并排序。
排序用到的结构与函数:为了讲清楚排序算法的代码,我们先提供一个用于排序用的顺序表结构,此结构也将用于之后我们要讲的所有排序算法。
#define MAXSIZE 10//用于要排序数组个数最大值,可根据需要修改
typedef struct
{
int r[MAXSIZE+1];//用于存储要排序数组,r[0]用作哨兵或临时变量
int length;//用于记录顺序表的长度
}SqList;
另外,由于排序最最常用到的操作是数组两元素的交换,我们将它写成函数,在之后的讲解中会大量的用到。
//交换L中数组r的下标为i和j的值
void swap(SqList *L,int i,int j)
{
int temp=L->r[i];
L->r[i]=L->r[j];
L->r[j]=temp;
}
好了,说了那么多,我们来看第一个排序算法。
最简单排序实现:冒泡排序是一种交换排序,它的基本思想:两两比较相邻记录的关键字,如果反序则交换,直到偶没有反序的记录为止。冒泡的实现在细节上可以有很多种变化,我们将分别就3种不同的冒泡实现代码,来讲解冒泡排序的思想。这里,我们就先来看看比较容易理解的一段。
void BubbleSort0(SqList *L)
{
int i,j;
for(i=1;i<L->length;i++)
{
for(j=i+1;j<=L->length;j++)
{
if(L->r[i]>L->r[j])
swap(L,i,j);//交换L->r[i]与L->r[j]的值
}
}
}
这段代码从严格意义上来说,不算是标准的冒泡排序算法,因为它不满足“两两比较相邻记录”的冒泡排序思想,它更应该是最最简单的交换排序而已,而且这个算法的效率是非常低的。我们来看看正宗的冒泡算法,有没有什么改进的地方。
//对顺序表L作冒泡排序
void BubbleSort(SqList *L)
{
int i,j;
for(i=1;i<L->length;i++)//每遍历一次都能找到最小值,并放在准确位置
for(j=L->length-1;j>=i;j--)//注意j是从后往前循环
{
if(L->r[j]>L->r[j+1])//若前者大于后者
swap(L,j,j+1);//交换L->r[j]与L->r[j+1]的值
}
}
冒泡排序优化
//对顺序表L作改进冒泡算法
void BubbleSort2(SqList *L)
{
int i,j;
Status flag=TRUE;//flag用来作为标记
for(i=1;i<L->length&&flag;i++)//若flag为true则退出循环
{
flag=FALSE;//初始为false;
for(j=L->length-1;j>=j;j--)
{
if(L->r[j]>L->r[j+1])
{
swap(L,j,j+1);//交换L->r[j]与r[j+1]的值
flag=TRUE;//如果有数据交换,则flag为true
}
}
}
}
代码改动的关键就是在i的变量的for循环中,增加了对flag是否为true的判断。经过这样的改进,冒泡排序在性能上就有了一些提升,可以避免因已经有序的情况下的无意义循环判断,总的时间复杂度为O(n2)。
简单选择排序法就是通过n-i此关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录交换之,我们来看代码。
//void SelectSort(SqList *L)
{
int i,j,min;
for(i=1;i<L->length;i++)
{
min=i;//将当前下标定义为最小值下标
for(j=i+1;i<=L->length;j++)//循环之后的数据
{
if(L->r[min]>L->r[j])//如果小于当前最小值的关键字
min=j;//将此关键字的下标赋值给min
}
if(i!=min)//若min不等于i,说明找到最小值,交换
swap(L,i,min);//交换L->r[i]与L->r[min]的值
}
}
从简单选择排序的过程来看,它最大的特点就是交换移动数据次数相当少,这样也就节约了时间,总的时间复杂度为O(n2),但简单选择排序的性能上还是要略优于冒泡排序。
直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
//对顺序表L作直接插入排序
void InsertSort(SqList *L)
{
int i,j;
for(i=2;i<=L->length;i++)
{
if(L->r[i]<L->r[i-1])//需将L->r[i]插入有序子表
{
L->r[0]=L->r[i];//设置哨兵
for(j=i-1;L->r[j]>L->r[0];j--)
{
L->r[j+1]=L->r[j];//记录后移
}
L->r[j+1]=L->r[0];//输入到正确位置
}
}
}
在这之前排序算法的时间复杂度基本都是O(n2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。其基本思想是:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接排序后得到的结果是基本有序而不是局部有序。在介绍具体代码后,我们通过演示一个实例的希尔排序过程加深理解。
//对顺序表L作希尔排序
void ShellSort(SqList *L)
{
int i,j;
int increment=L->length;
do
{
increment=increment/3+1;
for(i=increment+1;i<=L->length;i++)
{
if(L->r[i]<L->r[i-increment])
{
//需将L->r[i]插入有序增量子表
L->r[0]=L->r[i];//暂存在L->r[0]
for(j=i-increment;j>0&&L->r[0]<L->r[j];j-=increment)
{
L->r[j+increment]=L->r[j];//记录后移,查找插入位置
}
L->r[j+increment]=L->r[0];//插入
}
}
}while(increment>1);
}
堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列了。
如图1所示,是一个大顶堆,90为最大值,将90与20互换位置,如图2所示,此时90就成了整个堆序列的最后一个元素。
将20经过调整,使得除90以外的结点继续满足大顶堆定义(所有结点大于等于其子孩子),见图3所示,然后再考虑将30与80互换。
相信大家有些明白堆排序的基本思路了,不过要实现它还需要解决两个问题:
1.如何由一个无序序列构建成一个堆?
2.如果在输出堆顶元素后,调整剩余元素成为一个新的堆?
要解释清楚它们,让我们来看代码。
//对顺序表L进行堆排序
void HeapSort(SqList *L)
{
int i;
for(i=L->length;i>0;i++)//把L中的r构建成一个大顶堆
{
HeapAdjust(L,i,L->length);
}
for(i=L->length;i>1;i--)
{
swap(L,1,i);//将堆顶记录和当前未经排序子序列的最后一个记录交换
HeapAdjust(L,1,i-1);//将L->r[1..r-1]重新调整为大顶堆
}
}
void HeapAdjust(SqList *L,int s,int m)
{
int temp,i;
temp=L->r[s];
for(j=2*s;j<=m;j*=2)//沿关键字较大的孩子结点向下筛选
{
if(j<m&&L->r[j]<L->r[j+1])
++j;//j为关键字中较大的记录的下标
if(temp>=L->r[j])
break;//rc应插入在位置s上
L->r[s]=L->r[j];
s=j;
}
L->r[s]=temp//插入
}
在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为[log2i]+1),并且需要取n-1此堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
基本思想:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列;再两两归并,…如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。有了归并排序的初步认识后,我们来看代码。
void MergeSort(SqList *L)
{
MSort(L->r,L->r,1,L->length);
}
void MSort(int SR[],int TR1[],int s,int t)
{
int m;
int TR2[MAXSIZE+1];
if(s==t)
TR1[s]=SR[s];
else
{
m=(s+t)/2;//将SR[s..t]评分为SR[s..m]和SR[m+1..t]
MSort(SR,TR2,s,m);//递归SR[s..m]归并为有序的TR2[s..m]
MSort(SR,TR2,m+1,t);//递归SR[m+1..t]归并为有序的TR2[m+1..t]
Merge(TR2,TR1,s,m,t);//将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]
}
}
void Merge(int SR[],int TR[],int i,int m,int n)
{
int j,k,l;
for(j=m+1,k=i;i<=m&&j<=n;k++)//将SR中记录由小到大归并入TR
{
if(SR[i]<SR[j])
TR[k]=SR[i++];
else
TR[k]=SR[j++];
}
if(j<=m)
{
for(l=0;l<=m-i;l++)
TR[k+l]=SR[i+l];//将剩余SR[i..m]复制到TR
}
if(j<=n)
{
for(l=0;i<=n-j;l++)
TR[k+l]=SR[j+1];//将剩余的SR[j..n]复制到TR
}
}
由于归并排序在归并过程中需要与原始记录序列同样的数量的存储空间存放归并结果以及递归深度为log2n的栈空间,因此空间复杂度为O(n+logn)。
基本思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,已达到整个序列有序的目的。
我们来看代码。
//对顺序表L作快速排序
{
QSort(L,1,L->length);
}
void QSort(SqList *L,int low,int high)
{
int privot;
while(low<high)
{
pivot=Partition(L,low,high);//将L->r[low..high]一分为二,算出枢纽值pivot
QSort(L,low,pivot-1);//对低子表递归排序
QSort(L,pivot+1,high);//对高子表递归排序
}
}
//交换顺序表L中子表的记录,使枢纽记录到位,并返回其所在位置
//此时在它之前(后)的记录均不大(小)于它
int Partition(SqList *L,int low,int high)
{
int pivotkey;
pivotkey=L->r[low];//用子表的第一个记录作枢纽记录
while(low<high)//从表的两端交替向中间扫描
{
while(low<high&&L->r[high]>=pivotkey)
high--;
swap(L,low,high);//将此枢纽记录小的记录交换到低端
while(low<high&&L->r[low]<=pivotkey)
low++;
swap(L,low,high);//将比枢纽大的记录交换到高端
}
return low;//返回枢纽所在位置
}
在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。在最坏的情况下,时间复杂度为O(n2)。由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。