排序-快速排序(Quick Sort)

快排的简介

快速排序(Quick Sort)是一种高效的排序算法,采用分治法的策略,其基本思想是选择一个基准元素,通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

快速排序的基本步骤:

  1. 选择基准:从数组中挑选一个元素作为“基准”(pivot)。
  2. 分区操作(Partitioning):
    • 将所有小于基准的元素放置在基准之前,所有大于基准的元素放置在基准之后。这个操作结束后,基准元素就处于数组的中间位置,此位置称为“分割点”。
    • 分区操作完成后,基准元素就位于其最终排序后的位置。
  3. 递归排序
    • 对基准元素左边的子数组递归执行快速排序。
    • 对基准元素右边的子数组递归执行快速排序。

快速排序的特点:

  • 时间复杂度
    • 最好情况(每次划分都很均匀):O(n log n)。
    • 平均情况:O(n log n)。
    • 最坏情况(每次划分都将数组分成一个元素与剩余所有元素两部分,例如已排序或逆序数组):O(n^2)。但通过随机选取基准或使用三数取中法等技巧可大幅降低这种情况发生的概率。
  • 空间复杂度
    • O(log n),主要来自于递归调用栈的深度。
  • 稳定性
    • 快速排序不是稳定的排序算法,因为在交换过程中相等的元素可能会改变原有的相对顺序。

快排的Java代码实现如下所示:

/* 元素交换 */
void swap(int[] nums, int i, int j) {int tmp = nums[i];nums[i] = nums[j];nums[j] = tmp;
}/* 哨兵划分 */
int partition(int[] nums, int left, int right) {// 以 nums[left] 为基准数int i = left, j = right;while (i < j) {while (i < j && nums[j] >= nums[left])j--;          // 从右向左找首个小于基准数的元素while (i < j && nums[i] <= nums[left])i++;          // 从左向右找首个大于基准数的元素swap(nums, i, j); // 交换这两个元素}swap(nums, i, left);  // 将基准数交换至两子数组的分界线return i;             // 返回基准数的索引
}
/* 快速排序 */
void quickSort(int[] nums, int left, int right) {// 子数组长度为 1 时终止递归if (left >= right)return;// 哨兵划分int pivot = partition(nums, left, right);// 递归左子数组、右子数组quickSort(nums, left, pivot - 1);quickSort(nums, pivot + 1, right);
}

 快排流程图如下所示:

快排优化1:基数优化

快速排序在某些输入下的时间效率可能降低。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 𝑛−1、右子数组长度为 0 。我们可以在数组中选取三个候选元素(通常为数组的首、尾、中点元素),并将这三个候选元素的中位数作为基准数。这样一来,基准数“既不太小也不太大”的概率将大幅提升。

Java示例代码如下:

/* 选取三个候选元素的中位数 */
int medianThree(int[] nums, int left, int mid, int right) {int l = nums[left], m = nums[mid], r = nums[right];if ((l <= m && m <= r) || (r <= m && m <= l))return mid; // m 在 l 和 r 之间if ((m <= l && l <= r) || (r <= l && l <= m))return left; // l 在 m 和 r 之间return right;
}/* 哨兵划分(三数取中值) */
int partition(int[] nums, int left, int right) {// 选取三个候选元素的中位数int med = medianThree(nums, left, (left + right) / 2, right);// 将中位数交换至数组最左端swap(nums, left, med);// 以 nums[left] 为基准数int i = left, j = right;while (i < j) {while (i < j && nums[j] >= nums[left])j--;          // 从右向左找首个小于基准数的元素while (i < j && nums[i] <= nums[left])i++;          // 从左向右找首个大于基准数的元素swap(nums, i, j); // 交换这两个元素}swap(nums, i, left);  // 将基准数交换至两子数组的分界线return i;             // 返回基准数的索引
}

快排优化2:尾递归优化

尾递归优化是编程中一种优化技术,旨在减少递归调用的开销,尤其是当递归调用是函数体中的最后一个操作时。在快速排序算法中,通常有两个递归调用,分别处理基准元素左侧和右侧的子数组。尾递归优化尝试减少这种递归调用的栈消耗,但需要注意的是,Java等许多现代编程语言默认并不自动执行尾递归优化。尽管如此,我们可以通过手动调整快速排序的递归结构来模拟尾递归的效果,减少递归深度。

在某些输入下,快速排序可能占用空间较多。以完全有序的输入数组为例,设递归中的子数组长度为 𝑚 ,每轮哨兵划分操作都将产生长度为 0 的左子数组和长度为 𝑚−1 的右子数组,这意味着每一层递归调用减少的问题规模非常小(只减少一个元素),递归树的高度会达到 𝑛−1 ,此时需要占用 𝑂(𝑛) 大小的栈帧空间。

为了防止栈帧空间的累积,我们可以在每轮哨兵排序完成后,比较两个子数组的长度,仅对较短的子数组进行递归。由于较短子数组的长度不会超过 𝑛/2 ,因此这种方法能确保递归深度不超过 log⁡𝑛 ,从而将最差空间复杂度优化至 𝑂(log⁡𝑛) 。

为了优化,我们可以使用循环而不是直接递归来控制递归过程,这样做的目的是尽量复用当前函数栈帧,避免每次递归调用都生成新的栈帧。代码如下所示:

/* 快速排序(尾递归优化) */
void quickSort(int[] nums, int left, int right) {// 子数组长度为 1 时终止while (left < right) {// 哨兵划分操作int pivot = partition(nums, left, right);// 对两个子数组中较短的那个执行快速排序if (pivot - left < right - pivot) {quickSort(nums, left, pivot - 1); // 递归排序左子数组left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]} else {quickSort(nums, pivot + 1, right); // 递归排序右子数组right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]}}
}

