快速排序与其例题

一、快速排序

1、简单介绍:快速排序(Quick Sort)是一种高效的排序算法,由计算机科学家Tony Hoare在1960年提出。它是基于分治法的排序算法,其基本思想和步骤如下:

基本概念

快速排序的核心思想是将待排序的数据集分成两个子集,使得一个子集中的所有元素都小于或等于另一个子集中的所有元素,然后递归地对这两个子集进行排序。这样,最终整个数据集就会被排序好。

算法步骤

  1. 选择基准元素(Pivot): 从待排序数组中选择一个元素作为基准(pivot)。基准元素的选择方法有很多种,比如选择第一个元素、最后一个元素、中间元素或随机选择等。

  2. 分区(Partition): 重新排列数组,使得所有小于基准元素的元素都位于基准元素的左侧,而所有大于基准元素的元素都位于基准元素的右侧。基准元素最终被放置在其正确的位置上。

  3. 递归排序(Recursion): 对基准元素左侧和右侧的子数组分别应用快速排序算法。

  4. 终止条件(Base Case): 当子数组的长度为零或一时,递归结束,因为它们已经是有序的。

详细步骤

  1. 选择基准: 例如,选择数组中的最后一个元素作为基准。

  2. 分区过程:

    • 设定两个指针,i 和 ji 指向数组的开头,j 从数组的开头到倒数第二个元素移动。
    • 当 j 指向的元素小于等于基准元素时,将 i 指向的元素与 j 指向的元素交换,并将 i 向右移动一位。
    • 最终,将基准元素与 i 指向的元素交换,使基准元素位于其正确位置上。
  3. 递归调用: 对基准元素左侧和右侧的子数组递归执行快速排序。

示例

假设我们有一个数组 [3, 6, 8, 10, 1, 2, 1],并选择最后一个元素 1 作为基准:

  1. 选择基准: 1

  2. 分区过程:

    • 初始数组: [3, 6, 8, 10, 1, 2, 1]
    • 移动指针 j 并将小于等于 1 的元素放在左侧,结果会改变为: [1, 6, 8, 10, 3, 2, 1]1 和 3 交换位置。
  3. 基准元素位置: 1 已经在它应该在的位置上,分区完成。

  4. 递归调用:

    • 对基准左侧 [1] 和右侧 [6, 8, 10, 3, 2, 1] 分别进行递归排序。

时间复杂度

  • 平均情况时间复杂度: O(n log n)
  • 最坏情况时间复杂度: O(n^2)(发生在每次选择的基准都是最小或最大元素的情况,如已排序数组)

优点与缺点

优点:

  • 平均情况下时间复杂度较低,适合大多数情况下的排序。
  • 原地排序,不需要额外的存储空间(除了递归调用栈)。

缺点:

  • 最坏情况下时间复杂度较高。
  • 不稳定排序(相等元素的相对顺序可能会改变)。

快速排序因其高效和简单的特性,在实际应用中非常受欢迎。

2、模板题

给定你一个长度为 n的整数数列。

请你使用快速排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式

输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在 1~ 10^{9}范围内),表示整个数列。

输出格式

输出共一行,包含 n个整数,表示排好序的数列。

数据范围

1≤n≤100000

输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5

#include<iostream>using namespace std;const int N=1e5+10;
//这个1e5是科学计数法,表示的是100000
int q[N];void quick_sort(int q[],int l,int r){if(l>=r) return;int x=q[l+r>>1],i=l-1,j=r+1;//i和j指针相错一位,会解决不必要的冲突。
//这是以中点为分界点,但是也可以两边的边界点为分界点,看个人习惯。while(i<j){do i++; while (q[i]<x);do j--; while (q[j]>x);if(i<j) swap(q[i],q[j]);}quick_sort(q,l,j);quick_sort(q,j+1,r);}int main(){int n;scanf("%d",&n);for(int i=0;i<n;i++) scanf("%d",&q[i]);quick_sort(q,0,n-1);for(int i=0;i<n;i++) printf("%d ",q[i]);return 0;
}

