在 C++ 编程中,排序算法是非常基础且重要的知识。今天我们就来深入探讨两种常见的排序算法:插入排序和计数排序,包括它们的代码实现、时间复杂度、空间复杂度、稳定性分析以及是否有优化提升的空间。
一、插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法,它的工作原理类似于我们扑克牌理牌的过程,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1. 代码实现
#include <iostream>
#include <vector>
void insertionSort(std::vector<int>& arr) {
int n = arr.size();
for (int i = 1; i < n; ++i) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
int main() {
std::vector<int> arr = { 12, 11, 13, 5, 6 };
insertionSort(arr);
for (int num : arr)
std::cout << num << " ";
std::cout << std::endl;
return 0;
}
在上述代码中,我们从数组的第二个元素开始,将当前元素 key 与已排序部分的元素依次比较,找到合适位置插入,使得已排序部分始终保持有序。
2. 时间复杂度
- 最好情况:当输入数组已经有序时,每次比较只需一次,时间复杂度为 O(n),这里的n是数组的元素个数。因为只需要遍历一遍数组,每次比较都能确定当前元素位置无需移动。
- 最坏情况:当输入数组是逆序时,对于每个元素,都需要与前面所有已排序元素比较并移动,时间复杂度为 O()。
- 平均情况:平均时间复杂度也是 O() ,因为在平均意义下,每个元素大约需要与已排序部分一半的元素进行比较和移动。
3. 空间复杂度
插入排序是原地排序算法,它只需要常数级别的额外空间,空间复杂度为O(1) 。因为在排序过程中,我们仅仅是在原数组上通过交换元素位置来实现排序,不需要额外开辟大量的数据存储空间。
4. 稳定性
插入排序是稳定的排序算法。所谓稳定,就是在排序过程中,如果两个元素相等,它们在排序前后的相对顺序保持不变。在插入排序代码中,当遇到相等元素时,我们不会交换它们的位置,只是将当前元素插入到合适位置,所以相等元素的相对顺序得以保留。
二、计数排序
计数排序(Counting Sort)是一种非比较排序算法,它适用于一定范围内的整数排序,利用元素值作为索引,统计每个元素出现的次数,然后根据统计结果将元素依次放回原数组。
1. 代码实现
#include <iostream>
#include <vector>
#include <algorithm>
void countingSort(std::vector<int>& arr) {
int maxVal = *std::max_element(arr.begin(), arr.end());
std::vector<int> count(maxVal + 1, 0);
std::vector<int> output(arr.size(), 0);
// 统计每个元素出现次数
for (int num : arr)
count[num]++;
// 计算累计次数,确定元素在输出数组中的位置
for (int i = 1; i <= maxVal; ++i)
count[i] += count[i - 1];
// 从后向前遍历原数组,将元素放入正确位置
for (int i = arr.size() - 1; i >= 0; --i) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
// 将排序后结果复制回原数组
for (int i = 0; i < arr.size(); ++i)
arr[i] = output[i];
}
int main() {
std::vector<int> arr = { 4, 2, 2, 8, 3, 3, 1 };
countingSort(arr);
for (int num : arr)
std::cout << num << " ";
std::cout << std::endl;
return 0;
}
这段代码首先找出数组中的最大值,以此确定计数数组的大小。接着统计每个元素出现次数,再通过累计次数确定元素在输出数组中的最终位置,最后将排序好的元素放回原数组。
2. 时间复杂度
- 计数排序的时间复杂度为O(n+m) ,其中n是输入数组的元素个数, m是输入数据的范围(最大值与最小值的差值加 1)。在统计元素出现次数和计算累计次数这两个步骤,都需要遍历一遍范围为m的计数数组,而遍历原数组需要n次操作,所以总的时间复杂度是两者之和。
- 当m = n时,时间复杂度接近线性,这使得计数排序在特定场景下比基于比较的排序算法(如快速排序、归并排序等,时间复杂度O(nlogn)更快。
3. 空间复杂度
计数排序需要额外开辟计数数组和输出数组,所以空间复杂度为O(n+m) 。当数据范围 较大时,会占用较多的额外空间,这也是它的一个缺点。例如,如果要对 0 到 1000000 范围内的 100 个整数排序,就需要开辟一个长度为 1000001 的计数数组。
4. 稳定性
计数排序是稳定的排序算法。在代码实现中,从后向前遍历原数组放置元素到输出数组,相同元素后出现的会被放在靠后的位置,保证了稳定性。例如对于序列 [2, 1, 2],排序后仍然是 [1, 2, 2]。
总之,插入排序和计数排序各有优劣,在实际编程中,需要根据数据规模、数据范围、稳定性要求等因素综合选择合适的排序算法,以达到最优的性能表现。希望这篇文章能帮助大家深入理解这两种排序算法,在 C++ 编程中运用自如。