【有营养的算法笔记】基础算法 —— 快速排序思路梳理和常见错误拔毛

简介: 【有营养的算法笔记】基础算法 —— 快速排序思路梳理和常见错误拔毛

一、思路


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


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


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


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


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


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

c4154febb59980ab7ed9fc5feac71b8a.png

确定分界点:左边界点 q[l] 或 右边界点 q[r] 或 中间点 q[l + r >> 1],其中任意一个位置的值为 key

调整区间:小于等于 key 的值在左边,大于等于 key 的值在右边


递归处理左右区间,主要方法为使用双指针法分别在左边找不符合条件的值和右边不符合条件的值,然后对它们进行交换,递归处理从而使序列有序。




二、模板讲解


通用模板:

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)


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


   快排递归的截止条件是什么?


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


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


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


   如果取的分界点不同时,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)
                        当数据量足够大时,就会超时,我们简单画一下图,就拿这个序列来说


5611efb89e1c9edce1bd68dd8d4eb6f4.png



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


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


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


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


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


   问题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也走不了,也无法交换,总的循环条件又满足,所以就会造成 死循环 。这样写是 典型的错误 。


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

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 循环其实更好理解,就是先让 i 和 j 走一步。所以这种模板也可以:

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 处理的区间分别可以是什么?一些区间划分为什么不对?

常见情况 :


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


   当 分界点为左边界点 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测试我们的模板是否正确:


描述:


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


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


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


输入格式:


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


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


输出格式:


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


数据范围:


1≤ n ≤ 100000


输入样例

5
3 1 2 4 5


输出样例

1 2 3 4 5


代码

0528785f494f56a1d67c8e19b1d5aba7.png



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




四、加练 —— 第 K 个数


描述


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


输入格式:

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


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


输出格式:


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


数据范围:


   1 ≤ n ≤ 100000

   1 ≤ k ≤ n


输入样例

5 3
2 4 1 5 3

输出样例

3




思路:


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


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


   确定分界点:左边界点 q[l] 或 右边界点 q[r] 或 中间点 q[l + r >> 1],其中任意一个位置的值为 key

   调整区间:小于等于 key 的值在左边,大于等于 key 的值在右边

   根据左右区间元素个数,确定 k 所在区间,对单个区间进行递归


d311f6bc4a15f1bcf86204cc50355ba9.png


这里分情况讨论:


   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;
}







相关文章
|
1月前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
57 4
|
22天前
|
存储 搜索推荐 Python
用 Python 实现快速排序算法。
快速排序的平均时间复杂度为$O(nlogn)$,空间复杂度为$O(logn)$。它在大多数情况下表现良好,但在某些特殊情况下可能会退化为最坏情况,时间复杂度为$O(n^2)$。你可以根据实际需求对代码进行调整和修改,或者尝试使用其他优化策略来提高快速排序的性能
115 61
|
2月前
|
算法 API 计算机视觉
人脸识别笔记(一):通过yuface调包(参数量54K更快更小更准的算法) 来实现人脸识别
本文介绍了YuNet系列人脸检测算法的优化和使用,包括YuNet-s和YuNet-n,以及通过yuface库和onnx在不同场景下实现人脸检测的方法。
73 1
|
2月前
|
JSON 算法 数据可视化
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
这篇文章是关于如何通过算法接口返回的目标检测结果来计算性能指标的笔记。它涵盖了任务描述、指标分析(包括TP、FP、FN、TN、精准率和召回率),接口处理,数据集处理,以及如何使用实用工具进行文件操作和数据可视化。文章还提供了一些Python代码示例,用于处理图像文件、转换数据格式以及计算目标检测的性能指标。
74 0
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
|
2月前
|
数据可视化 搜索推荐 Python
Leecode 刷题笔记之可视化六大排序算法:冒泡、快速、归并、插入、选择、桶排序
这篇文章是关于LeetCode刷题笔记,主要介绍了六大排序算法(冒泡、快速、归并、插入、选择、桶排序)的Python实现及其可视化过程。
21 0
|
2月前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
34 0
|
16天前
|
算法
基于WOA算法的SVDD参数寻优matlab仿真
该程序利用鲸鱼优化算法(WOA)对支持向量数据描述(SVDD)模型的参数进行优化,以提高数据分类的准确性。通过MATLAB2022A实现,展示了不同信噪比(SNR)下模型的分类误差。WOA通过模拟鲸鱼捕食行为,动态调整SVDD参数,如惩罚因子C和核函数参数γ,以寻找最优参数组合,增强模型的鲁棒性和泛化能力。
|
22天前
|
机器学习/深度学习 算法 Serverless
基于WOA-SVM的乳腺癌数据分类识别算法matlab仿真,对比BP神经网络和SVM
本项目利用鲸鱼优化算法(WOA)优化支持向量机(SVM)参数,针对乳腺癌早期诊断问题,通过MATLAB 2022a实现。核心代码包括参数初始化、目标函数计算、位置更新等步骤,并附有详细中文注释及操作视频。实验结果显示,WOA-SVM在提高分类精度和泛化能力方面表现出色,为乳腺癌的早期诊断提供了有效的技术支持。
|
2天前
|
供应链 算法 调度
排队算法的matlab仿真,带GUI界面
该程序使用MATLAB 2022A版本实现排队算法的仿真,并带有GUI界面。程序支持单队列单服务台、单队列多服务台和多队列多服务台三种排队方式。核心函数`func_mms2`通过模拟到达时间和服务时间,计算阻塞率和利用率。排队论研究系统中顾客和服务台的交互行为,广泛应用于通信网络、生产调度和服务行业等领域,旨在优化系统性能,减少等待时间,提高资源利用率。
|
10天前
|
存储 算法
基于HMM隐马尔可夫模型的金融数据预测算法matlab仿真
本项目基于HMM模型实现金融数据预测,包括模型训练与预测两部分。在MATLAB2022A上运行,通过计算状态转移和观测概率预测未来值,并绘制了预测值、真实值及预测误差的对比图。HMM模型适用于金融市场的时间序列分析,能够有效捕捉隐藏状态及其转换规律,为金融预测提供有力工具。
下一篇
DataWorks