齐姐漫画:排序算法(三)之「快排」

简介: 快排

image.png

算法


首先选一个基准 pivot,然后过一遍数组,


  • 把小于 pivot 的都挪到 pivot 的左边,


  • 把大于 pivot 的都挪到 pivot 的右边。


这样一来,这个 pivot 的位置就确定了,也就是排好了 1 个元素。


然后对 pivot 左边 ? 的数排序,


对 pivot 右边 ? 的数排序,


就完成了。


那怎么排左边和右边?


答:同样的方法。


所以快排也是用的分治法的思想。


「分」


选择一个 pivot,就把问题分成了


  • pivot 左边


  • pivot 右边


这两个问题。


「治」


就是最开始描述的方法,直到每个区间内没有元素或者只剩一个元素就可以返回了。


「合」


放在一起自然就是。


但是如何选择这个 pivot?


取中间的?


取第一个?


取最后一个?


举个例子:{5, 2, 1, 0, 3}.


比如选最后一个,就是 3.


然后我们就需要把除了 3 之外的数分成「比 3 大」和「比 3 小」的两部分,这个过程叫做 partition(划分)


这里我们仍然使用「挡板法」的思想,不用真的弄两个数组来存这两部分,而是用两个挡板,把区间划分好了。


我们用「两个指针」(就是挡板)把数组分成「三个区间」,那么


  • 左边的区间用来放小于 pivot 的元素;


  • 右边的区间用来放大于 pivot 的元素;


  • 中间是未排序区间。


image.png


那么初始化时,我们要保证「未排序区间」能够包含除了 3 之外的所有元素,所以


  • 未排序区间 = [i, j]


这样左边和右边的区间就成了:


  • [0, i):放比 3 小的数;


  • (j, array.length -2]:放比 3 大的数


注意 ⚠️ i, j 是不包含在左右区间里的呢。


那我们的目的是 check 未排序区间里的每一个数,然后把它归到正确的区间里,以此来缩小未排序区间,直到没有未排序的元素。


从左到右来 check:


Step1.


5 > 3, 所以 5 要放在右区间里,所以 5 和 j 指向的 0 交换一下:


image.png


这样 5 就排好了,指针 j --,这样我们的未排序区间就少了一个数;


Step2.


0 < 3,所以就应该在左边的区间,直接 i++;


image.png


Step3.


2 < 3,同理,i++;


image.png


Step4.


1 < 3,同理,i++;


image.png


所以当两个指针错位的时候,我们结束循环。


但是还差了一步,3 并不在正确的位置上呀。所以还要把它插入到两个区间中间,也就是和指针 i 交换一下。


image.png


image.png


齐姐声明:这里并不鼓励大家把 pivot 放最左边。


基本所有的书上都是放右边,既然放左右都是一样的,我们就按照大家默认的、达成共识的来,没必要去“标新立异”。


就比如围棋的四个星位,但是讲究棋道的就是先落自己这边的星位,而不是伸着胳膊去够对手那边的。


那当我们把 pivot 换回到正确的位置上来之后,整个 partition 就结束了。


之后就用递归的写法,对左右两边排序就好了。


最后还有两个问题想和大家讨论一下:


  1. 回到我们最初选择 pivot的问题,每次都取最后一个,这样做好不好?


答:并不好。


因为我们是想把数组分割的更均匀均匀的时间复杂度更低;但是如果这是一个有序的数组,那么总是取最后一个是最不均匀的取法。


所以应该随机取 pivot,这样就避免了因为数组本身的特点总是取到最值的情况。


  1. pivot 放在哪


随机选取之后,我们还是要把这个 pivot 放到整个数组的最右边,这样我们的未排序区间才是连续的,否则每次走到 pivot 这里还要想着跳过它,心好累哦。


