数据结构_手撕七大排序(快排,归并,堆排,希尔,选择,插入,冒泡)

✨✨所属专栏:数据结构✨✨

✨✨作者主页:嶔某✨✨

排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序

function/数据结构-排序算法 · 钦某/c-language-learning - 码云 - 开源中国 (gitee.com)

常见排序算法 

插入排序 

示意图:

 排序过程:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

 时间复杂度:

O(n^2) 

代码实现: 

//插入排序
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = a[end + 1];while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];end--;}elsebreak;}a[end + 1] = tmp;}
}

希尔排序

示意图:

排序过程:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

 随着增量因子(gap)的减少,这其实是一个使数组逐渐变有序的过程。那么有个问题,gap如何变化才能使算法得到最大的优化呢?

gap越大,大的数可以越快跳到后面,小的数可以越快跳到前面,越不接近有序;gap越小,跳的越慢,但是越接近有序。当 gap == 1时就相当于插入排序。

这个算法的发明者用的是gap /= 2,有人曾今算过(这里需要很高的数学水平,我还是算了)gap = gap / 3 + 1,为最优解。加一是为了让最后一趟排序的gap为1。

时间复杂度:

第一趟排序分为gap = n / 3组(忽略+1),每组三个数据,最坏情况就是逆序,向前调整(1+2)次,第一趟就的消耗是 n。第二趟gap = n / 9 组每组 9 个数据,最坏情况调整(1+2+......+7+8)次,第二趟的消耗是 4n。

但是,第二趟以后的每一趟都不是最坏的情况,所以第二趟的消耗到不了4n,具体是多少?这里需要加一些概率的公式

最后一趟gap = 1,直接就是插入排序,消耗是n,根据这个我们大概画出了下图。

 最后这个排序的时间复杂度大约为:n^(1.3)

代码实现: 

// 希尔排序
void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > a[end + gap]){a[end + gap] = a[end];end -= gap;}elsebreak;}a[end + gap] = tmp;}}
}

选择排序

示意图:

排序过程:

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

另外我们在遍历时可以同时将无序区中最大的数和最小的数的下标分别记录,将最大数放在无序区的末尾,将最小数放在无序区的开头。

注意这里有个坑 :当一趟排序记录的mini == end时,将max处的数交换到end处,这是end处的数就是最大的数了,之后再将mini处的数交换到start处(这时mini处的数是最大的),这样就没有完成任务。

解决办法是:在交换之前,做一个判断更新mini的值。

时间复杂度:

O(n^2) 

代码实现:

//选择排序
void SelectSort(int* a, int n)
{int start = 0;int end = n - 1;while (end > start){int mini = start;int maxi = end;for (int i = start; i <= end; i++){if (a[mini] > a[i])mini = i;if (a[maxi] < a[i])maxi = i;}if (mini == end){Swap(&a[maxi], &a[end]);mini = maxi;Swap(&a[mini], &a[start]);}else{Swap(&a[maxi], &a[end]);Swap(&a[mini], &a[start]);}end--;start++;}
}

堆排序

示意图:

排序过程:

这里参考下数据结构_堆的代码:function/数据结构-堆 · 钦某/c-language-learning - 码云 - 开源中国 (gitee.com)

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆(堆的元素个数减一),然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

时间复杂度:

O(n*log(n))

代码实现: 

void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){if (a[child] < a[child + 1] && child + 1 < n)child++;if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}elsebreak;}
}//堆排序
void HeapSort(int* a, int n)
{for (int i = (n - 2) / 2; i >= 0; i--)AdjustDown(a, n, i);for (int i = n - 1; i > 0; i--){Swap(&a[0], &a[i]);AdjustDown(a, i, 0);}
}

冒泡排序 

示意图:

排序过程:

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

时间复杂度:

O(n^2)

代码实现: 

//冒泡排序
void BubbleSort(int* a, int n)
{for (int i = n; i > 0; i--){int prev = 0;int cur = 1;int falg = 1;while (cur < i){if (a[prev] > a[cur]){falg = 0;Swap(&a[prev], &a[cur]);}prev = cur;cur++;}if (falg == 1)break;}
}

快速排序

示意图:

排序过程:

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

hoare: 

 单趟:我们定义一个key = left,右边先走,右边end--找小(比a[key]小),左边begin++找大,都找到了就交换,当begin > end跳出循环,再将a[key]和a[begin]交换。

