当前位置: 首页 > news >正文

【排序】快速排序

算法介绍

快速排序(Quick Sort)有多种实现方式,包括左右指针法、前后指针法、挖坑法和荷兰国旗法等等。这些方法的核心思想都是通过分治策略将数组划分为两部分,但具体的划分方式和实现细节有所不同。下面我会详细解释它们的区别和特点。
本文所有代码用c++实现,想要c语言代码以及更多细节分析,可以到我的GitHub查看更多内容,以下是快速排序的c语言代码链接:
GitHub: dante329/Data-Structure/QuickSort/QuickSort/QuickSort.c

核心原理

  1. 选取一个基准值(pivot),按照基准值的大小将数组分为[小于基准值,等于基准值,大于基准值]三部分,当这三个区域被分好时,基准值已经处于正确的位置了。
  2. 对基准值两侧的区间递归处理,直到区间不存在或者区间长度为1。

基准值的选择

基准值选择的是否恰当对快排整体的时间复杂度影响是非常大的,如果基准值选择不当,可能导致分区不平衡,从而使算法的时间复杂度退化为 O(n^2)。

随机选择基准值

int get_random(int left,int right) //随机选基准值 
{
	return rand() % (right - left + 1) + left; //返回下标
}

注意:要引用头文件 <ctime> ,在主函数中种下随机数种子srand(time(0));

三数取中法

从数组的左端、右端和中间位置选择三个元素,取它们的中位数作为基准值。减少了选择到极端值的概率,使分区更平衡。

int medianOfThree(int a[], int left, int right) 
{
    int mid = (left + right) >> 1;
    if (a[left] > a[mid]) swap(a[left], a[mid]);
    if (a[left] > a[right]) swap(a[left], a[right]);
    if (a[mid] > a[right]) swap(a[mid], a[right]);
    return mid; // 返回中位数的下标
}

排序实现方式

为了方便作演示,这里的基准值直接选择第一个元素或者最后一个元素。

左右指针法

这是最经典的快速排序实现方式。
实现步骤:

  1. 选择一个基准值。

  2. 使用两个指针,begin 从左向右移动,end 从右向左移动。
    初始时 left 指向数组的第一个元素,end 指向数组的最后一个元素

  3. left 指针找到第一个大于基准值的元素,end 指针找到第一个小于基准值的元素,然后交换它们。

  4. 重复上述过程,直到 left 和 end 指针相遇。

  5. 将基准值放到相遇的位置,此时数组被划分为两部分。

  6. 递归处理左右两部分。

void quickSort(int arr[], int left, int right)
{
    if (left >= right) return;
    int begin = left, end = right;
    int pivot = arr[left];  // 选择第一个元素作为基准值
    while (begin < end) 
    {
        while (begin < end && arr[end] >= pivot) end--;  // 从右向左找小于基准值的元素
        while (begin < end && arr[begin] <= pivot) i++;  // 从左向右找大于基准值的元素
        if (begin < end) swap(arr[begin], arr[end]);  // 交换
    }
    swap(arr[left], arr[begin]);  // 将基准值放到正确的位置
    quickSort(arr, left, begin - 1);   // 递归处理左半部分
    quickSort(arr, begin+ 1, right);  // 递归处理右半部分
}

注意!!!:在快速排序的左右指针法中,先从右向左找第一个小于基准值的元素,再从左向右找第一个大于基准值的元素这个顺序非常重要,必须先找小后找大。如果你先从左向右找大,那么就会在一个比基准值大的位置停下,最终就会导致基准值的归位错误。

Q:为什么 end 先从右向左找小,相遇点的元素就一定小于或等于基准值?
A:end 如果先移动,那么就会先 begin 一步,end 的任务就是找小,所以当 end 找小成功,end 就会占据那个位置。即使 begin 向右找不到大数,那也会被迫停在 end 目前的位置上(当 begin 和 end 相遇时,begin 和 end 指向同一个元素),而end位置的元素就是小于基准值的,这时候再与基准值交换,就满足左边的元素都小于基准值。
即使end没有找到小的,而是碰到 begin 停下了。
1.此时如果 begin 在while (begin < end)这个大循环范围内没有与 end 交换过的话,那么此时停下的位置的元素就是基准值本身
2.如果 begin 在while (begin < end)这个大循环范围内与 end 交换过的话,那么此时停下的位置的元素肯定小于基准值,因为在上一轮找大找小过程中,begin 找的是大值,end 找的是小值,交换过后 这时 begin 的位置上的值就是小于基准值的数,此时 end 在 begin 处停下,停下的位置的值就是小于基准值的,再与数组最左边的元素交换,得到的数列就是基准值左边的数都是小于等于基准值的,不管是哪种情况,都是符合要求的。

演示数列 [6,1,2,9,8] ,先找小和先找大的区别。
先从右向左找小:
在这里插入图片描述
先从左向右找大:
在这里插入图片描述
补充(基准值的位置):
取数组第一个元素为基准值就要将最终停下的位置与第一个元素(基准值所在位置)交换,取数组最后一个元素为基准值就要将最终停下的位置与最后一个元素(基准值所在位置)交换。
如果想要使用优化方法取基准值,那么只需要将通过优化方法得到的基准值先与数组的第一个元素或者最后一个元素先交换,再执行各种排序方法。
例如随机选择基准值+左右指针法代码实现:

