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

C++实现常见的排序算法

排序算法

  • 前言
  • 一、 插入排序
    • 直接插入排序
      • 算法步骤
      • 代码实现
    • 希尔排序
      • 为什么需要希尔排序?
      • 算法步骤
      • 代码实现
  • 二、 选择排序
    • 选择排序
      • 算法步骤
      • 代码实现
    • 堆排序
      • 代码实现
  • 三、 交换排序
    • 冒泡排序
      • 算法步骤
      • 代码实现
    • 快速排序
      • 算法步骤
      • 代码实现
  • 四、 归并排序
    • 归并排序
    • 算法步骤
    • 代码实现
  • 总结
  • 每文推荐


前言

本文将介绍一些常用的排序算法及其C++代码的实现,主要为比较类排序,分为以下部分:
1. 插入排序
直接插入排序
希尔排序
2. 选择排序
选择排序
堆排序
3. 交换排序
冒泡排序
快速排序
4. 归并排序
归并排序


一、 插入排序

直接插入排序

直接插入排序是一种简单直观的排序算法,其核心思想是 “将待排序元素逐个插入到已排序序列的合适位置”,类似于玩扑克牌时整理手牌的过程。

算法步骤

假设要对数组 arr 进行排序(以升序为例),步骤如下:

  1. 初始化已排序序列:默认数组的第 1 个元素(arr[0])为 “已排序序列”(因为单个元素本身就是有序的)。
  2. 遍历待排序元素:从第 2 个元素开始(i=1 到 i=arr.length-1),每个元素 arr[i] 为 “待插入元素”。
  3. 寻找插入位置:将 “待插入元素” 与 “已排序序列” 中的元素从后往前对比(设对比索引为 j,初始 j=i-1):
  4. 若 arr[j] > arr[i](说明 “已排序元素” 需后移),则将 arr[j] 赋值给 arr[j+1],j 减 1(继续向前对比);若 arr[j] <= arr[i](说明找到插入位置),停止对比。
  5. 插入元素:将 “待插入元素”arr[i] 插入到最终位置 j+1(若 j 减到 -1,说明该元素是最小的,插入到数组开头)。
  6. 重复步骤 2-4:直到所有元素都插入到 “已排序序列” 中,数组排序完成。

代码实现

// InsertSort 直接插入排序
// 平均时间复杂度:O(N*N)
// 空间复杂度:O(1)
// 稳定排序
void InsertSort(vector<int>& arr)
{int gap = 1;int n = arr.size();for (size_t i = gap; i < n; i++){int tmp = arr[i];int j = i - gap;while (j >= 0 && tmp < arr[j])	{arr[j + gap] = arr[j];j -= gap;}arr[j + gap] = tmp;}
}

希尔排序

希尔排序(Shell Sort)是对直接插入排序的高效改进,由计算机科学家 Donald Shell 于 1959 年提出。它的核心思想是:通过 “分组” 减少数据间的 “距离”,让数组先 “大致有序”,再用直接插入排序完成最终排序,从而大幅降低插入排序的时间成本。

为什么需要希尔排序?

直接插入排序的效率在数组 “基本有序” 或 “数据量小” 时很高(最佳时间复杂度 O(n)),但在数组 “无序且规模大” 时,元素需要频繁移动(最坏 O(n²))。
希尔排序的改进逻辑是:先通过 “分组排序” 让数组变得 “基本有序”(此时元素间的 “逆序对” 大幅减少),最后用一次直接插入排序收尾。由于 “基本有序” 的数组对插入排序更友好,整体效率会显著提升。

核心思想:步长(增量)与分组

  • 希尔排序的关键是 “步长(Increment)”—— 通过步长将数组分成若干 “子序列”,对每个子序列单独做直接插入排序;然后逐步减小步长,重复分组排序,直到步长为 1(此时就是一次完整的直接插入排序)。
  • 步长的作用:步长决定了 “分组的粒度”。初始步长较大(比如 n/2),子序列中的元素 “间隔远”,排序后能快速减少数组的 “无序程度”;随着步长减小(比如逐步减半),子序列逐渐 “合并”,数组越来越接近有序;最终步长为 1 时,只需少量移动就能完成排序。