这上面的这个题就是最普通的快速排序,是可以作为模板的,可以适当的理解与记忆。

对上述代码的自我理解:掌握一个算法最重要的是搞清楚思想,一般来说,代码是抽象的思想具化的表现,所以说,明白了思想,自然而然就会写了,对于里面过分的细节就不用追究,只需要将里面核心的思想用代码表示出来,这样一般就会写了。

  上面的l+r>>1的意思是(l+r)/2,之所以写成位运算的形式是因为方便,不用加括号啥的,一个数右移一位就像于除以2,所以可以这样用。

void quick_sort(int q[],int l,int r){if(l>=r) return;int x=q[l+r>>1],i=l-1,j=r+1;while(i<j){do i++; while (q[i]<x);do j--; while (q[j]>x);if(i<j) swap(q[i],q[j]);}quick_sort(q,l,j);quick_sort(q,j+1,r);}

这一部分是核心代码,首先,假如一个序列里面只有一个元素,那就没必要进行快速排序,所以就有if(l>=r) return;这句代码,表示的是直接返回,退出递归。然后勒,因为这个快速排序他是没有选择另外开辟数组的,就是在一个数组里面进行各种各样的操作,所以就直接开始咯。

以中点为分界点,注意:分界点是随情况而定的,不一定就能恰好分一半,而且这个如果不能整除的话,是要取整的,这点要注意。

整体的逻辑就是:分界点将整个序列分成了两部分,怎么分呢,在一个整体的数组里面。因为我们要左边的部分的值都小于分界点的值,右边部分的值都要大于分界点的值,i指针从左往右走,j指针从右往左走。当i指针所指的值确实是小于分界点的值,那么就是对的,就是放在左面的,这个时候i++;当j指针所指的值确实是大于分界点的值,那么就是是对的,就是放在右面的,这个时候j--。在这个过程中,若是没有满足这两种情况,那么在没有越界,符合范围的情况下,就将不符合情况的两个指针所指的数交换一下就可以了,就解决了问题,所以上面代码就是这么写的,按照我说的这个一步一步的逻辑。

  最后分界点已找到位置,就成功的分成了正式的两部分,然后这个时候在分别去递归左面部分和右面部分的序列,最后把整体的结合起来就是整个的最后的答案,也就是上面的那个最终代码,所以说,写代码,还是得按照逻辑来写,特别是涉及到递归的更是如此,有些时候过分的细节不用去细究,想明白大概运行逻辑就可以写了。

注意:  quick_sort(q,l,j);
    quick_sort(q,j+1,r);

这两句代码是递归左边部分和右边部分的,但是要注意写的时候的细节,我这里是以中点为分界点,所以左边是l~j,右边是j+1,r;不要将j换成i来表示,就算换的一摸一样,但是会出现边界问题,会出现一些莫名的错误,所以我说:要是你取左边界来当分界点,那么就尽量用j来表示范围,中点也是如此;取右边界当分界点的时候,尽量用i表示范围。

具体叙述:

(1)int x = q[l]int x = q[r]int x = q[l + r >> 1] 有本质的区别,主要在于访问数组元素的具体位置以及它们在程序中的作用和结果。

1. int x = q[l] 和 int x = q[r]

  • int x = q[l]: 这行代码从数组 q 中获取索引 l 处的元素,并将其赋值给 x。这个操作将 x 设置为区间左边界 l 的值。

  • int x = q[r]: 类似地,这行代码从数组 q 中获取索引 r 处的元素,并将其赋值给 x。这个操作将 x 设置为区间右边界 r 的值。

2. int x = q[l + r >> 1]

  • int x = q[l + r >> 1]: 这行代码从数组 q 中获取区间中点位置 l + r >> 1 处的元素,并将其赋值给 x。这个操作计算了区间 [l, r] 的中间位置的值。