对两个子数组中较短的那个执行快速排序,可以这样理解其好处:

  • 减少递归深度:因为处理完较短的子数组后,再次进入递归时,处理的子数组规模相对减小,有助于控制递归调用的总深度。
  • 平衡负载:在某些情况下,比如数组已经部分排序,若总是先处理较短的子数组,可以在一定程度上避免递归树严重倾斜,使得递归调用更均匀地分布在左右两侧,从而更高效地利用递归栈空间。

这个策略是一种平衡快速排序递归深度的方法,尤其是在处理大数据集或递归深度受限的环境下尤为重要。通过这样的策略,可以提高算法在极端情况下的健壮性,减少潜在的栈溢出风险。

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

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

相关文章

ViewModel 完全指南:实践与背后原理全解

一、引言 在现代Android应用开发中&#xff0c;处理UI数据的有效管理和状态保持是开发者面临的重要挑战之一。Google推出的Jetpack组件库中的ViewModel已成为解决这些问题的关键工具。ViewModel旨在以生命周期意识的方式存储和管理界面相关的数据&#xff0c;从而使数据在配置…

从iconfont引入线上字体库

如果是长期使用建议直接下载字体包 /* 在线链接服务仅供平台体验和调试使用&#xff0c;平台不承诺服务的稳定性&#xff0c;企业客户需下载字体包自行发布使用并做好备份。 */ 例如使用阿里妈妈数黑体 https://www.iconfont.cn/fonts/detail?spma313x.fonts_index.i1.d9df…

福建建设工程造价信息网2023年1至12月工程材料信息价期刊汇总

福建省最新造价信息网工程材料信息价期刊可打开 www.zgjct.com 直接下载,历年造价信息期刊也可查询,所有材料信息价格都由官方发布,如有疑问可咨询网站人工客服微信 造价信息期刊更新明细如下&#xff1a; 福州市造价信息网各类工程信息价期刊下载 福州市工程材料信息价2023年…

在全志H616核桃派开发板上进行PyQt5的代码编写和运行

核桃派本地 在上一节我们通过Qt Designer设计了ui窗口并转换成了Python代码&#xff0c;由于是Python编程&#xff0c;因此我们可以在核桃派开发板打开Python代码进行编程。 在核桃派上推荐使用Thonny来打开编写Python文件, 使用请参考&#xff1a;Thonny IDE。 打开上一节生…

BGP学习二:BGP通告原则,BGP反射器,BGP路径属性细致讲解,新手小白无负担

目录 一.AS号 二.BGP路由生成 1.network 2.import-route引入 三.BGP通告原则 1.只发布最优且有效的路由 2.从EBGP获取的路由&#xff0c;会发布给所有对等体 3.水平分割原则 4.IBGP学习BGP默认不发送给EBGP&#xff0c;但如果也从IGP学习到了这条路由&#xff0c;就发…

乡村振兴与乡村旅游深度融合:依托乡村自然和文化资源,发展乡村旅游产业,促进农民增收致富,打造特色美丽乡村

目录 一、引言 二、乡村振兴与乡村旅游的内在联系 三、依托乡村自然和文化资源发展乡村旅游产业 &#xff08;一&#xff09;挖掘乡村自然资源优势&#xff0c;打造特色旅游品牌 &#xff08;二&#xff09;挖掘乡村文化资源内涵&#xff0c;丰富旅游活动内容 四、促进农…

PG的事务ID回卷逻辑

PG到目前为止使用的事务ID仍然是32位的&#xff0c;在内存计算时虽然已经使用64位事务ID&#xff0c;但是存储在页中tuple仍然使用32位事务ID&#xff0c;这就是说&#xff0c;事务ID回卷仍然是必须处理的问题。 所谓PG事务ID回卷&#xff0c;简单地说&#xff0c;就是在数据库…

网页版五子棋的自动化测试

目录 前言 一、主要技术 二、测试环境的准备部署 三、测试用例 四、执行测试 4.1、公共类设计 创建浏览器驱动对象 测试套件 释放驱动类 4.2、功能测试 登录页面 注册页面 游戏大厅页面 游戏房间页面 测试套件结果 4.3、界面测试 登录页面 注册页面 游戏大…

python如何单步调试

Python怎么单步调试&#xff1f;下面给大家介绍一下单步调试&#xff1a; 方法一&#xff1a;执行 python -m pdb myscript.py (Pdb) 会自己主动停在第一行。等待调试&#xff0c;这时你能够看看帮助。 方法二&#xff1a;在所调试程序的开头中&#xff1a;import pdb 并在你…

Vue入门到关门之Vue3学习

一、常用API 注意&#xff1a;本文项目均使用脚手架为 Vite 1、setup函数 &#xff08;1&#xff09;介绍 如果在项目中使用配置项API&#xff0c;那么写起来就和vue2的写法是一样的&#xff1b;但是如果在项目中写的是组合式API&#xff0c;那么组件中所用到的&#xff1a…

电脑提示找不到ffmpeg.dll无法继续执行代码怎么办?

电脑提示找不到找不到ffmpeg.dll无法继续执行代码怎么办&#xff0c;有什么好的解决办法&#xff0c;出现这样的弹出就会导致软件无法打开或者是异常关闭&#xff0c;找不到dll文件&#xff0c;是一个非常重要的电脑使用问题&#xff0c;会给使用者带来许多的麻烦。那么找不到d…

PyWebIO,用 Python 写网站

在Python的世界里&#xff0c;PyWebIO是一个简单而强大的库&#xff0c;它能让你的Python脚本快速拥有一个交互式的网页界面。想象一下&#xff0c;你不需要懂得前端开发&#xff0c;就能创建出用户友好的网页应用&#xff0c;这是多么酷的一件事&#xff01;今天&#xff0c;我…