万字长文总结八大经典内部排序算法
写在前面:博主是一位普普通通的19届双非软工在读生,平时最大的爱好就是听听歌,逛逛B站。博主很喜欢的一句话花开堪折直须折,莫待无花空折枝:博主的理解是头一次为人,就应该做自己想做的事,做自己不后悔的事,做自己以后不会留有遗憾的事,做自己觉得有意义的事,不浪费这大好的青春年华。博主写博客目的是记录所学到的知识并方便自己复习,在记录知识的同时获得部分浏览量,得到更多人的认可,满足小小的成就感,同时在写博客的途中结交更多志同道合的朋友,让自己在技术的路上并不孤单。
目录:
1.冒泡排序
2.选择排序
简单选择排序
树形选择排序
3.堆排序
4.插入排序
直接插入排序
折半插入排序
2-路插入排序算法
表插入排序
5.希尔排序
6.快速排序
7.归并排序
8.基数排序
9.总结
本篇博客部分图片及代码参考一文总结十大经典排序算法(思维导图 + 动图演示 + 代码实现 C/C++/Python + 致命吐槽)
1.冒泡排序
两两比较,把较大的数放在后面,每一轮循环循环结束会找出一个最大值,完成排序
//C void swap(int *a,int *b){int temp = *a;*a = *b;*b = temp; } void bubble_sort(int arr[], int len){int i, j, temp;for (i = 0; i < len - 1; i++)for (j = 0; j < len - 1 - i; j++)if (arr[j] > arr[j + 1]) {swap(&arr[j], &arr[j + 1]);} }冒泡排序的改进:
比如我们给出一个数组[2,1,3,4,5,6,7,8,9]从小到大排序,我们可以看出只需要交换一次就完成排序,但是普通的冒泡排序即使在完成排序后执行完两个for循环里边的代码,导致时间复杂度为O(n2),所以我们需要对冒泡排序进行改进
冒泡排序的改进思路:
如果在某一轮冒泡过程中,没有一次交换,那么说明后面已经正确排序不用再进行下一轮比较,直接跳出循环
void swap(int *a,int *b){int temp = *a;*a = *b;*b = temp; } void bubble_sort(int arr[], int len){int i, j, temp;bool flag;for (i = 0; i < len - 1; i++){flag=false;for (j = 0; j < len - 1 - i; j++)if (arr[j] > arr[j + 1]) {false=true;swap(&arr[j], &arr[j + 1]);}if(flag==false)break;} }时间复杂度:最好情况O(n),最坏情况O(n²),平均时间复杂度O(n²)
空间复杂度:只需要一个变量作为辅助空间,空间复杂度O(1)
算法特点:
1.稳定排序。
2.移动的次数比较多,当初始记录无序,n较大时,算法不适用。
2.选择排序
2.1简单选择排序
描述:
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
//C void swap(int *a,int *b){int temp = *a;*a = *b;*b = temp; } void selection_sort(int arr[], int len){int i,j;for (i = 0 ; i < len - 1 ; i++){int min = i;//遍历未排序的元素for (j = i + 1; j < len; j++){if (arr[j] < arr[min]) //找到目前最小值min = j; //记录最小值swap(&arr[min], &arr[i]); //做交換}} }时间复杂度:最好情况O(n²),最坏情况O(n²),平均时间复杂度O(n²)
空间复杂度:只需要一个变量作为辅助空间,空间复杂度O(1)
算法特点:
1.是一种稳定的排序算法。
2.移动次数较少,每当一记录占用空间比较多的时候,这种排序比插入排序快
2.2树形选择排序
树形选择排序(又称“锦标赛排序”),是一种按照锦标赛的思想进行选择排序的方法,即所有记录采取两两分组,筛选出较小(较大)的值;然后从筛选出的较小(较大)值中再两两分组选出更小(更大)值,依次类推,直到最后选出一个最小(最大)值。同样可以采用此方式筛选出次小(次大)值等。
整个排序的过程,可以用一棵具有 n 个叶子结点的完全二叉树表示。例如对无序表{49,38,65,97,76,13,27,49}采用树形选择的方式排序,过程如下:
首先将无序表中的记录采用两两分组,筛选出各组中的较小值(如图中的(a)过程);然后将筛选出的较小值两两分组,筛选出更小的值,以此类推(如图中的(b)(c)过程),最终整棵树的根结点中的关键字即为最小关键字:
筛选出关键字 13 之后,继续重复此方式找到剩余记录中的最小值,此时由于关键字 13 已经筛选完成,需要将关键字 13 改为“最大值”,继续重复此过程,如下图 所示:
通过不断地重复此过程,可依次筛选出从小到大的所有关键字。该算法的时间复杂度为O(nlog2n),同简单选择排序相比,该算法减少了不同记录之间的比较次数,但是程序运行所需要的空间较多。
3.堆排序
堆:在含有 n 个元素的序列中,如果序列中的元素满足下面其中一种关系时,此序列可以称之为堆
对于堆的定义也可以使用完全二叉树来解释,因为在完全二叉树中第 i 个结点的左孩子恰好是第 2i 个结点,右孩子恰好是 2i+1 个结点。如果该序列可以被称为堆,则使用该序列构建的完全二叉树中,每个根结点的值都必须不小于(或者不大于)左右孩子结点的值。
以无序表{49,38,65,97,76,13,27,49}来讲,其对应的堆用完全二叉树来表示为:
堆用完全二叉树表示时,其表示方法不唯一,但是可以确定的是树的根结点要么是无序表中的最小值,要么是最大值。
讲完了堆,那么我们来讲堆排序:
通过将无序表转化为堆,可以直接找到表中最大值或者最小值,然后将其提取出来,令剩余的记录再重建一个堆,取出次大值或者次小值,如此反复执行就可以得到一个有序序列,此过程为堆排序。
堆排序过程的代码实现需要解决两个问题:
如下例子:
当根节点13输出后,我们用最后一个节点代替根节点即:
此时由于结点 97 比左右孩子结点的值都大,破坏了堆的结构,所以需要进行调整:首先以 堆顶元素 97 同左右子树比较,同值最小的结点交换位置,即 27 和 97 交换位置:
由于替代之后破坏了根结点右子树的堆结构,所以需要进行和上述一样的调整,即令 97 同 49 进行交换位置:
通过上述的调整,之前被破坏的堆结构又重新建立。从根结点到叶子结点的整个调整的过程,被称为“筛选”。这样我们的第2个问题就被解决了
那么第一个问题如何解决呢,别急!还是这个无序表{49,38,65,97,76,13,27,49}初步建立的完全二叉树
在对上图做筛选工作时,规律是从底层结点开始,一直筛选到根结点。对于具有 n 个结点的完全二叉树,筛选工作开始的结点为第 ⌊n/2⌋个结点(此结点后序都是叶子结点,无需筛选)。
所以,对于有 9 个结点的完全二叉树,筛选工作从第 4 个结点 97 开始,由于 97 > 49 ,所以需要相互交换,交换后如下图所示:
然后再筛选第 3 个结点 65 ,由于 65 比左右孩子结点都大,则选择一个最小的同 65 进行交换,交换后的结果为:
然后筛选第 2 个结点,由于其符合要求,所以不用筛选;最后筛选根结点 49 ,同 13 进行交换,交换后的结果为:
交换后,发现破坏了其右子树堆的结构,所以还需要调整,最终调整后的结果为:
最好情况O(nlogn)
最坏情况O(nlogn)
平均时间复杂度O(nlogn)
空间复杂度:
只需要一个记录大小交换用的辅助空间,空间复杂度O(1)
算法特点:
1.是一种不稳定的排序算法。
2.建立堆所需要的比较次数比较多,因此记录数较少的时候不宜采用。
4.插入排序
4.1直接插入排序
插入排序算法是所有排序方法中最简单的一种算法,其主要的实现思想是将数据按照一定的顺序一个一个的插入到有序的表中,最终得到的序列就是已经排序好的数据。
直接插入排序是插入排序算法中的一种,采用的方法是:在添加新的记录时,使用顺序查找的方式找到其要插入的位置,然后将新记录插入。
例如采用直接插入排序算法将无序表{3,1,7,5,2,4,9,6}进行升序排序的过程为:
首先考虑记录 3 ,由于插入排序刚开始,有序表中没有任何记录,所以 3 可以直接添加到有序表中,则有序表和无序表可以如图
向有序表中插入记录 1 时,同有序表中记录 3 进行比较,1<3,所以插入到记录 3 的左侧,如图
向有序表插入记录 7 时,同有序表中记录 3 进行比较,3<7,所以插入到记录 3 的右侧,如图
向有序表中插入记录 5 时,同有序表中记录 7 进行比较,5<7,同时 5>3,所以插入到 3 和 7 中间,如图
向有序表插入记录 2 时,同有序表中记录 7进行比较,2<7,再同 5,3,1分别进行比较,最终确定 2 位于 1 和 3 中间,如图
最终:
动态图:
4.2折半插入排序
直接插入排序算法在查找插入位置时,采用的是顺序查找的方式,而在查找表中数据本身有序的前提下,可以使用折半查找来代替顺序查找,这种排序的算法就是折半插入排序算法。
#include <stdio.h> void print(int a[], int n ,int i){printf("%d:",i);for(int j=0; j<8; j++){printf("%d",a[j]);}printf("\n"); }void BInsertSort(int a[],int size){int i,j,low = 0,high = 0,mid;int temp = 0;for (i=1; i<size; i++) {low=0;high=i-1;temp=a[i];//采用折半查找法判断插入位置,最终变量 low 表示插入位置while (low<=high) {mid=(low+high)/2;if (a[mid]>temp) {high=mid-1;}else{low=mid+1;}}//有序表中插入位置后的元素统一后移for (j=i; j>low; j--) {a[j]=a[j-1];}a[low]=temp;//插入元素print(a, 8, i);}} int main(){int a[8] = {3,1,7,5,2,4,9,6};BInsertSort(a, 8);return 0; }4.3 2-路插入排序算法
2-路插入排序算法是在折半插入排序的基础上对其进行改进,减少其在排序过程中移动记录的次数从而提高效率。
具体实现思路为:另外设置一个同存储记录的数组大小相同的数组 d,将无序表中第一个记录添加进 d[0] 的位置上,然后从无序表中第二个记录开始,同 d[0] 作比较:如果该值比 d[0] 大,则添加到其右侧;反之添加到其左侧。在这里的数组 d 可以理解成一个环状数组。
使用 2-路插入排序算法对无序表{3,1,7,5,2,4,9,6}排序的过程如下:
插入1:
插入7:
插入5,由于其比 7小,但是比 3 大,所以需要移动 7 的位置,然后将 5 插入:
将记录 2 插入到数组 d 中,由于比 1大,比 3 小,所以需要移动 3、7、5 的位置,然后将 2 插入:
将记录 4 插入到数组 d 中,需要移动 5 和 7 的位置:
将记录 9 插入到数组 d 中:
将记录 6 插入到数组 d 中:
最终存储在原数组时,从 d[7] 开始依次存储。
C语言完整代码实现:
#include <stdio.h> #include <stdlib.h> void insert(int arr[], int temp[], int n) {int i,first,final,k;first = final = 0;//分别记录temp数组中最大值和最小值的位置temp[0] = arr[0];for (i = 1; i < n; i ++){// 待插入元素比最小的元素小if (arr[i] < temp[first]){first = (first - 1 + n) % n;temp[first] = arr[i];}// 待插入元素比最大元素大else if (arr[i] > temp[final]){final = (final + 1 + n) % n;temp[final] = arr[i];}// 插入元素比最小大,比最大小else {k = (final + 1 + n) % n;//当插入值比当前值小时,需要移动当前值的位置while (temp[((k - 1) + n) % n] > arr[i]) {temp[(k + n) % n] =temp[(k - 1 + n) % n];k = (k - 1 + n) % n;}//插入该值temp[(k + n) % n] = arr[i];//因为最大值的位置改变,所以需要实时更新final的位置final = (final + 1 + n) % n;}}// 将排序记录复制到原来的顺序表里for (k = 0; k < n; k ++) {arr[k] = temp[(first + k) % n];} }int main() {int a[8] = {3,1,7,5,2,4,9,6};int temp[8];insert(a,temp,8);for (int i = 0; i < 8; i ++){printf("%d ", a[i]);}return 0; }4.4表插入排序
表插入排序,即使用链表的存储结构对数据进行插入排序。在对记录按照其关键字进行排序的过程中,不需要移动记录的存储位置,只需要更改结点间指针的指向。
链表的存储结构:
#define SIZE 100 typedef struct {int rc;//记录项int next;//指针项,由于在数组中,所以只需要记录下一个结点所在数组位置的下标即可。 }SLNode; typedef struct {SLNode r[SIZE];//存储记录的链表int length;//记录当前链表长度 }SLinkListType;在使用数组结构表示的链表中,设定数组下标为 0 的结点作为链表的表头结点,并令其关键字取最大整数。则表插入排序的具体实现过程是:首先将链表中数组下标为 1 的结点和表头结点构成一个循环链表,然后将后序的所有结点按照其存储的关键字的大小,依次插入到循环链表中。
例如,将无序表{49,38,76,13,27}用表插入排序的方式进行排序,其过程为:
首先使存储 49 的结点与表头结点构成一个初始的循环链表,完成对链表的初始化,如下表所示:
然后将以 38 为关键字的记录插入到循环链表中(只需要更改其链表的 next 指针即可),插入后的链表为:
再将以 76 为关键字的结点插入到循环链表中,插入后的链表为:
直到最后:
最终:
从表插入排序的实现过程上分析,与直接插入排序相比只是避免了移动记录的过程(修改各记录结点中的指针域即可),而插入过程中同其它关键字的比较次数并没有改变,所以表插入排序算法的时间复杂度仍是O(n2)。
5.希尔排序
希尔排序,又称“缩小增量排序”,也是插入排序的一种,但是同前面几种排序算法比较来看,希尔排序在时间效率上有很大的 改进。 在使用直接插入排序算法时,如果表中的记录只有个别的是无序的,多数保持有序,这种情况下算法的效率也会比较高;除此之 外,如果需要排序的记录总量很少,该算法的效率同样会很高。希尔排序就是从这两点出发对算法进行改进得到的排序算法。
希尔排序的具体实现思路是:先将整个记录表分割成若干部分,分别进行直接插入排序,然后再对整个记录表进行一 次直接插入排序
例如无序表{49,38,65,97,76,13,27,49,55,4}进行希尔排序的过程为:
首先对 {49,13},{38,27},{65,49},{97,55},{76,4} 分别进行直接插入排序(如果需要调换位置也只是互换存储 位置上图中两两进行比较,例如 下图49 和 13 进行比较,13<49,所以交换存储位置。)
通过一次排序,无序表中的记录已基本有序,此时还可以再进行一次分割,这一次我们把数据分成三组,如下图所示:
经过两次分割,无序表中已基本有序,此时对整张表进行一次直接插入排序(只需要做少量的比较和插入操作即可),最 终希尔排序的结果为:
动图展示:
代码实现:
//C void shell_sort(int arr[], int len) {int gap, i, j;int temp;for (gap = len >> 1; gap > 0; gap >>= 1){for (i = gap; i < len; i++) {temp = arr[i];for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)arr[j + gap] = arr[j];arr[j + gap] = temp;}} }时间复杂度:
最好情况O(nlog²n)
最坏情况O(nlog²n)
平均时间复杂度O(nlogn)
空间复杂度:
只需要一个变量作为辅助空间,空间复杂度O(1)
算法特点:
1.这种跳跃式得的移动导致算法不是很稳定
2.这个增量序列有各种不同的取法Hibbard增量和sedgewick增量,据说这两种序列会降低其算法复杂度
3.n较大时,效果越明显,适用于n较大的情况
6.快速排序
C 语言中自带函数库中就有快速排序——qsort 函数 ,包含在 <stdlib.h> 头文件中
具体思路:
快速排序算法是在起泡排序的基础上进行改进的一种算法,其实现的基本思想是:通过一次排序将整个无序表分成相互独立的两 部分,其中一部分中的数据都比另一部分中包含的数据的值小,然后继续沿用此方法分别对两部分进行同样的操作,直到每一个 小部分不可再分,所得到的整个序列就成为了有序序列。
例如,对无序表{49,38,65,97,76,13,27,49}进行快速排序,大致过程为:
对于上述描述我们需要设置两个指针 low 和 high,分别指向无序表的表头和表尾:
先由 high 指针从右往左依次遍历,直到找到一个比 49 小的关键字,所以 high 指针走到 27 的地方停止。找到之后将 该关键字同 low 指向的关键字进行互换:
然后指针 low 从左往右依次遍历,直到找到一个比 49 大的关键字为止,所以 low 指针走到 65 的地方停止。同样找 到后同 high 指向的关键字进行互换:
指针 high 继续左移,到 13 所在的位置停止(13<49),然后同 low 指向的关键字进行互换:
指针 low 继续右移,到 97 所在的位置停止(97>49),然后同 high 指向的关键字互换位置:
指针 high 继续左移,此时两指针相遇,第一趟快速排序结束;
当所有数据的支点前后都只有一个元素的时候整个快速排序过程结束
动态图:
时间复杂度:
最好情况O(nlogn)
最坏情况O(n²)
平均时间复杂度O(nlogn)
空间复杂度:
执行时需要有一个栈来存放相应的数据,所以最大递归调用次数与递归树的深度一致,最好情况为O(logn),最坏情况下为O(n)
算法特点:
1.是一种不稳定的排序算法。
2.是所有内部排序的最快的排序算法。
3.缺点较多,但是c++STL库中针对其缺点已经做出了优化。
7.归并排序
其排序的实现思想是先将所有的记录完全分开,然后两两合 并,在合并的过程中将其排好序,最终能够得到一个完整的有序表。
例如对于含有 n 个记录的无序表,首先默认表中每个记录各为一个有序表(只不过表的长度都为 1),然后进行两两合并,使 n 个有序表变为 ⌈ n/2⌉ 个长度为 2 或者 1 的有序表(例如 4 个小有序表合并为 2 个大的有序表),通过不断地进行两两合 并,直到得到一个长度为 n 的有序表为止。这种归并排序方法称为:2-路归并排序。
例如对无序表{49,38,65,97,76,13,27}进行 2-路归并排序的过程如图
归并过程中,每次得到的新的子表本身有序,所以最终得到的为有序表。
动图展示:
归并排序算法在具体实现时,首先需要将整个记录表进行折半分解,直到分解为一个记录作为单独的一张表为 止,然后在进行两两合并。整个过程为分而后立的过程。该算法相比于堆排序和快速排序,其主要的优点是:当记录表中含有值相同的记录时, 排序前和排序后在表中的相对位置不会改变。例如,在记录表中记录 a 在记录 b 的前面(记录 a 和 b 的关键字的值相等),使用归并排序之后记录 a 还在记录 b 的前 面。这就体现出了该排序算法的稳定性。而堆排序和快速排序都是不稳定的。
时间复杂度:
最好情况O(nlogn)
最坏情况O(nlogn)
平均时间复杂度O(nlogn)
空间复杂度:
只需要一个跟待排数组大小相同的辅助空间,空间复杂度为O(n)
算法特点:
1.是一种稳定的排序算法。
2.比较占用内存。
8.基数排序
基数排序不同于之前所介绍的各类排序,前边介绍到的排序方法或多或少的是通过使用比较和移动记录来实现排序,而基数排序 的实现不需要进行对关键字的比较,只需要对关键字进行“分配”与“收集”两种操作即可完成。
例如对无序表{50,123,543,187,49,30,0,2,11,100}进行基数排序,由于每个关键字都是整数数值,且其中的最大 值由个位、十位和百位构成,每个数位上的数字从 0 到 9,首先将各个关键字按照其个位数字的不同进行分配分配表如下图所 示
通过按照各关键字的个位数进行分配,按照顺序收集得到的序列变为:{50,30,0,100,11,2,123,543,187,49}。在 该序列表的基础上,再按照各关键字的十位对各关键字进行分配,得到的分配表如下图所示:
通过按照各关键字的个位数进行分配,按照顺序收集得到的序列变为:{50,30,0,100,11,2,123,543,187,49}。在 该序列表的基础上,再按照各关键字的十位对各关键字进行分配,得到的分配表如下图所示:
由上表顺序收集得到的记录表为:{0、100、2、11、123、30、543、49、50、187}。在该无序表的基础上,依次将表中的记 录按照其关键字的百位进行分配,得到的分配如下图所示:
最终通过三次分配与收集,最终得到的就是一个排好序的有序表:{0、2、11、30、49、50、100、123、187、543}。
例子中是按照个位-十位-百位的顺序进行基数排序,此种方式是从最低位开始排序,所以被称为最低位优先法(简称“LSD 法”)。 同样还可以按照百位-十位-各位的顺序进行排序,称为最高位优先法(简称“MSD 法”),使用该方式进行排序同最低位优先 法不同的是:当无序表中的关键字进行分配后,相当于进入了多个子序列,后序的排序工作分别在各个子序列中进行(最低位优 先法每次分配与收集都是相对于整个序列表而言的)。 例如还是对{50,123,543,187,49,30,0,2,11,100}使用最高位优先法进行排序,首先按照百位的不同进行分配,得 到的分配表为
由上图所示,整个无序表被分为了 3 个子序列,序列 1 和序列 2 中含有多个关键字,序列 3 中只包含了一个关键字,最高 位优先法完成排序的标准为:直到每个子序列中只有一个关键字为止,所以需要分别对两个子序列进行再分配,各自的分配表如 下图所示:
上表中,序列 1 中还有含有两个关键字的子序列,所以还需要根据个位进行分配,最终按照各子序列的顺序同样会得到一个有 序表。
动图:
时间复杂度:
最好情况O(nk)
最坏情况O(nk)
平均时间复杂度O(n*k)
空间复杂度:
空间复杂度(n+k)
算法特点:
1.是一种稳定的排序算法。
2.时间复杂度可以突破基数关键词比较一类方法的下界O(nlogn)达到O(n)
3.使用条件具有严格的要求。
9.总结
本篇博客转载C语言中文网
总结
以上是生活随笔为你收集整理的万字长文总结八大经典内部排序算法的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 一文搞定哈希(六种构建、四种冲突解决方法
- 下一篇: 计组第一章(唐朔飞)——计算机系统概述章