比较与问题

  1. 访问的元素位置不同

    • q[l] 和 q[r] 访问的是区间边界位置的元素,而 q[l + r >> 1] 访问的是区间中间的元素。
    • 如果你在做二分查找或者分治算法,通常需要访问中间位置的元素(q[l + r >> 1])来决定下一步的操作。
  2. 作用不同

    • 访问 q[l] 和 q[r] 通常用于获取边界值,用于边界比较或者特定操作。而 q[l + r >> 1] 用于获取中点值,这在许多算法(如二分查找、分治法)中至关重要。
  3. 算法逻辑

    • 在二分查找中,访问 q[l + r >> 1] 是为了获取当前中点的值,以决定下一个搜索区间。在这种情况下,直接访问边界元素 (q[l] 或 q[r]) 不符合算法逻辑,可能会导致错误的搜索范围或不正确的操作。
  4. 具体场景

    • 如果你只需要边界值来进行比较或某些操作(例如寻找最大/最小值),那么访问 q[l] 或 q[r] 是合适的。
    • 如果你在进行中点分割(如在递归中分割区间),那么访问中点元素 (q[l + r >> 1]) 是必要的。

总结

  • q[l] 和 q[r] 用于获取区间边界的值,不适合用来确定区间的中点。
  • q[l + r >> 1] 用于获取区间中点的值,适合用于需要中点信息的算法,如二分查找和分治法。

根据你具体的算法需求,选择适当的数组索引访问方式非常重要。如果你在实现二分查找或类似的算法,使用中点索引 q[l + r >> 1] 是必要的,否则可能无法正确地执行算法逻辑。

(2)在算法中,尤其是涉及到查找、排序或分治的场景时,选择左边界点、右边界点还是中间点对算法性能有重要影响。特别是对于二分查找等分治算法,选择中间点通常是优化性能的关键

为什么选择中间点通常更优?

  1. 时间复杂度

    • 二分查找:二分查找的时间复杂度是 (O(\log n)),因为每次将问题规模减半。为了达到这个效果,你必须在每一步中准确地将搜索范围分成两半,这就需要选择中间点 (l + r >> 1) 来决定下一步的搜索区间。
    • 选择边界点:如果你总是选择左边界点 (q[l]) 或右边界点 (q[r]),你将无法缩小搜索区间,算法的复杂度可能会退化,导致性能问题。例如,如果总是选择最小值或最大值,你实际上可能需要遍历整个区间,从而导致线性时间复杂度 (O(n)),这会显著增加运行时间。
  2. 搜索效率

    • 中间点:选择中间点可以有效地将问题规模减半,每一步操作都使搜索区间减小。这种策略可以快速收敛到目标位置,从而提高查找效率。
    • 边界点:选择边界点(如 q[l] 或 q[r])通常意味着你可能需要重新计算搜索区间的大小,从而增加了不必要的操作,无法利用有效的分治策略。

具体影响

  1. 超时问题

    • 如果你在进行查找或排序等操作时选择边界点而不是中间点,算法可能会变得非常慢,特别是在处理大规模数据时。对于二分查找,如果选择边界点,你实际上可能会失去二分查找的优势,从而导致超时。
  2. 实际例子

    • 二分查找:有效使用中间点确保每一步都在减小问题规模,保持 (O(\log n)) 的时间复杂度。
    • 线性查找:如果你仅使用边界点,可能会退化为线性查找,时间复杂度变成 (O(n)),这在处理大量数据时可能导致程序超时。

结论

  • 中间点 是处理二分查找和其他分治算法时的最佳选择,因为它可以确保问题规模的有效减少和算法的高效执行。
  • 边界点 的选择可能导致搜索效率降低,特别是在需要快速定位或处理大数据时,可能会导致超时或性能问题。

因此,在需要高效查找和排序的算法中,始终选择中间点(尤其是在二分查找等算法中)是避免性能问题和超时的关键。

