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

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

一、交换排序

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.rhkb.cn/news/300851.html

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

相关文章

vitepress系列-04-规整sideBar左侧菜单导航

规整左侧菜单导航 新建navConfig.ts 文件用来管理左侧导航菜单&#xff1a; 将于其他的配置分开&#xff0c;避免config.mts太大 在config目录下&#xff0c;新建 sidebarModules文件目录用来左侧导航菜单 按模块进行分类&#xff1a; 在config下新建sidebarConfig.ts文件&…

【引子】C++从介绍到HelloWorld

C从介绍到HelloWorld 一、C的介绍1. 简介2. 应用场景3. C的标准![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e3efb0f207f647729b92c0b5bcd4b330.png)4. C的运行过程 二、Visual Studio的安装1. 什么是Visual Studio2. Visual Studio的安装 三、完成HelloWorld1.…

白色磨砂质感html5页源码

白色磨砂质感html5页源码&#xff0c;简约的基础上加上了团队成员&#xff0c;自动打字特效音乐播放器存活时间 源码下载 https://www.qqmu.com/2980.html

书籍《笔记的方法》读后感

读完《笔记的方法》有几周的时间&#xff0c;书里有些记录的内容&#xff0c;觉得非常有价值的&#xff0c;自己的观点&#xff0c;当下读书&#xff0c;其实并没有那么高大尚&#xff0c;就是存粹陶冶下情操&#xff0c;读书还是有一定作用的&#xff0c;毕竟看书只能慢慢来&a…

软件设计师:11-结构化开发与UML

结构化开发&#xff08;3-4分&#xff09; 一、模块化 二、耦合&#xff08;背&#xff09; 三、内聚&#xff08;背&#xff09; 四、设计原则&#xff08;背&#xff09; 五、系统文档 六、数据流图 数据流的起点或终点必须有一个是加工 判断依据&#xff1a; 1、…

Python | NCL风格 | EOF | 相关 | 回归

这里在linux系统上使用geocat实现NCL风格的图片绘制 Linux上安装 geocat conda update conda conda create -n geocat -c conda-forge geocat-viz conda activate geocat conda update geocat-vizDataset - NOAA Optimum Interpolation (OI) SST V2 # 海温月平均数据 - lsmas…

内容创作策略:打造影响力强大的技术博客

CSDN的朋友你们好&#xff0c;我是未来&#xff0c;今天给大家带来专栏【程序员博主教程&#xff08;完全指南&#xff09;】的第6篇文章——“博客内容创作策略”。本文为技术博主提供了一个精简的内容创作策略指南&#xff0c;涵盖了设定目标、分析竞争、关键词研究、内容规划…

24 个Intellij IDEA好用插件

24 个Intellij IDEA好用插件 一. 安装插件 Codota 代码智能提示插件 只要打出首字母就能联想出一整条语句&#xff0c;这也太智能了&#xff0c;还显示了每条语句使用频率。 原因是它学习了我的项目代码&#xff0c;总结出了我的代码偏好。 Key Promoter X 快捷键提示插件 …

vscode 安装vim插件配置ctrl + c/v功能

搜索Vim插件 插件介绍部分有提示操作 首先安装该插件&#xff0c;然后按照下述步骤设置ctrl相关的快捷键&#xff0c;以便于脱离im快捷键而愉快的敲代码。 1.在“设置”搜索框内搜索vim.handleKeys&#xff0c;选择 Edit in settings.json 2. 设置ctrl-c,ctrl-v等快捷键置为fa…

室友打团太吵?一条命令让它卡死

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;更多干货&#xff0c;请关注专栏《网络安全自学教程》 SYN Flood 1、hping3实现SYN Flood1.1、主机探测1.2、扫描端…

Unity类银河恶魔城学习记录12-7-1 p129 Craft UI - part 1源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili UI_CraftList.cs using System.Collections; using System.Collections.Gen…

zdpdjango_argonadmin使用Django开发一个美观的后台管理系统

初始代码 安装依赖 pip install -r requirements.txt生成管理员账户 迁移模型&#xff1a; python manage.py makemigrations python manage.py migrate创建超级用户&#xff1a; python manage.py createsuperuser启动服务 python manage.py runserver浏览器访问&#xf…

2024新版PHP在线客服系统多商户AI智能在线客服系统源码机器人自动回复即时通讯聊天系统源码PC+H5

搭建环境&#xff1a; 服务器 CPU 2核心 ↑ 运存 2G ↑ 宽带 5M ↑ 服务器操作系统 Linux Centos7.6-7.9 ↑ 运行环境&#xff1a; 宝塔面板 Nginx1.18- 1.22 PHP 7.1-7.3 MYSQL 5.6 -5.7 朵米客服系统是一款全功能的客户服务解决方案&#xff0c;提供多渠道支持…

计算机组成原理(超详解!!) 第四节 存储系统和结构

1.存储器概述 1.存储器分类 存储器&#xff1a;用来存储程序和数据的记忆设备。 存储介质&#xff1a;具有两种明显区别且稳定的物理状态&#xff0c;在外界的作用下&#xff0c;能够相互转化&#xff1b;一种稳定状态表示“0”&#xff0c;则另一种状态表示“1”。目前主要…

es6新增set、map两种数据结构(超级详细-附加代码)

文章目录 一、Set增删改查add()delete()has()clear()遍历 二、Map增删改查sizeset()get()has()delete()clear()遍历 三、WeakSet 和 WeakMapWeakSetWeakMap 参考文献 如果要用一句来描述&#xff0c;我们可以说 Set是一种叫做集合的数据结构&#xff0c;Map是一种叫做字典的数…

数据结构——链表

目录 一、链表 1、单向链表 单向链表的遍历方式&#xff1a; 2、循环链表 3、双向链表 二、自行车停放&#xff08;双向链表&#xff09; 一、链表 链表是由许多相同数据类型的数据项按特定顺序排列而成的线性表特性&#xff1a;存放的位置是不连续且随机的&#xff0c;动…

全坚固笔记本丨工业笔记本丨三防笔记本相较于普通笔记本有哪些优势?

三防笔记本和普通笔记本在设计和性能方面存在显著差异&#xff0c;三防笔记本相较于普通笔记本具备以下优势&#xff1a; 三防笔记本通常采用耐磨、耐摔的材料&#xff0c;并具有坚固的外壳设计&#xff0c;能够承受恶劣环境和意外碰撞&#xff0c;有效保护内部组件不受损坏。相…

armlinux裸机-uart

uart是一对一的串行异步全双工通信通信协议&#xff0c;串行速度较慢&#xff08;usart支持同步通信&#xff09; 传输原理 多个参数可以设置 为满足使用需求&#xff0c;我们一般都用带fifo缓冲中断。 我们使用S3C2440芯片&#xff0c;具体寄存器操作可以查看用户手册

NOI - OpenJudge - 2.5基本算法之搜索 - 1490:A Knight‘s Journey - 超详解析(含AC代码)

点赞关注吧~ 1490:A Knights Journey 查看提交统计提问 总时间限制: 1000ms 内存限制: 65536kB 描述 Background The knight is getting bored of seeing the same black and white squares again and again and has decided to make a journey around the world. When…

【WEEK6】 【DAY7】MD5 Encryption Transactions【English Version】

2024.4.7 Sunday Following the previous article 【WEEK6】 【DAY3】MySQL Functions【English Version】 Contents 5.3. MD5 Encryption5.3.1. Introduction5.3.2. Testing MD5 Encryption5.3.2.1. Plain Text Passwords5.3.2.2. Implementing Data Encryption5.3.2.3. Encry…