【数据结构 · 初阶】- 快速排序

目录

一. Hoare 版本

1. 单趟

2. 整体

3. 时间复杂度

4. 优化(抢救一下)

4.1 随机选 key

4.2 三数取中

二. 挖坑法

格式优化

三. 前后指针(最好)

四. 小区间优化

五. 改非递归


快速排序是 Hoare 提出的一种基于二叉树结构的交换排序方法。统一排升序

一. Hoare 版本

1. 单趟

目的:选出一个关键字/关键值/基准值 key把他放到排好序后,最终在的位置
key 都喜欢在最左/右边,其他位置不好排

例如这样的数组:

单趟结束后要达成这样的效果:(选择,插入,冒泡排序的单趟没有这种附加效果)
此时6就在排好序后,所在的位置


实现:
R 往左走,找比 key 小的
;L 往右走,找比 key 大的,相等无所谓。都找到之后,交换。直至相遇
结论:key 在左,让 R 先走,能保证相遇位置一定比 key 小。        key 在右,让 L 先走。
相遇位置既然比 key 小,就把 key 换到左边

    
    

void QuickSort(int* a, int left, int right)
{int begin = left, end = right;int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]) // 右边找小right--; // 且要防止本来就有序,right 飘出去while (left < right && a[left] <= a[keyi]) // 左边找大left++;Swap(&a[left], &a[right]);}Swap(&a[keyi], &a[left]);keyi = left;
}

易错:
        1. 不能认为外面的 while 判断过了,里面就不用判断。里面的 while 会多走几次,left 和 right 的相对位置变了,所以要再加判断。 
        2. 一定是 >= 否则可能出现死循环

2. 整体

递归:
上面排好单趟,被分成三段区间,[begin, keyi-1] keyi [keyi+1, end]。左右区间都无序,递归左区间。
选出 key 分成左右区间 …… 左区间有序,递归右区间。右区间有序,整体有序
递归返回条件:区间只剩一个值或区间不存在

递归的过程虽然图上像是分出来了,其实都是在原数组上走的

和二叉树的前序很像。单趟排是处理根(key),再处理左子树(左区间),右子树(右区间)

void QuickSort(int* a, int left, int right)
{// 递归返回条件if (left >= right) // = 是只剩一个值。> 是没有值return;int begin = left, end = right;int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]) // 右边找小right--; // 且要防止本来就有序,right 飘出去while (left < right && a[left] <= a[keyi]) // 左边找大left++;Swap(&a[left], &a[right]);}Swap(&a[keyi], &a[left]);keyi = left;// 递归 [begin, keyi-1] keyi [keyi+1, end]QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}

3. 时间复杂度

理想情况下:(小于)N * logN

N 个数,最终接近满二叉树 ==》logN

当 N = 100W,只需递归 20 层        N = 10亿,递归 30 层        空间消耗不大,每层减的数也不大
最终每一层也还是 N 的量级


最坏:O( N^2 ) 抢救后忽略        已经顺/逆序

递归 N 层,建立 N 个栈帧,会栈溢出

4. 优化(抢救一下)

影响快排性能的是 keyi
keyi 越接近中间的位置,越二分,越接近满二叉树,深度越均匀,效率越高

不是让左边的值做 key ,而是让 key 在最左边的位置

4.1 随机选 key

(生成位置% 区间大小)+ 左边

void QuickSort(int* a, int left, int right)
{// 递归返回条件 ......int begin = left, end = right;// 优化1.随机选 keyint randi = left + (rand() % (left - right));Swap(&a[left], &a[randi]); // 还是让最左边做 keyint keyi = left;while (left < right){ ...... }
}

管你有序无序,都把你变成无序

     

4.2 三数取中

有序 / 接近有序的情况下,选中间位置做 key 最好。但不一定是有序 / 接近有序
三数取中:选 左右中 3个位置,不是最小,也不是最大的数的位置        两两比较

int GetMidNumi(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[left] > a[right]) // mid 不是中间,是最大的。{return left; // 剩下两个:left 和 right 大的就是中间}else{return right;}}else // a[left] > a[mid]{if (a[mid] > a[right]){return mid;}else if (a[left] < a[right]) // mid 是最小的{return left; // 剩下两个:left 和 right 小的就是中间}else{return right;}}
}// 快速排序
void QuickSort(int* a, int left, int right)
{// 递归返回条件 ......int begin = left, end = right;// 优化2.三数取中int midi = GetMidNumi(a, left, right);Swap(&a[midi], &a[left]);int keyi = left;while (left < right){ ...... }
}

     

