【有营养的算法笔记】快速排序

news2024/11/15 11:28:17

👑作者主页:@进击的安度因
🏠学习社区:进击的安度因(个人社区)
📖专栏链接:有营养的算法笔记

文章目录

  • 一、思路
  • 二、模板讲解
  • 三、模板测试
  • 四、加练 —— 第 K 个数

今天正式开启算法笔记专栏。本文是专栏的第一篇笔记。主要讲解了快排模板和练习题,让我们一起学习算法吧!

一、思路

快速排序,简称快排,是一个常用的算法。

但是对于快排来说,边界问题是比较难处理的,所以写快排时,背出算法模板,可以帮助我们快速的解决问题。通过板子我们也不需要处理很繁琐的bug。

今天的模板不仅简洁,并且可以完美的解决边界问题。

接下来说一下 快排的主要思想

快排的思想为 分治 ,说白了就是递归,按照区间,通过递归的方式将序列排成有序。

我们将快排的步骤分为三步:

image-20221205214041400

  1. 确定分界点:左边界点 q[l] 或 右边界点 q[r] 或 中间点 q[l + r >> 1],其中任意一个位置的值为 key
  2. 调整区间:小于等于 key 的值在左边,大于等于 key 的值在右边
  3. 递归处理左右区间,主要方法为使用双指针法分别在左边找不符合条件的值和右边不符合条件的值,然后对它们进行交换,递归处理从而使序列有序。

二、模板讲解

通用模板:

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

时间复杂度:O(N * logN) 最差O(N^2) 空间复杂度:O(N)

看完板子,我们提出几个问题:

  1. 快排递归的截止条件是什么?
  2. key 为什么取中间值,这样可以规避什么问题?
  3. 为什么 ij 初始化的值在 l - 1r + 1,如果不这么初始化会有什么问题?
  4. 处理左右区间的主体思路是什么?为什么要 ++i,不这样写有什么问题,能不能这么写:while (q[i] < key) i++;
  5. 如果取的分界点不同时,quick_sort 处理的区间分别可以是什么?一些区间划分为什么不对?

我们接下来带着这些问题来剖析这个板子:

问题1:快排递归的截止条件是什么?

当递归处理区间,截止条件那么就是无需递归,常见情况就是只有一个数,所以理论上是 l == r 就可以截止,但是板子中为了更加严谨,写成了 l >= r也是完全没有问题的。

问题2:key 为什么取中间值,这样可以规避什么问题?

我们设想一下如果 key 取左边界点 q[l],那么当 序列的数全部相同 或 有序 的时候,那么时间复杂度就退化到了 O(N^2) ,当进行排序时,就可能超时。

比如:1 2 3 4 5 6 7 8 9

									1    2    3    4    5    6    7    8    9
   								   key
								l												r
                        对于这种情况,每一次 key 都会取在左边界点,第一次处理左右边界的时候,就会不断的++i,--j,交换值
                        对于当前情况就是一直在 --j
                        那么最后循环停止后,递归处理左右区间时,也是相同情况
                        就相当于把所有情况都走了一遍,这时 时间复杂度为 O(N^2)
                        当数据量足够大时,就会超时,我们简单画一下图,就拿这个序列来说

image-20221205224608660

所以当我们 边界点取中 时就可以尽可能规避掉这个问题。

问题3:为什么 ij 初始化的值在 l - 1r + 1,如果不这么初始化会有什么问题?

初始化 l - 1r + 1 就是让 i 和 j 在序列 最左边的前一个位置序列最右边的后一个位置

设想一下,如果初始化为 ij 再套用这个模板,会出现什么情况?第一个数必定会错过。就算加以改进,可能还会有很多潜在的问题。

另外初始化为 lr 的某个弊端在下个问题就会提及。

问题4:处理左右区间的主体思路是什么?为什么要 ++i,不这样写有什么问题,能不能这么写:while (q[i] < key) i++;

处理左右区间的主题思路就是 双指针

i 用来找 >= x 的值,遇到就停止;j 用来找 <= x 的值,遇到就停止;然后用 swap 库函数交换它们的值,就这么处理达到在每一层函数中左右区间都有序。

那么我们为什么要 ++i ,能不能写成这样?

i = l, j = r;
while (i < j)
{
    while (q[i] < key) i++; // 1
    while (q[j] > key) j--; // 2
    if (i < j) swap(q[i], q[j]);
}

这种情况是可能会导致死循环的,比如序列:3 1 3 6 3

  • l = 0,q = 4
  • key = q[l + r >> 1] = 3
  • i = 0, j = 4
  • q[i] = 3, q[j] = 3, key = 3

那么在循环处就会卡死,1处走不了,跳到2,但是2也走不了,也无法交换,总的循环条件又满足,所以就会造成 死循环 。这样写是 典型的错误

