C++ 交换排序算法:从基础冒泡到高效快排

排序是编程中最基础也最核心的算法之一,无论是笔试面试还是实际开发,都绕不开它。

冒泡排序作为入门级的交换排序,原理简单、容易上手,是新手理解 "交换排序" 的绝佳案例;

而快速排序(Quick Sort)则是冒泡排序的进阶版,凭借 "分治" 思想实现了更高的效率,也是实际开发中最常用的排序算法之一。今天我们先快速回顾冒泡排序,再重点拆解快速排序的核心逻辑,配合代码和模拟步骤。

一、冒泡排序(快速回顾)

1. 核心思想

冒泡排序的核心是 "相邻比较,逆序交换":每一轮遍历数组,将相邻的逆序元素交换,最终把当前未排序部分的最大元素 "冒泡" 到末尾。重复这个过程,直到整个数组有序。

2. 极简实现(带优化)

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 打印数组(通用函数)
void printArr(const vector<int>& arr) 
{
    for (int num : arr) cout << num << " ";
    cout << endl;
}

// 冒泡排序(优化版:提前退出无交换的轮次)
void bubbleSort(vector<int>& arr) 
{
    int n = arr.size();
    // 共n-1轮(最后一个元素无需比较)
    for (int i = 0; i < n - 1; ++i) 
    { 
        bool swapped = false; // 标记本轮是否有交换    
        // 每轮少比较i个(已排序的末尾)
        for (int j = 0; j < n - 1 - i; ++j) 
        { 
            // 逆序则交换
            if (arr[j] > arr[j+1]) 
            { 
                swap(arr[j], arr[j+1]);
                swapped = true;
            }
        }
        if (!swapped) break; // 本轮无交换,说明数组已有序,提前退出
    }
}

// 冒泡排序测试
int main_bubble() 
{
    vector<int> arr = {5, 3, 8, 4, 2};
    cout << "冒泡排序前:";
    printArr(arr);
    bubbleSort(arr);
    cout << "冒泡排序后:";
    printArr(arr);
    return 0;
}

3. 简单分析

  • 时间复杂度:最好 O (n)(数组已有序),最坏 / 平均 O (n²);
  • 空间复杂度:O (1)(原地排序);
  • 稳定性:稳定(相等元素不交换)。

冒泡排序胜在简单,但 O (n²) 的时间复杂度使其在数据量稍大时效率极低 ------ 这也是我们需要快速排序的原因。


二、快速排序

快速排序由霍尔(Hoare)在 1960 年提出,核心是分治思想 + 基准值分区:将数组以 "基准值(Pivot)" 为界分成两部分,左边≤基准值,右边≥基准值;然后递归处理左右两部分,最终整个数组有序。

1. 核心三步法)

快排的逻辑可拆解为 3 个核心步骤,我们从易到难讲清楚:

步骤 1:选基准值(Pivot)

基准值是快排的 "分割点",选择方式直接影响效率:

  • 入门版:选数组最后一个元素(易理解,适合新手);
  • 优化版:三数取中法(首、中、尾选中间值),避免有序数组退化;
  • 进阶版:随机选基准(进一步降低退化概率)。

先从 "选最后一个元素为基准" 入手,降低理解门槛。

步骤 2:分区(Partition)------ 快排的核心!

分区的目标:把数组中≤基准值的元素放到左边,≥基准值的放到右边,最终返回基准值的正确位置。

分区核心逻辑(单路分区)

  • 定义low指针(指向当前可交换的位置),基准值选最后一个元素;
  • 遍历数组[left, right-1],遇到≤基准值的元素,就和low位置的元素交换,然后low++
  • 遍历结束后,将基准值和low位置的元素交换,此时low就是基准值的正确位置。

手动模拟分区(必看) :以数组[5, 3, 8, 4, 2]为例(选最后一个元素 2 为基准):