#include <iostream>
#include <algorithm>
#include <cstdlib>  // 用于 rand() 函数
#include <ctime>    // 用于 time() 函数

// 随机选择一个基准值,并将其与第一个元素交换
int choosePivot(int arr[], int left, int right) {
    // 生成一个随机索引
    int randomIndex = left + rand() % (right - left + 1);
    // 将随机选择的基准值与第一个元素交换
    std::swap(arr[left], arr[randomIndex]);
    return arr[left];  // 返回基准值
}

// 快速排序的左右指针法实现
void quickSort(int arr[], int left, int right) {
    if (left >= right) return;  // 递归终止条件:子数组长度为 0 或 1

    // 随机选择基准值,并将其与第一个元素交换
    int pivot = choosePivot(arr, left, right);

    // 初始化左右指针
    int i = left, j = right;

    // 划分过程
    while (i < j) {
        // 从右向左找第一个小于基准值的元素
        while (i < j && arr[j] >= pivot) j--;
        // 从左向右找第一个大于基准值的元素
        while (i < j && arr[i] <= pivot) i++;
        // 交换这两个元素
        if (i < j) std::swap(arr[i], arr[j]);
    }

    // 将基准值放到正确的位置(i 和 j 相遇的位置)
    std::swap(arr[left], arr[i]);

    // 递归处理左半部分(小于基准值的部分)
    quickSort(arr, left, i - 1);

    // 递归处理右半部分(大于基准值的部分)
    quickSort(arr, i + 1, right);
}

// 打印数组
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

// 测试代码
int main() {
    // 设置随机种子
    std::srand(std::time(0));

    // 测试用例 1
    int arr1[] = {3, 6, 8, 10, 1, 2, 1};
    int n1 = sizeof(arr1) / sizeof(arr1[0]);
    std::cout << "Original array 1: ";
    printArray(arr1, n1);
    quickSort(arr1, 0, n1 - 1);
    std::cout << "Sorted array 1: ";
    printArray(arr1, n1);

    // 测试用例 2
    int arr2[] = {9, 4, 2, 7, 5, 10, 1, 3};
    int n2 = sizeof(arr2) / sizeof(arr2[0]);
    std::cout << "Original array 2: ";
    printArray(arr2, n2);
    quickSort(arr2, 0, n2 - 1);
    std::cout << "Sorted array 2: ";
    printArray(arr2, n2);

    // 测试用例 3(包含重复元素)
    int arr3[] = {5, 3, 8, 3, 2, 5, 1};
    int n3 = sizeof(arr3) / sizeof(arr3[0]);
    std::cout << "Original array 3: ";
    printArray(arr3, n3);
    quickSort(arr3, 0, n3 - 1);
    std::cout << "Sorted array 3: ";
    printArray(arr3, n3);

    return 0;
}

前后指针法

实现步骤:

  1. 选择一个基准值。

  2. 使用两个指针,prev 和 cur,
    指针意义:
    prev:指向小于基准值部分的最后一个元素。(初始时 prev 指向数组的第一个元素的左边)
    curr:遍历数组,用于检查当前元素是否小于基准值。(初始时 curr 指针指向数组的第一个元素)

  3. cur 指针从左向右遍历数组,如果当前元素小于基准值,先++prev,然后将其与 prev 位置的元素交换,

  4. 将小于基准值的元素移动到 prev 指针的左侧,大于等于基准值的元素保留在右侧。最后将基准值放到 prev + 1 的位置,完成一次划分。

  5. 递归处理左右两部分。

void quickSort(int arr[], int left, int right) 
{
    if (left >= right) return;
    int pivot = arr[right];  // 选择最后一个元素作为基准值
    int prev = left - 1;
    for (int cur = left; cur < right; cur++) 
    {
        if (arr[cur] < pivot) 
        {
            prev++;
            swap(arr[prev], arr[cur]);
        }
    }
    swap(arr[prev + 1], arr[right]);  // 将基准值放到正确的位置
    quickSort(arr, left, prev);            // 递归处理左半部分
    quickSort(arr, prev + 2, right);       // 递归处理右半部分
}

挖坑法

实现步骤:

  1. 选择一个基准值,并将其位置作为坑。

  2. 使用两个指针,一个从左向右移动,另一个从右向左移动。
    从右向左找到第一个小于基准值的元素,将其填入坑中,然后该位置成为新的坑。
    从左向右找到第一个大于基准值的元素,将其填入坑中,然后该位置成为新的坑。

  3. 重复上述过程,直到两个指针相遇。

  4. 将基准值填入最后的坑中。

  5. 递归处理左右两部分。

