排序分为两类:内排序和外排序。
内排序:指排序过程中,待排序列全部存放在内存中处理,不需涉及数据的内、外存交换。适用于元素序列不太大的小文件。
外排序:指排序过程中,待排序列不能全部存放在内存中处理,内、外存之间需要多次进行数据交换。适用于元素序列太大,不能一次将其全部放入内存的大文件。
内排序分为六类:插入排序、交换排序、选择排序、归并排序、分配排序和计数排序。这里主要介绍前四类。
一、插入排序
插入排序是指将无序子序列中的一个或几个元素“插入”到有序子序列中。插入排序主要有直接插入排序,折半插入排序,和希尔(shell)排序。
直接插入排序:时间复杂度为O(n^2),特殊情况(原表有序)为O(n)。(稳定)
//如果升序排序
void SimpleInsertSort(int* pData, int length)
{
if(NULL == pData || length <= 0)
return ;
int i, j, temp;
for(i = 1; i < length; i++)
{//对待排元素序列进行扫描。从第二个元素开始循环到最后一个元素。
for(j = i - 1; j >= 0; j--)
{//从有序序列最后一个元素开始向前扫描,将待插入元素放入合适位置。
if(pData[j+1] > pData[j])
break;
temp = pData[j+1];
pData[j+1] = pData[j];
pData[j] = temp;
}
}
}
2)折半插入排序:由于插入排序的基本操作是在一个有序表中进行查找和插入,这个查找操作可以利用折半查找来实现,由此称之为折半插入排序。折半插入排序只是减少了元素间的比较次数,而元素的移动次数不变,因此时间复杂度为O(n^2)。(稳定)
void BinaryInsertSort(int data[], int length)
{
if(NULL == data || length <= 0)
return ;
int i, j;
for(i = 1; i < length; i++)
{//对待排元素序列进行扫描。从第二个元素开始循环到最后一个元素。
int low = 0;
int high = i - 1;
int mid = 0;
int temp = data[i]; //定义一个temp来存data[i]的值,避免移动序列将其覆盖
while(low <= high)
{//在[low...high]中折半查找出有序插入的位置,位置为high+1
mid = (low + high) >> 1;
if(data[mid] > temp)
high = mid - 1;
else
low = mid + 1;
}
for(j = i - 1; j > high; j--)
data[j+1] = data[j];
data[high+1] = temp;
}
}
3)希尔(shell)排序:是对直接插入排序的一种改进,又称“缩小增量排序”。基本思想是先将待排记录序列以一定的增量间隔d分割成多个子序列,对每个子序列分别进行一次直接插入排序,然后逐步减小增量间隔,重复上述的分组和排序,直至所取的增量为1,即所有记录放在同一组中进行直接插入排序为止。时间复杂度为O(n^(1+u)) (0 < u < 1) 。(不稳定)
//增量初始值不容易选择,代码只是参考
void ShellSort(int* pData, int length)
{
int d = length;
while(d > 1)
{
int i, temp;
d = (d + 1)>> 1;
for(i = 0; i < length - d; ++i)
{
if(pData[i] > pData[i+d])
{
temp = pData[i];
pData[i] = pData[i+d];
pData[i+d] = temp;
}
}
}
}
二、交换排序
交换排序的基本思想是两两比较待排序记录的关键字,两个记录的次序相反时即进行交换,直到没有反序的记录为止。交换排序主要有冒泡排序和快速排序。
1)冒泡排序:时间复杂度为O(n^2),特殊情况(原表有序)为O(n)。(稳定)
void BubbleSort(int data[], int length)
{
int i, j, temp;
for(i=0; i
{ //外层for循环控制趟数,共n-1趟,大数放在最后面
for(j=0; j
{//内层for循环控制相邻元素比较次数
if(data[j]>data[j+1])
{
temp = data[j];
data[j] = data[j+1];
data[j+1] = temp;
}
}
}
}
2)快速排序:是对冒泡排序的一种改进,又称“分区交换排序”。基本思想是从待排序列中任选一个记录(通常可选第一个记录),以它作为枢轴点,分别把小于和大于枢轴点的记录移到枢轴点两边。然后在两边序列中重复上述操作,直至全部序列有序。
时间复杂度是O(n*log2(n)), 特殊情况(原表有序,退化为冒泡排序)O(n^2),空间复杂度是O(log2(n))。(不稳定)
//划分算法
int Partition(int data[], int low, int high, int length)
{
if(NULL==data || low<0 || high>=length)
{
printf("输入无效参数\n");
exit(-1);
}
int temp = data[low];
while(low < high)
{
while(low < high && data[high]>=temp)
high--;
if(low < high)
data[low++] = data[high];
while(low < high && data[low]<=temp)
low++;
if(low < high)
data[high--] = data[low];
}
data[low] = temp;
return low;
}
void QuickSort(int data[], int low, int high, int length)
{
int index;
if(low < high)
{
index = Partition(data, low, high, length);
QuickSort(data, low, index-1, length);
QuickSort(data, index+1, high, length);
}
}
三、选择排序
选择排序的基本思想是每一次从待排序序列中选出最小(或最大)的一个元素,存放在已排序列的最后位置,直到全部待排序的记录排定。选择排序主要有简单选择排序和堆排序。
1)简单选择排序:时间复杂度为O(n^2)。(不稳定)
//大小到大排序
void SimpleSelectSort(int data[], int length)
{
int i, j, temp;
for(i = 0; i < length-1; ++i)
{
for(j = i+1; j < length; ++j)
{
if(data[i] > data[j])
{
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
}
2)堆排序:堆实质上是一棵完全二叉树,树中任一非叶子结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。堆分为小根堆和大根堆两种。小根堆要求父结点小于等于其2个子结点;大根堆要求父结点大于等于其2个子结点。
N(N>1)个节点的的完全二叉树编号原则是:从上到下,从左自右。最后一个分枝结点(非叶子结点)的编号为 N/2 取整。且对于编号 i(1<=i<=N)有:父结点为 i/2 向下取整;若2i>N,则结点i没有左孩子,否则其左孩子为2i;若2i+1>N,则没有右孩子,否则其右孩子为2i+1。
注:使用完全二叉树只是为了好描述算法,它只是一种逻辑结构,真正在实现时我们还是使用数组来存储这棵完全二叉树的。
堆排序时间,主要由“建堆”和反复“调整”重建堆这两部分时间构成。时间复杂度为O(n*log2(n)),空间复杂度为O(1)。(不稳定)
void Swap(int* pFirstData, int* pSecondData)
{
int temp = *pFirstData;
*pFirstData = *pSecondData;
*pSecondData = temp;
}
void HeapAdjust(int data[], int startIndex, int length)
{
int i;
for(i = 2*startIndex+1; i < length; i = 2*startIndex+1)
{
if(i
i++; //较大的记录下标
if(data[startIndex] >= data[i])
break; //不用调整了,满足堆的定义
Swap(&data[startIndex], &data[i]);
startIndex = i;
}
}
void HeapSort(int data[], int length)
{
int i;
for(i = length/2-1; i >= 0; --i)
{
//先建堆,从最后一个非叶结点开始调整,完全二叉树的最后一个非叶结点是n/2
HeapAdjust(data, i, length);
}
for(i = length-1; i >= 0; --i)
{
Swap(&data[0], &data[i]);
HeapAdjust(data, 0, i);
}
}
四、归并排序
归并排序是采用分治法的一个典型应用。该算法通过归并操作,将已有序的子序列合并,得到一个整体有序的序列。归并排序主要有二路归并排序。
1)二路归并排序:将一个具有N个待排序记录的序列看成N个长度为1的有序序列,然后进行两两归并,得到(N/2取整)个长度为2的有序序列,再进行两两归并。如此重复,直到得到一个长度为N的有序序列为止。时间复杂度是O(n*log2(n)),空间复杂度是O(n)。(稳定)
void Merge(int data[], int copy[], int first, int mid, int last)
{
int indexCopy = first;
int i = first; // 前半段第一个数字下标
int j = mid+1; // 后半段第一个数字下标
while(i <= mid && j <= last)
{
if(data[i] < data[j])
copy[indexCopy++] = data[i++];
else
copy[indexCopy++] = data[j++];
}
while(i <= mid)
{
copy[indexCopy++] = data[i++];
}
while(j <= last)
{
copy[indexCopy++] = data[j++];
}
for(i = first; i <= last; ++i)
data[i] = copy[i];
}
/*** 使用递归实现 ***/
void BiMergeSort(int data[], int copy[], int first, int last)
{
if(first < last)
{
int mid = (first+last)>>1;
BiMergeSort(data, copy, first, mid);
BiMergeSort(data, copy, mid+1, last);
Merge(data, copy, first, mid, last);
}
}
外部排序最常用的是归并排序算法,它由两个阶段组成:
1) 生成初始归并段:将外存文件中的信息分段输入内存,使用内部排序算法对其进行排序,生成初始归并段,并将其写回外部文件,直至外部文件的信息全部转换成初始归并段时为止;(把原始数据分成M段,每段都排好序,分别存入M个文件中,称为顺串文件)
2) 归并:从M个顺串文件中读出头条记录,进行M路归并排序,最小的放到输出文件,同时删除对应的顺串文件中的记录。直至整个外部文件归并为单一归并段时为止。
例如:假设有一个含10000个记录的磁盘文件,而当前所用的计算机一次只能对 1,000个记录进行内部排序,则首先利用内部排序的方法得到10个初始归并段,然后进行逐趟归并。假设进行二路归并(即两两归并),则第一趟由10个归并段得到5个归并段;第二趟由 5 个归并段得到3个归并段;第三趟由3个归并段得到2个归并段;最后一趟归并得到整个记录的有序序列。