“掌握更多的快速排序技巧:三路划分、双路快排和非递归的深入理解”(上)

简介: “掌握更多的快速排序技巧:三路划分、双路快排和非递归的深入理解”(上)

快速排序是一种基于分治思想的排序算法,它能够以极快的速度将一个乱序的数组重新排列成有序的序列。不仅如此,快速排序还具有简洁的实现代码和良好的可扩展性,成为最受欢迎的排序算法之一。接下来,让我带你了解一下它的魅力吧!💫


快排基本思想:分而治之



快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法.


快速排序的核心思想是“分而治之”。它将一个未排序的数组划分为两个子数组,然后对这两个子数组分别进行排序,最后再将排好序的子数组合并在一起。这个过程在递归的帮助下不断重复,直到整个数组有序为止。这种将问题切分成更小的子问题处理的方法,使得快速排序能够高效地处理大规模的数据。🌟


快速排序的核心操作是“划分”,通常是选择一个基准元素,将返回的基准位置分为左右两边,数组中比基准元素小的移到基准元素的左边,比基准元素大的移到基准元素的右边。这个过程称为“分区”,它保证了在完成一轮分区后,基准元素的位置是确定了的。接下来,基准元素的左右子数组将分别作为新的子问题继续递归处理。直到所有元素都排列在相应位置上为止。💥



  1. div就在最终位置(排好序的位置)
  2. 左边有序,右边有序,整题就有序了
  3. 细节已写在代码注释上


// 假设按照升序对a 数组中[left, right]区间中的元素进行排序
void QuickSort(int* a, int left, int right)
{
//1、区间只有一个值
//2、区间不存在  就无需进行递归了
//递归的结束条件是子数组的长度小于等于1,此时子数组已经有序,不需要再进行排序。
 if(left >= right )
   return;
 // 按照基准值对a数组的 [left, right]区间中的元素进行划分
 int div = partSort(a, left, right); 
 //  返回的div已经确定了位置,无需在递归,只需要递归他的左右区间
 // [begin, div-1] div [div+1, end]
 // 划分成功后以div为边界形成了左右两部分 [left, div-1] 和 [div+1, right)
 // 递归排[left, div-1]
 QuickSort(a, left, div-1);
 // 递归排[div+1, right]
 QuickSort(a, div+1, right);
}


上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。


  • 将区间按照基准值划分为左右两半部分的常见方式有:

1.双路快排(三种方法)

 1. hoare版本

 2. 挖坑法版本

 3. 前后指针版本

2.三路划分


当然我们还会介绍我们的非递归方法完成快速排序.

以上的 非递归方法,双路快排,三路划分版本只需要学会两个即可 双路快排(任选之一方法),与非递归版本.


双路快排(三种方法)



hoare版本


Hoare版本的基本思想是::

选择序列的第一个元素作为基准值,并分别从序列的两端开始向中间遍历,交换不符合规则的元素,直到两个指针相遇。然后将基准值与指针相遇的位置的元素交换,此时基准值左侧的元素都小于等于它,右侧的元素都大于等于它。再对左右两个子序列分别递归进行同样的操作,直到排序完成。


单趟排序:

首先我们要确定第一个元素为基准值,命名为key.先从右边指针移动,查找比基准值(key)要少的值,在从左边指针开始移动,查找比基准值(key)要大的值,然后左右指针交换,直到两个指针相遇结束。将基准值与指针相遇的位置的元素交换. 此时基准值的左边元素都是比基准值小或者等于基准值,右边都比他大或者等于基准值。最后返回两个指针相遇位置下标



然后返回key,在递归他的左右区间,重复此过程,就完成整个排序了.蓝色标记的是基准值



  • 代码实现


// Hoare版本
int Part_Sort1(int* a, int left, int right)
{
    int midIndex = GetMidIndex(a, left, right); // 获取基准值的索引
    swap(&a[left], &a[midIndex]); // 将基准值移到数组最左边
    int keyi = left; // 基准值的索引
    while (left < right)
    {
        // 右边找小于基准值的元素
        while (left < right && a[right] >= a[keyi])//left < right防止越界
        {
            right--;
        }
        // 左边找大于基准值的元素
        while (left < right && a[left] <= a[keyi])//left < right防止越界
        {
            left++;
        }
        swap(&a[left], &a[right]); // 交换左右两边的元素
    }
    swap(&a[keyi], &a[right]); // 将基准值放到正确的位置上
    return right; // 返回基准值的索引
}
void QuickSort(int* a, int left, int right)
{
 if(left >= right )
   return;
 // 按照基准值对a数组的 [left, right]区间中的元素进行划分
 int div = part_Sort1(a, left, right); 
 //  返回的div已经确定了位置,无需在递归,只需要递归他的左右区间
 // [begin, div-1] div [div+1, end]
 // 划分成功后以div为边界形成了左右两部分 [left, div-1] 和 [div+1, right)
 // 递归排[left, div-1]
 QuickSort(a, left, div-1);
 // 递归排[div+1, right]
 QuickSort(a, div+1, right);
}


