快速排序(C/C++实现)—— 简单易懂系列

前言

排序作用的重要性是不言而喻的,例如成绩的排名、预约时间的先后顺序、不同路程的消耗与利润等。快排可以实现O(n * logn)的时间复杂度,O(logn)的空间复杂度来实现排序【虽然结果是不稳定的】。

算法思想

快速排序实际上是采用分治的思想,每次迭代在当前区间中选取一个数作为哨兵,通过一系列的交换操作将当前区间分为左区间和右区间【使得左区间的值全部小于等于哨兵,右区间的值全部大于等于哨兵】。然后再对左区间、右区间执行这种划分区间的策略操作,当区间的长度为1时停止。等到所有分治的区间长度都为1时,此时的原数组就已经是一个排好序的数组了。

具体步骤

假设数组名称为q,具体步骤如下:

  1. 如果区间长度小于等于1了,则结束循环。否则执行下一步。
  2. 先从本区间中取出第一个数作为哨兵 mid,即令mid等于本区间最左端元素的值。执行下一步。
  3. i等于本区间最左端元素在原数组中的下标j等于本区间最右端元素在原数组中的下标。执行下一步。
  4. 判断 i < j是否成立,如果满足,则执行下一步。否则跳转到第9点
  5. 判断q[j] >= mid && i < j是否成立,如果满足,则j向左移动一位【j--】,再次执行本轮【即本次步骤是循环】。否则执行下一步
  6. q[i] = q[j]。执行下一步。【目的:进行元素移动,保证右区间的值都是大于等于哨兵的值,此时 j右侧的值都不小于哨兵的值,且一定会有下一步来使得 q[j]的值大于等于哨兵的值】
  7. 判断q[i] <= mid && i < j是否成立,如果满足,则i向右移动一位【i++】,再次执行本轮【即本次步骤是循环】。否则执行下一步
  8. q[j] = q[i]。跳转到第4点【目的:进行元素移动,保证左区间的值都是小于等于哨兵的值,此时 i左侧的值都不大于哨兵的值,且一定会有下一步来使得 q[i]的值小于等于哨兵的值】【第4点~第8点是一轮大循环】
  9. q[i] = mid。执行下一步。【循环结束后,i的位置即是哨兵的位置,此时令q[i] = mid即可。这一步操作保证了第6、8点担忧的地方,即这里一定可以使得最终的 q[i]、q[j]等于哨兵的值。
  10. 划分两个区间【本区间左端点,i - 1】,【i + 1, 本区间右端点】,将这两个区间再次执行第一步的操作。【整个步骤是快排的分治操作的循环】

图表演示

假设我们拥有一个数组:a,长度为:5,内容为:3 1 2 4 5,需要对其进行从小到大排序。则流程为:

第一次递归:

此时数组为:3 1 2 4 5

基础数据:

  • l = 0:本轮区间左边界在数组中的下标
  • r = 4:本轮区间右边界在数组中的下标
  • mid = a[l] = 3:哨兵的值
  • i = l = 0:左指针
  • j = r = 4:右指针

初始化数据:

3 1 2 4 5
i、l j、r
哨兵【3】
  1. 此时q[j] = 5 > 哨兵,满足右指针移动条件,右指针左移。
3 1 2 4 5
i、l j r
哨兵【3】
  1. 此时q[j] = 4 < 哨兵,满足右指针移动条件,右指针左移。
3 1 2 4 5
i、l j r
哨兵【3】
  1. 此时q[j] = 2 > 哨兵,不满足右指针移动条件,进行元素移动【保证j右侧的值都大于哨兵的值】,接下来进行左指针移动
2 1 2 4 5
i、l j r
哨兵【3】

此时 q[l]的值不见了,但是!!!我们的哨兵存的就是 q[l]的值,在最后 q[l]的值会回到数组中,故一个元素的值都不会少。

  1. 此时q[i] = 2 < 哨兵,满足左指针移动条件,左指针右移。【第一次交换左右指针移动时左指针条件一定满足,因为此时q[i]的值是刚才q[j]的值,而刚才的q[j]是一定小于哨兵的值】
2 1 2 4 5
l i j r
哨兵【3】
  1. 此时i = j,循环条件结束,此时左右指针都不会再移动了,则执行q[i] = q[j]q[j] = q[i]是没有意义的,因为此时i = j
2 1 2 4 5
l i、j r
哨兵【3】
  1. 此时循环结束,令q[i] = mid
2 1 3 4 5
l i、j r
哨兵【3】

即此时 q[l]的值回到数组中了,故数组中的一个元素的值都没有少。

  1. 划分两个新的区间l, i - 1i + 1, r。对这两个新区间进行递归处理。

