数据结构进阶篇 之 【交换排序】(冒泡排序,快速排序递归、非递归实现)

在这里插入图片描述
当你觉的自己不行时,你就走到斑马线上,这样你就会成为一个行人

一、交换排序

1.冒泡排序 BubbleSort

1.1 基本思想

1.2 实现原理

1.3 代码实现

1.4 冒泡排序的特性总结

2.快速排序 QuickSort

2.1 基本思想

2.2 递归实现

2.2.1 hoare版
2.2.2 前后指针版本

2.3 快速排序优化

2.3.1 随机数选key
2.3.2 三数取中选key
2.3.3 递归到小的子区间使用插入排序

2.4 非递归实现

2.5 快速排序的特性总结

二、完结撒❀

前言:

所谓交换排序,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是(以升序为例):将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀-正文开始-❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–

一、冒泡排序

对于冒泡这个初学者必学的排序,我想大家应该都不陌生,现在我们以排升序为例再简单学习回顾一下,为下面快速排序做铺垫。

1.1 基本思想

冒泡排序是从头开始让两个相邻位置的数进行大小比较,不符合升序的将两者进行交换,再继续向后比较两个数据大小,反复执行一轮上述操作,便可以将数组中最大数据排到队尾,再从头进行一轮上述操作便可以将数组中第二大的数据排到数组中倒数第二个位置,反复执行n-1次即可完成排序(n为数据总个数)

1.2 实现原理

在数组arr[n]中,开始将数组arr[0]与arr[1]进行大小比较,若arr[1]<arr[0]就将两者数值进行交换后继续进行arr[1]与arr[2]的大小比较,若arr[1]>arr[0]就继续向后进行arr[1]与arr[2]的大小比较,直到比较到arr[n-1]为止,这时arr[n-1]便是数组中最大的值,再执行一轮上述操作便可以将数组中第二大的数值存放到arr[n-2]当中,反复执行n-1次便完成数组的总排序

动态图解:
在这里插入图片描述

1.3 代码实现