cpp 复制代码
初始状态:arr=[5,3,8,4,2],left=0,right=4,pivot=2,low=0
1. 遍历j=0(元素5):5>2 → 不交换,j++
2. 遍历j=1(元素3):3>2 → 不交换,j++
3. 遍历j=2(元素8):8>2 → 不交换,j++
4. 遍历j=3(元素4):4>2 → 不交换,j++
5. 遍历结束,交换low(0)和right(4) → arr=[2,3,8,4,5],基准值2的位置是0(正确)。

递归处理右区间[3,8,4,5](left=1,right=4),pivot=5,low=1
1. j=1(3≤5)→ 交换low(1)和j(1)(无变化)low++;,low=2 指向元素8
2. j=2(8>5)→ 不交换,j++
3. j=3(4≤5)→ 交换low(2)元素8 和 j(3)元素4交换 → arr=[2,3,4,8,5],low=3指向元素8
4. 交换low(3)和right(4)元素5 → arr=[2,3,4,5,8],基准值5的位置是4(正确)。

最终递归处理剩余子数组,数组完全有序。
步骤 3:递归处理左右子数组

基准值位置确定后,左子数组[left, pivotPos-1]和右子数组[pivotPos+1, right]重复 "选基准→分区→递归",直到子数组长度≤1(天然有序)。

2. 基础版快排递归实现

cpp 复制代码
// 分区的目标:把数组中≤基准值的元素放到左边,≥基准值的放到右边,最终返回基准值的正确位置。
int partition(vector<int>& arr, int left, int right) 
{
    int pivot = arr[right]; // 选最后一个元素作为基准值
    int low = left;         // 遍历指针:指向当前可交换的位置

    // 遍历  已排序区间[0,cur-1]  cur   [cur+1,right]未排序等待遍历区间
    for (int cur = left; cur < right; ++cur)  
    {
        // 找到≤基准值的元素,交换到low位置
        if (arr[cur] <= pivot) 
        { 
            swap(arr[low++], arr[cur]); 
        }
    }
    //遍历完之后的数组   <=基准值[0,low-1]   >基准值 [low,right-1]   最后一个值下标right-》pivot基准值
    
    // 把基准值放到正确位置(low)
    swap(arr[low], arr[right]);

    // 返回基准值的下标索引,用于递归分割数组
    return low; 
}

// 快速排序递归函数
void _quickSort(vector<int>& arr, int left, int right) 
{
    // 递归终止条件:子数组长度≤1(left >= right)
    if (left >= right) return;

    // 1. 分区
    int pivotPos = partition(arr, left, right);

    // 2. 递归处理左子数组
    _quickSort(arr, left, pivotPos - 1);

    // 3. 递归处理右子数组
    _quickSort(arr, pivotPos + 1, right);
}

// 快速排序入口函数(封装递归,方便调用)
void quickSort(vector<int>& arr) 
{
    if (arr.empty()) return; 
    _quickSort(arr, 0, arr.size() - 1);
}

// 快速排序测试
int main() 
{
    vector<int> arr = { 5, 3, 8, 4, 2 };
    quickSort(arr);
    return 0;
}

3. 快排优化(解决核心痛点)

基础版快排存在两个痛点:① 有序数组会导致分区极不均衡,时间复杂度退化到 O (n²);② 大量重复元素会降低效率。以下是两种核心优化:

优化 1:三数取中法选基准(首、中、尾选中间值,避免有序数组退化)

cpp 复制代码
// 分区函数(优化:三数取中法选基准)
int partition(vector<int>& arr, int left, int right)
{
    // ===== 三数取中核心逻辑 start =====
    // 
    // 1. 计算中间位置(避免left+right溢出)
    int mid = left + (right - left) / 2;

    // 2. 调整left、mid、right三个位置的元素,让arr[right]成为这三个数的「中间值」 right>mid>left
    if (arr[left] > arr[mid])  
    {
        swap(arr[left], arr[mid]);
    }
    if (arr[left] > arr[right])  
    {
        swap(arr[left], arr[right]);
    }
    if (arr[mid] > arr[right])   
    {
        swap(arr[mid], arr[right]);
    }
    // ===== 三数取中核心逻辑 end =====

    // 基准仍选arr[right],但已是三数中间值
    int pivot = arr[right]; // 基准值
    int low = left;         // 遍历指针:指向当前可交换的位置

    // 遍历  已排序区间[0,cur-1]  cur   [cur+1,right]未排序等待遍历区间
    for (int cur = left; cur < right; ++cur)
    {
        // 找到≤基准值的元素,交换到low位置(low++ 等价先交换再右移)
        if (arr[cur] <= pivot)
        {
            swap(arr[low++], arr[cur]);
        }
    }
    //遍历完之后的数组   <=基准值[0,low-1]   >基准值 [low,right-1]   最后一个值下标right-》pivot基准值

    // 把基准值放到正确位置(low)
    swap(arr[low], arr[right]);

    // 返回基准值的下标索引,用于递归分割数组
    return low;
}

