C++实现常见的排序算法
排序算法
- 前言
- 一、 插入排序
- 直接插入排序
- 算法步骤
- 代码实现
- 希尔排序
- 为什么需要希尔排序?
- 算法步骤
- 代码实现
- 二、 选择排序
- 选择排序
- 算法步骤
- 代码实现
- 堆排序
- 代码实现
- 三、 交换排序
- 冒泡排序
- 算法步骤
- 代码实现
- 快速排序
- 算法步骤
- 代码实现
- 四、 归并排序
- 归并排序
- 算法步骤
- 代码实现
- 总结
- 每文推荐
前言
本文将介绍一些常用的排序算法及其C++代码的实现,主要为比较类排序,分为以下部分:
1. 插入排序
直接插入排序
希尔排序
2. 选择排序
选择排序
堆排序
3. 交换排序
冒泡排序
快速排序
4. 归并排序
归并排序
一、 插入排序
直接插入排序
直接插入排序是一种简单直观的排序算法,其核心思想是 “将待排序元素逐个插入到已排序序列的合适位置”,类似于玩扑克牌时整理手牌的过程。
算法步骤
假设要对数组 arr 进行排序(以升序为例),步骤如下:
- 初始化已排序序列:默认数组的第 1 个元素(arr[0])为 “已排序序列”(因为单个元素本身就是有序的)。
- 遍历待排序元素:从第 2 个元素开始(i=1 到 i=arr.length-1),每个元素 arr[i] 为 “待插入元素”。
- 寻找插入位置:将 “待插入元素” 与 “已排序序列” 中的元素从后往前对比(设对比索引为 j,初始 j=i-1):
- 若 arr[j] > arr[i](说明 “已排序元素” 需后移),则将 arr[j] 赋值给 arr[j+1],j 减 1(继续向前对比);若 arr[j] <= arr[i](说明找到插入位置),停止对比。
- 插入元素:将 “待插入元素”arr[i] 插入到最终位置 j+1(若 j 减到 -1,说明该元素是最小的,插入到数组开头)。
- 重复步骤 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),步骤如下:
- 初始化步长:设初始步长 gap = n / 2(通常从数组长度的一半开始)。
- 按步长分组排序:
- 按 gap 将数组分成 gap 个 “子序列”(每个子序列的元素下标相差 gap,比如下标 0, gap, 2gap… 为一组,1, 1+gap, 1+2gap… 为另一组)。
- 对每个子序列单独执行 “直接插入排序”(子序列内部排序,不影响其他组元素的相对位置)。
- 减小步长:更新 gap = gap / 2(通常每次减半),重复步骤 2,直到 gap = 0(停止条件)。
- 最终排序:当 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 进行升序排序,选择排序的执行过程可拆解为以下步骤:
- 初始状态:将数组分为 “已排序部分” 和 “待排序部分”。初始时,“已排序部分” 为空,“待排序部分” 为整个数组(索引 0 到 n-1)。
- 第一轮选择:在 “待排序部分”(索引 0 到 n-1)中找到最小元素,将其与 “待排序部分” 的第一个元素(索引 0)交换。此时,“已排序部分” 扩展为索引 0,“待排序部分” 缩小为索引 1 到 n-1。
- 后续轮次:重复 “选择 - 交换” 操作:第 i 轮(i 从 1 到 n-2)中,在 “待排序部分”(索引 i 到 n-1)中找到最小元素,与 “待排序部分” 的第一个元素(索引 i)交换。每轮结束后,“已排序部分” 增加一个元素,“待排序部分” 减少一个元素。
- 结束:当 “待排序部分” 仅剩 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,步骤如下:
- 第一轮遍历(从索引 0 到 n-2):
- 依次比较相邻的两个元素 arr[i] 和 arr[i+1](i 从 0 开始)。
- 如果 arr[i] > arr[i+1](顺序错误),则交换这两个元素的值。
- 遍历结束后,数组中最大的元素会被 “浮” 到数组的末尾(索引 n-1 处),该元素已确定位置,不再参与后续比较。
- 第二轮遍历(从索引 0 到 n-3):
- 重复步骤 1 的比较和交换操作,但遍历范围缩小到 “未排序部分”(排除已确定位置的末尾元素)。
- 遍历结束后,数组中第二大的元素会被 “浮” 到数组的倒数第二位(索引 n-2 处)。
- 重复上述过程:
- 每一轮遍历都将 “未排序部分” 中最大的元素放到对应位置,遍历的终点索引依次减 1(从 n-2 逐步减到 0)。
- 直到完成第 n-1 轮遍历(或提前判断数组已有序),排序结束。
- 优化:提前终止遍历
- 如果在某一轮遍历中没有发生任何元素交换,说明数组已经完全有序,无需继续后续遍历,可直接终止算法。这一优化能显著减少不必要的计算,尤其适合接近有序的数组。
代码实现
// 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))、实际应用中性能优异,是最常用的排序算法之一。
快速排序的核心思想
- 选择基准(pivot):从数组中挑选一个元素作为 “基准”(通常选第一个、最后一个或中间元素)。
- 分区(partition):将数组重新排列,所有比基准小的元素放基准左边,比基准大的元素放基准右边(等于基准的元素可放任意一侧)。
- 递归排序:对基准左右两侧的子数组重复上述 “选基准 - 分区” 过程,直到子数组长度为 1(天然有序)。
算法步骤
假设要排序数组 arr,范围为 [left, right](初始 left=0,right=n-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:递归处理子数组
- 对基准左侧子数组 [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)是一种基于 “分治” 思想的高效排序算法,其核心思路是将数组不断拆分至最小单元,再通过合并两个有序子数组的方式逐步构建完整的有序数组。归并排序以稳定性和稳定的时间复杂度著称,是处理大规模数据排序的理想选择之一。
归并排序的核心思想
- 分(Divide):将待排序数组递归拆分为两个规模大致相等的子数组,直到子数组长度为 1(单个元素天然有序)。
- 治(Conquer):将两个已排序的子数组合并为一个更大的有序数组(核心操作是 “合并”)。
- 合(Combine):重复 “分” 与 “治” 的过程,最终得到完整的有序数组。
算法步骤
假设要排序数组 arr,步骤如下:
- 步骤 1:拆分(分阶段)
- 将数组 arr 从中间分为左子数组 left 和右子数组 right(若长度为奇数,左子数组可少一个元素)。
- 递归拆分 left 和 right,直到所有子数组长度为 1(终止条件)。
- 步骤 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:递归合并
- 从最小的子数组开始,逐步向上合并,最终得到完整的有序数组。
代码实现
这里给出归并排序的递归版本和非递归版本给大家参考。
// 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);
}
总结
以上就是今天要讲的内容,本文简单给大家介绍了常见的几种排序算法,这些排序算法还是比较重要的,在找工作面试的过程中可能也会被问到,所以还是尽可能将其掌握。
如对您有所帮助,可以来个三连,感谢大家的支持。
每文推荐
林俊杰–达尔文
张惠妹–掉了
何润东–我记得我爱过
学技术学累了时可以听歌放松一下。