一. 一些概念
1. 排序:排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。(将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。)分内排序和外排序。
2.内排序:在排序的整个过程中,待排序的所有记录全部被放置在内存中的排序。
3.外排序:由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
4.稳定性:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。
5.对于内排序来说,排序算法的性能主要受3个方面影响:
(1).时间性能:高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。
(2).辅助空间:辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。
(3).算法的复杂性:这里指的是算法本身的复杂度,而不是指算法的时间复杂度。
二.详细讲解内排序的七种排序方法
内排序分为:交换排序(冒泡排序,快速排序),插入排序(直接插入排序,希尔排序)、选择排序(简单选择排序,堆排序)、归并排序。
(一)冒泡排序:
两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
1.最简单的冒泡排序实现:
它的思路就是让每一个关键字都和它后面的每一个关键字比较。若要求按从小到大排列如果大则交换,这样第一位置的关键字在一次循环后一定变成最小值,第二位置则变为第二小的值,以此类推。反之,若要求按从大到小排列如果小则交换,这样第一位置的关键字在一次循环后一定变成最大值,第二位置则变为第二大的值,以此类推。代码如下:
//冒泡排序初级版
void BubblsSort(int a[],int n)//a[]表示从a[1]开始的数组,n表示数组长度 即a[1]到a[n]
{
int t;
for (int i=1; i<n; i++)
{
for (int j=i+1; j<=n; j++)
{
if (a[i]>a[j])
{
t=a[i];
a[i]=a[j];
a[j]=t;
}
}
}
}
但是这段代码不满足“两两比较相邻记录”的冒泡排序思想,所以从严格意义上不算是标准的冒泡排序算法,它更应该是最最简单的交换排序而已。它应该算是最最容易写出的排序代码了,不过这个算法的效率非常低。
2.冒泡排序算法:
以下是正宗的冒泡算法:从最后面开始,两个两个比,若按从小到大排序,则将较小的交换到前面,让较小值像气泡一样冒上来最终将最小的放第一位,第二小的放第二位,以此类推。若按从大到小排序,则从最后开始两个两个比将大的移到前面。
代码如下:
//按从小到大排序
void BubblsSort(int a[],int n)//a[]表示从a[1]开始的数组,n表示数组长度 即a[1]到a[n]
{
int t;
for (int i=1; i<n; i++)
{
for (int j=n-1; j>=i; j--) //从后往前循环
{
if (a[j]>a[j+1]) //若前者大于后者,交换
{
t=a[j+1];
a[j+1]=a[j];
a[j]=t;
}
}
}
}
3.冒泡排序优化:
如果经过几个循环的排序后,序列已经有序,但是算法的循环还未结束,尽管并没有数据交换,但是之后的大量比较还是大大地多于了。所以我们可以增加一个标记变量flag来标记说明这个序列已经有序,不需要再继续后面的循环判断工作了。
改进代码如下:
//改进后的冒泡算法
void BubblsSort(int a[],int n)//a[]表示从a[1]开始的数组,n表示数组长度 即a[1]到a[n]
{
int t;
bool flag=true; //bool用于判断
for (int i=1; i<n&&flag; i++)//若flag为false则退出循环
{
flag = false;
for (int j=n-1; j>=i; j--) //从后往前循环
{
if (a[j]>a[j+1]) //若前者大于后者,交换
{
t=a[j+1];
a[j+1]=a[j];
a[j]=t;
flag = true ;//如果有数据交换则flag为true
}
}
}
}
经过这样的改进,冒泡排序在性能上就有了一些提升,可以避免因已经有序的情况下的无意义循环判断。(二).快速排序:
快速排序其实就是冒泡排序的升级,它们都属于交换排序类。它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。
快速排序的基本思想是:通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
下面我们来看代码:
//快速排序算法代码,按从小到大排列
//交换a[low]到a[high]的记录,是枢轴记录到位,并返回其所在位置
//此时在它之前(后)的记录均不大(小)于它
int Partition(int a[],int low,int high)
{
int pivotkey=a[low],t;//用第一个记录作枢轴记录
while (low<high)
{
while(low<high && a[high]>=pivotkey)
high--;
//将比枢轴记录小的记录交换到前面
t=a[low];
a[low]=a[high];
a[high]=t;
while(low<high&&a[low]<=pivotkey)
low++;
//将比枢轴记录大的记录交换到后面
t=a[low];
a[low]=a[high];
a[high]=t;
}
return low; //返回枢轴所在位置
}
void QSort(int a[],int low,int high)
{
//对序列a[low]到a[high]做快速排序
int pivot;
if ( low < high )
{
pivot=Partition(a,low,high);
//算出枢轴值pivot,将a[low]到a[high]一分为二
QSort(a,low,pivot-1);//对前半部分递归排序
QSort(a,pivot+1,high);//对后半部分递归排序
}
}
void QuickSort(int a[],int n)//a[]表示从a[1]开始的数组,n表示数组长度 即a[1]到a[n]
{
//由于需要递归调用,因此我们外封装了一个函数。
QSort(a,1,n);
}
其优化:
1.优化选取枢轴:第六行 用第一个记录作枢轴记录,第一个记录太大或者太小都会影响性能。我们可以用随机选取,三数取中,九数取中等方法来进行优化。
下面的代码中我们选用三数取中法:即取三个关键字先进行排序,将中间数作为枢轴,一般是取左端,右端和中间三个数,也可以随机选取。
2.优化不必要的交换:Partition函数中的while循环中的数据交换语句可以改为:先将pivotkey的值存到a[0]中,将循环中的交换改为赋值,最后将pivotkey赋值给最后一个赋值的值。减少交换次数。
3.优化小数组时的排序方案:如果数组非常小,其实快速排序反而不如直接插入排序来得更好(直接插入排序是简单排序中性能最好的)。其原因在于快速排序用到了递归操作,在大量数据排序时,这点性能影响相当于它的整体算法优势而言是可以忽略的。在数组长度为7时直接插入排序算法效率最高,所以我们以7为分界线来判断。大于七用快速排序,小于七用直接插入排序(后面会讲到)。
代码如下:
//快速排序算法代码,按从小到大排列
#define MAX_LENGTH_INSERT_SORT 7
void jiaohuan(int a[],int i,int j)
{
int t;
t=a[i];
a[i]=a[j];
a[j]=t;
}
//交换a[low]到a[high]的记录,是枢轴记录到位,并返回其所在位置
//此时在它之前(后)的记录均不大(小)于它
int Partition(int a[],int low,int high)
{
int pivotkey,t,m=low+(high-low)/2;
//m用来计算数组中间元素的下标
//三数取中法
if (a[low]>a[high])
jiaohuan(a,low,high);
if (a[m]>a[high])
jiaohuan(a,m,high);
if (a[m]>a[low])
jiaohuan(a,m,low);
//此时a[low]为整个序列左中右三个关键字的中间值
pivotkey=a[low];
a[0]=pivotkey;//将枢轴关键字备份到a[0]
while (low<high)
{
while(low<high && a[high]>=pivotkey)
high--;
a[low]=a[high];//采用替换而不是交换的方式进行操作
while(low<high&&a[low]<=pivotkey)
low++;
a[high]=a[low];//采用替换而不是交换的方式进行操作
}
a[low]=a[0];
return low; //返回枢轴所在位置
}
void InsertSort(int a[],int n)
{
int i,j;
for (i=2; i<=n; i++)
{
if (a[i]<a[i-1])
{
a[0]=a[i];
for (j=i-1; a[j]>a[0]; j--)
a[j+1]=a[j];
a[j+1]=a[0];
}
}
}
void QSort(int a[],int low,int high,int n)
{
//对序列a[low]到a[high]做快速排序
int pivot;
if ( (high-low)>MAX_LENGTH_INSERT_SORT )
{
//当high-low大于常数时用快速排序
pivot=Partition(a,low,high);
//算出枢轴值pivot,将a[low]到a[high]一分为二
QSort(a,low,pivot-1,n);//对前半部分递归排序
QSort(a,pivot+1,high,n);//对后半部分递归排序
}
else //当high-low小于常数时用直接插入排序
InsertSort(a,n);
}
void QuickSort(int a[],int n)//a[]表示从a[1]开始的数组,n表示数组长度 即a[1]到a[n]
{
//由于需要递归调用,因此我们外封装了一个函数。
QSort(a,1,n,n);
}
(三).直接插入排序:其基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表。
代码如下:
void InsertSort(int a[],int n)//a[]为从a[1]开始到a[n]的数组
{
int i,j;
for (i=2; i<=n; i++)
{
if (a[i]<a[i-1])//将a[i]插入有序数组
{
a[0]=a[i]; //设置哨兵,防止数组越界
for (j=i-1; a[j]>a[0]; j--)//如果这个数前面的数比它大
a[j+1]=a[j]; //向后移,将前一个元素赋给后一个a[j]
a[j+1]=a[0]; // 将a[i]插到正确的位置
}
}
}
(四).希尔排序:
我们分割待排序记录,以减少待排序记录的个数,并使整个序列向基本有序(小的关键字基本在前面,大的关键字基本在后面,不大不小的基本在中间)发展。而分完组后就各自排序的方法达不到我们的要求。因此,我们需要采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
代码如下:
void ShellSort(int a[],int n)//对a[1]到a[n]的数组排序
{
int i,j,increment=n;
do
{
increment=increment/3+1; //增量序列
for (i=increment+1; i<=n; i++)
{
if (a[i]<a[i-increment])
{
a[0]=a[i];
for (j=i-increment; j>0&&a[0]<a[j]; j-=increment)
a[j+increment]=a[j];//记录后移,查找插入位置
a[j+increment]=a[0];//插入
}
}
}
while(increment>1);
}
希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。这里“增量”的选取就非常关键了。我们在第六行是用increment=increment/3+1的方式来选取的。可究竟应该选取什么样的增量才是最好,目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。不过大量研究表明,当增量序列为dlta[k]=2^(t-k+1) -1 (0≤k≤t≤ log2(n+1)的向下取整)时,可以获得不错的效率。需要注意的是增量序列的最后一个增量必须等于1才行。
(五).简单选择排序:
简单选择排序就是通过n-i次关键词间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换。
代码如下:
void SelectSort(int a[],int n)
{
int i,j,m,t;
for (i=1; i<n; i++)
{
m=i; //将当前下标定义为最小值下标
for (j=i+1; j<=n; j++) //循环a[i]之后的数据
{
if (a[m]>a[j])//如果有小于当前最小值的关键字
m=j; //将此关键字下标赋给m
}
if (i!=m)//若i!=m说明a[i]后存在值比a[i]小
{
//进行交换
t=a[i];
a[i]=a[m];
a[m]=t;
}
}
}
(六).堆排序:
堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是顶堆的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。
代码如下:
//已知a[s]...a[m]中记录的关键字除a[s]之外均满足堆定义
//本函数调整a[s]的关键字,使a[s]...a[m]成为大顶堆
void HeapAdjust(int a[],int s,int m)
{
int t,j;
t=a[s];
for (j=2*s; j<=m; j*=2)
{
//沿关键字较大的孩子结点向下筛选
if(j<m && a[j]<a[j+1])
++j; //j为关键字较大的记录的下标
if (t >= a[j])
break;
a[s]=a[j];
s=j;
}
a[s]=t; //插入
}
//对数组 a[1]到a[n]进行堆排序
void HeapSort(int a[],int n)
{
int i,t;
for (i=n/2; i>0; i--) //构建大顶堆
HeapAdjust(a,i,n);
for (i=n; i>1; i--)
{
t=a[1];
a[1]=a[i];
a[i]=t;
HeapAdjust(a,1,i-1);
//将a[1]...a[i-1]重新调整为大顶堆
}
}
它的运行时间主要是消耗在初始构建堆和在重建堆时的反复筛选上。
在构建堆的过程中,我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的交换,对于每个非终端结点来说,其实最多进行两次比较的交换操作。
在空间复杂度上,它只有一个用来交换的暂存单元,也非常不错。不过由于记录的比较与交换是跳跃进行,因此堆排序是一种不稳定的排序方法。
由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数缺少的情况。
(七).归并排序:
“归并”一词的中文含义就是合并、并入的意思,而在数据结构中的定义是将两个或者两个以上的有序数列合成一个新的有序数列。
归并排序就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 不小于(n/2)的最小整数 个长度为2或1的有序子序列;两两归并,......,如此重复,直到得到一个长度为n的有序序列为止。这种排序方法称为2路归并排序。
代码如下:
#include <stdio.h>
#include <stdlib.h>
//归并排序代码(递归实现)
//将有二个有序数列a[s...mid]和a[mid...e]合并。
void mergearray(int a[], int s, int mid, int e, int temp[])
{
int i = s, j = mid + 1;
int m = mid, n = e;
int k = 0;
while (i <= m && j <= n)
{
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[s + i] = temp[i];
}
void MSort(int a[], int s, int e, int temp[])
{
if (s < e)
{
int mid = (s+ e) / 2;
MSort(a, s, mid, temp); //左边有序
MSort(a, mid + 1, e, temp); //右边有序
mergearray(a, s, mid, e, temp); //再将二个有序数列合并
}
}
int MergeSort(int a[], int n)
{
int *p = (int *)malloc(n*sizeof(int));
if (p == NULL)
return -1;
MSort(a, 1, n, p);
free(p);
}
三.总结