class Solution {
  public void quickSort(int[] array) {
    if (array == null || array.length <= 1) {
      return;
    }
    quickSort(array, 0, array.length - 1);
  }
  private void quickSort(int[] array, int left, int right) {
    // base case
    if (left >= right) {
      return;
    }
    // partition
    Random random = new Random(); // java.util 中的随机数生成器
    int pivotIndex = left + random.nextInt(right - left + 1);
    swap(array, pivotIndex, right);
    int i = left;
    int j = right-1;
    while (i <= j) {
      if (array[i] <= array[right]) {
        i++;
      } else {
        swap(array, i, j);
        j--;
      }
    }
    swap(array, i, right);
    //「分」
    quickSort(array, left, i-1);
    quickSort(array, i+1, right);
  }
  private void swap(int[] array, int x, int y) {
    int tmp = array[x];
    array[x] = array[y];
    array[y] = tmp;
  }
}


这里的时空复杂度和分的是否均匀有很大关系,所以我们分情况来说:


1. 均分


时间复杂度


如果每次都能差不多均匀分,那么


  • 每次循环的耗时主要就在这个 while 循环里,也就是 O(right - left);


  • 均分的话那就是 logn 层;


  • 所以总的时间是 O(nlogn).


image.png


空间复杂度


  • 递归树的高度是 logn,


  • 每层的空间复杂度是 O(1),


  • 所以总共的空间复杂度是 O(logn).


2. 最不均匀


如果每次都能取到最大/最小值,那么递归树就变成了这个样子:


image.png


时间复杂度


如上图所示:O(n^2)


空间复杂度


这棵递归树的高度就变成了 O(n).


3. 总结


实际呢,大多数情况都会接近于均匀的情况,所以均匀的情况是一个 average case.

为什么看起来最好的情况实际上是一个平均的情况呢?


因为即使如果没有取到最中间的那个点,比如分成了 10% 和 90% 两边的数,那其实每层的时间还是 O(n),只不过层数变成了以 9 为底的 log,那总的时间还是 O(nlogn).

所以快排的平均时间复杂度是 O(nlogn)。


稳定性


那你应该能看出来了,在 swap 的时候,已经破坏了元素之间的相对顺序,所以快排并不具有稳定性。


这也回答了我们开头提出的问题,就是


  • 为什么对于primitive type 使用快排


  • 因为它速度最快;


  • 为什么对于object 使用归并


  • 因为它具有稳定性且快。


以上就是快排的所有内容了,也是很常考的内容哦!那下一篇文章我会讲几道从快排引申出来的题目,猜猜是什么??


image.png

