首页 > 编程语言 >算法 | 快速排序详解

算法 | 快速排序详解

时间:2023-05-07 15:12:12浏览次数:40  
标签:nums int 区域 元素 算法 详解 key 排序 left

1 快速排序基本思想

从待排序记录序列中选取一个记录(随机选取)作为基点,其关键字设为key,然后将其余关键字小于key的记录移到前面,而将关键字大于key的记录移到后面,结果将待排序记录序列分为两个子表,最后将关键字key的记录插入到分界线的位置。这个过程称为一趟快速排序

经过这一趟划分之后,就可以关键字key为界将整个待排序序列划分为两个子表,前面的子表均不大于key,后面的子表均不小于key。继续对分割后的子表进行上述划分,直至所有子表的表长不超过1为止,此时待排序的记录就成为了一个有序序列(执行多趟快速排序)。

图片

Q:如何实现一趟快速排序呢?

A1:一般实现。

先选取基点key=4,然后创建2个临时数组tmp1和tmp2分别用于保存“<4的元素”和“≥4的元素”,然后遍历原数组,将相应元素存于临时数组,最后,按照tmp1→key→tmp2的顺序将各元素填充回原数组。

但是这种做法效率并不高,因为需要开辟新的内存空间,最后还需要将元素填回,非常耗时。

A2:不开辟新内存,一边遍历一边整理。

step1:选取首元素作为基点key,将数组划分成多个片段。

  • 橙色区域:基点;
  • 黄色&绿色区域:已处理序列;
    · 黄色区域:元素均<key;
    · 绿色区域:元素均≥key,且绿色区域紧挨着黄色区域;
  • 灰色区域:待处理序列。
    即:
  • nums[left]:基点key;
  • nums[left+1...k):黄色区域,<key;
  • nums[k...i):绿色区域,≥key;
  • nums[i...n):灰色区域,待处理序列;
    初始时:i=left+1,k=left+1,保证初始时刻黄色区域和绿色区域均为空。

图片

step2:遍历灰色区域元素。

  • 若当前元素nums[i]≥key,表示属于绿色区域,将当前元素追加至绿色区域尾部(i++即可);
    图片

  • 若当前元素nums[i]<key,表示属于黄色区域,将当前元素与绿色区域首元素进行交换,然后k++(表示黄色区域边界右扩且绿色区域边界左缩)。

交换之后,黄色区域和绿色区域仍满足各自区域内元素要么全<key,要么全≥key。
图片

step3:灰色区域遍历结束,将基点与黄色区域尾元素(即nums[k-1])进行交换。

交换之后,黄色区域和绿色区域仍满足各自区域内元素要么全<key,要么全≥key。
此时,完成一趟快速排序,基点对应下标为k-1,即key_index = k-1。
图片

step4:对黄色区域和绿色区域分别进行快速排序,key_index = k-1。

  • 黄色区域:nums[left ... key_index-1];
  • 绿色区域:nums[key_index+1 ... right]。

2 代码实现

class Solution {
public:
    int partition(vector<int>& nums, int left, int right){
        int privot = nums[left];
        int i = left+1;
        int k = i; // [left+1...k):<privot; [k...i):≥privot
        for(; i<=right; i++){
            if(nums[i] < privot){
                swap(nums[i], nums[k++]);
            }
        }
        swap(nums[left], nums[k-1]);
        return k-1;
    } 
    void quickSort(vector<int>& nums, int left, int right){
        if(left >= right){
            return;
        }
        int privotIndex = partition(nums, left, right);
        quickSort(nums, left, privotIndex-1);
        quickSort(nums, privotIndex+1, right);
    }
    vector<int> sortArray(vector<int>& nums) {
        quickSort(nums, 0, nums.size()-1);
        return nums;
    }
};

有效性测试

LeeCode.912. 排序数组

图片

针对近乎有序的数组,将超出时间限制。

3 随机选取基点

Q:若选取首个元素作为基点,那么划分操作(即partition)在顺序数组或逆序数组上的效果将很差(如上面的超出时间限制)。

A:

  • 拆分的子问题只比原来减少了1个元素;
  • 每一次划分只能确定一个元素的位置;
  • 导致递归树高度增加(非常不平衡、递归树倾斜);
  • 快速排序退化成选择排序,时间复杂度为O(N^2)。