常见误区


  • 常见误区1: 为什么中间两个while循环中判断条件是 a[right] >= a[keyi] 和 a[left] <=

a[keyi] 还要继续做++ 和 – 的操作尼?

其实很好理解,举个案例就完全清晰了.

假如是 a[right] > a[keyi] 和 a[left] < a[keyi]


fffc992c03ee40b5b24fffdea571166a.png

假设 left 和 right 都达到了和key相同元素位置,就会造成一直交换,a[right] > a[keyi] 和 a[left] < a[keyi]没有机会进入循环做++和- -操作.最后造成死循环.


  • 常见误区2: 为什么left的起始位置不是在keyi的后面,即keyi+1.

如果是写成 left = keyi + 1 是起始位置那这样对吗.

跟上面一样举个案例就完全清晰了.

c8aa48508d734e4d92a4d2a4324e7ff3.png

此时left是从key+1出发的,right一路向左移动找比key小的,直到遇见了left.最后循环结束,将基准值与指针相遇的位置的元素交换.


3c0a8d17b367427a84f0d8187a8235a5.png


如图所示,right 和 left 在 keyi+1 的位置相遇,可能导致错误的交换。因此,要避免这个问题,确保 left 不从 keyi+1 开始。


  • 常见误区3: 能不能先从右边指针开始移动。

直接说结果: 如果是从左边做基准值是不行的,大家可以拿这个例子试试 {6,1,2,7,9,3,4,5,10,8}模拟一下过程.

结论:


1.左边做key,右边先走; 保障了相遇位置的值比key小

2.右边做key,左边先走; 保障了相遇位置的值比key大

我们说下这一种情况:左边做key,右边先走; 保障了相遇位置的值比key小 or 就是key

L和R相遇无非就是两种情况,L遇R和R遇l

1.情况一: L遇R,R是停下来,L在走,R先走,R停下来的位置一定比key小相遇的位置就是R   停下的位置,就一定比key要小.


5b09e1d86d944499887efa25974cc6dc.png


2.情况二: R遇L,在相遇这一轮,L就没动,R在移动,跟L相遇,相遇位置就是L的位置,L的位置就是key的位置 or 交换过一些轮次,相遇L位置一定比key小


0cc43790a5724d7e87ff54eeade5b572.png

其实大家举个反例推理一下思路会更加清晰,一目了然了.


挖坑法版本


挖坑法版本相比hoare版本更加好理解,坑也没有hoare版本那么多,虽然他叫挖坑法哈哈.但是他会自己填坑.其思路其实是差不多的.


基本思想:


  • 选择基准元素(通常是数组的第一个元素)。坑一开始的位置 (通常也是数组第一个元素

下标的位置) 。

  • 从数组的右端开始移动,找到一个比基准元素小的元素,将其填入所在的坑位置,然后自

己形成一个新坑。

  • 在从数组的左端开始移动,找到一个比基准元素大的元素,将其填入上一步形成的坑中。

然后自己在形成一个新的坑。

  • 重复步骤2和步骤3,直到左右指针相遇。
  • 循环结束后将基准元素放置到最后一个坑中。
  • 返回最后一个坑位置下标


单趟排序:


57e4a01c56a5467f97c304d6772f02bd.gif

  • 为了更仔细的观看,我自己手动模拟一下,


7ccdd40779314ba3b4522595debde22b.png


1c027b284f184735a587dd7ea4e63a3b.png

f047ffdf109a4ef79a049fbf6cfe62c6.png

778c34ac652c4d7fab8bf38212e8b1de.png



8293e31f2f8d445bbd342a5d0ab349b0.png



cb8f14e9b8364b76a2ec16c74c024573.png


2743d14cfefb4dfa930a264df025ddd3.png

c42d33cc815d4907bc915c0fbefcc0cf.png


通过这种方式,每一趟排序都会将一个基准元素放置到正确的位置上,并形成一个新的坑,然后再对左右两部分进行排序。这样不断重复,直到整个数组有序。挖坑法的关键在于通过交替填坑的方式实现元素的分割和排序。


每一趟的递归我就不写了这里,大家可以看看hoare版本那个图,只是单趟处理数据的方式不一样.


  • 代码实现