为了规避这个问题,和让 ij 落到相应的位置,于是就有了我们板子里的方案:

i = l - 1, j = r + 1;
while (i < j)
{
    while (q[++i] < key) ;
    while (q[--j] > key) ;
    if (i < j) swap(q[i], q[j]);
}

另外其实 do...while 循环其实更好理解,就是先让 ij 走一步。所以这种模板也可以:

i = l - 1, j = r + 1;
while (i < j)
{
    do i++; while (q[i] < key) ;
    do j--; while (q[j] > key) ;
    if (i < j) swap(q[i], q[j]);
}

问题5:如果取的分界点不同时,quick_sort 处理的区间分别可以是什么?一些区间划分为什么不对?

常见情况

  1. 分界点为右边界点 q[r]中间点 q[l + r + 1 >> 1] 时,区间为 [l ~ i - 1][i ~ r]

  2. 分界点为左边界点 q[l]中间点 q[l + r >> 1] 时,区间为 [l ~ j][j + 1 ~ r]

上面两种为常见的区间的划分,由于博主能力有限,就不深入证明了。下面就举一个简单的例子,说明为什么要这么划分:

我们拿 情况1 举例:

假设我们当前分界点为 中界点 q[l + r >> 1] ,序列为:1 2

  • l = 0, r = 1
  • l + r >> 1 = 0 key = q[l + r >> 1] = 0
第一次快排
	1	2
i  key	   j
  • while (q[++i] < key) ; i 往后走一步,不满足循环条件,i 在 0 下标处停住;
  • while (q[--j] > key) ; j 往前走一步,满足循环条件,继续循环;
  • while (q[--j] > key) ; j 往前走一步,不满足循环条件,j 在 0 下标处停住;

此刻下标位置情况

	1	2
   key	   
	ij

i == j 不进行 swap 交换,接下来就要开始划分区间递归;

接下来再看区间的划分:

  • quick_sort(q, l, i - 1); 划分区间为 [0 ~ -1] 区间不存在,这边递归不用进行;

  • quick_sort(q, i, r); 划分区间为 [0 ~ 1],又是第一次快排开始时的区间,这就是 死递归 ,如果测试会 内存超限 ,就是典型的 Memory Limit Exceeded 错误。

所以我们要加以改进 key = q[l + r + 1 >> 1] ,让其进行 上取整 ,防止下取整到错误情况。

(其他边界情况如果大家有兴趣的话可以自己下去证明一下,博主比较菜…就不献丑了,目前只要背住我们当前这个板子,对大多数情况都是没有问题的)

三、模板测试

我们用一道OJ测试我们的模板是否正确:

描述

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

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

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

输入格式

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

第二行包含 nn 个整数(所有整数均在 1∼1091∼109 范围内),表示整个数列。

输出格式

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

数据范围

1≤ n ≤ 1000001 ≤ n ≤ 100000

输入样例

5
3 1 2 4 5

输出样例

1 2 3 4 5

代码

image-20221206002641913

AC了,那么说明是没有问题的

四、加练 —— 第 K 个数

描述

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

输入格式

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

输出格式

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

数据范围

  • 1 ≤ n ≤ 100000
  • 1 ≤ k ≤ n

输入样例

5 3
2 4 1 5 3

输出样例

3

思路

对于 快速选择算法 ,其实就是 快排的一个变形

对于 快速选择算法 我们这里主要分三步:

  1. 确定分界点:左边界点 q[l] 或 右边界点 q[r] 或 中间点 q[l + r >> 1],其中任意一个位置的值为 key
  2. 调整区间:小于等于 key 的值在左边,大于等于 key 的值在右边
  3. 根据左右区间元素个数,确定 k 所在区间,对单个区间进行递归

image-20221206004228216

这里分情况讨论:

  • k ≤ sl,则递归 左区间 ,由于 k 在 左半区间 ,所以依然是找的 k 个数
  • k > sl,则递归 右区间 ,由于 k 在 右半区间 ,所以找的是 k - sl 个数

那么到这里,我们也可以计算出它的时间复杂度:由于我们只需要一边递归,那么我们的总执行次数大约为:n + n/2 + n/4.... < 2n ,时间复杂度为 O(N)

接下来我们看一下代码怎么写:

#include <iostream>

using namespace std;

const int N = 100010;

int q[N], n, k;

int quick_sort(int l, int r, int k)
{
    if (l >= r)
        return q[l];
    int key = q[l + r >> 1], i = l - 1, j = r + 1;
    while (i < j)
    {
        while (q[++i] < key) ;
        while (q[--j] > key) ;
        if (i < j)
            swap(q[i], q[j]);
    }
    
    int sl = j - l + 1; // 左半区间的元素个数
    if (k <= sl)
        return quick_sort(l, j, k); // 递归左半区间
    else
        return quick_sort(j + 1, r, k - sl); // 递归右半区间
}