解决:通过随机选取基点,打破原数组的有序性。

代码实现

#include<time.h>
#include<random>  
int partition(vector<int>& nums, int left, int right){
      srand((unsigned)time(NULL)); // 设置随机种子
      int randomIndex = rand() % (right - left + 1) + left;
      swap(nums[randomIndex], nums[left]);
      int privot = nums[left];
      int i = left+1;
      int k = i; // [left+1...k):<privot [k...i):≥privot
      for(; i<=right; i++){
          if(nums[i] < privot){
              swap(nums[i], nums[k++]);
          }
      }
      swap(nums[left], nums[k-1]);
      return k-1;
  } 

有效性测试:LeeCode.912. 排序数组
图片

针对原数组近乎有序的测试用例已经通过,但是针对含有大量重复元素的数组,依旧超时。

4 双路快排和三路快排

Q:针对数组含有大量重复元素(甚至所有元素均相同)的场景,随机选取基点无效!

A:由于存在大量重复元素,多次随机选取基点可能选中的元素值没有发生变化,也就未打破原数组的顺有序性,所以依旧超时。

解决方法如下:

图片

4.1 双路快排

双路快排的目的:

将与基点相等的元素均匀地分配到数组两侧(即黄色区域和绿色区域,分别代表≤pivot和≥pivot的元素集合)。

Q:为什么黄色区域和绿色区域都可以=pivot,即二者区域内元素取值有重合呢?
A:因为双路快排是要将与基点值相等的元素均匀地分布到黄色区域和绿色区域,也就是使得黄色和绿色各自区域内均存在多个值=pivot的元素
• 若二者取值不重合,如黄色区域代表≤pivot,绿色区域代表>pivot,那么经一趟快排之后,值=pivot的元素将全部在黄色区域内。
• 若二者取值重合,可保证值=pivot的元素不会扎堆在某一单色区域。

具体步骤:

step1:选取首元素作为基点key(随机选取基点),将数组划分成多个片段。

  • 橙色区域:基点;
  • 黄色&绿色区域:已处理序列,分别位于数组的最左侧(忽略基点)和最右侧;
    · 黄色区域:元素均≤key;
    · 绿色区域:元素均≥key;
  • 灰色区域:待处理序列。
    即:
  • nums[left]:基点key;
  • nums[left+1...i):黄色区域,≤key;
  • nums(j...right]:绿色区域,≥key;
  • nums[i...j]:灰色区域,待处理序列;
    初始时,i = left+1,j = right,保证初始时刻黄色区域和绿色区域为空。

图片

step2:通过指针i和指针j双向遍历灰色区域元素。

  • 正向遍历:
    • 若当前元素nums[i]<key,表示属于黄色区域,将当前元素追加至黄色区域尾部(i++即可);
    • 若当前元素nums[i]≥key,表示属于绿色区域,i暂停遍历,等待反向遍历至合适位置(即等j停住)。
  • 反向遍历:
    • 若当前元素nums[j]>key,表示属于绿色区域,将当前元素加入绿色区域头部(j--即可);
    • 若当前元素nums[j]≤key,表示属于黄色区域,j暂停遍历,等待正向遍历至合适位置(即等i停住)。
  • 当i和j都停住时,代表i指向的元素应该放入绿色区域,j指向的元素应该放入黄色区域,遂交换nums[i]和nums[j],然后i++,j--,继续遍历。

Q:为什么i和j扫描到nums[i]=key或nums[j]=key时,也要停住,这种情况不是满足≤key或≥key
A:因为双路快排是要将与基点值相等的元素均匀地分布到黄色区域和绿色区域,不仅要使得黄色和绿色各自区域内均存在多个值=pivot的元素,还要尽可能使得每个颜色区域内值=pivot的元素离散分布不要连续扎堆,否则在后续对子表继续进行快排时,就可能会出现值=pivot的元素连续扎堆的情况。
• i和j扫描到nums[i]=key或nums[j]=key时,不停住,只有当nums[i]>key和nums[j]<key时才停住:出现值=pivot的元素连续扎堆的情况;
• i和j扫描到nums[i]=key或nums[j]=key时,停住然后二者交换,实现值=pivot的元素在各颜色区域内离散分布。

step3:灰色区域遍历结束,此时i≥j,将基点与nums[j]进行交换。

  • i=j时:

由于i停住的条件是nums[i]≥key,j停住的条件是nums[j]≤key,因此,当某一时刻i和j都停住且i=j时,此时两个指针指向的元素值必定等于key,所以将基点与其交换之后,基点之前的黄色区域相当于在头部加了一个值为基点值的元素,黄色区域依旧连续。
图片

  • i>j时:

此时i超出黄色区域指向绿色区域首元素,j超出绿色区域指向黄色区域尾元素。将基点与黄色区域尾元素进行交换,交换之后黄色区域依旧连续,如下图。
图片

否则,若将基点与nums[i]交换,将会破坏绿色区域连续性,如下图。
图片

代码实现:

int partition(vector<int>& nums, int left, int right){
    /* step1: 随机选取基点privot */
    srand((unsigned)time(NULL)); // 设置随机种子
    int randomIndex = rand() % (right - left + 1) + left;
    swap(nums[randomIndex], nums[left]);
    int privot = nums[left];

    /* step2: 执行一趟快排 */
    int i = left+1;
    int j = right;
    while(1){
        while(i <= j && nums[i] < privot){
            i++;
        }
        while(i <= j && nums[j] > privot){
            j--;
        }
        if(i >= j){
            break;
        }
        swap(nums[i], nums[j]);
        i++;
        j--;
    }
    /* step3: 将基点放在分界线处 */
    swap(nums[left], nums[j]);
    return j;
} 

4.2 三路快排

三路快排目的:

将与基点相等的元素集中放置在数组的中央位置,即实现下图效果。这样,相较于二路快排,三路快排经过1次快速排序就可以将多个元素放在它正确的位置上(多个与基点值相等的元素挤到了数组中央),而二路快排每次仅能确定一个元素在正确位置上。

图片

具体步骤:

step1:选取首元素作为基点key(随机选取基点),将数组划分成多个片段。

  • nums[left]:基点key;
  • nums[left+1...lt):黄色区域,<key;
  • nums(gt...right]:绿色区域,>key;
  • nums[i...gt]:灰色区域,待处理序列;

初始时,lt = i = gt = left + 1,保证初始时刻黄色、橙色、绿色区域均为空。
图片

step2:遍历灰色区域。

  • 若nums[i]=key,将其加入橙色区域,即i++即可;

  • 若nums[i]<key,需要将其加入黄色区域尾部,通过交换nums[i]和nums[lt]实现,然后lt++,i++;
    图片

  • 若nums[i]>key,需要将其加入绿色区域头部,通过交换nums[i]和nums[gt]实现,然后gt--;
    图片

代码实现:

#include<time.h>
#include<random>
class Solution {
public:
    void quickSort(vector<int>& nums, int left, int right){
        if(left >= right){
            return;
        }
        /* step1: 随机选取基点privot */
        srand((unsigned)time(NULL)); // 设置随机种子
        int randomIndex = rand() % (right - left + 1) + left;
        swap(nums[randomIndex], nums[left]);
        int privot = nums[left];
        /* step2: 执行一趟快排 */
        int i = left + 1;
        int lt = left + 1, gt = right;
        while(i <= gt){
            if(nums[i] == privot){
                i++;
            }else if(nums[i] < privot){
                swap(nums[i++], nums[lt++]);
            }else{
                swap(nums[i], nums[gt--]);
            }
        }
        /* step3: 将基点放在分界线处 */
        swap(nums[left], nums[lt-1]);
        /* step4: 对左右子表进行快排 */
        quickSort(nums, left, lt-2);
        quickSort(nums, gt+1, right);
    }
    vector<int> sortArray(vector<int>& nums) {
        quickSort(nums, 0, nums.size()-1);
        return nums;
    }
};

4.3 有效性测试

LeeCode.912. 排序数组

图片

5 三路快排partition思路的应用

问题描述:

LeetCode.75. 颜色分类

算法思想:

将数组分组,如下图所示:

图片

  • nums[left...k)内元素均==0;

  • nums[k...i)内元素均==1;

  • nums[i...j]为待处理序列;

  • nums(j...right]内元素均==2。
    初始时,i=left, k=left, j=right,保证初始时刻黄色、橙色、绿色三个区域均为空,然后遍历灰色区域直至结束:

  • 若nums[i]==1,应将其追加至橙色区域尾部,i++即可;

  • 若nums[i]==0,应将其追加至黄色区域尾部,通过交换nums[i]和nums[k]实现,交换完毕需要执行i++, k++;

  • 若nums[i]==2,应将其插入到绿色区域头部,通过交换nums[i]和nums[j]实现,交换完毕执行j--。