二. 挖坑法

先将第一个数据存放在临时变量 key 中,形成一个坑位

piti 在左,不用想,肯定让 right 先走


right 找到比 key 小的后,把 a[right] 扔到坑里,自己变成坑。left 走。


left 找到比 key 小的后,把 a[left] 扔到坑里,自己变成坑。right 走。

重复以上过程,直到 left 和 right 相遇。相遇点一定是坑,再把 key 扔到坑里

   
   

void QuickSort2(int* a, int left, int right)
{// 递归返回条件if (left >= right)return;int begin = left, end = right;// 优化2.三数取中int midi = GetMidNumi(a, left, right);Swap(&a[midi], &a[left]);int piti = left;int key = a[left];while (left < right){while (left < right && a[right] >= key) // 右边找小right--;a[piti] = a[right]; // 扔到左边的坑piti = right; // 自己成新的坑,坑到右边去了while (left < right && a[left] <= key) // 左边找大left++;a[piti] = a[left]; // 扔到右边的坑piti = left; // 自己成新的坑,坑到左边去了}a[piti] = key;// 递归 [begin, piti-1] piti [piti+1, end]QuickSort2(a, begin, piti - 1);QuickSort2(a, piti + 1, end);
}

格式优化

如果写单趟,上面的写法就可以

快排的递归框架是不变的,变的是单趟