算法步骤

假设要排序数组 arr(长度为 n),步骤如下:

  1. 初始化步长:设初始步长 gap = n / 2(通常从数组长度的一半开始)。
  2. 按步长分组排序:
    • 按 gap 将数组分成 gap 个 “子序列”(每个子序列的元素下标相差 gap,比如下标 0, gap, 2gap… 为一组,1, 1+gap, 1+2gap… 为另一组)。
    • 对每个子序列单独执行 “直接插入排序”(子序列内部排序,不影响其他组元素的相对位置)。
  3. 减小步长:更新 gap = gap / 2(通常每次减半),重复步骤 2,直到 gap = 0(停止条件)。
  4. 最终排序:当 gap = 1 时,数组已 “基本有序”,此时对整个数组执行一次直接插入排序,完成最终排序。

代码实现

// ShellSort 希尔排序
// 平均时间复杂度:O(N^1.3)
// 空间复杂度:O(1)
// 不稳定排序
void ShellSort(vector<int>& arr)
{int n = arr.size();int gap = n;while (gap > 1){gap = gap / 3 + 1;for (size_t i = gap; i < n; i++){int tmp = arr[i];int j = i - gap;while (j >= 0 && tmp < arr[j]){arr[j + gap] = arr[j];j -= gap;}arr[j + gap] = tmp;}}
}

二、 选择排序

选择排序

选择排序(Selection Sort) 是一种简单直观的排序算法,其核心思想是 “每一轮从待排序元素中选出最小(或最大)的元素,将其与待排序部分的起始位置交换,以此逐步完成整个序列的排序”。

算法步骤

假设要对一个长度为 n 的数组 arr 进行升序排序,选择排序的执行过程可拆解为以下步骤:

  1. 初始状态:将数组分为 “已排序部分” 和 “待排序部分”。初始时,“已排序部分” 为空,“待排序部分” 为整个数组(索引 0 到 n-1)。
  2. 第一轮选择:在 “待排序部分”(索引 0 到 n-1)中找到最小元素,将其与 “待排序部分” 的第一个元素(索引 0)交换。此时,“已排序部分” 扩展为索引 0,“待排序部分” 缩小为索引 1 到 n-1。
  3. 后续轮次:重复 “选择 - 交换” 操作:第 i 轮(i 从 1 到 n-2)中,在 “待排序部分”(索引 i 到 n-1)中找到最小元素,与 “待排序部分” 的第一个元素(索引 i)交换。每轮结束后,“已排序部分” 增加一个元素,“待排序部分” 减少一个元素。
  4. 结束:当 “待排序部分” 仅剩 1 个元素时(无需再比较),整个数组排序完成。

代码实现

为了提高效率,我们可以在进行一次遍历时就分别找到当前最大最小的元素。

// SelectSort 选择排序
// 平均时间复杂度:O(N*N)
// 空间复杂度:O(1)
// 不稳定排序
void SelectSort(vector<int>& arr)
{int begin = 0, end = arr.size() - 1;while (begin < end){int minIdx = begin;int maxIdx = end;for (int i = begin; i <= end; i++){if (arr[minIdx] > arr[i]){minIdx = i;}if (arr[maxIdx] < arr[i]){maxIdx = i;}}swap(arr[minIdx], arr[begin]);if (maxIdx == begin){maxIdx = minIdx;}swap(arr[maxIdx], arr[end]);begin++;end--;}
}

堆排序

堆排序(Heap Sort) 是一种基于 “堆” 这种数据结构的高效排序算法,其核心思想是 通过构建 “大顶堆”(或 “小顶堆”)将数组中的最大(或最小)元素逐步 “提取” 到有序位置,最终完成整个数组的排序。堆排序兼具 “原地排序” 和 “O (nlogn) 时间复杂度” 的特点,是实用且经典的排序算法之一。如果不了解堆的,可以先去看我之前的文章:

堆排序之前也有讲解并用C语言实现的代码:
堆排序

代码实现

// AdjustDown 向下调整建堆
void AdjustDown(vector<int>& arr, int n, int parent)
{int child = parent * 2 + 1;while (child < n){if (child + 1 < n && arr[child] < arr[child + 1]){child++;}if (arr[parent] < arr[child]){swap(arr[parent], arr[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}
// HeapSort 堆排序
// 平均时间复杂度:O(N*logN)
// 空间复杂度:O(1)
// 不稳定排序
void HeapSort(vector<int>& arr)
{int n = arr.size();for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(arr, n, i);}for (int i = n - 1; i > 0; i--){swap(arr[0], arr[i]);AdjustDown(arr, i, 0);}
}

三、 交换排序

冒泡排序

冒泡排序是一种简单直观的排序算法,其核心思想是重复地走访要排序的数组,一次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。因为越大的元素会通过交换 “浮” 到数组的末尾,就像水中的气泡逐渐上浮一样,所以得名 “冒泡排序”。

算法步骤

假设要对数组 arr 进行升序排序,数组长度为 n,步骤如下:

  1. 第一轮遍历(从索引 0 到 n-2):
    • 依次比较相邻的两个元素 arr[i] 和 arr[i+1](i 从 0 开始)。
    • 如果 arr[i] > arr[i+1](顺序错误),则交换这两个元素的值。
    • 遍历结束后,数组中最大的元素会被 “浮” 到数组的末尾(索引 n-1 处),该元素已确定位置,不再参与后续比较。
  2. 第二轮遍历(从索引 0 到 n-3):
    • 重复步骤 1 的比较和交换操作,但遍历范围缩小到 “未排序部分”(排除已确定位置的末尾元素)。
    • 遍历结束后,数组中第二大的元素会被 “浮” 到数组的倒数第二位(索引 n-2 处)。
  3. 重复上述过程:
    • 每一轮遍历都将 “未排序部分” 中最大的元素放到对应位置,遍历的终点索引依次减 1(从 n-2 逐步减到 0)。
    • 直到完成第 n-1 轮遍历(或提前判断数组已有序),排序结束。
  4. 优化:提前终止遍历
    • 如果在某一轮遍历中没有发生任何元素交换,说明数组已经完全有序,无需继续后续遍历,可直接终止算法。这一优化能显著减少不必要的计算,尤其适合接近有序的数组。

代码实现

// BubbleSort 冒泡排序
// 平均时间复杂度:O(N*N)
// 空间复杂度:O(1)
// 稳定排序
void BubbleSort(vector<int>& arr)
{int n = arr.size();for (int i = 0; i < n - 1; i++){bool flag = true;for (int j = 0; j < n - 1 - i; j++){if (arr[j + 1] < arr[j]){swap(arr[j + 1], arr[j]);flag = false;}}if (flag){break;}}
}

快速排序

快速排序是一种高效的排序算法,基于 “分治” 思想,通过选择一个 “基准元素” 将数组分为两部分,最终实现整体排序。其核心特点是平均时间复杂度低(O (nlogn))、实际应用中性能优异,是最常用的排序算法之一。
快速排序的核心思想

  1. 选择基准(pivot):从数组中挑选一个元素作为 “基准”(通常选第一个、最后一个或中间元素)。
  2. 分区(partition):将数组重新排列,所有比基准小的元素放基准左边,比基准大的元素放基准右边(等于基准的元素可放任意一侧)。
  3. 递归排序:对基准左右两侧的子数组重复上述 “选基准 - 分区” 过程,直到子数组长度为 1(天然有序)。

算法步骤

假设要排序数组 arr,范围为 [left, right](初始 left=0,right=n-1):

  1. 步骤 1:分区操作(核心)
    • 选择基准元素 pivot = arr[right](以最右侧元素为例)。
    • 初始化 “小于基准区域的边界” i = left - 1(初始为空)。
    • 遍历数组 [left, right-1],对每个元素 arr[j]:
      • 若 arr[j] <= pivot,则 i += 1,交换 arr[i] 和 arr[j](将当前元素纳入 “小于基准区域”)。
    • 遍历结束后,交换 arr[i+1] 和 arr[right](将基准放到 “小于区域” 和 “大于区域” 之间),此时基准的最终位置为 i+1。
  2. 步骤 2:递归处理子数组
    • 对基准左侧子数组 [left, i] 递归执行快速排序。
    • 对基准右侧子数组 [i+2, right] 递归执行快速排序。
    • 递归终止条件:left >= right(子数组长度为 0 或 1)。

代码实现

// 原始快排
void QuickSortSubfunc1(vector<int>& arr,int begin,int end)
{if (begin >= end){return;}int left = begin, right = end;// 将最左边的值作为key//int key = left;// 随机key//int key = rand() % (right - left + 1) + left;//swap(arr[key], arr[left]);//key = left;// 三数取中int key = GetMid(arr, left, right);swap(arr[key], arr[left]);key = left;while (left < right){while (left < right && arr[right] >= arr[key]){right--;}while (left < right && arr[left] <= arr[key]){left++;}swap(arr[right], arr[left]);}swap(arr[key], arr[left]);QuickSortSubfunc1(arr, begin, left - 1);QuickSortSubfunc1(arr, left + 1, end);
}
// QuickSort 快速排序
// 平均时间复杂度:O(N*logN)
// 空间复杂度:O(logN)
// 不稳定排序
void QuickSort(vector<int>& arr)
{int n = arr.size();QuickSortSubfunc1(arr, 0, n - 1);
}

快速排序的实现有很多细节和一些不同的写法,例如快慢指针法,非递归版本,三路快排等,在这里就不一一列举了,如果想要了解其他的实现方式,可以看这里:
快速排序详细实现


四、 归并排序

归并排序

归并排序(Merge Sort)是一种基于 “分治” 思想的高效排序算法,其核心思路是将数组不断拆分至最小单元,再通过合并两个有序子数组的方式逐步构建完整的有序数组。归并排序以稳定性和稳定的时间复杂度著称,是处理大规模数据排序的理想选择之一。
归并排序的核心思想

  1. 分(Divide):将待排序数组递归拆分为两个规模大致相等的子数组,直到子数组长度为 1(单个元素天然有序)。
  2. 治(Conquer):将两个已排序的子数组合并为一个更大的有序数组(核心操作是 “合并”)。
  3. 合(Combine):重复 “分” 与 “治” 的过程,最终得到完整的有序数组。

算法步骤

假设要排序数组 arr,步骤如下:

  1. 步骤 1:拆分(分阶段)
    • 将数组 arr 从中间分为左子数组 left 和右子数组 right(若长度为奇数,左子数组可少一个元素)。
    • 递归拆分 left 和 right,直到所有子数组长度为 1(终止条件)。
  2. 步骤 2:合并(治阶段)
    • 合并两个有序子数组 left 和 right 为一个有序数组 result:
    • 初始化两个指针 i(指向 left 起点)和 j(指向 right 起点),以及结果数组 result。
    • 比较 left[i] 和 right[j]:
      • 若 left[i] <= right[j],将 left[i] 加入 result,i += 1;
      • 否则,将 right[j] 加入 result,j += 1。
    • 当一个子数组遍历完毕后,将另一个子数组的剩余元素直接追加到 result。
    • 返回 result 作为合并后的有序数组。
  3. 步骤 3:递归合并
    • 从最小的子数组开始,逐步向上合并,最终得到完整的有序数组。

代码实现

这里给出归并排序的递归版本和非递归版本给大家参考。

// MergeSortSubfunc 归并排序子函数
void MergeSortSubfunc(vector<int>& arr, vector<int>& tmp, int left, int right)
{if (left >= right){return;}int mid = (right - left) / 2 + left;MergeSortSubfunc(arr, tmp, left, mid);MergeSortSubfunc(arr, tmp, mid + 1, right);int i = left, j = mid + 1, k = left;while (i <= mid && j <= right){if (arr[i] <= arr[j]){tmp[k] = arr[i];i++;}else{tmp[k] = arr[j];j++;}k++;}while (i <= mid){tmp[k] = arr[i];i++;k++;}while (j <= right){tmp[k] = arr[j];j++;k++;}for (int i = left; i <= right; i++){arr[i] = tmp[i];}
}
// NonRecursiveMergeSort 非递归归并排序
void NonRecursiveMergeSort(vector<int>& arr)
{int n = arr.size();if (n <= 1){return;}vector<int> tmp(n);for (int gap = 1; gap < n; gap *= 2){for (int left = 0; left < n; left += 2 * gap){int mid = min(left + gap - 1, n - 1);int right = min(left + 2 * gap - 1, n - 1);int i = left, j = mid + 1, k = left;while (i <= mid && j <= right){if (arr[i] <= arr[j]){tmp[k] = arr[i];i++;}else{tmp[k] = arr[j];j++;}k++;}while (i <= mid){tmp[k] = arr[i];i++;k++;}while (j <= right){tmp[k] = arr[j];j++;k++;}for (int i = left; i <= right; i++){arr[i] = tmp[i];}}}
}
// MergeSort 归并排序
// 平均时间复杂度:O(N*logN)
// 空间复杂度:O(N)
// 稳定排序
void MergeSort(vector<int>& arr)
{int n = arr.size();vector<int> tmp(n);//MergeSortSubfunc(arr, tmp, 0, n - 1);NonRecursiveMergeSort(arr);
}

总结

以上就是今天要讲的内容,本文简单给大家介绍了常见的几种排序算法,这些排序算法还是比较重要的,在找工作面试的过程中可能也会被问到,所以还是尽可能将其掌握。

如对您有所帮助,可以来个三连,感谢大家的支持。


每文推荐

林俊杰–达尔文
张惠妹–掉了
何润东–我记得我爱过

学技术学累了时可以听歌放松一下。


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

相关文章:

  • STM32窗口看门狗(WWDG)深度解析:精准守护嵌入式系统的实时性
  • day39-keepalived
  • How to Use Managed Identity with ACS?
  • 全面解析主流AI模型:功能对比与应用推荐
  • douyin_search_tool:用python开发的抖音关键词搜索采集软件
  • 低功耗全双工远距离无线对讲解决方案
  • 【数位DP】D. From 1 to Infinity
  • 数据库字段类型深度解析:从关系型到 NoSQL 的全面指南
  • Placement new是什么
  • CUDA和torch的安装
  • 【LeetCode】363. 矩形区域不超过 K 的最大数值和 (二分 + 前缀和)
  • 拓扑排序|hash
  • 深入剖析Spring Boot应用启动全流程
  • MySQL GPG 密钥更新问题解决文档
  • Centos7.9 Docker26容器化部署 MySql9.4 一主一从的同步复制部署
  • 【51单片机非精准延时演示来回流水灯效果】2022-11-10
  • 【机器学习深度学习】自然语言与多模态大模型
  • 【KO】前端面试一
  • git的工作使用中实际经验
  • 关于Highcharts的数据参考与产品系列
  • Camera performance analysis
  • 智能系统与未来生态演进初步思考
  • 告别图片背景违和!autohue.js 让图片与背景自动 “无缝衔接”
  • 基于51单片机自动智能浇花系统设计
  • 【序列晋升】13 Spring Cloud Bus微服务架构中的消息总线
  • 研究生方向:在传统赛道与交叉领域间寻找破局之路
  • 第三阶段数据库-2:数据库中的sql语句
  • 重审文字的本体论地位:符号学转向、解构主义突围与视觉性革命
  • 1电吉他环境搭建:效果器再探
  • C++算法题—— 小C的细菌(二维偏序离线 + 树状数组 + 坐标压缩)