【数据结构与算法篇】手撕八大排序算法之交换排序

在这里插入图片描述

​👻内容专栏: 《数据结构与算法篇》
🐨本文概括:常见交换排序包括冒泡排序与快速排序,本篇讲述冒泡排序与快速排序的思想及实现、复杂度分析。
🐼本文作者: 花 蝶
🐸发布时间:2023.8.27

一、冒泡排序

基本思想

冒泡排序(Bubble Sort)是一种简单的排序算法,其基本思想是通过两两交换相邻元素的位置,使得较大(或较小)的元素逐步“冒泡”到数组的一端(顶部或底部),重复“冒泡”的过程,直到序列没有要交换的元素为止,从而实现排序。

​算法步骤

1、 从数组的起始位置开始,依次比较相邻的两个元素。如果相邻元素的顺序错误(前者大于后者,或者前者小于后者,取决于是升序还是降序排序),则交换这两个元素的位置;
2、继续进行相邻元素的比较和交换,直到遍历到数组的末尾。这样一轮下来,最大(或最小)的元素就会“冒泡”到数组的一端。
3、重复上述步骤,但不再考虑已经排序好的末尾部分。每一轮操作都会将一个最大(或最小)的元素移到正确的位置。
4、 经过多轮的比较和交换,最终整个数组会变得有序。如果在某一轮没有进行任何交换操作时,说明数组已经有序,可以直接跳出循环。

动图演示

在这里插入图片描述

代码实现

//冒泡排序
//时间复杂度:O(N^2)
void BubbleSort(int* a, int n)
{//控制冒泡排序的趟数for(int i = 0; i < n - 1; i++){//假设数组有序int flag = 1;//控制每趟的过程for (int j = 0; j < n - 1 - i; j++){if (a[j] > a[j + 1]){Swap(&a[j], &a[j + 1]);flag = 0;}}//说明没有发生交换,数组已经有序,跳出循环即可if (flag == 1) break;}
}

冒泡排序的特性

冒泡排序的核心思想就是通过反复比较相邻元素并交换它们的位置,从而逐步将最大(或最小)的元素移动到合适的位置。尽管冒泡排序的时间复杂度较高(平均和最坏情况下都是 O(N^2),可以看作是一个等差数列的求和),但它的实现相对简单,适用于小规模的数据集,因此具有较强的教学意义,想必大家在刚开始学习编程时学的一个排序就是冒泡排序吧哈哈。

二、快速排序(递归版本)

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

//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{if (begin >= end) return;int keyi = Partition(a, begin, end); //前序遍历//递归基准值左边的区间QuickSort(a, begin, keyi - 1);//递归基准值右边的区间QuickSort(a, keyi + 1, end);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,大家在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后续只需分析如何按照基准值来对区间中数据进行划分的方式即可。
下面用三个版本实现:hoare版本、挖坑法、前后指针法

1.hoare版本

1、选择基准元素key,通常是数组中的第一个元素。
2、使用两个指针LR,一个指向数组的开头(较小值的区域),一个指向数组的末尾(较大值的区域)。
3、交换的目标是L找到比基准大的元素,R找到比基准小的元素。不断交换指针所指的元素,直到两个指针相遇。
4、当两个指针相遇时,交换基准元素与当前相遇位置的元素,此时数组被划分为左右两个子数组。
5、对左右两个子数组递归地应用相同的分区步骤,直到所有子数组都有序

算法分析

👇①:原始序列
在这里插入图片描述
👇②:right向前移动,找比key小的位置,left往后移动找比key大的位置,然后交换。
在这里插入图片描述
👇③:继续往后寻找,直到两个指针相遇。
在这里插入图片描述
👇④:交换基准元素与当前相遇位置的元素。
在这里插入图片描述
👇⑤:这样子我们发现基准值左边的区间里面的元素都比基准值小,右边的区间里面的元素都比基准值大。我们可以按照前序遍历的思想对左边区间进行递归操作,右边区间也进行递归操作。

代码实现

//hoare版本
int Partition1(int* a, int left, int right)
{int keyi = left;while (left < right){//注意: 要加上left < right 否则会出现越界//若不判断等于基准值,也会出现死循环的情况//右边找小while (left < right && a[right] >= a[keyi]){right--;}//左边找大while (left < right && a[left] <= a[keyi]){left++;}Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);return left;
}//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{//当区间不存在或者只剩一个元素,就回溯if (begin >= end) return;int keyi = Partition1(a, begin, end);//前序遍历//递归基准值左边的区间QuickSort(a, begin, keyi - 1);//递归基准值右边的区间QuickSort(a, keyi + 1, end);
}

