当前位置: 首页 > news >正文

数据结构 之 【排序】(递归实现快速排序)

目录

1.快速排序的思想

2.基准值的选取

2.1三数取中

2.2随机选数

2.3基准值选取代码

3.单趟排序的三种方法

3.1hoare法

3.1.1hoare法单趟图解

3.1.2hoare法单趟代码

3.2挖坑法

3.2.1挖坑法单趟图解

3.2.2挖坑法单趟代码

3.3前后指针法

3.3.1前后指针法单趟图解

3.3.2前后指针法单趟代码

4.快速排序排序图解及完整代码

5.快速排序的时间复杂度与空间复杂度


升序为例

1.快速排序的思想

以升序为例,单趟排序时,选取一个基准值并将其放在数组中的正确位置,即让左边的数小于等于基准值,右边的数大于等于基准值,然后以该基准值所在位置为界,将数组分割为左右两个部分(均不包含基准值位置)并分别对这两个部分重复进行选取基准值并放到正确位置的操作,直到区间不存在时停止,此时数组有序

2.基准值的选取

固定选取当前数组的首元素或尾元素作为基准值,可能会因递归层次太深(后续讲解)导致效率低下(O(N^2))甚至栈溢出

一般使用三数取中或随机选数来确定基准值然后将其与首或尾位置元素进行交换

2.1三数取中

int GetMidi(int* a, int left, int right){int midi = left + (right - left) / 2;if (a[left] > a[midi]){if (a[midi] > a[right])return midi;else if (a[left] > a[right])return right;else return left;}else//a[left] <= a[midi]{if (a[left] > a[right])return left;else if (a[midi] > a[right])return right;else return midi;}
}

三数取中就是说在数组的首元素、中间位置元素和最后一个元素当中选择中位数

这里我们返回该元素的下标,便于后续交换

int midi = (left + right) / 2,可能会导致溢出 
int midi = left + (right - left) / 2; 中间偏左 

int midi = left + (right - left + 1) / 2; 中间偏右

中间偏左与标准库一致,减少维护成本。除非有明确证据表明中间偏右在特定场景下性能显著更优,否则应默认使用中间偏左的计算方式

2.2随机选数

srand((unsigned int)time(NULL));
int midi = left + rand() % (right - left + 1);

left 是 当前区间的首元素的下标,通过rand函数产生随机值以随机选择基准值

随机选数的方法,可能会因分区不平衡导致效率降低,

三数取中能够保持平均性能,降低极端分区不平衡的风险

所以我们一般使用三数取中来选取基准值

2.3基准值选取代码

	int midi = GetMidi(a, left, right);if (midi != left)Swap(&a[left], &a[midi]);

3.单趟排序的三种方法

我们选好了基准值,就需要将其放置在正确的位置上此时有

hoare法、挖坑法、前后指针法实现该操作

3.1hoare法

3.1.1hoare法单趟图解

未作三数取中处理

升序为例

hoare版本的思想就是:

(1)left、right初始值分别是当前区间首元素的下标、尾元素的下标(也可以是指针)

(1)选定的基准值Key是首元素,就让right 向左移动到比Key的元素,left 向右移动到比Key的元素,找到之后交换两元素的位置,再继续让right先走找小left后走找大, 直到left\right相遇为止,此时交换Ke和相遇位置的元素的位置

(1)选定的基准值Key是尾元素,就让left先向右走找大,right后走找小......

首元素是基准值时,必须让right先走才能使相遇位置的元素小于等于基准值

(1)right找到小,left找不到大,left碰right直接相遇,交换位置可满足正确位置的定义

(2)right找不到小,直接与left位置即基准值相遇,自己与自己交换,不影响

(3)right找到小,left找到大,交换之后再发生上面两种情况而已

如果left先走1个位置,就可能发生right找不到小而直接与left相遇的情况

此时,基准值本应自己与自己交换变为了与下一个位置交换,不符合正确位置的定义