// 快速排序递归函数(完全复用你的代码)
void _quickSort(vector<int>& arr, int left, int right)
{
    // 递归终止条件:子数组长度≤1(left >= right)
    if (left >= right) return;

    // 1. 分区(此时分区的基准已是三数中间值)
    int pivotPos = partition(arr, left, right);

    // 2. 递归处理左子数组
    _quickSort(arr, left, pivotPos - 1);

    // 3. 递归处理右子数组
    _quickSort(arr, pivotPos + 1, right);
}

// 快速排序入口函数(封装递归,方便调用)
void quickSort(vector<int>& arr)
{
    if (arr.empty()) return;
    _quickSort(arr, 0, arr.size() - 1);
}

// 测试函数(新增打印,方便学生观察排序过程)
int main()
{
    vector<int> arr = { 5, 3, 8, 4, 2 };
    cout << "排序前:";
    printArr(arr);

    quickSort(arr);

    cout << "排序后:";
    printArr(arr);
    return 0;
}

优化 2:尾递归优化(减少递归栈开销)

cpp 复制代码
// 分区函数(三数取中法选基准)
int partition(vector<int>& arr, int left, int right)
{
    // ===== 三数取中核心逻辑 start =====
    int mid = left + (right - left) / 2; // 计算中间位置(避免left+right溢出)

    // 调整left、mid、right三个位置的元素,让arr[right]成为这三个数的「中间值」
    if (arr[left] > arr[mid])
    {
        swap(arr[left], arr[mid]);
    }
    if (arr[left] > arr[right])
    {
        swap(arr[left], arr[right]);
    }
    if (arr[mid] > arr[right])
    {
        swap(arr[mid], arr[right]);
    }
    // ===== 三数取中核心逻辑 end =====

    // 基准仍选arr[right],但已是三数中间值
    int pivot = arr[right]; // 基准值
    int low = left;         // 遍历指针:指向当前可交换的位置

    // 遍历  已排序区间[0,cur-1]  cur   [cur+1,right]未排序等待遍历区间
    for (int cur = left; cur < right; ++cur)
    {
        // 找到≤基准值的元素,交换到low位置(low++ 等价先交换再右移)
        if (arr[cur] <= pivot)
        {
            swap(arr[low++], arr[cur]);
        }
    }
    //遍历完之后的数组   <=基准值[0,low-1]   >基准值 [low,right-1]   最后一个值下标right-》pivot基准值

    // 把基准值放到正确位置(low)
    swap(arr[low], arr[right]);

    // 返回基准值的下标索引,用于递归分割数组
    return low;
}

// 快速排序递归函数(优化:尾递归优化,减少递归栈开销)尾递归优化核心:用循环替代「较长子数组」的递归,只递归处理「较短子数组」
// 2. 优先递归处理「较短的子数组」,降低递归栈深度
// a: 左子数组短 ,递归处理左子数组 。右子数组较长,用循环替代递归,更新left后继续分区
// b: 右子数组短 ,递归处理右子数组 。左子数组较长,用循环替代递归,更新right后继续分区
void _quickSort(vector<int>& arr, int left, int right)
{
    while (left < right) 
    {
        // 1. 分区
        int pivotPos = partition(arr, left, right);

        if (pivotPos - left < right - pivotPos)  // a: 左子数组短
        {
            _quickSort(arr, left, pivotPos - 1);
            left = pivotPos + 1;
        }
        else // b: 右子数组短
        {
            _quickSort(arr, pivotPos + 1, right);
            right = pivotPos - 1;
        }
    }
}