2.挖坑法

挖坑法的基本思想是:

  • 1、首先,选择一个基准元素(通常是数组中的第一个元素)作为“坑”(hole)。前提需要将这个基准元素用一个临时变量key存起来。
  • 2、将基准元素挖出,形成一个空位(坑)。
  • 3、从数组的另一端开始,从右向左遍历,找到一个比基准元素小的元素,然后将这个元素填入之前的坑中。
  • 4、继续从左向右遍历,找到一个比基准元素大的元素,然后将这个元素填入上一步的坑中。
  • 5、重复执行步骤 3 和步骤 4,直到左右指针相遇。
  • 6、此时,基准元素的位置就是这个相遇的位置,将基准元素填入这个坑中。

算法分析

👇①:原始序列
在这里插入图片描述
👇②:将基准值存放到临时变量key中,形成一个坑位。
在这里插入图片描述
👇③:从右向左遍历,R找到比基准值小的元素,将这个元素填入到之前的坑中,此时当前位置就形成了新坑。
在这里插入图片描述
👇④:紧接着,从左往右遍历,L寻找比基准值大的元素,将这个元素填入到上一步的坑中,此时当前位置形成了新坑。
在这里插入图片描述
👇⑤:重复以上步骤后,直到左右指针相遇,最后会形成一个坑,将临时变量放入坑中。
在这里插入图片描述
👇⑥:这样子我们发现基准值左边的区间里面的元素都比基准值小,右边的区间里面的元素都比基准值大。我们可以按照前序遍历的思想对左边区间进行递归操作,右边区间也进行递归操作。

代码实现

//挖坑法
int Partition2(int* a, int left, int right)
{int key = a[left];//挖坑int hole = left;while (left < right){//右边找小while (left < right && a[right] >= key){right--;}a[hole] = a[right];hole = right;//左边找大while (left < right && a[left] <= key){left++;}a[hole] = a[left];hole = left;}a[hole] = key;return hole;
}
//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{//当区间不存在或者只剩一个元素,就回溯if (begin >= end) return;int keyi = Partition2(a, begin, end);//前序遍历//递归基准值左边的区间QuickSort(a, begin, keyi - 1);//递归基准值右边的区间QuickSort(a, keyi + 1, end);
}

3.前后指针法

基本思想:在快速排序的划分过程中,使用前后指针法来确定基准元素的位置,最后将数组划分成两部分,一部分小于基准元素,另一部分大于基准元素。

  • 1、首先,选择一个基准元素key(通常是数组中的第一个元素)。
  • 2、使用两个指针,prev指向数组的开头,cur指向prev的后一个位置。
  • 3、判断cur指向的数据是否小于key。若小于,则prev后移一位,cur指向的内容与prev指向的内容交换,然后cur++
  • 4、若cur指向的数据大于key,则cur++
  • 重复步骤3和步骤4,直到cur指针走完(最后一个元素的下一个位置)
  • 最后,将keyprev指向的元素交换。

动图演示

在这里插入图片描述

//前后指针法
int Partition3(int* a, int left, int right)
{int prev = left;int cur = prev + 1;int keyi = left;//cur小于key,交换++prev与cur的位置//将大的位置翻滚时往后挪动,小的位置移动前面while (cur <= right){if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}//将key与prev指向的元素交换。Swap(&a[prev], &a[keyi]);return prev;
}//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{//当区间不存在或者只剩一个元素,就回溯if (begin >= end) return;int keyi = Partition3(a, begin, end);//前序遍历//递归基准值左边的区间QuickSort(a, begin, keyi - 1);//递归基准值右边的区间QuickSort(a, keyi + 1, end);
}

快速排序(递归版本)的特性

时间复杂度

选择基准元素影响时间复杂度:若基准元素key偏向于所有元素中的中位数,则时间复杂度处于较好的情况,若基准元素key是所有元素中最小的,或者接近最小值,那么时间复杂度就处于较坏的情况。

  • 平均情况下的时间复杂度: 在平均情况下,快速排序的时间复杂度为 O(n log n)。这是因为在每次分区过程中,数组会被划分成大致相等的两部分。在每次递归中,都会将问题的规模减半,递归的展开图可以看作是一颗满二叉树,所以递归的深度为 O(log n),每层递归的分区操作都需要 O(n) 的时间,因此总的时间复杂度为 O(n*log n)
  • 最坏情况下的时间复杂度: 在最坏情况下,快速排序的时间复杂度为 O(n^2)。这种情况发生在每次划分后,一个子数组为空,另一个子数组包含所有的元素。这样会导致递归树变得很不平衡,每次递归的问题规模只减少一个元素。虽然快速排序通常能够避免这种情况,但在某些情况下(例如已经有序的数组),最坏情况可能出现。