目录
相关文章
|
搜索推荐 算法 IDE
齐姐漫画:排序算法(二)之「 归并排序」和「外排序」
那我们借用 cs50 里的例子,比如要把一摞卷子排好序,那用并归排序的思想是怎么做的呢?
186 0
齐姐漫画:排序算法(二)之「 归并排序」和「外排序」
|
搜索推荐 算法
齐姐漫画:排序算法(一)
借用《算法导论》里的例子,就是我们打牌的时候,每新拿一张牌都会把它按顺序插入,这,其实就是插入排序。
158 0
齐姐漫画:排序算法(一)
|
13天前
|
机器学习/深度学习 算法
基于改进遗传优化的BP神经网络金融序列预测算法matlab仿真
本项目基于改进遗传优化的BP神经网络进行金融序列预测,使用MATLAB2022A实现。通过对比BP神经网络、遗传优化BP神经网络及改进遗传优化BP神经网络,展示了三者的误差和预测曲线差异。核心程序结合遗传算法(GA)与BP神经网络,利用GA优化BP网络的初始权重和阈值,提高预测精度。GA通过选择、交叉、变异操作迭代优化,防止局部收敛,增强模型对金融市场复杂性和不确定性的适应能力。
145 80
|
1天前
|
机器学习/深度学习 数据采集 算法
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
本项目基于MATLAB2022a实现时间序列预测,采用CNN-GRU-SAM网络结构。卷积层提取局部特征,GRU层处理长期依赖,自注意力机制捕捉全局特征。完整代码含中文注释和操作视频,运行效果无水印展示。算法通过数据归一化、种群初始化、适应度计算、个体更新等步骤优化网络参数,最终输出预测结果。适用于金融市场、气象预报等领域。
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
|
1天前
|
算法
基于龙格库塔算法的锅炉单相受热管建模与matlab数值仿真
本设计基于龙格库塔算法对锅炉单相受热管进行建模与MATLAB数值仿真,简化为喷水减温器和末级过热器组合,考虑均匀传热及静态烟气处理。使用MATLAB2022A版本运行,展示自编与内置四阶龙格库塔法的精度对比及误差分析。模型涉及热传递和流体动力学原理,适用于优化锅炉效率。
|
6天前
|
机器学习/深度学习 算法
基于遗传优化的双BP神经网络金融序列预测算法matlab仿真
本项目基于遗传优化的双BP神经网络实现金融序列预测,使用MATLAB2022A进行仿真。算法通过两个初始学习率不同的BP神经网络(e1, e2)协同工作,结合遗传算法优化,提高预测精度。实验展示了三个算法的误差对比结果,验证了该方法的有效性。
|
9天前
|
机器学习/深度学习 数据采集 算法
基于PSO粒子群优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
本项目展示了基于PSO优化的CNN-GRU-SAM网络在时间序列预测中的应用。算法通过卷积层、GRU层、自注意力机制层提取特征,结合粒子群优化提升预测准确性。完整程序运行效果无水印,提供Matlab2022a版本代码,含详细中文注释和操作视频。适用于金融市场、气象预报等领域,有效处理非线性数据,提高预测稳定性和效率。
|
5天前
|
算法
基于梯度流的扩散映射卡尔曼滤波算法的信号预处理matlab仿真
本项目基于梯度流的扩散映射卡尔曼滤波算法(GFDMKF),用于信号预处理的MATLAB仿真。通过设置不同噪声大小,测试滤波效果。核心代码实现数据加载、含噪信号生成、扩散映射构建及DMK滤波器应用,并展示含噪与无噪信号及滤波结果的对比图。GFDMKF结合非线性流形学习与经典卡尔曼滤波,提高对非线性高维信号的滤波和跟踪性能。 **主要步骤:** 1. 加载数据并生成含噪测量值。 2. 使用扩散映射捕捉低维流形结构。 3. 应用DMK滤波器进行状态估计。 4. 绘制不同SNR下的轨迹示例。
|
10天前
|
机器学习/深度学习 算法 索引
单目标问题的烟花优化算法求解matlab仿真,对比PSO和GA
本项目使用FW烟花优化算法求解单目标问题,并在MATLAB2022A中实现仿真,对比PSO和GA的性能。核心代码展示了适应度计算、火花生成及位置约束等关键步骤。最终通过收敛曲线对比三种算法的优化效果。烟花优化算法模拟烟花爆炸过程,探索搜索空间,寻找全局最优解,适用于复杂非线性问题。PSO和GA则分别适合快速收敛和大解空间的问题。参数调整和算法特性分析显示了各自的优势与局限。
|
4天前
|
算法 5G
基于MSWA相继加权平均的交通流量分配算法matlab仿真
本项目基于MSWA(Modified Successive Weighted Averaging)相继加权平均算法,对包含6个节点、11个路段和9个OD对的交通网络进行流量分配仿真。通过MATLAB2022A实现,核心代码展示了迭代过程及路径收敛曲线。MSWA算法在经典的SUE模型基础上改进,引入动态权重策略,提高分配结果的稳定性和收敛效率。该项目旨在预测和分析城市路网中的交通流量分布,达到用户均衡状态,确保没有出行者能通过改变路径减少个人旅行成本。仿真结果显示了27条无折返有效路径的流量分配情况。

热门文章

最新文章