【数据结构与算法】——插入排序
概要
本文将介绍插入排序方法——直接插入、希尔排序
想了解数据结构其他内容,本人主页 恋风诗
获取源码,gitte仓库:mozhengy
正文
1.排序的分类
目前将主要介绍下面几种排序:
后续学习更多内容后会及时更新
2、插入排序
2.1 直接插入排序
基本思想:把待排序的记录按其关键码值的⼤⼩逐个插⼊到⼀个已经排好序的有序序列中,直到所有的记录插⼊完为⽌,得到⼀个新的有序序列。
直白的说:就是构建一个有序序列(初始将第一个元素视为有序序列),要插入的元素从有序序列的最后一个位置逐步向前比较,找到合适的位置插入。
详细解释:
- 定义end和tmp:
end表示有序序列最后一个元素的位置
tmp表示当前要插入到已排好序子数组中的元素 - 外层循环:外层循环控制排序的轮数,从第 0 个元素开始,到第 n - 2 个元素结束。每一轮都会将一个新元素插入到已排序的序列中。
- 内层循环:内层循环从已排序序列的末尾开始向前扫描,若当前元素大于待插入元素 tmp,就把当前元素向后移动一位,然后继续向前扫描;若当前元素小于等于 tmp,则跳出循环。
- 插入元素:在内层循环结束后,将待插入元素 tmp 插入到合适的位置。
void InsertSort(int* arr, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = arr[end + 1];while (end >= 0){if (arr[end] > tmp){arr[end + 1] = arr[end];end--;}else{break;}}arr[end + 1] = tmp;}
}
时间复杂度分析
最好情况下:数组已经是有序的。在这种情形下,对于每一轮的插入操作,待插入元素 tmp 都比已排序部分的最后一个元素大,所以内层 while 循环只需要执行一次比较操作,就会因条件不满足而跳出。时间复杂度是
O(n)
。
最坏情况:数组完全逆序。此时,对于每一轮的插入操作,待插入元素 tmp 都要和已排序部分的所有元素进行比较,并且需要将已排序部分的元素依次向后移动一位。外层循环执行 n - 1 次,第 1 次内层循环执行 1 次比较,第 2 次执行 2 次比较,以此类推,第 n - 1 次执行 n - 1 次比较。总的比较次数为
1+2+3+⋯+(n−1)= n(n−1)/2。所以,最坏情况下插入排序的时间复杂度是 O(n ^2 )
。
由此可见,要想优化代码,则必须将大的数据尽可能放在后面,小的数据尽可能放在前面,如何怎么办呢?请看希尔排序
2.2 希尔排序
**概念:**希尔排序法⼜称缩⼩增量法。希尔排序法的基本思想是:先选定⼀个整数(通常是gap=n/3+1),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。
基本思想:将原始数据分成多个子序列来进行插入排序,这些子序列的间隔(即增量)会逐渐缩小,直到增量为 1,此时整个序列就会被合并成一个,再进行一次普通的插入排序。通过这种方式,能让元素更快地移动到它们大致的正确位置,从而减少了比较和交换的次数,提高了排序效率。
详细解释:
- gap 表示设置当前的间隔,初始值设为数组的长度 n。这个初始值会在后续的排序过程中逐渐减小。
- 外层循环:控制间隔 gap 的变化
(1).该循环的作用是不断缩小间隔 gap 的值,直到 gap 等于 1。
(2).gap = gap / 3 + 1 是一种常见的间隔缩小方式。每次将 gap 缩小为原来的 1/3 再加 1,这样能保证在 gap 逐渐减小时,最后一次 gap 一定为 1,从而完成最终的插入排序。当 gap 等于 1 时,循环结束,因为如果继续使用 gap = gap / 3 + 1 会陷入死循环(例如 gap 为 1 时,1 / 3 + 1 还是 1)。 - 中层和内层以及插入部分与直接插入排序类似,只需要将end+1都改为end+gap(间隔由一改为gap)
//希尔排序
void ShellSort(int* arr, int n)
{int gap = n;while (gap > 1)//等于1执行gap/3+1会成死循环{gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;int tmp = arr[end + gap];while (end >= 0){if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;}}
}
(动图等我学会画再补)
时间复杂度分析
这里有人就要问了,这不是三重循环吗?不应该更复杂了吗?
其实不然,
(1)最好情况
最好情况是数组已经有序。在这种情况下,对于每个间隔 gap,在进行子序列插入排序时,内层的 while 循环几乎不会执行(因为元素已经有序,待插入元素总是大于等于前面的元素)。时间复杂度为:O(nlogn)
(2) 最坏情况
最坏情况的时间复杂度分析较为困难,并且没有一个精确的通用公式,因为它取决于具体的增量序列。对于 gap = gap / 3 + 1 这种增量序列,最坏情况的时间复杂度接近 O(n ^2)
在最坏情况下,每次子序列插入排序都需要将元素移动到最前面的位置。随着 gap 的减小,子序列插入排序的工作量逐渐增加。当 gap 最终变为 1 时,就相当于进行一次普通的插入排序,而插入排序在最坏情况下(数组完全逆序)的时间复杂度是 O(n^2 )
。虽然希尔排序通过多次子序列排序提前将元素大致排序,但在某些特殊输入下,仍可能接近 O(n^2 )
的复杂度。
综上所述:希尔排序的时间复杂度与gap有关,至今仍是数学难题(等待你我的破解),通常情况下,有认为对于 gap = gap / 3 + 1 这种增量序列,平均时间复杂度大约为 O(n^1.3 )
。图片描述
测试代码
void arrprint(int* arr, int n)
{for (int i = 0; i < n; i++){printf("%d ", arr[i]);}printf("\n");
}void test01()
{int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf("排序之前:");arrprint(a, n);//InsertSort(a, n);ShellSort(a, n);printf("排序之后:");arrprint(a, n);
}
int main()
{test01();return 0;
}
3.测试排序性能代码
我们可以利用如下的代码测试排序的性能,下面是给每一种排序方法给10000个数排序所用时间(单位:毫秒)
// 测试排序的性能对⽐
void TestOP()
{srand(time(0));const int N = 100000;int* a1 = (int*)malloc(sizeof(int) * N);int* a2 = (int*)malloc(sizeof(int) * N);int* a3 = (int*)malloc(sizeof(int) * N);int* a4 = (int*)malloc(sizeof(int) * N);int* a5 = (int*)malloc(sizeof(int) * N);int* a6 = (int*)malloc(sizeof(int) * N);int* a7 = (int*)malloc(sizeof(int) * N);for (int i = 0; i < N; ++i){a1[i] = rand();a2[i] = a1[i];a3[i] = a1[i];a4[i] = a1[i];a5[i] = a1[i];a6[i] = a1[i];a7[i] = a1[i];}int begin1 = clock();InsertSort(a1, N);int end1 = clock();int begin2 = clock();ShellSort(a2, N);int end2 = clock();//int begin3 = clock();//SelectSort(a3, N);//int end3 = clock();//int begin4 = clock();//HeapSort(a4, N);//int end4 = clock();//int begin5 = clock();//QuickSort(a5, 0, N - 1);//int end5 = clock();//int begin6 = clock();//MergeSort(a6, N);//int end6 = clock();//int begin7 = clock();//BubbleSort(a7, N);//int end7 = clock();printf("InsertSort:%d\n", end1 - begin1);printf("ShellSort:%d\n", end2 - begin2);//printf("SelectSort:%d\n", end3 - begin3);//printf("HeapSort:%d\n", end4 - begin4);//printf("QuickSort:%d\n", end5 - begin5);//printf("MergeSort:%d\n", end6 - begin6);//printf("BubbleSort:%d\n", end7 - begin7);free(a1);free(a2);free(a3);free(a4);free(a5);free(a6);free(a7);
}
int main()
{TestOP();return 0;
}
结果:
希尔排序明显快的多