在这里插入图片描述

解决方案

  • 随机数选择基准元素: 在每次划分时,随机选择一个基准元素,这可以减少最坏情况的发生概率。
  • 三数取中法: 在选择基准元素时,不仅考虑第一个和最后一个元素,还考虑数组中间位置的元素,选择其中值大小居中的元素作为基准。

👇这里提供一种三数取中的方法,以hoare版本为例:

//三数取中,返回下标
int GetMidIndex(int* a, int left, int right)
{int mid = (left + right) >> 1;if (a[left] < a[mid]){if (a[mid] < a[right]) return mid;else if (a[left] > a[right]) return left;else return mid;}else //a[left] > a[mid]{if (a[mid] > a[right]) return mid;else if (a[right] > a[left]) return left;else return right;}
}
//hoare版本
int Partition1(int* a, int left, int right)
{//将中位数与数组的第一个元素(基准值)进行交换int midi = GetMidIndex(a, left, right);Swap(&a[left], &a[midi]);int keyi = left;while (left < right){//右边找小while (left < right && a[right] >= a[keyi]){right--;}//左边找大while (left < right && a[left] <= a[keyi]){left++;}Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);return left;
}

后面会更新快排的非递归版本……可到数据结构专栏查看🥰
更多数据结构与算法系列文章👉😉==> 【传送门】

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

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

相关文章

Mysql的page,索引,Explain Type等基本常识

Mysql的基本问题 Mysql 为什么建议使用自增id&#xff1f; 因为id&#xff08;主键&#xff09;是自增的话&#xff0c;那么在有序的保存用户数据到页中的时候&#xff0c;可以天然的保存&#xff0c;并且是在聚集索引&#xff08;id&#xff09;中的叶子节点可以很好的减少插…

Django报错:SystemCheckError: System check identified some issues解决办法

今天练习django自定义标签时&#xff0c;一开始在APPbook中写了自定义标签book_tags.py 测试成功&#xff0c;之后新建了一个APPblogs&#xff0c;测试在blogs中创建模板使用自定义标签&#xff0c;于是直接把book/templatetags包直接赋值到blogs目录里。在页面里加载自定义标…

84. 柱状图中最大的矩形

84. 柱状图中最大的矩形 C代码&#xff1a;暴力&#xff1a;遍历所有可能 int largestRectangleArea(int* heights, int heightsSize){// 直接遍历所有可能&#xff1a;超出时间限制int ans 0;for (int l 0; l < heightsSize; l) {int minHeight INT_MAX;for (int r l…

OpenCV(八):图像二值化

目录 1.固定值二值化 2.自适应阈值二值化 3.Android JNI完整代码 1.固定值二值化 固定阈值二值化是OpenCV中一种简单而常用的图像处理技术&#xff0c;用于将图像转换为二值图像。在固定阈值二值化中&#xff0c;像素值根据一个预定义的阈值进行分类&#xff0c;大于阈值的…

Redis—常用数据结构

Redis—常用数据结构 &#x1f50e;数据结构与内部编码 Redis 中常用的数据结构包括 Strings—字符串Hashes—哈希表Lists—列表Sets—集合Sorted sets—有序集合 Redis 底层在实现上述数据结构时, 会在源码层面针对上述实现进行特定优化, 以达到节省时间 / 节省空间的效果 …

Vue基础2:传值方法

Description 传值就是为了联动&#xff0c;能够及时准确传值获取值才是王道。 Valuation Methods props和$emit props是父传子&#xff0c;$emit是子传父。 props的使用 父组件传出值 <tableList ref"table" :options"options" :header-data"C…

Android Glide preload RecyclerView切入后台不可见再切换可见只加载当前视野可见区域item图片,Kotlin

Android Glide preload RecyclerView切入后台不可见再切换可见只加载当前视野可见区域item图片&#xff0c;Kotlin <uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name"android.permission.RE…

合宙Air724UG LuatOS-Air LVGL API控件--图表 (Chart)

图表 (Chart) 一幅图胜过一千个字&#xff0c;通过图表展示出的数据内容能让用户更快速有效的了解数据特征。 代码示例 – 创建图表 chart lvgl.chart_create(lvgl.scr_act(), nil) lvgl.obj_set_size(chart, 200, 150) lvgl.obj_align(chart, nil, lvgl.ALIGN_CENTER, 0, …

docker 笔记1

