一. 排序的概念及分类
1.1 排序的概念
排序,就是使一串数据,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
1.2 常见的排序算法
图1.1按照排序算法的思想,将排序分为四大类:插入排序、选择排序、交换排序、归并排序。本文对插入排序的实现思想和代码进行了详细解读,插入排序包括:直接插入排序和希尔排序。
二. 直接插入排序
前置说明:本文中的排序算法均以排升序为例进行讲解实现。
2.1 直接插入排序的实现逻辑
学习直接插入排序的实现逻辑,首先要学习单趟直接插入排序的实现。即:给定一组数据集arr(已经按要求被排序)和一个待插入的数据x,要将x插入到数据集中,x插入到数据中后,要求新的数组集中的数据按照升序(降序)排列。如:
- 数据集arr = {1, 3, 5, 6, 6, 8, 10, 12},待插入的数据x = 7。
- x插入后的数据集后:arr = {1, 3, 5, 6, 6, 7, 8, 10, 12}
直接插入排序的单趟实现逻辑:
- 找到数据集中最后一个小于或等于x的数据:从数据集中最后一个数据开始与x进行比较,如果大于x,则将数据集中的这个数据后移一位,当找到比x小的数据或者发现数据集中的所有数据都大于x时停止查找。
- 在数据集中插入x:在第一个小于或等于x的数据后面插入x,如果数据集中所有数据都大于x则是在首元素位置插入x。至此,直接插入排序单趟实现完成。
明确直接插入排序的单趟实现逻辑后,再将其延伸到对一组数据进行排序。假设给定一个有n个数据的数据arr[n],首先将第一个数据单独视为有序序列,将第二个数据插入有序序列,然后将数组的前两个数据视为有序序列,将第三个数据插入,以此类推,最后将数组中前n-1个数据视为有序序列,将第n个数据插入。如:
- 对arr[5] = {5, 4, 3, 2, 1}进行升序排列
- STEP1:将5单独视为有序序列,插入4,此时arr[5] = {4, 5, 3, 2, 1}。
- STEP2:将4、5视为有序序列,插入3,此时arr[5] = {3, 4, 5, 2, 1}。
- STEP3:将3、4、5视为有序序列,插入2,此时arr[5] = {2, 3, 4, 5, 1}。
- STEP4:将2、3、4、5视为有序序列,插入1,此时arr[5] = {1, 2, 3, 4, 5},排序完成。
2.2 直接插入排序的实现代码
typedef int DataType; //待排序数据的类型
#include<stdio.h>
//直接插入排序函数
//a为指向存储待排序数组首元素的指针,n为待排序数据的个数
void InsertSort(DataType* a, int n)
{
int i = 0; //循环参数
for (i = 0; i < n - 1; ++i)
{
int end = i; //已经排好序的数据的最大元素的下标
DataType x = a[end + 1]; //要插入的数据
while (end >= 0)
{
//如果已排序的数据大于待插入数据x,则将已排序的数据后移
//找到最靠后的小于或等于x的数据
if (a[end] > x)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
//此时end为-1或第一个小于等于x的数据的下标
a[end + 1] = x;
}
}
2.3 直接插入排序的时间复杂度分析
- 当待排序数据逆序时,直接插入排序所需要进行的操作次数最多
- 对n个数据采用直接插入法进行排序,所需要进行的单趟插入次数为n-1次,在每次单趟插入中,如果有序序列中的数据大于待插入数据,有序序列中的对应数据向后移动,假设有序序列中有m个数据,每次单趟插入最多进行移动操作m次(有序序列中所有数据都大于待插入数据)。
- 综上,完成一次直接插入排序最多进行的操作次数为:,根据大O渐进法规则,直接插入排序的时间复杂度为。
三. 希尔排序
3.1 希尔排序的实现逻辑
希尔排序是在直接插入排序的的基础上进行优化得来的,其基本实现逻辑为
- 将待排序数据分为gap组,先将每组数据进行预排序,使每组数据在预排序后都是升序或降序序列。
- 不断变化gap的值,一般来说,第一次分组取gap=n/2,n为待排序数据的个数,每次分组排序完成之后,采用gap /= 2对gap进行更新。
- gap最终会变为1,此时数组中的数据已经非常接近有序,这是再对数据采用直接插入排序的方法进行排序,消耗大大减少。gap=1时,分组排序等价于直接插入排序。
综上:希尔排序就是先对数据进行分组预排序,使数据接近于有序,然后采用直接插入排序的方法对这组接近于有序的数据进行排序,以此来达到减少时间消耗的目的。
3.2 希尔排序实现代码
void ShellSort(int* a, int n)
{
assert(a);
int i = 0; //循环参数,表示每组首元素下标
int j = 0; //循环参数,表示每组元素中数据的下标
int gap = n; //分组间距
while (gap > 1) //不断缩小排序间距,最终gap = 1相当于直接插入排序
{
gap /= 2;
for (i = 0; i < gap; ++i)
{
//分组排序
//每组中的相邻数据在数组中的下标相差gap
for (j = i; j < n - gap; j += gap)
{
int end = j; //已排序的最后一个数据下标
int x = a[end + gap]; //待插入排序的数据
while (end >= 0)
{
if (a[end] > x) //数据大于x就后移
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x; //插入x
}
}
}
}
3.3 希尔排序的效率测试
演示代码3.3通过rand函数随机生成100000个整型数据,分别采用直接插入排序和希尔排序对这100000个数据进行排序,通过clock函数,分别记程序运行到100000个随机数生成结束、直接插入排序结束和希尔排序结束的运行时间为end1、end2、end3。这样,end2 - end1的结果就是直接插入排序的运行时间(ms),end3 - end2的结果就是希尔排序的运行时间(ms)。
演示代码3.3:(测试希尔排序效率)
int main()
{
int n = 100000;
int* a1 = (int*)malloc(n * sizeof(int)); //数据集1(用于直接插入排序)
if (a1 == NULL)
{
return 1;
}
int* a2 = (int*)malloc(n * sizeof(int)); //数据集2(用于希尔排序)
if (a2 == NULL)
{
return 2;
}
srand((unsigned int)time(NULL));
for (int i = 0; i < n; ++i)
{
a1[i] = rand() / 1000;
a2[i] = a1[i];
}
int end1 = clock(); //程序运行到生成100000个随机数消耗的时间(ms)
InsertSort(a1, n); //直接插入排序
int end2 = clock(); //程序运行到直接插入排序结束消耗的时间(ms)
ShellSort(a2, n); //希尔排序
int end3 = clock(); //程序运行到希尔排序结束消耗的时间(ms)
printf("直接插入排序消耗时间:%d\n", end2 - end1);
printf("希尔排序消耗的时间:%d\n", end3 - end2);
return 0;
}
测试结果表明:
- 对100000个随机生成的数据进行排序,采用直接插入排序消耗时间3300ms左右,而采用希尔排序仅需9ms,可见希尔排序的效率远高于直接插入排序。