int main()
{
    cin >> n >> k;
    
    for (int i = 0; i < n; i++)
    {
        cin >> q[i];
    }
    
    cout << quick_sort(0, n - 1, k) << endl;
    
    return 0;
}

这里递归到最后会只剩下一个数的,所以结果是一定会找到的,不用担心找不到,不太理解的话可以画一下图。

这里还有一个更加容易理解的版本,我比较推荐

我们求 k 个数,其实就是求 k - 1 下标的值。

那么的主体思路的第三步就可以改为:

每次判断 k 是在 左半区间 还是 右半区间 ,递归查找 k 所在区间,递归到最后只剩一个数时,q[k]就是答案。

#include <iostream>

using namespace std;

const int N = 100010;

int q[N], n, k;

int quick_sort(int l, int r, int k)
{
    if (l >= r)
        return q[k];
    int key = q[l + r >> 1], i = l - 1, j = r + 1;
    while (i < j)
    {
        while (q[++i] < key) ;
        while (q[--j] > key) ;
        if (i < j)
            swap(q[i], q[j]);
    }
    if (k <= j) // k 在左半区间
        return quick_sort(l, j, k);
    else
        return quick_sort(j + 1, r, k);
}

int main()
{
    cin >> n >> k;
    
    for (int i = 0; i < n; i++)
    {
        cin >> q[i];
    }
    
    // k - 1 就是 第 k 个数 的下标
    cout << quick_sort(0, n - 1, k - 1) << endl;
    
    return 0;
}

到这里,本篇博客就到此结束了。建议看完博客后可以把模板通过理解的方式背住,下去再多练几遍代码,加深记忆。这样可以很快提高熟练度。
如果觉得anduin写的还不错的话,还请一键三连!如有错误,还请指正!
我是anduin,一名C语言初学者,我们下期见!

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

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

相关文章

生产型外协管理系统:功能解析篇

随着经济全球化与电商的全面发展&#xff0c;生产型企业的生产订单是在逐年生产的。由于自身的生产能力有限&#xff0c;很多企业不得不将些许业务进行外包生产&#xff0c;也就是所谓的外协生产。既然需要外协生产的话&#xff0c;那么对外协生产进行管理也就尤为重要了。作为…

[附源码]计算机毕业设计软考刷题小程序Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Express:JSONP 接口

JSONP 接口 1. 回顾 JSONP 的概念与特点 概念&#xff1a;浏览器端通过 <script> 标签的 src 属性&#xff0c;请求服务器上的数据&#xff0c;同时&#xff0c;服务器返回一个函数的调用。这种请求数据的方式叫做 JSONP。 特点&#xff1a; ①JSONP 不属于真正的 Aj…

ASEMI整流桥KBU808参数,KBU808尺寸,KBU808大小

编辑-Z ASEMI整流桥KBU808参数&#xff1a; 型号&#xff1a;KBU808 最大重复峰值反向电压&#xff08;VRRM&#xff09;&#xff1a;800V 最大RMS电桥输入电压&#xff08;VRMS&#xff09;&#xff1a;560V 最大直流阻断电压&#xff08;VDC&#xff09;&#xff1a;800…

Linux学习-72-GRUB加密方法(2种加密方式)

16.14 GRUB加密方法&#xff08;2种加密方式&#xff09; Linux 在启动时&#xff0c;在 GRUB 管理界面中是可以通过按"e"键进入编辑模式&#xff0c;修改启动选项的。每个启动选项都支持按"e"键进入编辑模式。在编辑模式中可以修改启动模式&#xff0c;比…

有必要好好学习一下dc_shell了

首先最好是有design compiler user guide&#xff0c;哪里不会找哪里&#xff1f;然后来看看常见的dc_shell 命令 一般先要起一个dc_shell。 dc_shell 启动以后&#xff0c;使用 read_ddc xxx/DBs/ddc/<block_name>.ddc 将design compiler的综合结果读入。 为了确认读入…

GitHub上热门的Java开源项目

1 JavaGuide https://github.com/Snailclimb/JavaGuide Star 26396 一份Java学习指南&#xff0c;涵盖大部分Java程序员所需要掌握的核心知识 2 DoraemonKit https://github.com/didi/DoraemonKit Star 4826 简称 "DoKit"&#xff0c;中文名哆啦A梦&#xff0c;意…

数学建模三大类模型适用场景及建模方法(纯干货)

目录 一&#xff0c;评价类算法 1&#xff0c;层次分析法 ●基本思想: ●基本步骤: ●优点: ●缺点 ●适用范围: ●改进方法: 2&#xff0c;灰色综合评价法&#xff08;灰色关联度分析&#xff09; ●基本思想: ●基本步骤: ●优点: ●缺点: ●适用范围: ●改进方…

