目录
前言
这里保存个人对常见插入排序算法的理解和实现思路过程。
供大家学习和参考。
一、几点注意
1.1 排序时长
这里对于代码的排序时长采用C语言中获取时间的方法。
引入头文件<time.h>和<sys/timeb.h>,其中定义了结构体timeb和函数ftime,可以获得秒和毫秒的时间。
struct timeb
{
time_t time; /* Seconds since epoch, as from `time'. */
unsigned short int millitm; /* Additional milliseconds. */
short int timezone; /* Minutes west of GMT. */
short int dstflag; /* Nonzero if Daylight Savings Time used. */
};
/* Fill in TIMEBUF with information about the current time. */
extern int ftime (struct timeb *__timebuf);
可以将结构体timeb作为ftime的传出参数来获取时间。
timeb中time为秒,millitm为毫秒,则time*1000+millitm则可以获得毫秒级的时间。
#include<time.h>
#include<sys/timeb.h>
long getSystemTime() {
struct timeb tb;
ftime(&tb);
return tb.time * 1000 + tb.millitm;
}
若要获取排序时长,则做两个差值即可。
long t_start = getSystemTime();
long t_end = getSystemTime();
printf("排序用时:%ld ms\n", t_end - t_start);
同理也可以使用在需要了解时间复杂度的地方。
1.2 获得随机数组
这里采用srand()来获得随机数组。
srand()为初始化随机数发生器,用于设置rand()产生随机数时的种子。传入的参数seed为unsigned int类型,通常我们会使用time(0)或time(NULL)的返回值作为seed。
想要获得<=99的元素即可rand()%100,其他同理。
1.3 单调性
这里均为递增序列,如果想要递减序列反之即可,自己琢磨琢磨。
二、插入类排序
2.1 插入类排序的分类
常见的插入类排序分为直接插入排序,折半插入排序,希尔排序等等,这里着重实现这三种插入排序。
2.2 直接插入排序
2.2.1 直接插入排序的思路
将第i个记录插入到前面i-1个已经排好序的记录中。
步骤就两步,第一步把比他大的往他后面挪,第二部,把他放进去。
这里最开始排序的序号从1开始,因为0号元素就一个,当只有一个元素时,默认有序。用temp保存1号元素,如果0号元素比他大,0号元素覆盖1号元素,再将temp插入。
2.2.2 具体代码
void InSertSort(int* arr, int n) {
printf("直接插入排序:\n");
int i, j;
for (i = 1; i < n; i++) {
int temp = arr[i];
for (j = i - 1; j >= 0 && arr[j] > temp; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
}
着重说一下插入算法的核心
for (j = i - 1; j >= 0 && arr[j] > temp; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
j的初值为i的前一个元素,如果该元素下标大于0并且该元素大于temp(temp保存的是arr[i],temp也是待排序的元素),那么就把这个元素往后放一格,也就是arr[j + 1] = arr[j]。
如果一直大就一直往后挪。如果待排元素为排好数组中最小的,那么j>=0的最后一次循环后,j=-1,则temp要放的位置也就是0号元素即j+1,因此arr[j + 1] = temp。
所以temp最后放置的下标也就是j+1。如果是最大的,那么第二层for循环就进不去,侧面说明直接插入排序是稳定的。
直接插入排序这里是一次放一格,步长为1,相当于将一个n个元素的数组按步长为1分成了n份,理解这里的步长对后面的希尔排序有很大帮助。
2.2.3 测试结果
这里先测试一个20个元素的数组。
因为元素个数太少,排序时长小于1ms所以显示0ms,再测试一个元素个数为10000的数组,就不打印了。
时间复杂度和空间复杂度之后分析。
2.3 折半插入排序
2.3.1 折半插入排序的思路
同直接插入排序一样,也是将第i个记录插入到前面i-1个已经排好序的记录中,也是从1号元素开始排序。(前提是没有哨兵)。
有所不同的是,这两个算法对于元素位置的确定的方法不同。对于大数组来说由明显区别。
直接插入排序是对有序数组一个一个从后往前进行比较,而折半插入排序是将排好元素一分为二来进行查找插入的位置。
同折半查找一样,折半查找就是在有序数组中找到值为key的元素,借助low,mid,high来实现,这里依旧采用。
2.3.2 具体代码
void BinarySort(int* arr, int n) {
printf("折半插入排序:\n");
int i, j;
int low, mid, high;
for (i = 1; i < n; i++) {
int temp = arr[i];
low = 0;
high = i-1;
while (low <= high) {
mid = (low + high) / 2;
if (arr[mid] > temp) {
high = mid - 1;
}
else {
low = mid + 1;
}
}
for (j = i - 1; j >= low; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
}
low初值为0,high初值为i-1,也就是排好数组的最后一个元素的下标,因为相当于是在0到i-1中找i的位置。
只要low<=high,mid就他俩一半,如果arr[mid]>temp,说明中值偏大,上限减少,即high=mid-1,反之中值偏小,下限增减,即low=mid+1,mid始终是中值的下标。
如果temp为排好元素中最大的,一直low=mid+1,最终low>high,出循环,j等于排好数组中最后一个元素的下标,low>high也就low>j,所以第二个for循环进不去,arr[j+1]=temp,也就是arr[i]=temp,自己等于自己了属于是,所以侧面说明折半插入排序也是稳定的。
2.3.3 测试代码
还是显示一个20个元素的数组
再来一个元素个数为10000的,也不打印了。
时间复杂度和空间复杂度之后分析。
2.4 希尔排序
2.4.1 折半插入排序的思路
将待排数组分成若干个稀疏的子序列,分别进行直接插入排序,使得稀疏的子序列较为有序,然后再全部进行次直接插入排序,即可完成。
这里采用划分子序列的方法采用业界统一的gap = gap / 3 + 1。
步骤也就两步:第一步,分组,第二步,分组排序。
首先分组,每回分gap = gap / 3 + 1。
按照分好的小组每组的第2号元素开始直接插入排序,每回增加的跨度为gap,也就是步长。
直到gap=1,进行一次直接插入排序。
2.4.2 具体代码
void ShellSort(int* arr, int n) {
printf("希尔排序:\n");
int gap = n;
int i, j, k;
do {
gap = gap / 3 + 1;
for (i = 0; i < gap; i++) {//分的小组
for (j = i + gap; j < n; j = j + gap) {//每个小组的第二号元素(默认一号元素有序)开始直接插入排序,步长为gap
int temp = arr[j];
for (k = j - gap; k >= 0 && arr[k] > temp; k = k - gap) {
arr[k + gap] = arr[k];
}
arr[k + gap] = temp;
}
}
} while (gap > 1);
}
do{}while;保证gap=1进行完可以退出。
但是希尔排序虽然组内是直接插入排序,是稳定的,但组外不同小组之间的元素则是不稳定的,所以希尔排序是不稳定的。
2.4.3 测试结果
先进行元素个数为20的数组。
再进行元素个数为10000的数组
快的夸张啊。
2.5 三种插入排序的分析
直接插入排序
时间复杂度:最坏情况下为O(N2),此时待排序列为逆序,或者说接近逆序。
最好情况下为O(N),此时待排序列为升序,或者说接近升序。
平均为O(N2)
空间复杂度:因为每回只移动一个所以空间复杂度为O(1)。
稳定性:稳定。
折半插入排序
时间复杂度:最坏情况下为O(N2)
最好情况下为O(Nlog2N)参考折半查找的时间复杂度。
平均O(N2)
空间复杂度:因为每回只移动一个所以空间复杂度为O(1)。
稳定性:稳定。
希尔排序
时间复杂度平均:业界统一认为为O(N1.3)
空间复杂度:因为每回只移动一个所以空间复杂度为O(1)
稳定性:不稳定。
总结
有什么问题欢迎大家指正!