快速排序与其例题

news2024/12/25 9:22:34

一、快速排序

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.coloradmin.cn/o/2076717.html

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

相关文章

一种商业模式既解决引流又解决复购 你想了解一下嘛?

欢迎各位&#xff0c;我是你们的电商策略顾问&#xff0c;吴军。今天&#xff0c;我将向大家介绍一种新颖的商业模式——循环购物模式。这种模式有何独特之处&#xff1f;商家真的在进行慷慨的赠金活动吗&#xff1f;消费者在购物的同时还能获得额外收益&#xff1f;甚至可以将…

Python控制流:条件语句(if, elif, else)①

文章目录 前言1. 基本条件语句1.1 if 语句1.2 else 语句1.3 elif 语句1.4 嵌套条件语句 2. 条件表达式3. 多条件判断4. 比较运算符和逻辑运算符5. 常见错误和最佳实践5.1 常见错误5.2 最佳实践 6. 综合详细的例子&#xff1a;学生成绩管理系统6.1 类和方法Student 类 6.2 主函数…

LD/T698.45 协议解析(新)

通信架构 客户机和服务器在开始通信前&#xff0c;通信信道必须先完成预连接。预连接建立后&#xff0c;默认具有一个最低权限的应用连接&#xff0c;客户机和服务器之间可直接进行数据交换。当客户机需要得到更高权限的服务器服务时&#xff0c;客户机必须发起建立更高权限的…

浦发银行不良堆积,新任领导的无奈

撰稿|芋圆 浦发银行在2023年进行了一波董监高人员大变动&#xff0c;董事长和行长两位掌舵人双双离职&#xff0c;在其任内&#xff0c;浦发银行自2020年起的营收、利润状况和资产质量就一直难有起色。 目前&#xff0c;距新任领导班子上任已差不多过去一年之久。在这一年里&a…

Redis(面试题【速记】)

Redis简介 Redis 是一个开源(BSD 许可)内存数据结构存储用作数据库、缓存、消息代理和流引擎。Redis 提供数据结构&#xff0c;例如 字符串、散列、列表、集合、带范围查询的排序集合、位图、超日志、地理空间索引和流。Redis 内置了复制、Lua 脚本、LRU 驱逐、事务和不同级别的…

【Linux —— 线程同步 - 条件变量】

Linux —— 线程同步 - 条件变量 条件变量的概念互斥量与条件变量的关系条件变量的操作代码示例 条件变量的概念 条件变量是一种用于线程间同步的机制&#xff0c;主要用于协调线程之间的执行顺序&#xff0c;允许线程在某个条件不满足时进入等待状态&#xff0c;直到其他线程通…

【Linux I/O】万字长文带思维导图,一文彻底掌握Linux I/O:深入解析操作系统数据交互的艺术

Linux I/O Linux I/O&#xff08;输入/输出&#xff09;是操作系统与外部设备进行数据交互的过程。在Linux系统中&#xff0c;I/O操作的管理和优化对于系统性能有着至关重要的影响。本文将详细介绍Linux中的各种I/O模型&#xff0c;包括它们的工作原理、优缺点以及适用场景&am…

ImportError: DLL load failed while importing _ssl: 找不到指定的模块。

windonw cmd下的输出&#xff1a; (python3.9) PS D:\git\ImageAnalysisService\core\medical_bills> python Python 3.9.19 (main, May 6 2024, 20:12:36) [MSC v.1916 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or …

计算机基础知识总结(八股文--计算机网络、操作系统、数据库、c++、数据结构与算法)

一、操作系统 0.内存管理 01.什么是虚拟内存&#xff1f;为什么需要虚拟内存&#xff1f; 虚拟内存为程序提供比实际物理内存更大的内存空间&#xff0c;同时提高内存管理的灵活性和系统的多任务处理能力。虚拟地址空间就是进程所能看到的内存空间&#xff0c;这段空间是连续…

苍穹外卖项目DAY11

