排序算法:插入、希尔、选择、推排、冒泡、快速、归并排序

news/2024/5/15 16:15:01

排序算法

目录

前言

一、排序的概念

1.1排序的概念

1.2 常见的排序算法

二、常见排序算法的实现

2.1 插入排序

2.2 希尔排序

2.3 选择排序

2.4 堆排序

2.5 冒泡排序

2.6 快速排序

2.6.1 hoare版本

2.6.2 前后指针版本

2.6.3 非递归版本

2.7 归并排序

归并排序

2.8 计数排序

三、算法性能

总结


前言

今天为我们来通过C语言来实现常见排序算法:直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、堆排序,了解算法的具体实现和算法的性能。


一、排序的概念

1.1排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排
序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 常见的排序算法

二、常见排序算法的实现

2.1 插入排序

思想:把第一个元素当成有序,然后把下一个元素插入到这个有序序列里,使得保持有序,重复步骤我们可以n-1个元素排成有序,把第n个元素插入到有序序列里。
步骤:1.先把第一个元素当成有序
2.取有序元素的最后下标end的下一个元素temp
3.将temp从最后一个往前比较(end--),比temp大则元素往后移动,小则插入到end的后面。
4.重复步骤2,3 循环n-1(n为数组长度)次

动画图解如下:

//插入排序
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; i++) {int end=i;int temp = a[end + 1];while (end >= 0){//比较与end位置元素大小if (temp < a[end]){a[end + 1] = a[end];end--;}//大于end位置元素,跳出else{break;}}//1.end大于零,temp放在end元素后一个//2.end小于零,temp也是放在end元素后,只不过此时为第一个元素a[end + 1] = temp;}
}

直接插入排序
时间复杂度:O(N^2)

空间复杂度:O(1)

稳定性:稳定
最坏情况:逆序
最好情况是多少:顺序有序或者接近有序  O(N)

2.2 希尔排序

思想:插入排序最好的情况是有序,我们可以在插入排序之前进行预排序,使得序列变得接近有序

,先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序,重复上述步骤。然后排序就变得简单了。

步骤:1.我们定义一个gap为每组元素的之间的距离。
2.把每个元素距离为gap的分为一组,进行插入排序
3.gap减小(通常除2),再进行步骤2
4.最后为gap减小到1,即每个元素各自为一组,即为插入排序
动画图解如下:
             
//希尔排序
void ShellSort(int* a, int n) {int gap=n;while (gap > 1) {gap/=2;//循环到n-gap-1元素for (int i = 0; i < n-gap; i++){int end = i;int temp = a[end + gap];while (end >= 0){if (temp < a[end]){a[end + gap] = a[end];//end移动距离为gapend -= gap;}else{break;}}//各自插入到每组元素a[end + gap] = temp;}}}
1. 希尔排序是对 直接插入排序的优化
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
希尔排序

时间复杂度:O(N^1.3)

空间复杂度:O(1)

稳定性:不稳定

2.3 选择排序

思路:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。同时我们还可以更加精简算法,再选出最小的同时选出最大的,把最小的放在最左边,最大的放在最右边。
步骤:1.从头到尾遍历序列找出最大最小的值
2.把最大最小值放在序列的尾和头
3.序列向中间收,头加1,尾减1,重复1,2步骤
只选最小的选择排序的图解
动画图解如下:
//选择排序
void SelectSort(int* a, int n) {int begin = 0, end = n - 1;while (begin < end){int maxi = begin, mini = begin;for (int i = begin + 1; i <= end; i++){if (a[i] < a[mini]){mini = i;}if (a[i] > a[maxi]){maxi = i;}}Swap(&a[begin], &a[mini]);//避免二次交换if (begin == maxi)maxi = mini;Swap(&a[end], &a[maxi]);//缩小范围begin++;end--;}}
直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
直接选择排序
时间复杂度: O(N^2)
空间复杂度: O(1)
稳定性:不稳定

2.4 堆排序

思路: 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过 来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
步骤:1.通过向下调整建堆( 排升序要建大堆,排降序建小堆)
2.交换第一个和最后一个元素
3.从零向下调整
4.从堆的长度减一,开始重复步骤2,3
图解:
//堆排序
//向下调整
void AdjustDown(int* a, int n, int parent) {int child = parent * 2 + 1;while (child < n){if (child + 1 < n && a[child + 1] > a[child]){child++;}if (a[child] > a[parent]){Swap(&a[parent], &a[child]);parent = child;child = 2 * parent + 1;}else{break;}}
}
void HeapSort(int* a, int n) {//建堆for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);end--;}}
堆排序使用堆来选数,效率就高了很多。
堆排序
时间复杂度: O(N*logN)
空间复杂度: O(1)
稳定性:不稳定

2.5 冒泡排序

思路:从头开始依次遍历整个序列,如果遇到大的就交换,最后我们就把大的交换到最后了。
步骤:1.遍历序列(遍历n-1次)
2.与下一个进行比较,比后一个大的就交换。
动画图解如下:
//冒泡排序
void BubbleSort(int* a, int n) {for (int i = 0; i < n - 1; i++){int exchang = 0;for (int j = 1; j < n - i; j++){//前一个比后一个大就交换if (a[j] < a[j - 1]){Swap(&a[j], &a[j - 1]);exchang = 1;}}if (exchang == 0)break;}
}

冒泡排序

时间复杂度: O(N^2)
空间复杂度: O(1)
稳定性:稳定

2.6 快速排序

思路:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

选择基准值有多种方法:

1.直接选择头或者尾为基准值(但是序列为有序或者接近有序时间复杂度大)

2.随机数取基准值

//随机取基准值
int randi = rand() % (right - left + 1);
randi += left;
Swap(&a[left], &a[randi]);//基准值放到序列头

3.三位数取中

取序列头尾和中间三个数,让不是最大的,也不是最小的作为基准值。

//三位数取中
int midi = GetMidi(a, left, right);//取中间值函数
Swap(&a[midi], &a[left]);//基准值放到序列头

将区间按照基准值划分为左右两半部分的常见方式有:

2.6.1 hoare版本

思路:选择key(基准值),从尾向头开始找比key小的,然后再从头找比key大的,交换两者,然后继续整个步骤,直到它们两个相遇,交换key位置到相遇位置处,这样key前面就全为比key小的,后面全为比key大的,此时key为正确位置,然后分割序列,分为key前面的和key后面,重复上面所有步骤。

我们这直接选择最左边为key

步骤:1.最左边为key,定义left,right代表key下一个位置,序列尾

           2.让right向前走,找到比key小的,找到了停止

           3.让left向后走,找到比key大的,找到了停止

           4.交换此时righ和left的位置上的元素,重复2,3步骤

           5.直到相遇,交换此时相遇位置和key的。

           6.分割序列,分为keyi前面的序列和keyi后面的位置,重复以上所有步骤。

动画图解如下:

//快速排序 hoare版本
void QuickSort1(int* a, int left, int right) {//当只有一个元素和为空序列返回空if (left >= right)return;int begin = left, end = right;//取序列开头为keyint keyi = left;while (left < right){    //找比key小的while (left < right && a[keyi] <= a[right]){right--;}//找比key大的while (left < right && a[keyi] >= a[left]){left++;}Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);keyi = left;//递归[begin,keyi-1] 和 [keyi+1,end]QuickSort1(a, begin, keyi - 1);QuickSort1(a, keyi+1, end);}

2.6.2 前后指针版本

思路:选择最左或者最右为key,定义两个指针prev和cur分别指向序列开头,prev下一个位置。然后向后走,如果遇到比key小的cur走,prev也向后走。直到遇到比key大的,只有cur走,prev不走,当再次遇到比key小的,prev先走,然后交换此时cur和prev的值,cur再走。重复上面步骤,直到cur走到序列尾,然后交换prev和key的值。通过上面操作我们可以让cur和prev中间的值为比key大的,此方法就是把比key大的往后推,小的往前移。

步骤:1.选择最左key,prev和cur分别指向序列开头,开头下一个

           2.cur先走,prev后走

           3.遇到比key大的,只有cur走

          4.再遇到key小的,prev先走,然后交换此时prev和cur的值,cur在走

          5.cur走到序列尾,交换prev和key的值

          6..分割序列,分为keyi前面的序列和keyi后面的位置,重复以上所有步骤。

动画图解如下:

//快速排序
//前后指针法
void QuickSort2(int* a, int left, int right) {if (left >= right)return;int prev = left, cur = left+1;int keyi = left;while (cur <= right){    //如果遇到比key小的才交换if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;//分割[left,keyi-1] [keyi+1,right]QuickSort2(a, left, keyi - 1);QuickSort2(a, keyi+1, right);}

2.6.3 非递归版本

思路:之前我们都是通过递归来解决的,我们需要的是排序的左右子区间,所以我们可以通过一个栈来存储排序的左右子空间,根据栈先进后出的特性。左右子区间依次进栈出栈从而达到递归的效果。

void QuickSortNonR(int* a, int left, int right) {ST st;StackInit(&st);//左右下标入栈//右边先进  左边后进StackPush(&st,right);StackPush(&st,left);while (!StackEmpty(&st)){left = StackTop(&st);StackPop(&st);right = StackTop(&st);StackPop(&st);//单趟int prev = left, cur = left + 1;int keyi = left;while (cur <= right){if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;//[left,key-1] && [key+1,right]//进左右子区间if (keyi + 1 < right){StackPush(&st, right);StackPush(&st, keyi + 1);}if (keyi - 1 > left){StackPush(&st, keyi - 1);StackPush(&st, left);}}StackDestroy(&st);}
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
快速排序
时间复杂度: O(N*logN)
空间复杂度: O(logN)
稳定性:不稳定

2.7 归并排序

思路:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

动画图解如下:
//归并排序
void _MergeSort(int* a, int begin, int end, int* temp) {//只有一个元素就返回if (begin == end)return;//从中间开始分割int mini = (begin + end) / 2;//指向两个序列起始位置int begin1 = begin, end1 = mini;int begin2 = mini + 1, end2 = end;_MergeSort(a, begin1, end1, temp);_MergeSort(a, begin2, end2, temp);//单趟//选下的插入到新的序列中int i = begin;//插入位置要保持相对位置while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]) {temp[i++] = a[begin1++];}else{temp[i++] = a[begin2++];}}//插入没有插入完的while (begin1 <= end1){temp[i++] = a[begin1++];}while (begin2 <= end2){temp[i++] = a[begin2++];}//拷贝位置也要保持相对位置memcpy(a + begin, temp + begin, sizeof(int) * (end - begin + 1));}void MergeSort(int* a, int n) {//开辟新的序列int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");return;}_MergeSort(a, 0, n-1, temp);free(temp);temp = NULL;}

非递归版本

我们之前是用递归先进行单个排序,在到组排。我们也可以不通过递归,通过回溯的思想,先定义一个gap,gap从1开始,即先单个为一组,然后两两排序,然后进行循环gap×2来进行gap数量为一组在进行归并。

void MergeSortNonR(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");return;}int gap = 1;while (gap < n) {for (int j = 0; j < n; j += gap * 2){//两个区间的起始位置int begin1 = j, end1 = j + gap - 1;int begin2 = begin1 + gap, end2 = begin2 + gap - 1;//判断是否有越界if (end1 >= n || begin2 >= n)break;if (end2 >= n)end2 = n - 1;int i = j;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){temp[i++] = a[begin1++];}else{temp[i++] = a[begin2++];}}while (begin1 <= end1){temp[i++] = a[begin1++];}while (begin2 <= end2){temp[i++] = a[begin2++];}//相对位置memcpy(a + j, temp + j, sizeof(int) * (end2 - j + 1));}gap *= 2;}free(temp);temp = NULL;
}

归并排序

时间复杂度:O(N*long N)

空间复杂度:O(N)

稳定性:稳定

2.8 计数排序

上面的排序,我们都是通过比较来排序的,而计数排序是非比较排序算法。
思路:遍历序列,找到最大最小的值,通过它们的差值来新开一个序列,在遍历序列,通过序列的值减去最小值作为下标在新开序列的位置计数。最后在遍历序列,该新序列有值则新序列的下标加最小值即为该序列上的值。
//计数排序
void CountSort(int* a, int n) {int max = a[0], min = a[0];for (int i = 0; i < n; i++){if (a[i] > max)max = a[i];if (a[i] < min)min = a[i];}int range = max - min + 1;int* count = (int*)malloc(sizeof(int) * range);if (count == NULL){perror("malloc fail");return;}memset(count, 0, sizeof(int) * range);//计数for (int i = 0; i < n; i++){count[a[i] - min]++;}//排序int j = 0;for (int i = 0; i < n; i++){while (count[i]--){a[i] = i + min;}}
}

计数排序

时间复杂度:O(N+range)
空间复杂度:O(range)

三、算法性能

算法性能再不同场景下有不一样的性能,下图参考:

总结

上述文章,我们介绍了各种排序算法,希望对你有所帮助。


http://www.mrgr.cn/p/66151351

相关文章

Module Federation of Micro-Frontends

一、什么是微前端 微前端是按照不同维度拆分成多个子应用,通过主应用加载子应用。微前端的概念最早由 thoughtworks 在 2016 年提出。其核心思路是借鉴后端微服务架构理念。Open image-20221108-012908.png 二、微前端解决哪些问题 1、不同团队,不同技术栈,可以同时开发一个…

Python生成GIF动图

菜鸟程序员带你揭秘python世界 GIF动图看起来是不是特别好看,其实制作的方法有很多,但今天,我们来用python编程来自己动手生成GIF动图 1、首先我们准备几张清晰的图片2、打开我们的编程工具,这里我使用的是pycharm,任意的python编辑器都可以,打开后,我们创建images目录,…

python自动化操作docx

使用Python自动化处理Word文档 在日常工作中&#xff0c;我们经常需要处理大量的Word文档&#xff0c;这时自动化脚本就显得尤为重要。本文将介绍如何使用Python中的python-docx库来创建和修改Word文档。 安装python-docx库 在开始之前&#xff0c;确保你已经安装了python-d…

vue开发环境搭建

安装nvm在工作中可能会遇到需要使用多个node版本的时候,nvm正为解决这个而生,NVM(Node Version Manager)是一个用于管理 Node.js 版本的工具。1、nvm换镜像源{安装地址}\settings.txt 中添加查看安装地址,where nvm#配置node镜像: node_mirror: https://npmmirror.com/mi…

IfcSIUnit

IfcSIUnit 实体定义IfcSIUnit既包括米和秒等标准基本国际单位制,也包括帕斯卡、平方米和立方米等衍生国际单位制。 注:定义依据ISO/CD 10303-41:1992国际单位制是用作标准的固定数量,根据ISO 1000(第2条)的定义,对项目进行测量。注:对应的ISO 10303名称:si-unit,正式标…

vue开发环境安装

安装nvm在工作中可能会遇到需要使用多个node版本的时候,nvm正为解决这个而生,NVM(Node Version Manager)是一个用于管理 Node.js 版本的工具。1、nvm换镜像源{安装地址}\settings.txt 中添加查看安装地址,where nvm配置node镜像: node_mirror: https://npmmirror.com/mir…

[图解]软件开发中的糊涂用语-04-为什么要追究糊涂用语

0 00:00:00,030 --> 00:00:05,620 今天呢&#xff0c;我们来说一个为什么要追究糊涂用语的问题 1 00:00:06,310 --> 00:00:06,548 2 00:00:06,548 --> 00:00:11,077 大家知道我们前些天都发了好几个视频 3 00:00:11,077 --> 00:00:13,461 追究这个糊涂用语 4 00…

CI/CD构建部署流程(bitbucket部分)

一、目前环境:lab 二、进入bitbucket的pipeline页面 三、查看CI构建流程详细信息 四、进入devops-pipeline-cd项目https://bitbucket.org/miktechnology/devops-pipeline-cd/pipelines/results/page/1Cant find link,查看CD部署日志 五、验证CI/CD构建部署流程是否成功 mi…

利用AI运动识别插件,可以实现那些应用场景?

「Ai运动识别」小程序插件已经推出一年有余,迭代了近十几个版本,收获了各类应用场景的众多用户,今天我们就带您深度解析一下插件的各类可应用场景,帮助已集成开发者进行一步拓宽应用场景,帮助有需求的开发者快速选型。 在解析应用场前,我们先来回顾一下插件的特点,插件旨…

mybatisPlus执行save方法获取自动填充的主键id

使用user1.getId(); 实测有效。 更多直接参考这篇文章:mybatis-plus中的save方法保存后会返回id吗 - CSDN文库

十大USDT交易平台大全XEX交易所

USDT是一种基于比特币区块链网络的加密代币&#xff0c;主要运用于数字货币交易平台&#xff0c;以稳定币为主。USDT的核心价值在于其与真实货币的固定兑换比率1:1&#xff0c;所以被称为Tether。随着加密货币市场的不断壮大&#xff0c;越来越多的交易平台开始支持USDT&#x…

流量特征分析-常见攻击事件 tomcat

简介 1、在web服务器上发现的可疑活动,流量分析会显示很多请求,这表明存在恶意的扫描行为,通过分析扫描的行为后提交攻击者IP flag格式:flag{ip},如:flag{127.0.0.1} 2、找到攻击者IP后请通过技术手段确定其所在地址 flag格式: flag{城市英文小写} 3、哪一个端口提供对web服…

如何准确的估计llm推理和微调的内存消耗

Command-R+, Mixtral-8x22b和Llama 3 70b都在最近的几周内发布了,这些模型是巨大的。它们都有超过700亿个参数: Command-R+: 104B参数 Mixtral-8x22b:具有141B参数的混合专家(MoE)模型 Llama 370b: 70.6B参数 你能在电脑上微调和运行这些模型吗? 在本文中,我将介绍如何计算…

流量特征分析-蚁剑流量分析

简介 1.木马的连接密码是多少 2.黑客执行的第一个命令是什么 3.黑客读取了哪个文件的内容,提交文件绝对路径 4.黑客上传了什么文件到服务器,提交文件名 5.黑客上传的文件内容是什么 6.黑客下载了哪个文件,提交文件绝对路径步骤#1.1步骤#1.2 黑客执行的第一个命令是什么,因为…

C#内存管理

前言 在职场中,确立自身的技术水平很重要,因为,如果你被标记成了技术菜鸟,那么你的工作一旦做快了,大家就会一致的认为这个任务比较简单;如果你未如期完成,则会被各种明嘲暗讽,你不但无法获得合理的表扬,还会无端被迫接受攻击。 但是,如果你被标记成了技术高手,那么…

权限维持-linux权限维持-隐藏

简介 ssh root@env.xj.edisec.net -p 密码 xjqxwcyc 1.黑客隐藏的隐藏的文件 完整路径md5 2.黑客隐藏的文件反弹shell的ip+端口 {ip:port} 3.黑客提权所用的命令 完整路径的md5 flag{md5} 4.黑客尝试注入恶意代码的工具完整路径md5 5.使用命令运行 ./x.xx 执行该文件 将查询…

【MySQL】SQL优化

SQL优化 插入数据 insert 一次插入数据和批量插入数据 insert into tb_test (id, name) values (1,Tom); insert into tb_test (id, name) values (1,Tom),(2,Jack),(3,Jerry);优化方案&#xff1a; 手动控制事务&#xff0c;且按主键顺序插入。start transaction; insert …

Prometheus源码解读系列 prometheus---告警处理源码剖析

Prometheus源码解读系列 prometheus---告警处理源码剖析一、Target数据采集scrape模块解读 1、scrape target业务流程框架1、由scrape.Manager管理所有的抓取对象; 2、所有的抓取对象按group分组,每个group是一个job_name; 3、每个group下含多个scrapeTarget,即具体的抓取目…

面试类 - Spring基础(三)

事务 Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的。Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。 23.Spring 事务的种类? 在 Spring 中,…

Os-ByteSec

主机发现1 netdiscover -i eth0 -r 192.168.170.0/24端口扫描1 nmap -sV 192.168.170.145访问80端口,提示使用smb攻击目录扫描,未发现有价值的信息1 dirb http://192.168.170.145/SMB渗透 SMB是一个网络协议名,它能被用于Web连接和客户端与服务器之间的信息沟通,它允许应用…