void quickSort(int arr[], int left, int right) 
{
    if (left >= right) return;
    int pivot = arr[left];  // 选择第一个元素作为基准值
    int i = left, j = right;
    while (i < j) 
    {
        while (i < j && arr[j] >= pivot) j--;  // 从右向左找小于基准值的元素
        if (i < j) arr[i++] = arr[j];          // 填坑
        while (i < j && arr[i] <= pivot) i++;  // 从左向右找大于基准值的元素
        if (i < j) arr[j--] = arr[i];          // 填坑
    }
    arr[i] = pivot;  // 将基准值填入最后的坑中
    quickSort(arr, left, i - 1);  // 递归处理左半部分
    quickSort(arr, i + 1, right); // 递归处理右半部分
}

荷兰国旗法

这种方法是对快速排序的改进,适用于有大量重复元素的情况。
建议在看之前学习下面这篇文章对于荷兰国旗问题的三指针法思想,就方便理解在快排中的荷兰国旗法
leetcode 75.颜色分类(荷兰国旗问题)

实现步骤:

  1. 选择一个基准值。

  2. 将数组划分为三部分:小于基准值、等于基准值、大于基准值。

  3. 使用三个指针:left、i 、right。
    left 指向小于基准值的部分的最后一个元素。
    right 指向大于基准值的部分的第一个元素。
    i 是当前遍历的指针。

  4. 遍历数组,根据当前元素与基准值的关系,将其放入对应的部分。

  5. 递归处理小于和大于基准值的部分。

void quick_sort(int a[],int left,int right)
{
	if(left >= right) return;
	int p = a[left]; //选基准值p 
	int l = left - 1, i = left, r = right + 1;
	while(i < r)
	{
		if(a[i] > p) swap(a[--r],a[i]); //大于p,与右边的交换,i不能加,新换来的元素还没判断 
		else if(a[i] < p) swap(a[++l],a[i++]); //小于p,与左边的交换 
		else i++; //与p相等,i++  
	}
	//数组被分为[left,l][l+1,r-1][r,right]三个区间,左边都是小于p的,中间是等于p的,右边是大于p的
	quick_sort(left,l);
	quick_sort(r,right);
} 

* 双基准值快速排序(选看)

选择两个基准值(通常是最左端和最右端的元素),将数组分为三部分:
小于左基准值的部分。
介于两个基准值之间的部分。
大于右基准值的部分。
这种方法在某些情况下比单基准值快速排序更快。

void dualPivotQuickSort(int a[], int left, int right) {
    if (left >= right) return;

    // 选择双基准值
    if (a[left] > a[right]) swap(a[left], a[right]);
    int p1 = a[left], p2 = a[right];

    // 分区
    int low = left + 1, high = right - 1;
    for (int i = low; i <= high; i++) 
    {
        if (a[i] < p1) 
        {
            swap(a[i], a[low]);
            low++;
        } else if (a[i] > p2) 
        {
            swap(a[i], a[high]);
            high--;
            i--; // 重新检查交换后的元素
        }
    }

    // 将基准值放到正确位置
    swap(a[left], a[low - 1]);
    swap(a[right], a[high + 1]);

    // 递归排序
    dualPivotQuickSort(a, left, low - 2);
    dualPivotQuickSort(a, low, high);
    dualPivotQuickSort(a, high + 2, right);
}

相关文章:

  • Python —— random.choice()的用法
  • 数学——A. K-divisible Sum + D. Exam in MAC
  • Unity屏幕适配——立项时设置
  • 案例驱动的 IT 团队管理:创新与突破之路:第一章 重构 IT 团队管理:从传统到创新-1.2.2 方法论提炼:可复用的管理模型
  • 【uni-app运行错误】SassError: expected selector @import “@/uni.scss“;
  • 用通义大模型写爬虫程序,汇总各科成绩
  • Datawhale coze-ai-assistant 笔记3
  • 初阶数据结构(C语言实现)——5.2 二叉树的顺序结构及堆的实现
  • promise和settimeout的区别,谈一谈eventloop
  • 六、实战开发 uni-app x 项目(仿京东)- 分类页
  • 【二分算法】-- 寻找旋转排序数组中的最小值
  • 2025 香港 Web3 嘉年华:全球 Web3 生态的年度盛会
  • 如何进行前端项目的自动化部署?请简述主要流程和常用工具。
  • 电子电气架构 --- 智能座舱和车载基础软件简介
  • Qt 窗口以及菜单栏介绍
  • 谷歌搜索基本规则
  • 算法014——找到字符串中所有字母异位词
  • C++|构造函数和析构函数
  • 基于PHP的网店进销存管理系统(源码+lw+部署文档+讲解),源码可白嫖!
  • 练习-依依的询问最小值(前缀和差分)
  • 天问二号探测器顺利转入发射区
  • 因救心梗同学缺席职教高考的姜昭鹏顺利完成补考
  • 原核试验基地司令员范如玉逝世,从事核试验研究超40年
  • 在美国,为什么夏季出生的孩子更容易得流感?
  • 101岁陕西省军区原司令员冀廷璧逝世,曾参加百团大战
  • 湖南慈利一村干部用AI生成通知并擅自发布,乡纪委立案