苍穹外卖项目DAY11 1、Apache ECharts 1.1、介绍 Apache ECharts是一款基于JavaScript的数据可视化图标库&#xff0c;提供直观&#xff0c;生动&#xff0c;可交互&#xff0c;可个性化定制的数据可视化图标 官网&#xff1a;Apache ECharts 1.3、入门案例 <!DOCTYPE…

LlamaIndex 实现 RAG(四)- RAG 跟踪监控

RAG 整个流程不复杂&#xff0c;集成三大部分包括文档解析并生成向量、根据查询问题查找语意相似的数据文档块、把查询问题和召回文档作为上下文的数据传给模型进行解答。大语言模型的应用开发和传统的开发方式区别很大&#xff0c;以前开发完成&#xff0c;只要逻辑正确&#…

解决IDEA 控制台中文乱码及无法输入中文

一、IDEA 控制台中文乱码&#xff1a; 问题描述&#xff1a; IntelliJ IDEA 如果不进行相关设置&#xff0c;可能会导致控制台中文乱码、配置文件中文乱码等问题。 解决方案&#xff1a; ①&#xff1a;设置字体为支持中文的字体&#xff1a; 点击菜单 File - > settings …

二分查找【算法 09】

二分查找算法详解 二分查找&#xff08;Binary Search&#xff09;是一种高效的查找算法&#xff0c;前提是数据必须是有序的。相比于线性查找&#xff0c;二分查找的时间复杂度从 O(n) 降低到了 O(log n)&#xff0c;适合处理大规模的数据查找问题。本文将详细介绍二分查找的原…

Catf1ag CTF Crypto(六)

前言 Catf1agCTF 是一个面向所有CTF&#xff08;Capture The Flag&#xff09;爱好者的综合训练平台&#xff0c;尤其适合新手学习和提升技能 。该平台由catf1ag团队打造&#xff0c;拥有超过200个原创题目&#xff0c;题目设计注重知识点的掌握&#xff0c;旨在帮助新手掌握C…

集团数字化转型方案(十六)

为了全面推进集团的数字化转型&#xff0c;我们将实施一系列战略举措&#xff0c;包括整合最新的人工智能、大数据分析和云计算技术&#xff0c;升级企业资源规划&#xff08;ERP&#xff09;系统&#xff0c;实现业务流程的自动化与优化&#xff1b;同时&#xff0c;建立全方位…

计算机是如何工作的(2)

文章目录 一. 寄存器和存储器二. 操作系统二. 进程PCB1. pid2. 内存指针3. 文件描述符表4. 属性1) 状态2) 优先级3) 上下文4) 记账信息 一. 寄存器和存储器 存储器是内存和硬盘的通称 内存, 存储空间比硬盘小, 速度比硬盘快, 价格比硬盘高, 掉电后数据流失寄存器是CPU上的一个…

ORACLE EBS R12系统的安装及维护案例

引言&#xff1a; Oracle E-Business Suite (EBS) R12 是企业中广泛应用的一体化管理解决方案&#xff0c;涵盖了财务、人力资源、供应链等多个业务领域。以下将详细介绍如何在 Windows 系统上安装 Oracle EBS R12&#xff0c;并分享一些日常维护的技巧和最佳实践。 点击下载…

基于imx6ull平台opencv的图像采集、ffmpeg推流和Windows端拉流(多线程)

目录 一、概述二、环境要求2.1 硬件环境2.2 软件环境三、开发流程3.1 编写测试3.2 验证功能一、概述 本文档是针对imx6ull平台opencv的图像采集、ffmpeg推流和Windows端拉流。首先创建一个线程opencv通过摄像头采集视频图像,接着再创建两个线程,其中一个线程获取采集的视频图…

Python编码系列—Python中的HTTPS与加密技术:构建安全的网络通信

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

驾驭高效编程:一探C++ STL的奥秘

1.什么是STL 2.:STL的版本 2.1:原始版本 2.2:P.J版本 2.3:RW版本 2.4:SGI版本 3:STL的六大组件 4:如何学习STL 5:STL的缺陷 1.什么是STL STL(standdard template library-标准模板库):是C标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包含数据结构与算法软…