第二轮递归

本轮的数组为:2 1【上一轮递归处理后得到的左区间】

基础数据:

  • l = 0:本轮区间左边界在数组中的下标
  • r = 1:本轮区间右边界在数组中的下标
  • mid = a[l] = 2:哨兵的值
  • i = l = 0:左指针
  • j = r = 1:右指针

初始化数据:

2 1
i、l j、r
哨兵【2】
  1. 此时q[j] = 1 < 哨兵,不满足右指针移动条件,进行元素移动【保证j右侧的值都大于哨兵的值】,接下来进行左指针移动。
1 1
i、l j、r
哨兵【2】
  1. 此时q[i] = 1 < 哨兵,满足左指针移动条件,左指针右移。【第一次交换左右指针移动时左指针条件一定满足,因为此时q[i]的值是刚才q[j]的值,而刚才的q[j]是一定小于哨兵的值】
1 1
l i、j、r
哨兵【2】
  1. 此时i = j,循环条件结束,此时左右指针都不会再移动了,则执行q[i] = q[j]q[j] = q[i]是没有意义的,因为此时i = j
1 1
l i、j、r
哨兵【2】
  1. 此时循环结束,令q[i] = mid
1 2
l i、j、r
哨兵【2】
  1. 划分两个新的区间l, i - 1i + 1, r。对这两个新区间进行递归处理。

接下来迭代的流程同上,不再演示。

实现代码:

cpp 复制代码
#include<stdio.h>
// 定义一个常量N,用来修饰数组的长度
#define N 100007
// 定义一个数组a,用来接受输入的数据
int a[N];
// 进行快速排序
void quickSort(int q[], int l, int r)
{
    // 当前区间的长度小于等于1时停止循环
    if (l >= r)  return;
    // 创建哨兵 mid
    int mid = q[l];
    // 创建i,j指针进行移动
    int i = l, j = r;
    // 进行区间数字交换,使得左侧区间全小于等于mid,右侧区间全大于等于mid
    while (i < j)
    {
        // j指针从右向左移动,至到遇到第一个小于哨兵的值
        while (q[j] >= mid && i < j) j--;
        // 将该值移动到左区间中
        q[i] = q[j];
        // i指针从左向右移动,至到遇到第大个小于哨兵的值
        while (q[i] <= mid && i < j) i++;
        // 将该值移动到右区间中
        q[j] = q[i];
    }
    // 交换结束后此时i,j指针指向的同一个位置,即哨兵应该放的位置
    // 而左区间已经是全部小于等于哨兵的值,右区间已经是全部大于等于哨兵的值了。
    q[i] = mid;
    // 对划分出来的左右区间的再一次进行快排
    quickSort(q, l, i - 1);
    quickSort(q, i + 1, r);
}

int main()
{
    int n; //要排序的数据量个数
    scanf("%d", &n);
    // 按顺序输入每一个数字
    for (int i = 0; i < n; i++)
    {
       scanf("%d", &a[i]);
    }
    // 进行快速排序
    quickSort(a, 0, n - 1);
    // 按顺序输入排序后的数组内容
    for (int i = 0; i < n; i++)
    {
        printf("%d ", a[i]);
    }
    return 0;
}

总结分析:

  1. 为什么是先移动j,而不是先移动i:因为哨兵等于q[i],那么先移动i,则此时q[r]的数据是没人保存的,如果发生交换了q[j] = q[i]之后,实际上q[r]的值就不见了。但如果先移动j,由于哨兵的值是mid,那么就算发生了交换q[i] = q[j],而q[l]的值还是存在的,即哨兵的值。
  2. 记住:哨兵存的数组中的值,而不是下标,他并不是一个抽象的内容,他实际上就是q[某个下标],而这个元素也会发生移动。即最开始哨兵的位置是在q[l],而最后哨兵已经被移动到q[i]了。
  3. 下一次迭代选中区间l, i - 1i + 1, r。不包含 i 是因为 i 这个位置已经是哨兵了,不需要再进行排序了,他的位置一定是这个地方。
  4. 初级版【或者说通用版本】的快速排序大部分情况下是可以使用的,但是效率并不能达到快速排序的预期值,比如该链接中的的题目是不能通过:活动 - AcWing 。需要将排序的步骤进行优化,才能达到真正快速排序的预期。【可参考下一篇链接】
相关推荐
倔强的石头1063 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
鸽鸽程序猿4 分钟前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd4 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo6178 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v13 分钟前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神1 小时前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人1 小时前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
机器视觉知识推荐、就业指导1 小时前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法