// Hoare 单趟
int PartSort1(int* a, int left, int right)
{// 三数取中int midi = GetMidNumi(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[left], &a[right]);}Swap(&a[keyi], &a[left]);keyi = left;return keyi;
}// 挖坑 单趟
int PartSort2(int* a, int left, int right)
{// 三数取中int midi = GetMidNumi(a, left, right);Swap(&a[midi], &a[left]);int piti = left;int key = a[left];while (left < right){while (left < right && a[right] >= key) // 右边找小right--;a[piti] = a[right]; // 扔到左边的坑piti = right; // 自己成新的坑,坑到右边去了while (left < right && a[left] <= key) // 左边找大left++;a[piti] = a[left]; // 扔到右边的坑piti = left; // 自己成新的坑,坑到左边去了}a[piti] = key;return piti;
}void QuickSort(int* a, int left, int right)
{if (left >= right)return;int keyi = PartSort2(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

三. 前后指针(最好)

1. cur 找到比 key 小的值,++prev,交换 cur 和 prev 位置的数据,++cur
2. cur 找到比 key 大的值,++cur

把比 key 大的值往右翻,比 key 小的值往左翻

   
   
   
   
   

1. prev 要么紧跟着 cur(prev 下一个就是 cur)
2. prev 跟 cur 中间隔着比 key 大的一段值

int PartSort3(int* a, int left, int right)
{// 三数取中int midi = GetMidNumi(a, left, right);Swap(&a[midi], &a[left]);int keyi = left;int prev = left, cur = left + 1;while (cur <= right) // [left, right],所以是 <={if (a[cur] < a[keyi]){++prev;Swap(&a[cur], &a[prev]);++cur;}else{++cur;}}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}void QuickSort(int* a, int left, int right)
{if (left >= right)return;int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}
while (cur <= right)
{if (a[cur] < a[keyi] && ++prev != cur)Swap(&a[cur], &a[prev]);++cur;
}

四. 小区间优化

小区间直接使用直接插入排序

希尔是当数据量特别大时,为了让大数快速往后跳才用
堆排还要建堆,很麻烦
冒泡只有教学意义,现实中几乎没用
选择排序,最好最坏都是 N^2,也没用

上面说递归图看着像二叉树

当区间特别小时,递归的次数会非常多。
光最后一层的递归数,就是总递归数的1/2。倒数第二次占1/4。倒数第三层占1/8

如果小区间直接使用直接插入排序,递归数量会少很多。现实中递归的不均匀,但怎么说也减少了50%的递归数量

void QuickSort(int* a, int left, int right)
{if (left >= right)return;// 小区间优化 - 小区间直接使用插入排序if (right - left + 1 > 10) // [left, right]左闭右闭区间,要 +1{int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}else{InsertSort(a + left, right - left + 1);}
}

不能写成:InsertSort(a, right - left + 1)

正确。但如果是这样就出错了:

[ left , right ] 左闭右闭区间,要 +1

五. 改非递归

递归的问题:1. 效率(影响不大)        2. 递归太深,栈溢出。不能调试

递归改非递归:
        1. 直接改循环。原来正着走,递归逆着来(简单)。eg:斐波那契数列。
        2. 用栈辅助改循环。(难)eg:二叉树

递归里,实际是用下标来 分割子区间
递归里参数条件变化的是什么,栈里面存的就是什么。具体情况具体分析


思路:
        1. 栈里面取一段区间,单趟排序
        2. 单趟分割子区间入栈
        3. 子区间只有一个值、不存在时就不入栈

为了和递归的过程一样,栈里先入右区间,再入左区间。这样就先排好左区间,再排好右区间
在栈里取单个区间时,若想先取左端点、再取右端点,就要先入右端点、再入左端点。

void QuickSortNonR(int* a, int left, int right)
{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 keyi = PartSort3(a, begin, end);// [begin,keyi-1] keyi [keyi+1, end]if (keyi + 1 < end){STPush(&st, end);STPush(&st, keyi + 1);}if (begin < keyi - 1){STPush(&st, keyi - 1);STPush(&st, begin);}}STDestroy(&st);
}

2 个 if 相当于递归的返回条件

本篇的分享就到这里了,感谢观看,如果对你有帮助,别忘了点赞+收藏+关注
小编会以自己学习过程中遇到的问题为素材,持续为您推送文章

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

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

相关文章

第2周 PINN核心技术揭秘: 如何用神经网络求解偏微分方程

1. PDEs与传统数值方法回顾 (Review of PDEs & Traditional Numerical Methods) 1.1 什么是偏微分方程 (Partial Differential Equations, PDEs)? 偏微分方程是描述自然界和工程领域中各种物理现象(如热量传播、流体流动、波的振动、电磁场分布等)的基本数学语言。 1.…

Neo4j(二) - 使用Cypher操作Neo4j

文章目录 前言一、Cypher简介二、数据库操作1. 创建数据库2. 查看数据库3. 删除数据库4. 切换数据库 三、节点、关系及属性操作1. 创建节点与关系1.1 语法1.2 示例 2. 查询数据2.1 语法2.2 示例 3. 更新数据3.1 语法3.2 示例 4. 删除节点与关系4.1 语法4.2 示例 5. 合并数据5.1…

RabbitMQ的Web管理页面给我看懵了,这都什么意思啊

文章目录 OverviewTotalsMessage RatesQueued Messages NodesChurn StatisticsPorts and ContextsExport DefinitionsImport Definitions ConnectionsChannelsExchangesQueuesAdmin他们之间的关联 在上一篇文章中我们讲到了如何在Windows中安装Rabbitmq&#xff0c; 小白也能搞…

安全基础与协议分析

5.1 Web安全基础 5.1.1 Web安全基础概览&#xff08;一、二&#xff09; Web安全的核心目标是保护Web应用及其数据免受攻击&#xff0c;涵盖以下关键领域&#xff1a; 攻击面&#xff1a; 前端漏洞&#xff08;XSS、CSRF&#xff09;。 后端漏洞&#xff08;SQL注入、RCE&a…

STM32项目实战:ADC采集

STM32F103C8T6的ADC配置。PB0对应的是ADC1的通道8。在标准库中&#xff0c;需要初始化ADC&#xff0c;设置通道&#xff0c;时钟&#xff0c;转换模式等。需要配置GPIOB的第0脚为模拟输入模式&#xff0c;然后配置ADC1的通道8&#xff0c;设置转换周期和触发方式。 接下来是I2C…

第十四章:数据治理之数据源:数据源的数据接入、业务属性梳理及监控

本章开始&#xff0c;将进入9大模块的介绍。第一个模块我们先介绍&#xff1a;数据源。数据源是整个数据中台数据的来源&#xff0c;是一个起点。更好的管理好数据源这个起点&#xff0c;是数据治理的一个好的开始。 在【数据&#xff1a;业务生数据&#xff0c;数据生“万物”…

【C/C++】多线程开发:wait、sleep、yield全解析

文章目录 多线程开发&#xff1a;wait、sleep、yield全解析1 What简要介绍详细介绍wait() — 条件等待&#xff08;用于线程同步&#xff09;sleep() — 睡觉&#xff0c;定时挂起yield() — 自愿让出 CPU 2 区别以及建议区别应用场景建议 3 三者协作使用示例 多线程开发&#…

阿里云CDN刷新预热--刷新URL

文章目录 一、全英文URL刷新预热二、掺杂中文的URL刷新预热2.1 对带中文URL进行编码2.2 预热刷新 三、CDN刷新-核心作用与价值3.1 核心作用3.2 核心价值3.3 典型使用场景 *最后我想说&#xff1a;请你不要相信我说的每一句话&#xff0c;这只是我的个人经验* 一、全英文URL刷新…

Oracle 19c DG备库报错ORA-00313、ORA-00312、ORA-27037

Oracle 19c DG备库报错ORA-00313、ORA-00312、ORA-27037 错误排查问题处理错误排查 DG同步完成后,DG Broker show database发现以下告警信息: Database Warning(s):ORA-16826: apply service state is inconsistent with the DelayMins propertyORA-16789: standby redo log…

开源与闭源之争:AI时代的创新博弈与未来抉择

在人工智能技术狂飙突进的今天&#xff0c;开源与闭源之争已不再局限于技术圈的讨论&#xff0c;而是演变为一场关乎技术伦理、商业格局乃至人类文明走向的深度博弈。当Meta的Llama 3开源模型下载量突破百万&#xff0c;当OpenAI的GPT-5继续加固技术壁垒&#xff0c;这场没有硝…

NIFI的处理器:JSLTTransformJSON 2.4.0

该处理器使用JSLT转换FlowFile JSON有效负载的格式。使用转换后的内容创建新的FlowFile&#xff0c;并将其路由到“成功”关系。如果JSLT转换失败&#xff0c;则将原始FlowFile路由到“失败”关系。 需要注意的是&#xff0c;编译JSLT转换可能相当昂贵。理想情况下&#xff0c…

MySQL 索引失效及其解决办法

一、前言 在数据库优化中,索引(Index)是一项至关重要的技术手段,可以显著提升查询性能。然而,在实际开发过程中,MySQL 索引并不总是如预期生效。本文将从原理出发,系统地介绍索引失效的常见场景及其解决方案,帮助开发者有效规避性能陷阱。 二、索引基础回顾 MySQL 支…

趋势触发策略

趋势触发策略(TS版)是一种基于TrendTriggerFactor(TTF)的交易策略,通过柱状图颜色变化指示市场趋势的强度,并根据TTF的穿越信号进行买卖操作。 TTF是通过计算买方力量和卖方力量的差值除以两者之和的一半再乘以100得到的。 当TTF大于100时,柱状图显示为绿色,表示市场…

DeepSeek-R1 模型现已在亚马逊云科技上推出

亚马逊云科技提供众多免费云产品&#xff0c;可以访问&#xff1a;亚马逊云科技 在刚刚过去的 Amazon re&#xff1a;Invent 期间&#xff0c;Amazon 首席执行官 Andy Jassy 分享了从 Amazon 自己在全公司开发近 1000 个生成式 AI 应用程序的经验中汲取的宝贵经验。从这种广泛…

中台项目-微前端qiankun-umimax

学习视频&#x1f50a; 基础&#xff1a; 黑马前端基于qiankun搭建微前端项目实战教程_哔哩哔哩_bilibili 路由、部署配置注意&#xff1a;qiankunvite微前端上线注意事项&#xff0c;base公共路径设置_哔哩哔哩_bilibili 微前端 什么是微前端&#xff1f; 微前端是将前端应…

【Java学习笔记】代码块

代码块 介绍&#xff1a;代码块又称为初始化块&#xff0c;属于类中的成员&#xff08;即是类的一部分&#xff09;&#xff0c;类似于方法&#xff0c;将逻辑语句封装在方法体中&#xff0c;通过{}包围起来 与类方法的不同点 无方法名 无返回类型 无参数 只有方法体&#…

spring boot 2.7集成旧的springfox-boot-starter swagger oas 3.0

旧版本目前已经不维护推荐使用 springdoc-openapi-ui&#xff0c;这里为了演示使用旧的最新依赖 <dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version> </dep…

Linux按键驱动测试方式详细介绍

Linux按键驱动测试可采用以下分层方法&#xff1a; 基础事件检测 使用输入子系统调试工具&#xff1a; sudo apt install evtest # 安装事件测试工具 evtest # 选择对应设备编号触发按键后观察终端输出&#xff0c;正常情况应显示&#xff1a; Event:…

USB学习【13】STM32+USB接收数据过程详解

目录 1.官方的描述2.HAL的流程把接收到的数据从PMA拷贝到用户自己定义的空间中 3.处理接收到的数据4.最后再次开启准备接收工作 1.官方的描述 2.HAL的流程 以上的官方说法我们暂时按下不表。 如果接收到数据&#xff0c;会激活中断进入到USB_LP_CAN1_RX0_IRQHandler&#xff0…

上海内推 | 上海算法创新研究院-上海交大联合招收空间智能/具身智能算法实习生

最近这一两周不少公司已开启春招和实习招聘。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友解惑答疑&am…