// 挖坑法
int Part_Sort2(int* a, int left, int right)
{
    int midIndex = GetMidIndex(a, left, right); // 获取基准值的索引
    swap(&a[left], &a[midIndex]); // 将基准值移到数组最左边
    int key = a[left]; // 基准值
    int tmp = left; // 坑的位置
    while (left < right)
    {
        // 右边找小于基准值的元素
        while (left < right && a[right] >= key)
        {
            right--;
        }
        a[tmp] = a[right]; // 将找到的元素放入坑中
        tmp = right;
        // 左边找大于基准值的元素
        while (left < right && a[left] <= key)
        {
            left++;
        }
        a[tmp] = a[left]; // 将找到的元素放入坑中
        tmp = left;
    }
    a[tmp] = key; // 将基准值放入坑中
    return tmp; // 返回基准值的索引
}
void QuickSort(int* a, int left, int right)
{
  if(left >= right )
    return;
 // 按照基准值对a数组的 [left, right]区间中的元素进行划分
    int div = part_Sort2(a, left, right); 
 //  返回的div已经确定了位置,无需在递归,只需要递归他的左右区间
 // [begin, div-1] div [div+1, end]
 // 划分成功后以div为边界形成了左右两部分 [left, div-1] 和 [div+1, right)
 // 递归排[left, div-1]
   QuickSort(a, left, div-1);
 // 递归排[div+1, right]
   QuickSort(a, div+1, right);
}
目录
相关文章
|
消息中间件 存储 大数据
一文读懂kafka的幂等生产者
一文读懂kafka的幂等生产者
|
5月前
|
弹性计算 人工智能 双11
2025年阿里云双十一优惠活动,云服务器租赁价格多少钱一年?
2025阿里云双11优惠来袭!轻量应用服务器38元/年起,ECS云服务器99元/年起,2核4G配置仅需199元/年,新老用户同享,续费不涨价。限量秒杀+大额代金券叠加,企业用户还可领专属补贴,最高享10万出海支持。点击直达活动页抢购→
744 5
|
9月前
|
人工智能 自然语言处理 数据可视化
AI-Compass LLM评估框架:CLiB中文大模型榜单、OpenCompass司南、RAGas、微软Presidio等构建多维度全覆盖评估生态系统
AI-Compass LLM评估框架:CLiB中文大模型榜单、OpenCompass司南、RAGas、微软Presidio等构建多维度全覆盖评估生态系统
 AI-Compass LLM评估框架:CLiB中文大模型榜单、OpenCompass司南、RAGas、微软Presidio等构建多维度全覆盖评估生态系统
|
数据采集 人工智能 自动驾驶
《突破AI数据标注高成本枷锁,势在必行!》
在人工智能快速发展的背景下,数据标注作为AI模型训练的基础,其高成本问题成为制约行业发展的关键因素。主要体现在人力、时间和管理成本上,尤其是在复杂领域和大规模数据处理中。为解决这一难题,行业探索了多种创新方案:技术层面,自动化标注工具与半监督学习技术显著提升效率;商业模式上,分布式众包和专业平台降低运营成本;人才培养方面,校企合作与激励机制优化标注质量。尽管仍存挑战,但通过多方协同,有望推动AI数据标注行业的高效发展,助力AI技术广泛应用。
571 9
|
机器学习/深度学习 人工智能 自然语言处理
结合DeepSeek-R1强化学习方法的视觉模型!VLM-R1:输入描述就能精确定位图像目标
VLM-R1 是基于强化学习技术的视觉语言模型,通过自然语言指令精确定位图像目标,支持复杂场景推理与高效训练。
1005 0
element UI 组件封装--搜索表单(含插槽和内嵌组件)
element UI 组件封装--搜索表单(含插槽和内嵌组件)
509 5
|
Java Go C#
编程语言C#、C++、Java、Python、go 选择哪个好?
我想说的是,不论选择哪种编程语言,决定选择的都是你最终的目的,做选择之前,先充分调研每一个选择项,再做选择思路就会非常清晰了。
629 3
|
机器学习/深度学习 人工智能 监控
探索人工智能在图像识别领域的创新应用
【5月更文挑战第25天】随着深度学习技术的飞速发展,人工智能(AI)在图像识别领域取得了重大进展。本文将深入探讨人工智能如何通过先进的算法和模型改进图像识别能力,并分析其在不同行业中的应用前景。我们将重点讨论卷积神经网络(CNN)与循环神经网络(RNN)的结合使用,以及生成对抗网络(GAN)在提高图像质量方面的作用。此外,文中还将提及数据增强、迁移学习等策略对提升模型泛化性能的重要性。
|
存储 安全 数据库
SNMP(简单网络管理协议)介绍
SNMP(简单网络管理协议)介绍
639 0
|
JSON 安全 API
API开发实战:从设计到部署的全流程指南
在数字化转型中,API成为系统集成的关键。本文引导读者逐步实践API开发: 1. 设计阶段确定需求,选择RESTful风格,例如天气查询API(/api/weather/{city}),返回JSON数据。 2. 使用Python和Flask实现API,处理GET请求,返回城市天气信息。 3. 进行测试,如用curl请求`http://localhost:5000/api/weather/Beijing`。 4. 文档化API,借助Flask-RESTPlus自动生成文档。 5. 部署到Heroku,创建`Procfile`,通过`heroku`命令推送代码。 【6月更文挑战第28天】
2587 0