如果让left先走找大,也可能出现找不到大而直接与right相遇的情况,交换后不符合正确位置的定义

3.1.2hoare法单趟代码

int PartSort1(int* a, int left, int right)
{//三数取中获取基准值int midi = GetMidi(a, left, right);if (midi != left)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[keyi], &a[right]);//返回相遇位置的下标,便于后续分区间递归keyi = right;return keyi;
}

(1)在找大、小的过程中要时刻注意left、right不要越界

(2)等于Key的数组元素既可以在正确位置的左边也可以在右边,所以

while (left < right && a[right] >= a[keyi])

这里面需要用>=、<=,如果不加等于,当数组中间有与Key相等的两个值时,就会出现死循环的现象

3.2挖坑法

3.2.1挖坑法单趟图解

基准值未作三数取中处理

升序为例

挖坑法的思想就是:

(1)left、right初始值分别是当前区间首元素的下标、尾元素的下标(也可以是指针)

(1)创建变量Key存储基准值

(1)选定的基准值Key是首元素,就让right 向左移动到比Key的元素,找到就填充坑位,坑位转移到右边,left 向右移动到比Key的元素,找到就填充坑位,坑位转移到左边再继续让right先走找小填充坑位并转移坑位left后走找大填充坑位并转移坑位,..... 直到left\right相遇为止,此时用Key填充坑位

(1)选定的基准值Key是尾元素,就让left先向右走找大,right后走找小......

首元素是基准值时,必须让right先走才能使相遇位置成为正确的坑位

(1)right找到小,填充坑位并正确转移之后,left找不到大,left碰right相遇,位置满足正确位置的定义

(2)right找不到小,直接与left位置即基准值相遇,原位置填充,不影响

(3)right找到小,left找到大,交换之后再发生上面两种情况而已

如果left先走1个位置,就可能发生right找不到小而直接与left相遇的情况

此时,基准值本应填充原位置变为了填充下一个位置,不符合正确位置的定义

如果让left先走找大,也可能出现找不到大而直接与right相遇的情况,填充坑位并正确转移之后,最终坑位不符合正确位置的定义

3.2.2挖坑法单趟代码

int PartSort2(int* a, int left, int right){//三数取中获取基准值int midi = GetMidi(a, left, right);if (midi != left)Swap(&a[left], &a[midi]);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;
}

3.3前后指针法

3.3.1前后指针法单趟图解

基准值未作三数取中处理

升序为例

选定的基准值Key是首元素时,前后指针的思想就是:

(1)prev、cur初始值分别是当前区间首元素的下标、第二个元素的下标(也可以是指针)

如果cur位置的值小于Key,++prev,然后交换cur位置与prev位置的值,再++cur

如果cur位置的值大于等于Key,++cur

这样之后,prev要么紧邻cur,要么与cur之间间隔着比Key大的值

反复进行上面操作之后,比Key大的值就会向后移动,比Key小的值就会向前移动

prev所在位置的元素小于等于Key,prev最终所在位置就是正确位置

3.3.2前后指针法单趟代码

int PartSort3(int* a, int left, int right)
{//三数取中获取基准值int midi = GetMidi(a, left, right);if (midi != left)Swap(&a[left], &a[midi]);int keyi = left;int prev = left;int cur = left + 1;//cur位置的值小于基准值,就与++prev位置的值进行交换,++cur//           大于      ,++curwhile (cur <= right){if (a[cur] < a[left] && ++prev != cur)Swap(&a[cur], &a[prev]);++cur;}//基准值与prev位置的值交换//prev位置的值始终是小于等于基准值的Swap(&a[keyi], &a[prev]);keyi = prev;return keyi;
}
if (a[cur] < a[left] && ++prev != cur)Swap(&a[cur], &a[prev]);

这段代码巧妙地运用了&&运算符短路求值及前置++先++再返回的特点来

减少自己与自己交换的冗余操作

4.快速排序排序图解及完整代码