TMS FMX Cloud提供集成元素

TMS FMX Cloud提供集成元素 TMS FMX云包能够为用户和开发人员提供从Firemonkey软件到云光解决方案的可访问性。TMS FMX云包基本上由能够与不同的云解决方案一起提供集成的元素组成。 每当FMX计数增加时&#xff0c;性能就会下降。 有一个基本的设计时集成可用。 基于架构的火猴…

探花交友_第2章_环境搭建(新版)

探花交友_第2章_环境搭建&#xff08;新版&#xff09; 文章目录探花交友_第2章_环境搭建&#xff08;新版&#xff09;课程介绍 《探花交友》1、项目介绍1.1、项目背景1.2、市场分析1.3、目标用户群体1.4、使用场景1.5、竞争对手分析1.5.1、竞品选择1.5.2、竞品分析1.6、项目简…

[数据结构] - 顺序表与链表详解

顺序表和链表同属于线性表&#xff0c;线性表顾名思义&#xff0c;就是连续的一条直线&#xff0c;但它在物理结构上是不一定连续的&#xff0c;通常的线性表用顺序表和链表来实现。下面我们介绍顺序表和链表 文章目录1. 顺序表1.1 顺序表的大致介绍1.2 顺序表的代码实现顺序表…

数据结构练级之路【链表带环问题】

一、链表带环问题的代码和几个经典的面试题&#xff08;重点在于如何算入口点&#xff09; 代码非常的简单&#xff0c;但是有几个关于带环问题的讲解就比较不好理解 1.有关链表是否带环的题目和代码 &#xff08;较难且较经典&#xff09;&#xff08;有关链表带环的问题&a…

《零基础学机器学习》笔记-第1课-MNIST数字识别

机器学习项目的实际过程大致可以分为5个环节&#xff0c;下面以卷积神经网络分析MNIST数据集为例实战一下。 MNIST数据集-卷积神经网络-python源码下载 一、问题定义 MNIST数据集&#xff0c;相当于机器学习领域的Hello World&#xff0c;非常经典&#xff0c;包括60000张训练…

JAVA队列及实现类

什么是队列&#xff1f; 队列是一种特殊的线性表&#xff0c;遵循先入先出、后入后出的基本原则&#xff0c;一般来说&#xff0c;它只允许在表的前端进行删除操作&#xff0c;而在表的后端进行插入操作&#xff0c;但是java的某些队列运行在任何地方插入删除&#xff1b;比如我…

常用网络接口自动化测试框架应用

一、RESTful&#xff08;resource representational state transfer)类型接口测试 (一&#xff09;GUI界面测试工具&#xff1a;jmeter 1、添加线程组 2、添加http请求 3、为线程组添加察看结果树 4、写入接口参数并运行 5、在查看结果树窗口查看结果 6、多组数据可增加CSVDat…

git原理浅析

1.git概念 我们的项目一般由文件夹和文件组成&#xff0c;在文件系统中,基本都是树形结构, 在git中&#xff0c;文件夹称为 “tree” &#xff0c;文件称为 “blob” &#xff0c;顶层文件夹称为 “top-level tree” 。下方的目录结构是个例子而已&#xff1a; . (top-level t…

Global Mapper栅格计算器,波段计算NDVI、NDSI、NDWI等

Global Mapper栅格计算器&#xff0c;波段计算NDVI、NDSI、NDWI等1. Global Mapper中的栅格计算器2. 查看数据属性&#xff0c;检查波段数量3. 打开栅格计算器&#xff0c;进行波段计算Global Mapper功能丰富&#xff0c;其栅格计算器工具内置很多遥感指数&#xff0c;方便进行…

TwineCompile高级编译系统

TwineCompile高级编译系统 TwineCompile是我们对C编译速度慢的解决方案。通过使用多线程、文档缓存和自动化后台编译技术&#xff0c;集成到CBuilder IDE中&#xff0c;大大降低了编译/制作/构建的次数。 TwineCompile是一个创新的电子书系统&#xff0c;它利用多线程工程和缓存…

Java项目:SSM学生选课管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 由SpringMVCMyBatis为主要框架&#xff0c;mysql8.0配置主从复制实现读写分离。前端主要由bootstrap完成&#xff0c;背景用particles.js插件。…

Spring Boot整合JWT实现用户认证

初探JWT 什么是JWT JWT(Json Web Token)&#xff0c;是一种工具&#xff0c;格式为XXXX.XXXX.XXXX的字符串&#xff0c;JWT以一种安全的方式在用户和服务器之间传递存放在JWT中的不敏感信息。 为什么要用JWT 设想这样一个场景&#xff0c;在我们登录一个网站之后&#xff0…