代码实现:

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        int i = 0, j = right, k = left;
        while(i <= j){
            if(nums[i] == 1){
                i++;
            }else if(nums[i] == 0){
                swap(nums[i++], nums[k++]);
            }else{
                swap(nums[i], nums[j--]);
            }
        }
    }
};

图片

标签:nums,int,区域,元素,算法,详解,key,排序,left
From: https://www.cnblogs.com/lijiuliang/p/17379349.html

相关文章

  • 基础算法
    位运算拆解:例如龟速乘和快速幂。状态压缩:可以用一个数字表示一个状态,不够长还可以用bitset。龟速乘通过对数字的每一位进行拆分,将乘法变成加法。代码#include<bits/stdc++.h>usingnamespacestd;typedeflonglongll;llmul(lla,llb,llp){ llans=0; while(b){ ......
  • C++虚函数详解:多态性实现原理及其在面向对象编程中的应用
    在面向对象的编程中,多态性是一个非常重要的概念。多态性意味着在不同的上下文中使用同一对象时,可以产生不同的行为。C++是一种面向对象的编程语言,在C++中,虚函数是实现多态性的关键什么是虚函数虚函数是一个在基类中声明的函数,它可以被子类重写并提供不同的实现。在C++中,使用关......
  • DVWA配置详解
    1.首先把dvwa文件夹放在小皮面板的www目录下之后打开c:\phpstudy-pro\www\DVWA-master\config之后把config.inc.php.dist重命名为config.inc.php如以下图片,2.之后进去修改一下mysql数据库的密码,为root因为系统默认密码就是root3.在之后去phpstudy_pro目录下的extensions目录下......
  • Java IO流详解
    文章和代码已经归档至【Github仓库:https://github.com/timerring/java-tutorial】或者公众号【AIShareLab】回复java也可获取。文件文件,对我们并不陌生,文件是保存数据的地方。文件在程序中是以流的形式来操作的。流:数据在数据源(文件)和程序(内存)之间经历的路径输入流:数据从数......
  • css3 flex弹性布局详解
    一、flexbox弹性盒子2009年,W3C提出了一种新的方案----Flex布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。二、基本概念Flex是FlexibleBox的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性......
  • 【二分查找】LeetCode 33. 搜索旋转排序数组思路
    题目链接33.搜索旋转排序数组思路思路都在注释里代码classSolution{publicintsearch(int[]nums,inttarget){intlen=nums.length;if(len==0){return-1;}intleft=0,right=len-1;//1.......
  • m基于POCS算法的空域序列图像超分辨率重建matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:     2.算法涉及理论知识概要       随着信息处理技术和视觉通信技术的高速发展,人们获取的知识量爆炸式增长,因此迫切的要求完善的信息处理技术为人们提供更加方便、快捷服务。数字图像及及其相关技术是信息处理技......
  • 为什么说程序=算法+数据结构
    听到`程序=数据结构+算法`,可能很多同学觉得不太好理解。那么如果我说`程序=变量+业务`,是不是就好理解了。其实我们开发一款应用程序,就是定义一些变量,然后围绕这些变量进行业务的开展。理解了,我们再来说。变量只是统称,我们可能针对不同的业务使用不同的变量类型(数据结构),来......
  • mysql 查询根据外部数据排序
    1、FIELD函数FIELD是一个MySQL函数,用于返回一个或多个表达式在列表中的位置。它可以用于对查询结果进行排序或筛选。2、根据外部数据排序在MySQL中,可以使用ORDERBYFIELD()函数根据外部数据对查询结果进行排序。FIELD()函数可以接受一个或多个参数,并返回第一个参数在......
  • 算法之高级算法
    关键字:算法之高级算法高级算法包括:动态规划,贪心算法(1)动态规划动态规划算法是通过整合子问题来解决整个问题的,也就是说通过子问题的求解,可以得出次问题的解。动态规划关键是找出问题求解方程,即找到子问题和问题解的关系。例如:跳台阶问题题目:一......