//冒泡排序
void BubbleSort(int* a, int n)
{assert(a);for (int t = 1; t < n; t++){int exchang = 0;//优化for (int i = 0; i < n - t; i++)//优化{if (a[i] > a[i + 1]){int tmp = a[i];a[i] = a[i + 1];a[i + 1] = tmp;exchang = 1;}}if (exchang == 0){break;}}
}

代码中对冒泡排序进行了两处优化,大家认真品味,加深理解。

1.4 冒泡排序的特性总结

1. 冒泡排序是一种非常容易理解的排序

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:稳定

2.快速排序

2.1 基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码(数组下标)将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

2.2 递归实现

2.2.1 Hoare版

Hoare版就是Hoare大佬自己创作的最原始的快速排序。

根据上述基本思想我们可以发现快速排序递归实现与二叉树的前序遍历规则非常像,在代码的实现中我们就可参照二叉树前序递归实现快速排序代码的递归框架。

我们先认真观察下面快速排序的动态图解:
在这里插入图片描述上面1动态图解表示的是快速排序的一次单趟过程:

以数组左边第一个值为key,R先向左走找小于key的数就停下来(5),接着L向右走找大于key的值就停下来(7),再将L和R对应位置数值进行交换,接着R继续向左走找小于key的值(4),找到后停下L向右走找大于key的值(9),之后停下再将两数值进行交换,继续重复上述操作直到L与R相遇,L与R相遇之后再将相遇下标对应的值与key下标对应的值进行交换,这样就完成了一次单趟快速排序,此时key的右边都为小于key的值,右边都是大于key的值。

快速排序单趟实现代码

void Swap(int* p, int* q)
{int tmp = *p;*p = *q;*q = tmp;
}//快速排序(单趟)
void PartSort(int* a, int left, int right)
{assert(a);int keyi = left;while (left < right){while (left<right && a[right] >= a[keyi]){--right;}while (left<right && a[left] <= a[keyi]){++left;}Swap(&a[right], &a[left]);}Swap(&a[left], &a[keyi]);keyi = left;
}

这里强调一下,一趟快速排序开始必须先让R往左边走,保证在最后L与R相遇时下标对应的值小于key下标对应的值

那么我们单趟排序便实现完成,下面就要开始展开研究递归问题。

递归就分为:1.递归子问题 2.最终子问题

我们对一串数组进行一次单趟的快速排序之后,数组并没有完全实现有序,还要“分割”key左右两边对其再分别进行快速排序,再将“分割”的一段再按照快排后key的位置在进行分割再进行单趟排序,直到分割为一个数据的时候就可以返回,也就是L = R,最后也可能为空

如图所示:
在这里插入图片描述
递归顺序我们就可以按照二叉树前序进行设计,所以代码实现如下:

//快速排序(霍尔递归)
void QuickSort1(int* a, int left, int right)
{assert(a);//递归子条件  区间只有一个值或者不存在if (left >= right){return;}int begin = left;int end = right;int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]){--right;}while (left < right && a[left] <= a[keyi]){++left;}Swap(&a[right], &a[left]);}Swap(&a[left], &a[keyi]);keyi = left;//区间划分[begin,key-1] [key] [key+1,end]QuickSort1(a, begin, keyi - 1);QuickSort1(a, keyi + 1, end);
}

我们可以试着用递归实现一组排序并画出递归展开图,有助于更深刻的理解递归。

2.2.2 前后指针版本

在Hoare大佬之后,有人在快速排序上增加其他实现玩法,前后指针实现快速排序就是其中u一种

大家可以先看一下下面递归图解进行学习:

在这里插入图片描述
可以看到,快慢指针最后也实现了key之前都小于key对应的值,key之后都大于key对应的值的效果

起初prev指向数组的开头,key也指向这个位置,cur指向prev前一个位置

判断cur指向的值是否大于key所指向的值

1.如果cur指向的值小于key指向的值,让prev加1向前移动一个位置,再交换cur和prev所指向的值,再将cur加1向前移动一个位置。
2.如果cur指向的值大于key指向的值,让cur+1向前移动一个位置。

持续循环判断上述逻辑,直到cur越界,最后再将prev和key所指向位置的对应值进行交换。

上面所讲的便是前后指针版实现一次单趟的整体逻辑。大家可以去尝试自行动手实现一下

前后指针单趟排完的效果与Hoare版的效果是一样的,所以我们只需要镶套上递归逻辑实现递归即可

代码如下:

//快速排序(双指针递归)
void QuickSort2(int* a, int left, int right)
{assert(a);if (left >= right)//递归终止条件{return;}int prev = left;int keyi = left;int cur = left + 1;//增加代码可读性int begin = left;int end = right;while (cur <= right){if (a[cur] < a[keyi] && ++prev != cur)//这里排除了当prev与cur位置相同时的情况,在同一位置时不用交换Swap(&a[prev], &a[cur]);++cur;}Swap(&a[keyi], &a[prev]);keyi = prev;QuickSort2(a, begin, keyi - 1);//leftQuickSort2(a, keyi + 1,end);//right
}

2.3 快速排序优化

这里我们先来计算一下快速排序的时间复杂度

对于上面所讲述的情况是以key最终取到的是中间位置然后在进行递归为例

相当于每次递归单趟排序只排好了数组中的一个数据

那么其时间复杂度就为O(N*logN)

在这里插入图片描述
而时间复杂度都是以最坏情况下计算的,那么快速排序在什么情况下是最坏的呢?

根据递归情况,当数组本身为有序的时候,对于快速排序来说的最坏的情况

在这里插入图片描述
此时每次都是以头部数据为key进行排序,那么递归深度为N,所以此时其时间复杂度为O(N^2)

有些同学可能就会好奇:“时间复杂度为N^2为什么效率会那么快”

因为每次进行排序,要排的数据本身就为有序概率是很低的,也可以说快速排序的适应性比较强,所以排序一般效率都非常快

但是有序出现的概率小并不代表不会出现,并且如果出现数据过多并且还是有序的情况下,不仅效率低下而且还可能会造成栈溢出,那么我应该怎么对其进行优化来解决这个问题呢?

2.3.1 随机数选key

上面所述问题的根本原因就是因为我们每次进行单趟排序的时候所选的key都是头部所指的位置数据,因为是有序数组,所以头部数据一定是数组中最小或最大的数据,这就造成遇到有序数据效率骤然下降

所以我们可以在每次递归都改变头部位置所指向的数据再进行单趟排序。

大家可以先看一下实现代码:

void Swap(int* p, int* q)
{int tmp = *p;*p = *q;*q = tmp;
}//快速排序(霍尔递归)
void QuickSort1(int* a, int left, int right)
{assert(a);//递归子条件  区间只有一个值或者不存在if (left >= right){return;}int begin = left;int end = right;//打破最坏情况有序---生成随机数int randi = rand() % (left - right);randi += left;Swap(&a[randi], &a[left]);int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]){--right;}while (left < right && a[left] <= a[keyi]){++left;}Swap(&a[right], &a[left]);}Swap(&a[left], &a[keyi]);keyi = left;//[begin,key-1] [key] [key+1,end]QuickSort1(a, begin, keyi - 1);QuickSort1(a, keyi + 1, end);
}

我们生成随机数来做为数组下标进行操作

那么生成的随机数肯定是需要在所要进行排序的数组范围内,我们将生成的随机数余上排序数组的队尾下标与队头下标之差,之后在加上对头下标,随机数的范围就一定是在所要排序的数组内。

再将随机数下标所对应的值与对头数据进行交换,那么key所对应的值就不是原来数组对头的值,这样就解决了有序数组的问题。

但这中也只是大大降低了在有序数组中key所对应的值为最值得概率,当随机数等于队尾与对头之差时余上便等于0,此时key指向得还是对头的值,所以有人觉得这种方法不妥,于是又有了下面一种方法。

2.3.2 三数取中选key

这里得三数是指队头数据,中间数据(队尾下标与对头下标之和除2)和队尾数据

在这三个数据中选出大小为中间的值做key,这样key对应的值就一定不是其数组中的最值了

实现代码:

int GetMidi(int* a,int left,int right)
{int midi = (left + right) / 2;if (a[midi] > a[left]){if (a[midi] < a[right]){return midi;}else //(a[midi]>a[right]){if (a[left] > a[right]){return left;}else{return right;}}}else //a[midi] < a[left]{if (a[midi] > a[right]){return midi;}else{if (a[left] > a[right]){return right;}else{return left;}}}
}//快速排序(霍尔递归)
void QuickSort1(int* a, int left, int right)
{assert(a);//递归子条件  区间只有一个值或者不存在if (left >= right){return;}int begin = left;int end = right;//三数取中int midi = GetMidi(a, left, right);Swap(&a[midi], &a[left]);int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]){--right;}while (left < right && a[left] <= a[keyi]){++left;}Swap(&a[right], &a[left]);}Swap(&a[left], &a[keyi]);keyi = left;//[begin,key-1] [key] [key+1,end]QuickSort1(a, begin, keyi - 1);QuickSort1(a, keyi + 1, end);}
}

这样就可以完全避免最坏情况的出现

2.3.3 递归到小的子区间使用插入排序

因为我们递归实现形成的是二叉树结构,而对于二叉树我们看下图:
在这里插入图片描述

二叉树递归最后一层递归开辟栈帧的空间次数是占总递归的50%,而倒数第二层占总空间的25%,所以最后的两次递归消耗都已经占总消耗的75%左右

而对于我们快排也是如此,在递归到最后两层只剩下部分数据需要排序的话我们可以不选择使用快排,我们可以选择使用插入排序来进行解决

代码实现

int GetMidi(int* a,int left,int right)
{int midi = (left + right) / 2;if (a[midi] > a[left]){if (a[midi] < a[right]){return midi;}else //(a[midi]>a[right]){if (a[left] > a[right]){return left;}else{return right;}}}else //a[midi] < a[left]{if (a[midi] > a[right]){return midi;}else{if (a[left] > a[right]){return right;}else{return left;}}}
}//快速排序(霍尔递归)
void QuickSort1(int* a, int left, int right)
{assert(a);//递归子条件  区间只有一个值或者不存在if (left >= right){return;}//小区间可以走插入,能减少90%的递归空间消耗if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);//a+left保证从每个递归区间进行排序}else{int begin = left;int end = right;//打破最坏情况有序---生成随机数/*int randi = rand() % (left - right);randi += left;Swap(&a[randi], &a[left]);*///三数取中int midi = GetMidi(a, left, right);Swap(&a[midi], &a[left]);int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]){--right;}while (left < right && a[left] <= a[keyi]){++left;}Swap(&a[right], &a[left]);}Swap(&a[left], &a[keyi]);keyi = left;//[begin,key-1] [key] [key+1,end]QuickSort1(a, begin, keyi - 1);QuickSort1(a, keyi + 1, end);}
}

这样就会减少大部分空间的开辟,减少消耗,减小空间复杂度

2.4 非递归实现

上面我们所讲述的都是利用递归进行实现快排的方法,那么大家可以想想如何不用递归来进行逻辑实现

非递归实现快排要用到栈来实现

我们想一想快排递归是如何实现,我们要怎样用栈来进行存储

递归中我们所传的参数有数组指针,队头下标和队尾下标,数组指针肯定不需要我们再往栈中进行存储,所以我们要想实现非递归我们就需要把每次递归所传的参数进行存储,将每次所传的参数进行单趟排序即可完成快速排序。

动态图解:

在这里插入图片描述
栈实现的是先进后出,上面动态图解中先往栈中入右子区间再入左区间那么实现逻辑就是先排左边再排右边,与二叉树前序遍历相符,当区间为1或是为空的时候便不在进行入栈操作最后实现效果与递归实现一样。

实现代码:

//利用栈 实现非递归快排
void QuickSortNonR(int* a, int left, int right)
{assert(a);ST st;STInit(&st);STPush(&st, right);STPush(&st, left);while (!STEmpty(&st)){int begin = STTop(&st);STPop(&st);int end = STTop(&st);STPop(&st);int prev = begin;int keyi = begin;int cur = begin + 1;//增加代码可读性while (cur <= end){if (a[cur] < a[keyi] && ++prev != cur)//在同一位置不用交换Swap(&a[prev], &a[cur]);++cur;}Swap(&a[keyi], &a[prev]);keyi = prev;//[begin]~[keyi-1] [keyi+1]~[end]if (begin < keyi - 1){STPush(&st, keyi - 1);STPush(&st,begin);}if (keyi + 1 < end){STPush(&st, end);STPush(&st, keyi+1);}}STDestory(&st);
}

上面代码中含有栈的函数,不知道的可以去我将的栈与队列的博客中进行复制学习:栈与队列

2.5 快速排序的特性总结

1.快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN)(优化后)
在这里插入图片描述
3. 空间复杂度:O(logN)

4. 稳定性:不稳定

二、完结撒❀

如果以上内容对你有帮助不妨点赞支持一下,以后还会分享更多编程知识,我们一起进步。
最后我想讲的是,据说点赞的都能找到漂亮女朋友❤
在这里插入图片描述

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

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

相关文章

NoSQL之Redis

目录 一、关系型数据库与非关系型数据库 1.关系数据库 2.非关系数据库 2.1非关系型数据库产生背景 3.关系型数据库与非关系型数据区别 &#xff08;1&#xff09;数据存储方式不同 &#xff08;2&#xff09;扩展方式不同 &#xff08;3&#xff09;对事物性的支持不同 …

微服务(基础篇-008-es、kibana安装)

目录 05-初识ES-安装es_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1LQ4y127n4?p81&vd_source60a35a11f813c6dff0b76089e5e138cc 1.部署单点es 1.1.创建网络 1.2.加载镜像 1.3.运行 2.部署kibana 2.1.部署 2.2.DevTools 3.安装IK分词器 3.1.在线安装ik…

装修避坑指南 | 定制家具你遇到过哪些坑?福州中宅装饰,福州装修

定制家具时可能会遇到以下一些常见问题&#xff1a; 尺寸不准确&#xff1a;由于定制家具需要按需定制&#xff0c;对尺寸的要求很高。如果尺寸不准确&#xff0c;很可能会导致安装困难或者家具不符合空间需求。 材料质量差&#xff1a;有些厂家可能会使用质量较差的材料来降…

[AutoSar]BSW_Memory_Stack_003 NVM与APP的显式和隐式同步

目录 关键词平台说明背景一、implicit synchronization1.1 Write requests 流程 (NvM_WriteBlock)1.2 Read requests 流程 (NvM_ReadBlock)1.3 Restore default requests 流程 (NvM_RestoreBlockDefaults)1.4 Multi block read requests 流程 (NvM_ReadAll)1.5 Multi block wri…

【Python系列】 yaml中写入数据

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

WEB漏洞-文件上传之基础及过滤方式

目录 案例1&#xff1a;百度搜索关键词&#xff0c;找到可能存在漏洞的网页 案例2&#xff1a;不同格式下的文件类型后门测试 案例3&#xff1a;配合解析漏洞下的文件类型后门测试 案例4&#xff1a;本地文件上传漏洞下的文件类型后门测试 案例5&#xff1a;某CVE漏洞利用…

MySQL索引原理

MySQL索引原理 1、Innodb中的B树是怎么产生的呢? 背景1.1、mysql索引使用B树&#xff0c;为什么&#xff1f;1.2、主键索引图示1.3、mysql最好使用自增ID&#xff1a;为什么呢&#xff1f;1.4、高度为3的B树能存多少条数据&#xff1f;a、假设2层b、假设3层 2、索引采用什么数…

从0到1构建uniapp应用-store状态管理

背景 在 UniApp的开发中&#xff0c;状态管理的目标是确保应用数据的一致性&#xff0c;提升用户体验&#xff0c;并简化开发者的工作流程。通过合理的状态管理&#xff0c;可以有效地处理用户交互、数据同步和界面更新等问题。 此文主要用store来管理用户的登陆信息。 重要…

Dubbo入门项目搭建【Dubbo3.2.9、Nacos2.3.0、SpringBoot 2.7.17、Dubbo-Admin 0.6.0】

B站学习视频 基于Dubbo3.2.9、Nacos2.3.0、SpringBoot 2.7.17、Dubbo-Admin 0.6.0、Jdk1.8 搭建的Dubbo学习Demo 一、前置安装 1-1、Nacos 安装 我本地是通过docker-compose来安装nacos的&#xff0c;如果需要其它方式安装可以去百度找下教程&#xff0c;版本是2.3.0的 docker…

新模因币MVP正在受到关注,预计将超越 SHIB 和 BONK

随着一种新的模因币TRUMP进入爆发式增长&#xff0c;加密市场开始对这种基于选举和权利的模因币充满了期待。并引发了人们对过去“玩笑式”模因币未来的疑问&#xff0c;因为当人们审视区块链与现实的意义时&#xff0c;发现&#xff08;SHIB) 和 Bonk (BONK) 等成熟模因币这样…

常见滤波算法(PythonC版本)

简介 受限于MCU自身的ADC外设缺陷&#xff0c;精度和稳定性通常较差&#xff0c;很多场景下需要用滤波算法进行补偿。滤波的主要目的是减少噪声与干扰对数据的影响&#xff0c;让数据更加接近真实值。 一阶低通滤波 一阶低通滤波是一种信号处理技术&#xff0c;用于去除信号中…

在project模式下自定义Implementation Strategies

Implementation Settings定义了默认选项&#xff0c;当要定义新的implementation runs时会使用这些选项&#xff0c;选项的值可以在Vivado IDE中进行配置。 图1展示了“Settings”对话框中的“implementation runs”对话框。要从Vivado IDE打开此对话框&#xff0c;请从主菜单中…

网络通信(一)

网络编程 互联网时代&#xff0c;现在基本上所有的程序都是网络程序&#xff0c;很少有单机版的程序了。 网络编程就是如何在程序中实现两台计算机的通信。 Python语言中&#xff0c;提供了大量的内置模块和第三方模块用于支持各种网络访问&#xff0c;而且Python语言在网络…

QT实现NTP功能

一.NTP基础 1.NTP定义 NTP&#xff08;Network Time Protocol&#xff0c;网络时间协议&#xff09;是由RFC 1305定义的时间同步协议&#xff0c;用于分布式设备&#xff08;比如电脑、手机、智能手表等&#xff09;进行时间同步&#xff0c;避免人工校时的繁琐和由此引入的误…

Web日志/招聘网站/电商大数据项目样例【实时/离线】

Web服务器日志分析项目 业务分析 业务背景 ​ 某大型电商公司&#xff0c;产生原始数据日志某小时达4千五万条&#xff0c;一天日志量月4亿两千万条。 主机规划 &#xff08;可略&#xff09;日志格式&#xff1a; 2017-06-1900:26:36101.200.190.54 GET /sys/ashx/ConfigH…

预处理指令详解

前言 上一节我们了解了文件操作的相关内容&#xff0c;本节我们来了解一下预处理指令&#xff0c;那么废话不多说&#xff0c;我们正式开始今天的学习 预定义符号 在C语言中&#xff0c;设置了一些预定义的符号&#xff0c;可以供我们直接使用&#xff0c;预定义符号是在程序…

一场人生的风险控制,商业社会识人指南

一、资料前言 本套社会识人资料&#xff0c;大小679.94M&#xff0c;共有37个文件。 二、资料目录 识人的终极目的&#xff1a;一整场人生的风险控制.pdf 信任的搭建&#xff1a;更多的时间与维度.pdf 没有搞不定的人&#xff01;角色人格与全面人格.pdf 政治不正确的正确…

程序员为什么不能一次性写好,需要一直改Bug?

程序员为什么不能一次性写好&#xff0c;需要一直改Bug&#xff1f; 我有一问&#xff1a; 你为什么不上清华呢&#xff0c;高考答满分不就行了&#xff1f; 程序员在软件开发过程中可能会遇到需要不断修改Bug的情况&#xff0c;这主要是由以下几个原因造成的&#xff1a; 复杂…

Linux简单介绍

Linux简单介绍 编译器VMware虚拟机Ubuntu——LinuxOS为什么使用LinuxOS&#xff1f; 目录结构Windows目录结构Linux操作系统home是不是家目录&#xff1f; Linux常用命令终端命令行提示符与权限切换命令tab 作用&#xff1a;自动补全上下箭头pwd命令ls命令mkdir命令touch命令rm…

智能革命:ChatGPT3.5与GPT4.0的融合,携手DALL·E 3和Midjourney开启艺术新纪元

迷图网(kk.zlrxjh.top)是一个融合了顶尖人工智能技术的多功能助手&#xff0c;集成了ChatGPT3.5、GPT4.0、DALLE 3和Midjourney等多种智能系统&#xff0c;为用户提供了丰富的体验。以下是对这些技术的概述&#xff1a; ChatGPT3.5是由OpenAI开发的一个自然语言处理模型&#x…