3、实战例题

给定一个长度为 n的整数数列,以及一个整数 k,请用快速选择算法求出数列从小到大排序后的第 k个数。

输入格式

第一行包含两个整数 n和 k。

第二行包含 n 个整数(所有整数均在 1∼10^{9} 范围内),表示整数数列。

输出格式

输出一个整数,表示数列的第 k 小数。

数据范围

1≤n≤100000,
1≤k≤n

输入样例:
5 3
2 4 1 5 3
输出样例:
3

//这个是快排选择排序
#include<iostream>
using namespace std;const int N = 1e5 + 10;
int q[N];void quick_sort(int q[], int l, int r, int k) {if (l >= r) return; /*我取的中点作为参照值,所以这里一旦发现只有一个元素,就直接退出递归了,直接返回,如果是以边界的l或者r作参照值的话,情况有不一样。*/int x = q[l + r >> 1], i = l - 1, j = r + 1;while (i < j) {do i++; while (q[i] < x);do j--; while (q[j] > x);if (i < j) swap(q[i], q[j]);//老规矩,快排的模板。}int leftSize = j - l + 1;//左半部分的长度,这个还是得单独表示出来,方便点且避免出错。if (k <= leftSize) {quick_sort(q, l, j, k);//小于等于左半部分长度,就只需要递归左边。} else {quick_sort(q, j + 1, r, k - leftSize);//大于左边部分长度,就只需要右边递归。}
}int main() {int n, k;cin >> n >> k;for (int i = 0; i < n; i++) scanf("%d", &q[i]);quick_sort(q, 0, n - 1, k);cout << q[k - 1] << endl;//因为下标是从0开始,所以第k小的数是在k-1位置上。return 0;
}

模板是可以直接去记忆和理解的,但是遇到实际的情况要根据题目的要求来改进,反正需要快排的地方大概就和模板是差不多的。

需要注意的小细节:

在快速排序算法中,选择 j 作为递归分界点之所以合适,而 i 可能不行,主要是因为 ij 的含义和位置不同。在分区过程中,ij 的值代表了不同的状态:

1. j 的位置

在分区过程中,j 是右指针向左移动的结果,它代表了数组中第一个不满足条件的元素的位置。在经典的快速排序实现中,j 的最终值表示基准元素的正确位置或者其右侧的分界点。换句话说,所有在 q[l..j] 的元素都小于等于基准元素,而 q[j+1..r] 的元素都大于基准元素。因此:

  • q[l..j] 是基准元素左边的子数组。
  • q[j+1..r] 是基准元素右边的子数组。

因此,递归地对这两个子数组进行排序是合适的。

2. i 的位置

i 是左指针向右移动的结果,它表示数组中第一个大于等于基准元素的位置。i 在分区操作过程中可能会在基准元素的右侧,也可能在基准元素的左侧。具体来说:

  • 在分区过程中,i 会不断向右移动,直到找到一个大于等于基准元素的值。
  • 当 i 和 j 相遇时,i 的位置实际上是 j 的位置,表示基准元素的最终位置。

为什么用 j 更合适

  1. 准确的分割

    • 使用 j 作为分界点可以确保子数组 [l..j] 是所有小于等于基准元素的元素,而 [j+1..r] 是所有大于基准元素的元素。这保证了子数组的正确分割,使得快速排序能够递归地对这两个部分进行排序。
  2. 避免错误的递归

    • 如果使用 i 作为递归的分界点,i 在分区结束时的值不一定能准确地表示左侧和右侧子数组的正确分界,特别是在 i 和 j 的位置不同的情况下,这可能导致递归调用错误的范围,导致无限递归或不正确的排序结果。

总结

在快速排序算法中,j 作为分界点是合适的,因为它正确地表示了基准元素的位置或者右侧的分界点。而 i 是在寻找下一个要交换的元素的过程中移动的,它可能不会在分区的最后准确地表示两个子数组的分界。因此,使用 j 可以确保正确地递归排序左边和右边的子数组。

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

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

相关文章

Debezium2.7 数据同步 MySQL/Oracle -- AI生成

Debezium是Red Hat开源的一个工具&#xff0c;用于实时捕获多种数据源&#xff08;包括MySQL、PostgreSQL、SQL Server、Oracle等&#xff09;的变更数据&#xff0c;并将这些数据作为事件流输出到Kafka等消息中间件中。通过Debezium&#xff0c;可以实现数据的实时同步和变更数…

【Qt】常用控件QCalendarWidget

常用控件QCalendarWidget的使用 QCalendarWidget表示一个日历 核心属性 属性说明 selectDate 当前选中的⽇期 minimumDate 最⼩⽇期 maximumDate 最⼤⽇期 firstDayOfWeek 每周的第⼀天(也就是⽇历的第⼀列) 是周⼏. gridVisible 是否显⽰表格的边框 selectionMode…

何为MethodHandles?

最近在梳理ThreadPoolExecutor&#xff0c;无意间看到其内部类Worker实现了一个名字叫做AbstractQueuedSynchronizer的抽象类。看到它&#xff0c;我便想起当年为了面试而疯狂学习这个知识点的场景。不过这种临时抱佛脚的行为&#xff0c;并未给我带来即时的收益。也是这次的疯…

软件上显示“mfc140.dll丢失”错误信息?那么mfc140.dll丢失该如何修复

mfc140.dll是 Microsoft Foundation Class (MFC) 库的一部分&#xff0c;这个库被用于基于 C 的 Windows 应用程序的开发。当 Windows 或软件上显示“mfc140.dll丢失”或“找不到 mfc140.dll”这类错误信息时&#xff0c;表示你的系统可能缺少与 Visual C 相关的组件或这些组件…

文本处理函数

1.文本的提取 left mid right 2.文本的查找与替换 replace&#xff0c;substitute 3.字符个数 len字符 lenb字节, office365好像没有此功能 4.数据的清理 clean , trim 5.找不同 exact

【Qt】多元素控件QTableWidget

多元素控件QTableWidget 使用QTableWidget表示一个表格控件&#xff0c;一个表格中包含若干行、每一个行又包含若干列。 表格中的每一个单元格&#xff0c;都是一个QTableWidget对象。 QTableWidget核心方法 方法说明 item(int row, int column) 根据⾏数列数获取指定的 Q…

WIN32实现远程桌面监控

文章目录 完整代码API简介调试代码 后记reference 完整代码 server.cpp #include <winsock2.h> #include <Ws2tcpip.h> #include <windows.h> #include <stdio.h> #include <vector> #pragma comment(lib, "ws2_32.lib")LRESULT CAL…

免费JSON在线解析工具网址

1&#xff0c;https://tool.juhe.cn/ JSON在线解析 (juhe.cn) 2&#xff0c;https://www.sojson.com/ JSON在线 | JSON解析格式化—SO JSON在线工具

Android Studio:模拟器页面闪烁,手机模拟器输入画面闪烁 android studio闪屏

主要解决&#xff0c;android studio 启动app测试&#xff0c;输入数据时&#xff0c;手机画面就会闪烁&#xff0c;闪屏 1. 如图所示&#xff0c;依照顺序找到Edit &#xff0c;并点击Edit 2. 找到Graphics 选择为SoftWare &#xff0c;并保存修改即可 3. 如果此处不能选择S…

MongoDB Compass初体验

入坑Mongodb也好多年了&#xff0c;客户端一直都是使用的Robomongo&#xff0c;后改名为Robo 3T了&#xff0c;现在又改名为Studio 3T&#xff0c;还分了免费版和付费版。 最近换了新电脑&#xff0c;需要重新安装Mongodb的客户端&#xff0c;加上公司对安装软件的各种限制&…

【C语言报错已解决】 `Buffer Overflow`

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 引言一、问题描述&#x1f469;‍&#x1f52c;报错示例&#x1f4da;报错分析&#x1f4da;解决思路 二、解决方法&a…

加速自动驾驶模型迭代,数据存算一体是关键

自动驾驶的每一个业务阶段都会涉及到 AI 深度学习算法和算力的参与&#xff0c;机器视觉&#xff0c;深度学习&#xff0c;传感器技术等均在自动驾驶领域发挥着重要的作用。自动驾驶系统不断迭代的前提是算法的持续优化&#xff0c;目前&#xff0c;自动驾驶发展的瓶颈主要在于…

Vue3.0项目实战(二)——大事件管理系统登录注册功能实现

目录 1. 登录注册页面 [element-plus 表单 & 表单校验] 1.1 注册登录 静态结构 & 基本切换 2. 注册功能 2.1 实现注册校验 2.2 注册前的预校验 2.3 封装 api 实现注册功能 3. 登录功能 3.1 实现登录校验 3.2 登录前的预校验 & 登录成功 1. 登录注册页面 […

交换排序(冒泡排序和快速排序)

一、基本思想 所谓交换&#xff0c;就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。 交换排序的特点是&#xff1a;将键值较大的记录向序列的尾部移动&#xff0c;键值较小的记录向序列的前部移动。 二、冒泡排序 1.核心思想 两两相邻的元素进行比…

大二必做项目贪吃蛇超详解之上篇win32库介绍

文章目录 1. 游戏背景2. 游戏效果演示3. 项目目标4. 前置知识5. Win32 API5. 1 控制台程序(Console)5. 2 控制台屏幕上的坐标 COORD5. 3 GetStdHandle5. 4 GetConsoleCursorlnfo5. 4. 1 CONSOLE_CURSOR_INFO5. 4. 2 SetConsoleCursorlnfo 5. 5 SetconsoleCursorPosition5. 6 Ge…

Linux(面试篇)

目录 什么是Linux 什么是Linux内核&#xff1f; Linux的基本组件是什么&#xff1f; Bash和Dos之间基本区别是什么&#xff1f; 什么是Root账户 什么是Bash? 什么时CLI? Linux的目录结构时怎样的&#xff1f; 什么是硬链接和软链接&#xff1f; 什么叫CC攻击&#…

【项目日记】高并发内存池 ---项目介绍及组件定长池的实现

余生还长&#xff0c;你别慌&#xff0c;也别回头&#xff0c;别念旧. --- 余华 --- 1 高并发内存池简介 高并发内存池项目是实现一个高并发的内存池&#xff0c;他的原型是google的一个开源项目tcmalloc&#xff0c;tcmalloc全称Thread-Caching Malloc&#xff0c;即线程缓存…

RocketMQ Dashboard

rocketmq-dashboard是一个可视化查看和管理RocketMQ消息队列的工具 官方地址&#xff1a;RocketMQ Dashboard | RocketMQ 1、点击下载源码 2、下载并解压&#xff0c;切换至源码目录rocketmq-dashboard-1.0.0 3、修改配置文件 4、编译 rocketmq-dashboard打成jar包 &#xf…

MySQL中的回表查询、索引覆盖、索引下推

本文重点介绍索引中的常见概念&#xff1a;回表查询、索引覆盖、索引下推 一、回表查询 我们首先理解&#xff1a;在InnoDB存储引擎中&#xff0c;根据索引的存储形式&#xff0c;又可以分为以下两种&#xff1a; 分类含义特点聚集索引 (Clustered Index)将数据存储与索引放到…

leetcode 438.找到字符串中所有字母异位词

目录 题目描述 示例1&#xff1a; 示例2&#xff1a; 提示&#xff1a; 解题思路 Collections库 介绍 滑动窗口法 概念 应用场景及特点&#xff1a; 思路 流程展示 代码 复杂度分析 题目描述 给定两个字符串s和p&#xff0c;找到s中所有p的异位词的子串&#xf…