一.前言
本文我们开始进入数据结构的难点——排序,当我们初步学习排序后就可以写出更高效的代码~。码字不易,希望大家多多支持我呀!(三连+关注,你是我滴神!)
二.排序的概念及其运用
1.1排序的概念
- 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保存不变,即在原序列中,r[i] = r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;反之不稳定。
- 内部排序:数据元素全部放在内存中的排序。
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.2 常用排序算法
三.常用排序算法的实现
3.1 插入排序
3.1.1 基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
实际中我们玩扑克牌时,就用了插入排序的思想
3.1.2 直接插入排序
当插入第i(i>=1)个元素时,前面的arr[0], arr[1]....arr[i-1]已经排行序,此时用arr[i]的排序码与arr[i],arr[i-1],arr[i-2]....的排序码顺序进行比较,找到插入位置即将arr[i]插入,原来位置上的元素顺序后移。
在学习该排序之前我们先完成一个简单的单趟排序。我们对两个有序数组分别插入5和1来分析。
当我们要插入5时,对比最后一位数字,如果该数字比5大则往后挪一位,与此同时end--往前移动继续与5比较,以此类推直到end指向4(比5小)的时候停下,然后在end的后面插入5.
接下来对于1也是同理,当对比完发现end指向-1时1成功插入(1为最小)。
基于这两个图例我们来开始写这个单趟代码:
首先要在有序数组插入数字的时候,那么5的位置肯定是end+1位,其次我们需要用tmp来保存5,因为当9往后挪动的时候会覆盖5的位置,要想让5继续与前面数字进行比较就得用存储5的tmp。
void InsertSort(int* arr, int n) { //单趟排序 int end = n - 2; int tmp = arr[end + 1]; while (end >= 0) { if (tmp < arr[end])//如果遇到比tmp大的数 { arr[end + 1] = arr[end];//往后挪 } else { break;//退出循环,在外面插入 } end--; } arr[end + 1] = tmp; } int main() { int arr[] = { 3,4,2,1,7,8,5 }; InsertSort(arr, 7); return 0; }
有一点需要注意,当我们在可以插入的时候不能选择在循环里面插入而是用break跳出循环,再写插入的代码。
之所以这样是因为如果tmp保存的值比数组中所有的值都要小,那么它只能是等到循环结束,是不能在循环里进行插入的。
既然我们控制了单趟有序,怎么样控制整体有序呢?
我们只需要控制end的位置就可以做到整体有序,因为这就像是一个在有序数组里不断插入的过程,而end的起始位置作用就是隔离有序数组。
所以end的范围起始是0结束应该是n-2。在n个数中,end指向倒数第2个,把倒数第一个当作要插入的数字即可完成所有的排序。
void InsertSort(int* arr, int n) { int i = 0; for (i = 0; i < n - 1; i++) { //单趟排序 int end = i; int tmp = arr[end + 1]; while (end >= 0) { if (tmp < arr[end])//如果遇到比tmp大的数 { arr[end + 1] = arr[end];//往后挪 } else { break;//退出循环,在外面插入 } end--; } arr[end + 1] = tmp; } } int main() { int arr[] = { 3,4,2,1,7,8,5 }; InsertSort(arr, 7); return 0; }
时间复杂度:
最坏情况(逆序时):O(N^2)
最好情况(顺序有序时):O(N)
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
3.1.3 希尔排序(缩小增量排序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成各组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序,然后去重复上述分组和排序的工作。当到达gap=1时,所有的记录在统一组内排好序。
希尔排序前景:
- 先预排序
- 直接插入排序
我们通过预排序让数组接近有序再来直接插入排序这样时间复杂度就可以接近O(N)
接着我们要在划分好的组里进行直接插入排序
蓝色组:
紫色组:
红色组:
在外面排完之后可以观察得到,现在这组数虽然不是有序,但对比原有顺序可以说是相对有序(大的数更往后排了,小的数更往前排了)。
接下来我们用代码来实现该思路:
在直接插入的基础上进行修改
以此类推完成排序
void ShellSort(int* a, int n) { //单趟排序 int gap = 3; int end = 0; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end])//如果遇到比tmp大的数 { a[end + gap] = a[end];//往后挪 } else { break;//退出循环,在外面插入 } end = end - gap; } a[end + gap] = tmp; }
我们可以发现如果gap为1时,那就是直接插入排序了~
这样就控制了蓝色一组
void ShellSort(int* a, int n) { int gap = 3; for (int i = 0; i < n ; i += gap) { //单趟排序 int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end])//如果遇到比tmp大的数 { a[end + gap] = a[end];//往后挪 } else { break;//退出循环,在外面插入 } end = end - gap; } a[end + gap] = tmp; } }
那么按照这个代码,最后end一定要落到这个位置上吗?
如果落到这个位置,那最后tmp再取end+1就越界了,而且按照我们上面学到的插排,end起始排序的位置是要落在插入数字之前的,但目前end的位置在蓝色组中是最后一个数字,使得其tmp取到随机值了。
所以最后我们要选的是n-gap,因为这里条件是i小于,所以最后end的位置才可以落在n-4即8所在的位置,而这在蓝色组的4个数字中排序是最后落到为倒数第二个数字,符合直接插入的规则。
void ShellSort(int* a, int n) { int gap = 3; for (int i = 0; i <n-gap; i+=gap) { //单趟排序 int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end])//如果遇到比tmp大的数 { a[end + gap] = a[end];//往后挪 } else { break;//退出循环,在外面插入 } end = end-gap; } a[end + gap] = tmp; } }
现在我们把蓝色组的预排序处理好了,那另外两组又要如何处理?
再来套个循环去改变它们的起始位置就行了。
void ShellSort(int* a, int n) { int gap = 3; for (int j = 0; j < gap; j++) { for (int i = j; i < n - gap; i += gap) { //单趟排序 int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end])//如果遇到比tmp大的数 { a[end + gap] = a[end];//往后挪 } else { break;//退出循环,在外面插入 } end = end - gap; } a[end + gap] = tmp; } } }
这样子是一组一组排,蓝色走完走紫色,紫色走完走红色。
下面是另一种版本(效果与上面一样):多组并排
void ShellSort(int* a, int n) { int gap = 3; for (int i = 0; i < n - gap; ++i) { //单趟排序 int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end])//如果遇到比tmp大的数 { a[end + gap] = a[end];//往后挪 } else { break;//退出循环,在外面插入 } end = end - gap; } a[end + gap] = tmp; } }
这个是挨个排序(蓝紫红蓝紫红,虽然效果跟前面的一样,但前面代码明显更好理解~
预排序意义:大的数更快的到后面去,小的数更快的到前面去。gap越大跳的越快,越不接近有序,gap越小跳的越慢,越接近有序。gap=1,直接有序。
完整希尔排序
void ShellSort(int* a, int n) { int gap = n; while (gap > 1) { gap = gap / 2; for (int j = 0; j < gap; j++) { for (int i = j; i < n - gap; i += gap) { //单趟排序 int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end])//如果遇到比tmp大的数 { a[end + gap] = a[end];//往后挪 } else { break;//退出循环,在外面插入 } end = end - gap; } a[end + gap] = tmp; } } } }
之所以这样是因为gap不好给固定的值,固定值有时候会不适合n的数量变化,所以我们最开始可以先让gap为n,然后通过/2的方式对数组不断进行预排序,而在最后gap一定是为1,这时候就可以进行整体的排序了。
有时候为避免预排序过多可以改变一下gap,/3+1是为了保证gap最后能够取到1
接下来我们来测试一下它的性能
这是通过生成100000个随机值测试用的代码片段。
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 = N-1; i >= 0; --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 begin7 = clock(); //BubbleSort(a7, N); int end7 = 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(); printf("InsertSort:%d\n", end1 - begin1); printf("ShellSort:%d\n", end2 - begin2); printf("BubbleSort:%d\n", end7 - begin7); printf("SelectSort:%d\n", end3 - begin3); printf("HeapSort:%d\n", end4 - begin4); printf("QuickSort:%d\n", end5 - begin5); printf("MergeSort:%d\n", end6 - begin6); free(a1); free(a2); free(a3); free(a4); free(a5); free(a6); free(a7); } int main() { TestOP(); //TestBubbleSort(); //TestHeapSort(); //TestSelectSort(); return 0; }
我们选择调用希尔和插排来对比在10万个随机数中二者排序完成的用时是多少。
我们再来看看1万个数据的效果
可以发现数据量越大越能突显希尔的性能优势。
希尔排序的时间复杂度
一般gap是多少就会划分有多少个组别
gap很大的时候,如n/3 整个数组逆序,n/3组数据,每组比较3次(1+2)间距为gap的这些数据,合计 n/3 *3 ==n
所以我们可以得知红框框住的这两组循环一开始是n次
gap很大的时候,如1 接近有序了 间距为1的这些数据,合计:n
中间会呈现由n变大再变小到n的过程。
我们粗略通过循环比较可以得出时间复杂度N(因为上图中n无法精确计算)*logN(以2为底或以3为底,看gap怎么处理)。
而最终结论是,时间复杂度为O(N^1.3),算法效率略差于n*logn
四.全部代码
sort.c
#define _CRT_SECURE_NO_WARNINGS 1 #include "sort.h" void InsertSort(int* arr, int n) { int i = 0; for (i = 0; i < n - 1; i++) { //单趟排序 int end = i; int tmp = arr[end + 1]; while (end >= 0) { if (tmp < arr[end])//如果遇到比tmp大的数 { arr[end + 1] = arr[end];//往后挪 } else { break;//退出循环,在外面插入 } end--; } arr[end + 1] = tmp; } } //void ShellSort(int* a, int n) //{ // int gap = 3; // for (int i = 0; i < n ; i += gap) // { // //单趟排序 // int end = i; // int tmp = a[end + gap]; // while (end >= 0) // { // if (tmp < a[end])//如果遇到比tmp大的数 // { // a[end + gap] = a[end];//往后挪 // } // else // { // break;//退出循环,在外面插入 // } // end = end - gap; // } // a[end + gap] = tmp; // } // // //} void ShellSort(int* a, int n) { int gap = n; while (gap > 1) { gap = gap / 2; for (int j = 0; j < gap; j++) { for (int i = j; i < n - gap; i += gap) { //单趟排序 int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end])//如果遇到比tmp大的数 { a[end + gap] = a[end];//往后挪 } else { break;//退出循环,在外面插入 } end = end - gap; } a[end + gap] = tmp; } } } } //void ShellSort(int* a, int n) //{ // int gap = 3; // for (int i = 0; i < n - gap; ++i) // { // //单趟排序 // int end = i; // int tmp = a[end + gap]; // while (end >= 0) // { // if (tmp < a[end])//如果遇到比tmp大的数 // { // a[end + gap] = a[end];//往后挪 // } // else // { // break;//退出循环,在外面插入 // } // end = end - gap; // } // a[end + gap] = tmp; // } // //} void PrintfSort(int* a, int n) { for (int i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n"); }
——————————————————————————————————————————
sort.h
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> void PrintfSort(int* a, int n); void ShellSort(int* a, int n); void InsertSort(int* arr, int n);
——————————————————————————————————————————
test.c
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include "sort.h" #include <time.h> #include <stdlib.h> //void InsertSort(int* arr, int n) //{ // int i = 0; // for (i = 0; i < n - 1; i++) // { // //单趟排序 // int end = i; // int tmp = arr[end + 1]; // while (end >= 0) // { // if (tmp < arr[end])//如果遇到比tmp大的数 // { // arr[end + 1] = arr[end];//往后挪 // } // else // { // break;//退出循环,在外面插入 // } // end--; // } // arr[end + 1] = tmp; // } // //} void TestShellSort() { int a[] = { 9,1,2,5,7,4,8,6,3,5,1,2,3,5,1,8,3 }; ShellSort(a, sizeof(a) / sizeof(int)); PrintfSort(a, sizeof(a) / sizeof(int)); } void TestOP() { srand(time(0)); const int N = 10000; 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 = N - 1; i >= 0; --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 begin7 = clock(); //BubbleSort(a7, N); int end7 = 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(); printf("InsertSort:%d\n", end1 - begin1); printf("ShellSort:%d\n", end2 - begin2); printf("BubbleSort:%d\n", end7 - begin7); printf("SelectSort:%d\n", end3 - begin3); printf("HeapSort:%d\n", end4 - begin4); printf("QuickSort:%d\n", end5 - begin5); printf("MergeSort:%d\n", end6 - begin6); free(a1); free(a2); free(a3); free(a4); free(a5); free(a6); free(a7); } int main() { //int arr[] = { 3,4,2,1,7,8,5 }; //InsertSort(arr, 7); /*int a[] = {1,5,8,7,4,6,9,85,2,4,1,7 }; ShellSort(a, sizeof(a) / sizeof(int)); PrintfSort(a, sizeof(a) / sizeof(int));*/ TestOP(); return 0; }
五.结语
插入排序只是排序中的冰山一角,它可以帮助我们处理更庞大的数据,不过由于排序算法有多种,我们得学会在各种情况下运用最佳算法进行排序。最后感谢大家的观看,友友们能够学习到新的知识是额滴荣幸,期待我们下次相见~