void quickSort(vector<int>& arr)
{
    if (arr.empty()) return;
    _quickSort(arr, 0, arr.size() - 1);
}

// 测试函数
int main()
{
    vector<int> arr = { 5, 3, 8, 4, 2 };
    quickSort(arr);
    return 0;
}
尾递归优化核心讲解
1. 为什么需要尾递归优化?

原始递归逻辑是:分区后递归处理左子数组 + 递归处理右子数组 。如果数组极不均衡(比如基准值每次都把数组分成「1 个元素」和「n-1 个元素」),递归栈会像 "叠罗汉" 一样越叠越深(最坏栈深度 O (n)),可能导致栈溢出,也会增加函数调用开销。

2. 尾递归优化的核心思路

"捡芝麻,丢西瓜":

  • 芝麻(较短子数组):用递归处理(栈深度只增加一点点);
  • 西瓜(较长子数组):用while循环替代递归(不新增栈帧,而是复用当前栈帧,更新 left/right 后重新分区)。最终递归栈深度最多只有O(logn),大幅减少栈开销。
3. 代码改动点(对比原始_quickSort)
原始版本 尾递归优化版本 改动原因
if (left >= right) return; while (left < right) { ... } 循环处理较长子数组,替代递归
递归处理左 + 右子数组 只递归处理较短子数组,循环更新 left/right 减少递归栈深度
4. 手动模拟优化过程(以测试数组[5,3,8,4,2]为例)
  1. 初始left=0, right=4,进入 while 循环,分区后pivotPos=1(基准值 3 的位置);
  2. 计算子数组长度:左[0,0](长度 1),右[2,4](长度 3)→ 左更短;
  3. 递归处理左子数组[0,0](直接返回,无开销);
  4. 更新left=2,循环继续处理右子数组[2,4]
  5. 分区[2,4]pivotPos=3,计算子数组长度:左[2,2](长度 1),右[4,4](长度 1);
  6. 递归处理较短的子数组,循环结束,全程递归栈深度仅 1 层。
5. 关键提示
  • 尾递归优化没有改变快排的核心逻辑(分区、分治),只是改变了 "递归的方式";
  • 尾递归优化是 "工程级优化",C++ 编译器对尾递归的优化支持有限,手动改循环是最稳妥的方式。
最终优化效果总结
优化点 解决的问题 效果
三数取中 有序数组导致快排退化到 O (n²) 稳定保持 O (nlogn)
尾递归优化 递归栈过深导致栈溢出 / 函数调用开销大 栈深度从 O (n)→O (logn)

这个版本既保留了原有的代码结构,又完成了核心优化

相关推荐
LYFlied2 小时前
【每日算法】LeetCode 226. 翻转二叉树
前端·算法·leetcode·面试·职场和发展
落羽的落羽2 小时前
【C++】深入浅出“图”——图的遍历与最小生成树算法
linux·服务器·c++·人工智能·算法·机器学习·深度优先
txp玩Linux2 小时前
rk3568上webrtc处理稳态噪声实践
算法·webrtc
CoovallyAIHub2 小时前
从空地对抗到空战:首个无人机间追踪百万级基准与时空语义基线MambaSTS深度解析
深度学习·算法·计算机视觉
"YOUDIG"2 小时前
从算法到3D美学——一站式生成个性化手办风格照片
算法·3d
Dream it possible!2 小时前
牛客周赛 Round 123_C_小红出对 (哈希表+哈希集合)
c++·哈希算法·散列表
落羽的落羽2 小时前
【C++】深入浅出“图”——图的基本概念与存储结构
服务器·开发语言·数据结构·c++·人工智能·机器学习·图搜索算法
LYFlied2 小时前
【每日算法】LeetCode 104. 二叉树的最大深度
前端·算法·leetcode·面试·职场和发展
大厂技术总监下海2 小时前
PyTorch 核心技术深度解读:从动态图到自动微分的工程实现
算法