交换排序----快速排序
快速排序
快速排序是一种高效的排序算法,它采用分治法策略,将数组分为较小和较大的两个子数组,然后递归排序两个子数组。
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序递归实现的主框架与二叉树前序遍历规则非常像,我们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。而将区间按照基准值划分为左右两半部分的常见方式有:hoare版本、挖坑法、前后指针版本。
hoare版本
讲解
接下来我们一起看一下hoare版本的快速排序是如何实现的:
选数组左端点为基准值(key)初始位置。
用双指针从两端向中间移动(这里需要注意,如果选数组左端的值为key,则右指针先走;如果选数组右端的值为key,则左指针先走。)。左指针遇到大于或等于key的值就停,右指针遇到小于或等于key的值就停。停后交换两指针所指元素,继续移动,直到相遇:
这里补充一个结论:left和right相遇的值一定是小于等于key的。为什么呢?这里有三种情况:①当right向左移动找小于或等于key的值,left向右移动找大于或等于key的值却没找到,而这时left和right相遇了,此时相遇的位置肯定比key小或相等;②当right向左移动找小于或等于key的值,但是没找到就直接遇到left,那这里的left肯定就在key值的位置上,那它们就在key值相遇,这就是等于key值的情况;③right向左移动时找到了小于或等于key的值,left向右移动占到了大于或等于key的值,left和right位置上的值交换,然后right继续向左移动找小于或等于key的值,但是right已经找不到小于或等于key的值了,那它就会与left相遇,此时left上的值是刚刚从right位置上交换过来的小于或等于key的值,所以他们还是在小于或等于key上的值相遇。
指针相遇后,交换key与相遇位置值。
key的索引要更新到相遇点,接下来准备对key左右子数组分别进行重复的操作。
我们发现,整体看每次对key左右子数组的排序很像二叉树结构,所以我们可以通过递归完成对子区间数据的排序。
代码
void swap(int* x, int* y) {int tmp = *x;*x = *y;*y = tmp;
}int hoare(int* a, int left, int right)
{int keyIndex = left;while (left < right){while (left < right && a[keyIndex] <= a[right]){--right;}while (left < right && a[keyIndex] >= a[left]){++left;}swap(&a[left], &a[right]);}swap(&a[keyIndex], &a[right]);keyIndex = right;return keyIndex;
}void quickSort(int* a, int left, int right)
{if (left >= right)return;int begin = left, end = right;int keyIndex = hoare(a, left, right);quickSort(a, begin, keyIndex - 1);quickSort(a, keyIndex + 1, end);}
在代码中有两个点要注意:hoare函数里的最内层的两个while循环中必须要有left<right这个条件,不然会越界;while (left < right && a[keyIndex] <= a[right])不能写成while (left < right && a[keyIndex] < a[right]),while (left < right && a[keyIndex] >= a[left])不能写成 while (left < right && a[keyIndex] > a[left]),不然出现与key一样的值会进入死循环。
挖坑法
接下来,我们再看一下挖坑法的快速排序是如何实现的:
讲解
选数组左端点为key,用双指针准备从两端向中间移动。(选数组左端的值为key,则还是右指针先走。)
双指针移动,右指针遇小于或等于key值的填左坑,左指针遇大于或等于key值的填右坑,直到两个指针相遇。
指针相遇,key值填入相遇的位置。(key填入相遇的位置后别忘了更新key的索引位置)
接下来准备对key左右子数组分别进行重复的操作,与hoare的做法是一样的,进行递归排序。
代码
void swap(int* x, int* y) {int tmp = *x;*x = *y;*y = tmp;
}int hole(int* a, int left, int right)
{int keyIndex = left;int key = a[keyIndex];while (left < right){while (left < right && a[right] >= key) {--right;}a[left] = a[right];while (left < right && a[left] <= key) {++left;}a[right] = a[left];}a[right] = key;keyIndex = right;return keyIndex;
}void quickSort(int* a, int left, int right)
{if (left >= right)return;int begin = left, end = right;int keyIndex = hoare(a, left, right);quickSort(a, begin, keyIndex - 1);quickSort(a, keyIndex + 1, end);}
快慢指针法
讲解
接下来,我们再看一下快慢指针法的快速排序是如何实现的:
初始化数组左端元素为key,pre(慢指针)和cur(快指针)都向右移动。
遍历数组,当 cur 指向元素小于key时,pre向前移动一个位置,交换 cur 和 pre 位置的值(pre和cur所在的位置如果相同,就不用交换);当cur指向元素大于或等于key,pre保持不动,cur继续往前走。
遍历结束后,交换 pre 位置的元素和key元素,然后更新key索引。
接下来准备对key左右子数组分别进行重复的操作,与hoare和挖坑法的做法是一样的,进行递归排序。
代码
void swap(int* x, int* y) {int tmp = *x;*x = *y;*y = tmp;
}int pre_cur(int* a, int left, int right)
{int keyIndex = left, pre = left, cur = left + 1;while (cur <= right){while (a[cur] < a[keyIndex] && ++pre != cur) {swap(&a[cur], &a[pre]);}++cur;}swap(&a[pre], &a[keyIndex]);keyIndex = pre;return keyIndex;
}void quickSort(int* a, int left, int right)
{if (left >= right)return;int begin = left, end = right;int keyIndex = pre_cur(a, left, right);quickSort(a, begin, keyIndex - 1);quickSort(a, keyIndex + 1, end);}
快排的时间复杂度与优化
我们前面讲了快速排序的三种写法,但其实三种写法的时间复杂度都差不多,只是第二种写法更好理解,第三种写法代码量少且不容易写错(因为在hoare和挖坑法版本的写法中,最内层的两个while循环有些细节问题需要注意)。所以总体来说更推荐第三中写法。
时间复杂度
在探讨快速排序算法时,我们注意到其递归展开图与二叉树结构有相似之处。为了深入理解其时间复杂度和最坏情况,我们可以从以下几个方面进行阐述:
1.时间复杂度分析
快速排序通过递归将数组不断划分,其递归深度与二叉树的深度相似。在理想情况下,递归深度最多为 logn,其中 n 是数组的长度。这是因为每次递归都将数组大致均匀地划分为两个子数组。
2.每层递归的工作量
在快速排序的每一层递归中,需要遍历当前子数组以定位key的最终位置。尽管随着递归的深入,之前的key元素不再需要考虑,但这对整体工作量的影响是有限的:
为了简化分析,我们可以假设每层递归都需要处理 n 个元素(尽管实际上这个数字会逐层减小,但减小的数量相对于 n 来说是可以忽略的)。
3.总体时间复杂度
结合上述两点,快速排序的总体时间复杂度为 O(n log n)。这是因为每层递归需要处理 n 个元素,而递归深度最多为 logn。
最坏情况分析
1.最坏情况的发生:
快速排序的最坏情况发生在数组已经接近排序完成(即顺序或逆序)时,或者key选择不当(如总是选择数组的第一个元素,而该元素恰好是最小值或最大值)时。
2.时间复杂度与栈溢出:
在最坏情况下,快速排序的时间复杂度可能达到 O(n²)。这是因为每次划分都极不均匀,导致递归深度接近 n,从而引发大量的递归调用和栈空间消耗,容易发生栈溢出。
3.key位置的影响:
key的位置对快速排序的性能至关重要。如果key总是选择接近数组的一端(如最小值或最大值),则划分将极不均匀,导致递归深度增加。
相反,如果key能够接近数组的中间位置,则划分将更加均匀,递归深度将更接近 logn,从而提高算法的效率。
对最坏情况的优化
当枢纽每次都接近数组的中间位置时,快速排序的递归展开图将更接近满二叉树。满二叉树的深度均匀,且每个节点都有两个子节点,这有助于减少递归调用的次数和栈空间的使用。因此,选择好的key策略(如随机选择、三数取中法等)对于提高快速排序的性能至关重要。
1.随机选择法
我们不一定让区间内第一个值作为key,我们每次在区间内随机选一个值做key。虽然随机选择的情况下也有可能选到当前区间内的最小值或最大值,但是那是一个低概率事件,大多数情况下还是不会取到区间内的最值。
代码:
void quickSort(int* a, int left, int right)
{if (left >= right)return;int begin = left, end = right;//随机选keyint randIndex = left + (rand() % (right - left));swap(&a[left], &a[randIndex]);int keyIndex = pre_cur(a, left, right);if ((right - left + 1) > 10) {quickSort(a, begin, keyIndex - 1);quickSort(a, keyIndex + 1, end);}else {insertSort(a, right);}
}
2.三数取中
三数取中,将区间内的left、right和mid位置的值比较大小,取中间值。这种方法是一定不会取到区间内的最值,所以更推荐这种写法。
代码:
int getMidIndex(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[left] < a[mid]) {if (a[mid] <= a[right]) {return mid;}else if (a[right] >= a[left]) {return right;}else {return left;}}else {if (a[left] <= a[right]) {return left;}else if (a[right] >= a[mid]) {return right;}else {return mid;}}
}void quickSort(int* a, int left, int right)
{if (left >= right)return;int begin = left, end = right;//三数取中int midIndex = getMidIndex(a, left, right);swap(&a[left], &a[midIndex]);int keyIndex = pre_cur(a, left, right);if ((right - left + 1) > 10) {quickSort(a, begin, keyIndex - 1);quickSort(a, keyIndex + 1, end);}else {insertSort(a, right);}
}
小区间优化
在快速排序算法中,当处理小规模数据时,递归调用可能会变得低效。为了优化这种情况,我们可以引入小区间优化策略。
背景
1.递归调用的低效性:在快速排序中,当数组被划分成较小的子数组时,递归调用可能会变得频繁且低效。特别是当子数组的长度非常小(如只有几个元素)时,递归调用的开销可能会超过直接排序这些元素所需的开销。
2.插入排序的优势:插入排序在处理小规模数据时通常表现较好。当数据已经部分有序或数据量很小时,插入排序的效率往往高于快速排序的递归调用。
原理
1.设定阈值:为了避免在小规模数据上进行不必要的递归调用,我们可以设定一个阈值(通常取10左右)。当子数组的长度小于这个阈值时,我们不再使用快速排序的递归调用,而是改用插入排序来排序这些元素。
2.优化二叉树结构:在快速排序的递归过程中,二叉树的深度和形状对算法的效率有很大影响。当子数组长度较小时,递归调用会生成大量的浅层节点,这些节点的处理开销较大。通过设定阈值并使用插入排序,我们可以减少这些浅层节点的数量,从而优化二叉树的结构,提高算法的整体效率。
3.利用插入排序的优势:插入排序在处理小规模或部分有序数据时非常高效。当子数组长度小于阈值时,这些子数组很可能已经部分有序(特别是在快速排序的后期阶段),因此插入排序能够更快地完成排序任务。
为什么阈值取10左右?
经验值:通过大量的实验和观察,人们发现当子数组长度小于10时,使用插入排序通常比继续递归调用快速排序更高效。这个阈值是一个经验值,但在大多数情况下都能取得良好的效果。
平衡效率与开销:选择10作为阈值是为了在效率(即排序速度)和开销(即递归调用的额外负担)之间找到一个平衡点。当子数组长度小于10时,递归调用的开销开始变得显著,而插入排序则能够更高效地处理这些小规模数据。
优化二叉树深层节点:通过设定阈值并使用插入排序,我们可以优化二叉树中深层节点的处理。这些节点通常对应着较小的子数组,使用插入排序能够减少不必要的递归调用,从而提高算法的整体效率。
代码
void quickSort(int* a, int left, int right)
{if (left >= right)return;int begin = left, end = right;//随机选key/*int randIndex = left + (rand() % (right - left));swap(&a[left], &a[randIndex]);*///三路取中int midIndex = getMidIndex(a, left, right);swap(&a[left], &a[midIndex]);int keyIndex = pre_cur(a, left, right);if ((right - left + 1) > 10) {quickSort(a, begin, keyIndex - 1);quickSort(a, keyIndex + 1, end);}else {//小区间优化insertSort(a, right);}
}
非递归版本
递归版本的快速排序有一个问题:递归的深度太深容易栈溢出。
思路
非递归版本的快速排序需要借助栈来实现。其实在递归版本的快速排序中,每一次递归中变化的主要是区间的大小,所以我们可以在每次单趟排序完后,让对应区间的子区间入栈。
具体步骤
1.初始化栈并推入初始索引
创建栈,将数组的起始和结束索引入栈:
2.开始循环处理栈顶元素
弹出栈顶的左右索引,确定当前子数组的范围。
这里需要注意,在入栈的时候,如果先入栈的是end,然后再入栈begin,那先出栈的就是begin,后出栈的就是end。(先进后出)
调用划分函数(即前面讲hoare版本的hoare函数、挖坑法的hole函数、快慢指针的pre_cur函数),获取key元素的位置:
根据key元素位置,将未排序的子数组索引范围入栈:
(如果子区间只有一个值或者没有值就不入栈)
重复2的步骤,直至栈为空,则排序完成。
3.最后销毁栈。
代码
void quickSortNonOrder(int* a, int n)
{st s;initStack(&s);int begin = 0, end = n - 1;pushStack(&s, end);pushStack(&s, begin);while (!isEmpty(&s)){int left = stackTop(&s);popStack(&s);int right = stackTop(&s);popStack(&s);int keyIndex = pre_cur(a, left, right);if (keyIndex - 1 > left) //左子区间的右端索引大于左端索引说明这个子区间不止一个值,就可以入栈{pushStack(&s, keyIndex - 1);pushStack(&s, left);}if (keyIndex + 1 < right) //右子区间的右端索引大于左端索引说明这个子区间不止一个值,就可以入栈{pushStack(&s, right);pushStack(&s, keyIndex + 1);}}destroyStack(&s);
}
这里的栈会用到前面讲解栈的代码:数据结构--栈和队列-CSDN博客
相关文章:

交换排序----快速排序
快速排序 快速排序是一种高效的排序算法,它采用分治法策略,将数组分为较小和较大的两个子数组,然后递归排序两个子数组。 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序…...

ES 与 MySQL 在较大数据量下查询性能对比
在进行数据查询性能测试的过程中,我的同事幺加明对 ES(Elasticsearch)和 MySQL 进行了相对较大数据量的测试,并整理了相关结果。在得到其授权的情况下,我将此对比案例分享给大家,在此再次向幺加明表示感谢。…...

C# 新语法中的字符串内插$和{}符号用法详解
自C#6.0开始提供一个新的语法糖,即"$" 符号,配合“{}”使用,它的作用除了是对String.format的简化,还可设置其格式模板,实现了对字符串的拼接优化。 语法格式: $"string {变量表达式}” 语…...

Nacos源码学习-本地环境搭建
本文主要记录如何在本地搭建Nacos调试环境来进一步学习其源码,如果你也刚好刷到这篇文章,希望对你有所帮助。 1、本地环境准备 Maven: 3.5.4 Java: 1.8 开发工具:idea 版本控制工具: git 2、下载源码 官方仓库地址 :https://git…...

windows 好工具
Windows文件夹目录大小分析工具WizTree...

计算机运行时提示错误弹窗“由于找不到 quazip.dll,无法继续执行代码。”是什么原因?“quazip.dll文件缺失”要怎么解决?
计算机运行时错误解析:解决“quazip.dll缺失”问题指南 在软件开发和日常计算机使用中,我们经常会遇到各种运行时错误。今天,我们将深入探讨一个常见的错误提示:“由于找不到quazip.dll,无法继续执行代码。”这一弹窗…...

创造未来:The Sandbox 创作者训练营如何赋能全球创造者
创作者训练营让创造者有能力打造下一代数字体验。通过促进合作和提供尖端工具,The Sandbox 计划确保今天的元宇宙是由一个个创造者共同打造。 2024 年 5 月,The Sandbox 推出了「创作者训练营」系列,旨在重新定义数字创作。「创作者训练营」系…...

R语言对简·奥斯汀作品中人物对话的情感分析
项目背景 客户是一家文学研究机构,他们希望通过对简奥斯汀作品中人物对话的情感分析,深入了解作品中人物的情感变化和故事情节的发展。因此,他们委托你进行一项情感分析项目,利用“janeaustenr”包中的数据集来构建情感分析模型。…...

股指期货基差为正数,这是啥意思?
在股指期货的世界里,有个挺重要的概念叫“基差”。说白了,基差就是股指期货的价格和它对应的现货价格之间的差价。今天,咱们就来聊聊当这个基差为正数时,到底意味着啥。 基差是啥? 先复习一下,基差 股指…...

黑马程序员MybatisPlus/Docker相关内容
Day01 MP相关知识 1. mp配置类: 2.条件构造器: 具体的实现例子: ①QuerryWapper: ②LambdaQueryWrapper: 3.MP的自定义SQL 4.MP的Service层的实现 5.IService下的Lambda查询 原SQL语句的写法: Lambda 查询语句的…...

使用 Vue 和 Canvas-Confetti 实现烟花动画特效
在开发中,为用户提供具有视觉冲击力的反馈是一种提升用户体验的好方法。今天,我们将结合 Vue 框架、canvas-confetti 和 Lottie 动画,创建一个动态对话框动画,其中包含炫酷的烟花特效。 效果图: 效果简介 当用户触发…...

【银河麒麟操作系统真实案例分享】内存黑洞导致服务器卡死分析全过程
了解更多银河麒麟操作系统全新产品,请点击访问 麒麟软件产品专区:https://product.kylinos.cn 开发者专区:https://developer.kylinos.cn 文档中心:https://documentkylinos.cn 现象描述 机房显示器连接服务器后黑屏ÿ…...

如何加强游戏安全,防止定制外挂影响游戏公平性
在现如今的游戏环境中,外挂始终是一个困扰玩家和开发者的问题。尤其是定制挂(Customized Cheats),它不仅复杂且隐蔽,更能针对性地绕过传统的反作弊系统,对游戏安全带来极大威胁。定制挂通常是根据玩家的需求…...

SpringBoot整合knife4j,以及会遇到的一些bug
这篇文章主要讲解了“Spring Boot集成接口管理工具Knife4j怎么用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Spring Boot集成接口管理工具Knife4j怎么用”吧! 一…...

城电科技|光伏廊道是什么?安装光伏廊道有什么好处?
光伏廊道是什么?光伏廊道专门设计用于集中安装太阳能光伏发电系统的建筑物或构筑物,它可以将光伏转换成可以用于供电的清洁绿电。光伏廊道通常由阳能电池板、太阳能电池、控制器、逆变器、混凝土、钢材等材料组成,具备发电、坚固、耐用、防水…...

当DHCP服务器分配了同一个IP地址
当DHCP服务器分配了同一个IP地址给多个设备时,这通常会导致网络问题,如IP地址冲突,进而影响设备的网络连接。以下是详细的分析和解决步骤: 原因分析: IP地址租约未过期: 租约管理:DHCP服务器维…...

储能能量自动化调配装置功能介绍
随着可再生能源的快速发展,光伏发电已成为全球能源结构转型的关键技术之一。与此同时,储能技术作为实现光伏发电稳定输出的核心技术,得到了广泛关注。在企业电网中,光伏储能系统的运维管理不仅关乎能源利用效率,还涉及…...

vite5+vue3+Ts5 开源图片预览器上线
images-viewer-vue3:一款Vue3的轻量级图像查看器,它基于Flip动画技术,支持PC和h5移动网页预览照片,如果它是Vue3开发的产品。 npm开源地址:https://www.npmjs.com/package/images-viewer-vue3?activeTabreadme Flip 动画 < …...

【深度学习】深入解析长短期记忆网络(LSTMs)
长短期记忆网络(Long Short-Term Memory networks, LSTMs)是一种特殊的递归神经网络(RNN),专门设计用来解决标准 RNN 在处理长序列数据时的梯度消失和梯度爆炸问题。LSTMs 在许多序列数据任务中表现出色,如…...

从Web3到智能合约:探索新一代数据交互模式
随着互联网技术的不断演进,Web3的到来标志着互联网的一个新纪元。与传统的Web2相比,Web3倡导去中心化、更加开放和透明的网络架构,而智能合约则是其中的核心技术之一。本文将介绍Web3与智能合约的概念、应用以及它们如何改变数据交互模式&…...

排查bug的通用思路
⭐️前言⭐️ APP点击某个按钮没有反应/PC端执行某个操作后,响应较慢,通用的问题排查方法: 从多个角度来排查问题 🍉欢迎点赞 👍 收藏 ⭐留言评论 🍉博主将持续更新学习记录收获,友友们有任何问题可以在评…...

如何利用Python爬虫获得商品类目
在当今数字化时代,获取和分析数据的能力对于任何希望在市场上保持竞争力的企业来说都是至关重要的。对于电子商务平台和市场研究公司而言,获取商品类目数据尤为重要,因为这些数据可以帮助他们更好地理解市场趋势、优化产品目录并制定有效的营…...

如何通过 Windows 自带的启动管理功能优化电脑启动程序
在日常使用电脑的过程中,您可能注意到开机后某些程序会自动运行。这些程序被称为“自启动”或“启动项”,它们可以在系统启动时自动加载并开始运行,有时甚至在后台默默工作。虽然一些启动项可能是必要的(如杀毒软件)&a…...

大模型学习有什么发展前景?
前景人工智能大模型是指拥有超大规模参数(通常在十亿个以上)、复杂计算结构的机器学习模型。它通常能够处理海量数据,完成各种复杂任务,如自然语言处理、图像识别等。 2024年政府工作报告提出“发展新质生产力”,并将…...

Excel技巧:如何批量调整excel表格中的图片?
插入到excel表格中的图片大小不一,如何做到每张图片都完美的与单元格大小相同?并且能够根据单元格来改变大小?今天分享,excel表格里的图片如何批量调整大小。 方法如下: 点击表格中的一个图片,然后按住Ct…...

独著与编著的区别是?
独著和编著主要有以下区别: 一、创作性质 - 独著 - 独著是作者完全独立进行创作的作品。其内容是作者自己的研究成果、观点见解或者经验总结。作者从最初的选题构思,到资料收集、分析研究,再到内容撰写、修改润色等全过程都是独立完成的。…...

vue中pdf.js的使用,包括pdf显示,跳转指定页面,高亮关键词
目录 一、下载pdf.js 二、引入到本地的项目中 三、实现预览pdf 四、跳转到指定页面 五、利用pdf里面的find查找关键词 六、修改页面大小为实际大小 一、下载pdf.js https://github.com/mozilla/pdf.js 里面有很多的版本, 高版本的可能浏览器不兼容或者还要考…...

【Spring Boot】自动装配机制详解
1. 传统的 Spring 注入方式(基于 XML 配置) 在传统的 Spring 中,依赖注入(DI)通常通过 XML 配置文件来进行管理。常见的方式有两种: 通过 <property> 元素进行属性注入: <bean id&qu…...

Flink集群搭建整合Yarn运行
Flink 集群 1. 服务器规划 服务器h1、h4、h5 2. StandAlone 模式(不推荐) 2.1 会话模式 在h1操作 #1、解压 tar -zxvf flink-1.19.1-bin-scala_2.12.tgz -C /app/#2、修改配置文件 cd /app/flink-1.19.1/conf vim conf.yaml ##内容:## j…...

Linux Ubuntu 安装配置RabbitMQ,springboot使用RabbitMQ
rabbit-Ubuntu 一篇文章学会RabbitMQ 在Ubuntu上查看RabbitMQ状态可以通过多种方式进行,包括使用命令行工具和Web管理界面。以下是一些常用的方法: 1-使用systemctl命令: sudo systemctl start rabbitmq-server sudo systemctl status ra…...