为什么这里要右边先走?为什么相遇时位置比key小?

左边做key,右边先走,可以保证相遇位置比key小

场景:

L遇R:R先走,停下来,R停下条件是遇到比key小的值,一定比key小,L没有找大的,遇到R停下了

R遇L:R先走,找小,没有找到比key小的,直接跟L相遇了。L停留的位置是上一轮交换的位置,上一轮交换,把比key小的值换到L的位置了

相反:如果让右边做key,左边先走,i可以保证相遇位置比key要大

双指针:

单趟:定义一个prev,一个cur,cur找小,找不到小的prev和cur一起加加,找到了就把cur位置和prev++位置的值交换,prev和cur中间的值都是大的,将大的值一起往后挪。最后prev位置的值一定比key位置的值小,将prev和key位置的值交换。

每次递归分别传begin两侧的区间。

算法优化:

小区间优化

我们知道,递归定义的快排随着区间的越来越小,需要递归的次数越来越多,如果次数很多,每次调用函数都要压栈,有可能导致栈溢出。类似二叉树,最后一次递归占总递归次数的50%,倒数第二次占了25%,倒数第三次占12.5%。如果我们能将这部分递归占用缓解,就能使算法优化。所以当区间小于10的时候,我们调用选择排序。

void QuickSort(int* a, int left, int right)
{if (left >= right)return;if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);//小区间优化return;}int keyi = partsort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

三数取中

当数组已经有序,或者第一个数很小,或者很大时,时间复杂度就会退化为O(n^2),这时我们可以改变key值,从第一个值,中间的值和末尾值,三个值中选出中间的那个作为key值,保证每次的key都接近中间数。

int Midofthree(int* a,int x, int y, int z)
{if (a[x] > a[y])if (a[x] > a[z])if (a[y] > a[z])return y;elsereturn z;elsereturn y;else//a<bif (a[x] < a[z])if (a[y] < a[z])return y;else//b>creturn z;else//a>creturn x;
}

时间复杂度:

O(n*log(n))

代码实现:

(1)hoare版本:

//hoare版本
int partsort1(int* a, int left, int right)
{int begin = left, end = right;int x = Midofthree(a, left, right, (right + left) / 2);Swap(&a[x], &a[left]);int key = left;while (begin < end){while (begin < end){if (a[end] < a[key])break;end--;}while (begin < end){if (a[begin] > a[key])break;begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);return begin;
}

(2)双指针版本:

//双指针版本
int partsort2(int* a, int left, int right)
{int x = Midofthree(a, left, right, (right + left) / 2);Swap(&a[x], &a[left]);int keyi = left;int prev = left;int cur = prev + 1;while (cur <= right){if (a[cur] < a[keyi] && ++prev != cur)Swap(&a[cur], &a[prev]);cur++;}Swap(&a[prev],&a[keyi]);return prev;
}

 (3)挖坑版本:

//挖坑版本
int partsort3(int* a, int left, int right)
{int x = Midofthree(a, left, right, (right + left) / 2);Swap(&a[x], &a[left]);int key = a[left];int pit = left;int begin = left;int end = right;while (begin < end){while (a[end] >= key && begin < end)end--;a[pit] = a[end];pit = end;while (a[begin] <= key && begin < end)begin++;a[pit] = a[begin];pit = begin;}a[pit] = key;return pit;
}

递归:

void QuickSort(int* a, int left, int right)
{if (left >= right)return;if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);//小区间优化return;}int keyi = partsort1(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

非递归: 

这里不一定要用栈,也可以用队列来存储,如果你想,甚至可以用顺序表。栈只是为了更好的模拟递归的过程。

function/数据结构_栈 · 钦某/c-language-learning - 码云 - 开源中国 (gitee.com)

void QuickSortNonR(int* a, int left, int right)
{ST s;STInit(&s);STPush(&s, right);STPush(&s, left);while (!STEmpty(&s)){int begin = STTop(&s);STPop(&s);int end = STTop(&s);STPop(&s);int mid = partsort1(a, begin, end);if (mid + 1 < end)//注意这里需判断{STPush(&s, end);STPush(&s, mid + 1);}if (begin < mid - 1)//注意这里需判断{STPush(&s, mid - 1);STPush(&s, begin);}}
}

归并排序

示意图:

排序过程:

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

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

时间复杂度:

O(n*log(n))

代码实现: 

void _MergeSort(int* a, int* tmp, int left, int right)
{if (left >= right)return;int mid = (left + right) / 2;_MergeSort(a, tmp, left, mid);_MergeSort(a, tmp, mid + 1, right);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int i = left;while (begin1 <= end1 && begin2 <= end2){if(a[begin1] < a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc is fail");return;}_MergeSort(a, tmp, 0, n - 1);free(tmp);tmp = NULL;
}

未完…… 

总结: 

本期博客到这里就结束了,如果有什么错误,欢迎指出,如果对你有帮助,请点个赞,谢谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/22412.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

企业数据安全管理容易忽视的关键点:云存储权限管控

云存储已经广泛应用于企业用户、教育领域、医疗领域以及政府和公共服务部门。具体应用场景包括文件共享、数据备份、在线课程、教学资源库、电子病历、医学影像、实验室数据、政务数据的集中管理和共享等。 云存储的优势非常明显&#xff1a; 可扩展性&#xff1a;云存储空间可…

Nginx漏洞解析及复现

Nginx漏洞 Nginx能做到正向代理、反向代理、负载均衡、HTTP服务器等&#xff0c;强大的功能不言而喻&#xff0c;但也伴随着使用 上的风险&#xff0c;深入理解Nginx的漏洞有助于创建安全的业务系统。 Nginx解析漏洞 漏洞原理 Nginx的解析漏洞的出现和Nginx的版本没有关系&…

基于深度学习的中文标点预测模型-中文标点重建(Transformer模型)【已开源】

基于深度学习的中文标点预测模型-中文标点重建&#xff08;Transformer模型&#xff09;提供模型代码和训练好的模型 前言 目前以深度学习对文本自动添加标点符号研究很少&#xff0c;已知的开源项目并不多&#xff0c;详细的介绍就更少了&#xff0c;但对文本自动添加标点符号…

三菱MR-J4系列伺服驱动器E7.1和32.3故障报警处理总结

三菱MR-J4系列伺服驱动器E7.1和32.3故障报警处理总结 三菱MR-J4系列伺服驱动器出现报警,故障代码为:E7.1和32.3,查阅手册可以看到E7.1和32.3的报警解释信息, 如下图所示,此时简单运动控制模块上的ERROR灯亮, 如下图所示,用GX WORKS3打开备份程序,找到FX5-80SSC-…

3 - 大的国家(高频 SQL 50 题基础版)

3.大的国家 -- 查询属性&#xff1a;国家名称、人口和面积 select name,population,area fromWorld where area>3000000 OR population>25000000;

组件框架信息泄露

后端spring-boot框架 actuator组件信息泄露 Actuator是Spring-Boot提供的服务监控和管理中间件&#xff0c;默认配置会出现接口未授权 访问&#xff0c;部分接口会泄露网站流量信息和内存信息等&#xff0c;使用Jolokia库特性甚至可以远程执行任意代码&#xff0c;获 取服务器…

科技赋能,无障碍出行的新纪元

在现代社会&#xff0c;公共设施的建设不仅是衡量城市文明程度的标尺&#xff0c;更是实现社会公平与包容的重要载体。对于盲人群体而言&#xff0c;一个完善的公共设施网络&#xff0c;意味着他们能够更加独立、自信地融入社会&#xff0c;享受与视力健全者同等的公共服务与便…

使用onnxruntime加载YOLOv8生成的onnx文件进行目标检测

在网上下载了60多幅包含西瓜和冬瓜的图像组成melon数据集&#xff0c;使用 LabelMe 工具进行标注&#xff0c;然后使用 labelme2yolov8 脚本将json文件转换成YOLOv8支持的.txt文件&#xff0c;并自动生成YOLOv8支持的目录结构&#xff0c;包括melon.yaml文件&#xff0c;其内容…

干货!如何在Jmeter中实现对NCR响应的解析

最近做接口测试时发现了一个问题&#xff0c;部分请求的响应是通过NCR编码实现的&#xff0c;这样就导致了无法对这些请求进行断言&#xff0c;为了解决这个问题进行了如下调研&#xff0c;大家可以参考下面两篇文章&#xff1a; 使用Java apache commons包五分钟搞定NCR解析&…

CCIG 2024:大模型技术及其前沿应用论坛深度解析

一、CCIG论坛介绍 中国图象图形大会&#xff08;CCIG 2024&#xff09;是一场备受瞩目的学术盛会&#xff0c;近期在陕西省西安市曲江国际会议中心举行。这次会议以“图聚智生&#xff0c;象合慧成”为主题&#xff0c;由中国图象图形学学会主办&#xff0c;旨在汇聚图像图形领…

ABAP 长文本编辑器弹窗控件

前言 用户想在ALV上编辑长文本&#xff0c;但是ALV只有128个字符肯定是不够用的&#xff0c;所以需要用一个长文本编辑器来输入&#xff0c;本来想自己写的&#xff0c;发现有标准的函数&#xff0c;还挺好用的 代码 在用户双击ALV字段时&#xff0c;触发下述form&#xff0…

使用Rufus工具制作Ubuntu To Go——很详细

一、准备工作 准备工具&#xff1a; 1、下载Rufus(主角)软件 2、准备一个U盘或硬盘&#xff08;小白128G足够&#xff0c;装Ubuntu系统&#xff09; 3、下载Ubuntu系统镜像文件 1、下载软件Rufus 先来看一下官网介绍&#xff1a; Rufus 是一款格式化和创建 USB 启动盘的辅助工…

“GPT-4o深度解析:技术演进、能力评估与个人体验综述“

文章目录 每日一句正能量前言对比分析模型架构性能应用场景用户体验技术创新社区和生态系统总结 技术能力语言生成能力语言理解能力技术实现总结 个人感受关于GPT-4o的假设性观点&#xff1a;关于当前语言模型的一般性观点&#xff1a; 后记 每日一句正能量 又回到了原点&#…

“智能分析赋能等保:大数据技术在安全审计记录中的应用“

智能分析技术在信息安全领域&#xff0c;尤其是等保&#xff08;等级保护&#xff09;合规性方面&#xff0c;发挥了关键作用。特别是结合大数据技术&#xff0c;安全审计记录的处理和分析能力得到了显著增强。以下几点阐述了大数据技术是如何赋能等保安全审计的&#xff1a; …

【Python】 Python中的functools.wraps:装饰器的优雅包装

基本原理 在Python中&#xff0c;装饰器是一种非常强大的工具&#xff0c;它允许我们以一种非常灵活的方式修改或增强函数的行为。装饰器本质上是一个函数&#xff0c;它接收一个函数作为参数&#xff0c;并返回一个新的函数。然而&#xff0c;当我们使用装饰器时&#xff0c;…

【笔记】Sturctured Streaming笔记总结(Python版)

目录 相关资料 一、概述 1.1 基本概念 1.2 两种处理模型 &#xff08;1&#xff09;微批处理 &#xff08;2&#xff09;持续处理 1.3 Structured Streaming和Spark SQL、Spark Streaming关系 二、编写Structured Streaming程序的基本步骤 三、输入源 3.1 File源 &a…

Docker 基础使用 (1) 使用流程概览

文章目录 Docker 软件安装Docker 镜像仓库Docker 仓库指令Docker 镜像指令Docker 容器指令Docker 使用实例 —— 搭建 nginx 服务nginx 概念nginx 使用用 docker 启动 nginx Docker 基础使用&#xff08;0&#xff09;基础认识 Docker 基础使用 (1) 使用流程概览 Docker 基础使…

f-stack和DPDK

GPT-4 (OpenAI) f-stack和DPDK&#xff08;数据平面开发套件&#xff09;都是与高性能网络处理相关的技术。它们的目的是提高数据包的处理速度&#xff0c;优化网络I/O的性能。以下是对这两者的简要解释&#xff1a; 1. **DPDK (Data Plane Development Kit):** DPDK 是一个…

【软件安全国产化替代解决方案】亮相2024澳门万讯论坛

近日&#xff0c;2024万讯论坛在澳门成功举办。本次论坛由万讯电脑科技主办&#xff0c;旨在引进国内尖端科技厂商&#xff0c;提供全方位的信创解决方案&#xff0c;分享信创化过程中所面临的挑战及阶段性转换经验。开源网安作为拥有软件安全领域全链条产品的厂商&#xff0c;…

【网络安全的神秘世界】安装burpsuite

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 java下载地址 burpsuite安装包 安装java 双击打开java安装包——>下一步 在java文件夹下创建jdk和jre文件夹&#xff08;不一定是C盘&#xff0c;根…