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

简介: 快排

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 里的例子,比如要把一摞卷子排好序,那用并归排序的思想是怎么做的呢?
173 0
齐姐漫画:排序算法(二)之「 归并排序」和「外排序」
|
搜索推荐 算法
齐姐漫画:排序算法(一)
借用《算法导论》里的例子,就是我们打牌的时候,每新拿一张牌都会把它按顺序插入,这,其实就是插入排序。
152 0
齐姐漫画:排序算法(一)
|
29天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
6天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
|
14天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
15天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
16天前
|
存储 算法 决策智能
基于免疫算法的TSP问题求解matlab仿真
旅行商问题(TSP)是一个经典的组合优化问题,目标是寻找经过每个城市恰好一次并返回起点的最短回路。本文介绍了一种基于免疫算法(IA)的解决方案,该算法模拟生物免疫系统的运作机制,通过克隆选择、变异和免疫记忆等步骤,有效解决了TSP问题。程序使用MATLAB 2022a版本运行,展示了良好的优化效果。
|
15天前
|
机器学习/深度学习 算法 芯片
基于GSP工具箱的NILM算法matlab仿真
基于GSP工具箱的NILM算法Matlab仿真,利用图信号处理技术解析家庭或建筑内各电器的独立功耗。GSPBox通过图的节点、边和权重矩阵表示电气系统,实现对未知数据的有效分类。系统使用MATLAB2022a版本,通过滤波或分解技术从全局能耗信号中提取子设备的功耗信息。
|
15天前
|
机器学习/深度学习 算法 5G
基于MIMO系统的SDR-AltMin混合预编码算法matlab性能仿真
基于MIMO系统的SDR-AltMin混合预编码算法通过结合半定松弛和交替最小化技术,优化大规模MIMO系统的预编码矩阵,提高信号质量。Matlab 2022a仿真结果显示,该算法能有效提升系统性能并降低计算复杂度。核心程序包括预编码和接收矩阵的设计,以及不同信噪比下的性能评估。
34 3
|
26天前
|
人工智能 算法 数据安全/隐私保护
基于遗传优化的SVD水印嵌入提取算法matlab仿真
该算法基于遗传优化的SVD水印嵌入与提取技术,通过遗传算法优化水印嵌入参数,提高水印的鲁棒性和隐蔽性。在MATLAB2022a环境下测试,展示了优化前后的性能对比及不同干扰下的水印提取效果。核心程序实现了SVD分解、遗传算法流程及其参数优化,有效提升了水印技术的应用价值。
下一篇
无影云桌面