目录 1.为什么有docker ? 2.Docker 的核心概念 3.容器与虚拟机比较 3.1传统的虚拟化技术 3.2容器技术 3.3Docker容器的有什么作用&#xff1f; 3.4应用案例 4. docker 安装下载 4.1CentOS Docker 安装 4.2 Docker的基本组成 &#xff1f;&#xff08;面试&#xff09…

OpenShift 4 - 用 Prometheus 和 Grafana 监视用户应用定制的观测指标(视频)

《OpenShift / RHEL / DevSecOps 汇总目录》 说明&#xff1a;本文已经在 OpenShift 4.13 的环境中验证 文章目录 OpenShift 的监控功能构成部署被监控应用用 OpenShift 内置功能监控应用用 Grafana 监控应用安装 Grafana 运行环境配置 Grafana 数据源定制监控 Dashboard 演示视…

剑指 Offer 57 - II. 和为s的连续正数序列(简单)

题目&#xff1a; class Solution { public:vector<vector<int>> findContinuousSequence(int target) { //本题使用滑动窗口&#xff08;双指针&#xff09;int i1, j1; //定义左右边界&#xff0c;一般是左闭右开int sum0; //窗口内的和vector&…

弹性盒子的使用

一、定义 弹性盒子是一种用于按照布局元素的一维布局方法&#xff0c;它可以简便、完整、响应式地实现各种页面布局。 容器中存在两条轴&#xff0c;主轴和交叉轴(相当于我们坐标轴的x轴和y轴)。我们可以通过flex-direction来决定主轴的方向。 主轴&#xff08;main axis&am…

设计模式行为模式-命令模式

文章目录 前言定义结构工作原理优点适用场景消息队列模式Demo实现分写业务总结 前言 定义 命令模式&#xff08;Command Pattern&#xff09;是一种行为型设计模式&#xff0c;用于将请求封装为对象&#xff0c;从而使你可以使用不同的请求、队列或者日志请求来参数化其他对象…

MySQL高阶语句(三)

一、NULL值 在 SQL 语句使用过程中&#xff0c;经常会碰到 NULL 这几个字符。通常使用 NULL 来表示缺失 的值&#xff0c;也就是在表中该字段是没有值的。如果在创建表时&#xff0c;限制某些字段不为空&#xff0c;则可以使用 NOT NULL 关键字&#xff0c;不使用则默认可以为空…

zookeeper 集群

zookeeper 集群 1、zookeeper 集群说明 initLimit 是Zookeeper用它来限定集群中的Zookeeper服务器连接到Leader的时限 syncLimit 限制了follower服务器与leader服务器之间请求和应答之间的时限 服务器名称与地址&#xff1a;集群信息&#xff08;服务器编号&#xff0c;服务器…

kubesphere安装Maven+JDK17 流水线打包

kubesphere 3.4.0版本&#xff0c;默认支持的jav版本是8和11&#xff0c;不支持17 。需要我们自己定义JenKins Agent 。方法如下&#xff1a; 一、构建镜像 1、我们需要从Jenkins Agent的github仓库拉取master最新源码&#xff0c;最新源码里已经支持jdk17了。 git clone ht…

代码随想录算法训练营第39天 | ● 62.不同路径 ● 63. 不同路径II

文章目录 前言一、62.不同路径二、63.不同路径II总结 前言 动态规划 一、62.不同路径 深搜动态规划数论 深搜&#xff1a; 注意题目中说机器人每次只能向下或者向右移动一步&#xff0c;那么其实机器人走过的路径可以抽象为一棵二叉树&#xff0c;而叶子节点就是终点&#…

JS算法与树(二)

前言 二叉搜索树&#xff08;BST&#xff09;存在一个问题&#xff1a;当你添加的节点数够多的时候&#xff0c;树的一边可能会非常的深。而其他的分支却只有几层。 AVL树 为了解决上面的问题&#xff0c;我们提出一种自平衡二叉搜索树。意思是任何一个节点左右两侧子树的高度之…

多级缓存 架构设计

说在前面 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;很多小伙伴拿到一线互联网企业如阿里、网易、有赞、希音、百度、网易、滴滴的面试资格&#xff0c;多次遇到一个很重要的面试题&#xff1a; 20w的QPS的场景下&#xff0c;服务端架构应如何设计&#xff1f;10w的QPS…

python爬虫-Selenium

一、Selenium简介 Selenium是一个用于Web应用程序测试的工具&#xff0c;Selenium 测试直接运行在浏览器中&#xff0c;就像真正的用户在操作一样。模拟浏览器功能&#xff0c;自动执行网页中的js代码&#xff0c;实现动态加载。 二、环境配置 1、查看本机电脑谷歌浏览器的版…