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

C++ 常见的排序算法详解

今天来系统地梳理一下常见的排序算法。包括冒泡、选择、插入、希尔、归并、快排、堆排,还有非比较类的如计数、桶排、基数。排序是算法的基础,深入理解它们对编程思维和性能优化至关重要。

接下来将从简单到复杂,逐一剖析它们的核心思想、步骤、复杂度、优缺点,以及C++的实现代码以及适用场景。


1. 概述

排序算法可以分为两大类:

  • 比较类排序:通过比较元素间的相对次序来进行排序。其平均时间复杂度不可能突破 O(n log n)。
  • 非比较类排序:不通过比较来决定元素次序,而是利用额外的信息(如整数的数值范围)。它可以突破基于比较排序的时间下界,达到线性时间复杂度,但通常有特定的适用条件。

2. 常见排序算法详解

一、 冒泡排序 (Bubble Sort)
  • 核心思想:重复地遍历待排序序列,一次比较两个相邻元素,如果它们的顺序错误就把它们交换过来。遍历序列的工作是重复地进行直到没有再需要交换,也就是说该序列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
  • 过程
    1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
    2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
    3. 针对所有的元素重复以上的步骤,除了最后一个(以及后续已经排序好的)。
    4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
  • C++实现
    void bubbleSort(vector<int>& arr) {int n = arr.size();for (int i = 0; i < n - 1; ++i) {bool swapped = false; // 优化:如果某一轮没有交换,说明已有序,可提前结束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; // 本轮无交换,提前结束}
    }
    
  • 复杂度分析
    • 时间复杂度:
      • 最好情况(已有序):O(n)(优化后)。
      • 最坏/平均情况:O(n²)。
    • 空间复杂度:O(1),是原地排序
    • 稳定性稳定(相等元素不会交换)。
  • 评价:效率很低,除了教学和极小的数据集,基本不会在实际中使用。

二、 选择排序 (Selection Sort)
  • 核心思想:在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
  • 过程
    1. 在序列中找到最小元素。
    2. 将其与序列第一个元素交换(如果第一个元素就是最小元素则和自己交换)。
    3. 在剩余未排序序列中重复上述过程。
  • C++实现
    void selectionSort(vector<int>& arr) {int n = arr.size();for (int i = 0; i < n - 1; ++i) {int minIndex = i;for (int j = i + 1; j < n; ++j) { // 寻找未排序部分的最小值下标if (arr[j] < arr[minIndex]) {minIndex = j;}}swap(arr[i], arr[minIndex]); // 将找到的最小值放到已排序序列的末尾}
    }
    
  • 复杂度分析
    • 时间复杂度:无论什么数据输入都是 O(n²)。因为寻找最小值的循环必须执行完。
    • 空间复杂度:O(1),是原地排序
    • 稳定性不稳定。例如序列 [5, 8, 5, 2, 9],第一个5会和2交换,导致两个5的相对顺序改变。
  • 评价:相比冒泡排序,它的交换次数更少(最多交换 n-1 次),但时间复杂度依然很高,适用于数据量小且对交换开销敏感的场景。

三、 插入排序 (Insertion Sort)
  • 核心思想:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
  • 过程
    1. 将第一个元素视为已排序序列。
    2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
    3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
    4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
    5. 将新元素插入到该位置后。
    6. 重复步骤2~5。
  • C++实现
    void insertionSort(vector<int>& arr) {int n = arr.size();for (int i = 1; i < n; ++i) { // i从1开始,认为arr[0]是已排序的int key = arr[i]; // 待插入的元素int j = i - 1;// 将arr[0..i-1]中比key大的元素都向后移动一位while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j--;}arr[j + 1] = key; // 插入到正确位置}
    }
    
  • 复杂度分析
    • 时间复杂度:
      • 最好情况(已有序):O(n)。
      • 最坏/平均情况:O(n²)。
    • 空间复杂度:O(1),是原地排序
    • 稳定性稳定
  • 评价:对于小规模或基本有序的数据集非常高效。STL中的 std::sort 在递归到小数组时也会转而使用插入排序。它是高级排序算法优化的重要组成部分。

四、 希尔排序 (Shell‘s Sort) - 插入排序的改进
  • 核心思想:是插入排序的一种更高效的改进版本。它通过将原始数组“分组”并进行插入排序,随着增量序列的减小,数组变得越来越“部分有序”,最后当增量为1时进行一次标准的插入排序,此时效率很高。
  • 过程
    1. 选择一个增量序列 gap,例如 n/2, n/4, ..., 1
    2. 按增量序列个数 k,对序列进行 k 趟排序。
    3. 每趟排序,根据对应的增量 gap,将待排序列分割成若干长度为 gap 的子序列,分别对各子序列进行直接插入排序。
    4. 当增量因子为1时,整个序列作为一个表来处理,进行最后一次插入排序。
  • C++实现(使用 gap = n/2, gap /= 2 序列):
    void shellSort(vector<int>& arr) {int n = arr.size();for (int gap = n / 2; gap > 0; gap /= 2) { // 增量序列for (int i = gap; i < n; ++i) { // 从第gap个元素开始,对各个分组进行插入排序int temp = arr[i];int j;for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {arr[j] = arr[j - gap];}arr[j] = temp;}}
    }
    
  • 复杂度分析:复杂度依赖于增量序列的选择,分析非常复杂。
    • 时间复杂度:使用上述增量序列最坏是 O(n²),一些更好的增量序列(如Hibbard)可以达到 O(n^(3/2)),甚至 O(n log² n)。
    • 空间复杂度:O(1)。
    • 稳定性不稳定。相同的元素可能在各自的分组排序中被打乱顺序。
  • 评价:是第一批突破 O(n²) 复杂度的算法之一,实现简单,在中等规模数据上表现良好。

五、 归并排序 (Merge Sort) - 分治法
  • 核心思想:采用分治法。将已有序的子序列合并,得到完全有序的序列。
    1. 分割:递归地把当前序列平均分割成两半。
    2. 治理:在不能再分割后,对子序列进行排序(通常子序列长度为1时自然有序)。
    3. 合并:将两个已经排序的子序列合并成一个有序序列。
  • 过程:关键是 merge 函数。
  • C++实现
    // 合并两个有序子数组 arr[l..m] 和 arr[m+1..r]
    void merge(vector<int>& arr, int l, int m, int r) {vector<int> left(arr.begin() + l, arr.begin() + m + 1);vector<int> right(arr.begin() + m + 1, arr.begin() + r + 1);int i = 0, j = 0, k = l;while (i < left.size() && j < right.size()) {if (left[i] <= right[j]) {arr[k++] = left[i++];} else {arr[k++] = right[j++];}}// 拷贝剩余元素while (i < left.size()) arr[k++] = left[i++];while (j < right.size()) arr[k++] = right[j++];
    }void mergeSort(vector<int>& arr, int l, int r) {if (l >= r) return; // 终止递归条件int m = l + (r - l) / 2; // 防止溢出,等效于 (l+r)/2mergeSort(arr, l, m);mergeSort(arr, m + 1, r);merge(arr, l, m, r);
    }// 包装函数
    void mergeSort(vector<int>& arr) {mergeSort(arr, 0, arr.size() - 1);
    }
    
  • 复杂度分析
    • 时间复杂度:最好、最坏、平均都是 O(n log n)。非常稳定。
    • 空间复杂度:O(n)。因为合并操作需要额外的空间。不是原地排序
    • 稳定性稳定merge时判断用 <= 即可保证)。
  • 评价:效率高且稳定,是外部排序(数据量大到在磁盘中)的基础。缺点是空间复杂度高。Java、Python等语言的通用排序算法之一(对于对象排序,稳定性很重要)。

六、 快速排序 (Quick Sort) - 分治法
  • 核心思想:同样采用分治法。选择一个“基准”元素,通过一趟排序将待排记录分割成独立的两部分,其中一部分的所有元素均比基准小,另一部分均比基准大。然后递归地对这两部分数据进行排序。
  • 过程:关键是 partition 操作。
    1. 从数列中挑出一个元素,称为 “基准”。
    2. 分区:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。
    3. 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
  • C++实现( Lomuto partition scheme ):
    // 分区函数,选择最右元素为基准
    int partition(vector<int>& arr, int low, int high) {int pivot = arr[high]; // 选择最右元素作为基准int i = (low - 1);     // 指向小于基准的区域的最后一个元素for (int j = low; j <= high - 1; j++) {if (arr[j] < pivot) { // 当前元素小于基准i++; // 扩大小于基准的区域swap(arr[i], arr[j]);}}swap(arr[i + 1], arr[high]); // 将基准放到正确位置return (i + 1);
    }void quickSort(vector<int>& arr, int low, int high) {if (low < high) {int pi = partition(arr, low, high); // pi是基准的最终位置quickSort(arr, low, pi - 1);quickSort(arr, pi + 1, high);}
    }// 包装函数
    void quickSort(vector<int>& arr) {quickSort(arr, 0, arr.size() - 1);
    }
    
    (注:Hoare partition scheme 是另一种更高效的选择,但逻辑稍复杂)
  • 复杂度分析
    • 时间复杂度:
      • 平均情况:O(n log n)。
      • 最坏情况(数组已有序或逆序):O(n²)。但通过随机选择基准或中位数基准可以极大避免。
    • 空间复杂度:平均 O(log n)(递归调用栈的深度),最坏 O(n)。是原地排序
    • 稳定性不稳定。分区操作会打乱顺序。
  • 优化
    • 随机化快排:随机选择一个元素作为基准,避免在有序数组上出现最坏情况。
    • 三数取中:选择头、中、尾三个元素的中位数作为基准。
    • 对于小数组切换到插入排序。
  • 评价:在平均情况下是内部排序中最快的算法,std::sort 的核心就是基于快排的IntroSort。

七、 堆排序 (Heap Sort)
  • 核心思想:利用这种数据结构的特性进行排序。堆是一个近似完全二叉树,且满足父节点的值总是大于等于(或小于等于)其子节点的值。
  • 过程
    1. 建堆:将待排序序列构建成一个大顶堆(升序排序用大顶堆)。
    2. 此时,整个序列的最大值就是堆顶的根节点。
    3. 将其与末尾元素进行交换,此时末尾就为最大值。
    4. 然后将剩余 n-1 个元素重新构造成一个大顶堆,这样会得到次大值。
    5. 如此反复执行,便能得到一个有序序列了。
  • C++实现
    // 调整以节点i为根的子树为大顶堆,n是堆的大小
    void heapify(vector<int>& arr, int n, int i) {int largest = i;       // 初始化最大值为根int left = 2 * i + 1;  // 左子节点int right = 2 * i + 2; // 右子节点if (left < n && arr[left] > arr[largest]) largest = left;if (right < n && arr[right] > arr[largest]) largest = right;if (largest != i) {swap(arr[i], arr[largest]);heapify(arr, n, largest); // 递归地调整受影响的子树}
    }void heapSort(vector<int>& arr) {int n = arr.size();// 构建大顶堆(从最后一个非叶子节点开始调整)for (int i = n / 2 - 1; i >= 0; i--) {heapify(arr, n, i);}// 一个个从堆顶取出元素for (int i = n - 1; i > 0; i--) {swap(arr[0], arr[i]); // 将当前最大值移到数组末尾heapify(arr, i, 0);   // 在缩小的堆上重新调整}
    }
    
  • 复杂度分析
    • 时间复杂度:建堆 O(n),每次调整 O(log n),总复杂度为 O(n log n)最好、最坏、平均都是 O(n log n)
    • 空间复杂度:O(1),是原地排序
    • 稳定性不稳定。例如 [5, 5, 5],建堆和交换过程可能打乱顺序。
  • 评价:时间复杂度稳定且是原地排序,常用于嵌入式系统等内存受限的场景。但由于数据交换是跳跃式的,缓存不友好,实际性能常不如好的快排实现。

八、 非比较排序 (计数排序、桶排序、基数排序)

这类排序用于特定情况,可以达到线性时间复杂度 O(n)

  • 计数排序 (Counting Sort)

    • 思想:将输入的数据值转化为键存储在额外开辟的数组空间中。要求输入的数据必须是有确定范围的整数
    • 过程:统计每个元素出现的次数,然后根据计数结果直接输出到有序序列中。
    • 复杂度:O(n + k),k是整数的范围。k过大时效率低下。
  • 桶排序 (Bucket Sort)

    • 思想:将数据分到有限数量的桶里,每个桶再分别排序(可能使用别的排序算法或递归方式继续使用桶排序)。
    • 复杂度:O(n),取决于桶的数量和桶内使用的排序算法。
  • 基数排序 (Radix Sort)

    • 思想:按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。
    • 复杂度:O(n * k),k是最大数字的位数。

3. 总结与对比

排序算法平均时间复杂度最好情况最坏情况空间复杂度稳定性核心思想
冒泡排序O(n²)O(n)O(n²)O(1)稳定相邻交换
选择排序O(n²)O(n²)O(n²)O(1)不稳定选择最小
插入排序O(n²)O(n)O(n²)O(1)稳定找位置插入
希尔排序O(n log n) ~ O(n²)O(n log² n)O(n²)O(1)不稳定分组插入
归并排序O(n log n)O(n log n)O(n log n)O(n)稳定分治、合并
快速排序O(n log n)O(n log n)O(n²)O(log n)不稳定分治、分区
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定堆结构

4. C++中的排序:std::sort

在C++实际开发中,你几乎不需要自己实现排序算法。标准库提供了极其高效的 std::sort 函数(位于 <algorithm> 头文件中)。

  • 实现原理:它并非单一的排序算法,而是一种混合算法 IntroSort

    1. 快速排序:作为主要排序方法。
    2. 堆排序:当递归深度过深(超过 log(n) 层)时,转为堆排序,避免快排的最坏O(n²)情况。
    3. 插入排序:当分区的元素数量少于一定阈值(如16)时,转为插入排序,因为对小数组插入排序常数项更小。
  • 用法

    #include <algorithm>
    #include <vector>int main() {std::vector<int> arr = {5, 2, 8, 1, 9};// 默认升序排序std::sort(arr.begin(), arr.end()); // arr becomes {1, 2, 5, 8, 9}// 降序排序:使用lambda表达式或greater<>()std::sort(arr.begin(), arr.end(), std::greater<int>());// 或 std::sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; });return 0;
    }
    

    它是绝大多数情况下你的最佳选择。

http://www.dtcms.com/a/343201.html

相关文章:

  • CPP学习之priority_queue的使用及模拟实现
  • 3维模型导入到3Dmax中的修改色彩简单用法----第二讲
  • Kotlin 中适用集合数据的高阶函数(forEach、map、filter、groudBy、fold、sortedBy)
  • AI客服系统架构与实现:大模型、知识库与多轮对话的最佳实践
  • 蛋白质分析常用数据库2
  • QT开发---QT布局与QSS样式设置
  • 网络打印机自动化部署脚本
  • 工业机器人远程监控与运维物联网解决方案
  • 精准评估新纪元:AI得贤招聘官AI面试智能体6.3,重新定义AI面试
  • 赛灵思ZYNQ官方文档UG585自学翻译笔记与代码示例:Quad-SPl Flash 闪存控制器
  • 深度剖析字节跳动VeOmni框架
  • MySQL索引优化之索引条件字段类型不同
  • POI读和写
  • C2ComponentStore
  • CMOS知识点 MOS管线性区电流公式
  • Linux 网络命令大全
  • 在VSCode中配置.NET项目的tasks.json以实现清理、构建、热重载和发布等操作
  • vue2 watch 用法
  • K8s安全管理与持久化存储实战指南
  • Seaborn数据可视化实战:Seaborn入门-环境搭建与基础操作
  • Seaborn数据可视化实战
  • AI对口型唱演:科技赋能,开启虚拟歌者新篇章
  • 刷机维修进阶教程-----如何清除云账号 修复wifi 指南针 相机 指纹等刷机故障
  • 自然处理语言NLP:One-Hot编码、TF-IDF、词向量、NLP特征输入、EmbeddingLayer实现、word2vec
  • Linux 802.11协议栈深度分析与实践指南
  • 车机两分屏运行Unity制作的效果
  • OpenAI重新开源!gpt-oss-20b适配昇腾并上线魔乐社区
  • WebSocket连接的例子
  • 链游开发新篇章:融合区块链技术的游戏创新与探索
  • 什么是撮合引擎