单趟排序将选取的基准值放置到正确位置之后,数组被分割为左右两部分

左右两部分再重复进行选值放值分割的操作,直到区间只有一个元素或不存在为止

例如,当数组只有 0、1两个元素时,hoare法返回的keyi是0,

区间就会被分割为[0, -1](不存在)、[1, 1](只有一个元素)

整个排序类似于二叉树的前序遍历

void QuickSort(int* a, int left, int right)
{if (left >= right)return;int keyi = PartSort1(a, left, right);//[left, keyi - 1] keyi [keyi + 1, right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}

小规模(小于16)数组使用递归会冗余

这点数量的数组选择直接插入的方法进行优化更好

void QuickSort(int* a, int left, int right)
{if (left >= right)return;if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);}else{int keyi = PartSort1(a, left, right);//[left, keyi - 1] keyi [keyi + 1, right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}
}

5.快速排序的时间复杂度与空间复杂度

递归实现快速排序时

(1)固定选取当前数组的首元素或尾元素作为基准值,而不采用三数取中、随机选数调参时,

当数组有序,会导致每次划分只能减少一个元素(即每次递归调用处理的子数组长度仅减少 1),导致递归层次太深,遍历次数变为(N + 1) * N) / 2 时间复杂度退化为O(N^2)

(1)调参之后

前面说了,递归调用相当于时二叉树的前序遍历

此时就约有logN层,而每一层的个数还是在N这个量级(尽管有减少),记住结论就行

所以时间复杂度可被优化为O(N*logN)

(1)快速排序的空间复杂度主要由递归调用栈的深度决定,此外还包括划分过程中的临时变量开销(可忽略)

所以时间复杂度最好为O(logN),最差为O(N)

http://www.lryc.cn/news/599989.html

相关文章:

  • 【补题】Codeforces Round 735 (Div. 2) B. Cobb
  • 中国移动融合企业网关H10G-13-中星微ZX296716处理器-破解教程
  • 基于springboot的小区车位租售管理系统
  • 学习:JS[6]环境对象+回调函数+事件流+事件委托+其他事件+元素尺寸位置
  • 利用DeepSeek测试kdb+x的tpch sf=10数据
  • Vue2-VueRouter
  • rtpengine的docker化
  • 【C语言进阶】一篇文章教会你文件的读写
  • 微服务架构中的资源调度与负载均衡实践
  • CSS3新特性深度解析:Position Sticky粘性定位完整指南
  • Android 15中的16KB大页有何优势?
  • 深度学习篇---预训练模型
  • 升级目标API级别到35,以Android15为目标平台(三 View绑定篇)
  • 【应急响应】进程隐藏技术与检测方式(二)
  • 三坐标和激光跟踪仪的区别
  • 重庆市傲雄司法鉴定所获准新增四项司法鉴定资质
  • 认识编程(3)-语法背后的认知战争:类型声明的前世今生
  • 利用Trae将原型图转换为可执行的html文件,感受AI编程的魅力
  • 使用python的头文件Matplotlib时plt.show()【标题字体过小】问题根源与解决方案
  • java每日精进 7.25【流程设计3.0(网关+边界事件)】
  • 【Linux系统】基础IO(下)
  • 解决笔记本合盖开盖DPI缩放大小变 (异于网传方法,Win11 24H2)
  • STM32的WI-FI通讯(HAL库)
  • 【电赛学习笔记】MaxiCAM 项目实践——二维云台追踪指定目标
  • 嵌入式Linux裸机开发笔记8(IMX6ULL)主频和时钟配置实验(3)
  • vue 渲染 | 不同类型的元素渲染的方式(vue组件/htmlelement/纯 html)
  • linux配置ntp时间同步
  • 前端核心进阶:从原理到手写Promise、防抖节流与深拷贝
  • ERNIE-4.5-0.3B 实战指南:文心一言 4.5 开源模型的轻量化部署与效能跃升
  • Agentic RAG理解和简易实现