std::sort
是 C++ 标准库中最常用的排序算法之一,其底层实现虽然在不同的编译器和标准库实现中有所差异,但大多数实现都遵循一定的准则,通常使用混合排序算法。常见的实现方案包括 快速排序(QuickSort) 、插入排序(Insertion Sort) 和 堆排序(HeapSort) 等,基于数据的大小和特性进行选择。我们将以 libc++ 和 libstdc++ 等实现为例来探讨 std::sort
的源码实现。
1. std::sort
的核心思想
std::sort
是一个 排序算法 ,它的目标是对一个区间中的元素进行排序,并且提供 O(n log n) 平均时间复杂度。根据不同的输入情况,std::sort
会采用不同的排序策略。例如,对于大数据集,它通常会使用快速排序,而对于小数据集或几乎有序的数组,它可能会使用插入排序。
2. 常见 STL 实现
2.1 libstdc++(GCC 的标准库实现)
libstdc++
中的 std::sort
实现通常使用 混合排序算法。具体的实现大致可以分为以下几个步骤:
- 小数据量切换到插入排序 :当数据规模小于一定阈值时,
std::sort
切换到插入排序。 - 使用快速排序 :对于较大数据集,
std::sort
会使用快速排序(QuickSort)。为了避免快速排序的最坏情况,std::sort
通常会采用 三数取中法(Median-of-Three) 来选择基准元素,从而减少退化为 O(n²) 的概率。 - 堆排序(HeapSort)作为备用 :如果快速排序的递归深度过大,或者递归调用导致栈溢出,
std::sort
会退回到堆排序。
为什么当数量<=16个时不继续用快排而是插入排序:
因为插入排序在CPU缓存中连续性好,这种小量排序哪怕时间复杂度时On2的,仍然可以比插入排序这种缓存经常跳跃的要快。
2.2 libc++(LLVM 的标准库实现)
libc++
的 std::sort
实现也采用类似的混合排序策略。它使用 快速排序 作为默认算法,并且结合 插入排序 和 堆排序 来优化不同的情况。特别地,libc++
在选择基准元素时,也会采用 三数取中法,并且在递归深度过大时,会使用堆排序。
3. std::sort
的源码实现(以 GCC libstdc++
为例)
为了更直观地了解 std::sort
的实现,我们将基于 libstdc++
(GCC 的标准库)来分析其实现。以下是简化版的 std::sort
实现:
3.1 快速排序的实现
在 std::sort
中,快速排序通常是主要的排序算法。为了避免最坏情况,std::sort
会在每次递归时使用 三数取中法 来选择基准元素。这个方法是通过选择序列的第一个元素、最后一个元素和中间元素中的中位数来作为基准。
cpp
template <typename RandomAccessIterator>
void quick_sort(RandomAccessIterator first, RandomAccessIterator last) {
if (last - first <= 1) return;
// 选择基准元素:三数取中法
RandomAccessIterator mid = first + (last - first) / 2;
if (*mid < *first) std::swap(*mid, *first);
if (*last < *first) std::swap(*last, *first);
if (*mid < *last) std::swap(*mid, *last);
RandomAccessIterator pivot = mid;
// 分区操作
RandomAccessIterator left = first;
RandomAccessIterator right = last - 1;
while (true) {
while (*left < *pivot) ++left;
while (*pivot < *right) --right;
if (left >= right) break;
std::swap(*left, *right);
++left;
--right;
}
quick_sort(first, right);
quick_sort(left, last);
}
3.2 插入排序的实现
当数据规模较小或数据已经有序时,std::sort
会切换到 插入排序。插入排序的时间复杂度是 O(n²),但在数据量较小时,插入排序比其他 O(n log n) 算法(如快速排序)表现更好。
cpp
template <typename RandomAccessIterator>
void insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
for (RandomAccessIterator i = first + 1; i != last; ++i) {
auto key = *i;
RandomAccessIterator j = i;
while (j > first && *(j - 1) > key) {
*j = *(j - 1);
--j;
}
*j = key;
}
}
3.3 堆排序的实现
如果快速排序的递归深度过大,或者出现堆栈溢出,std::sort
会切换到 堆排序。堆排序的时间复杂度为 O(n log n),并且在最坏情况下依然能保持这一复杂度。
cpp
template <typename RandomAccessIterator>
void heapify(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator root) {
RandomAccessIterator left = first + (2 * (root - first) + 1);
RandomAccessIterator right = first + (2 * (root - first) + 2);
RandomAccessIterator largest = root;
if (left < last && *left > *largest) largest = left;
if (right < last && *right > *largest) largest = right;
if (largest != root) {
std::swap(*root, *largest);
heapify(first, last, largest);
}
}
template <typename RandomAccessIterator>
void heap_sort(RandomAccessIterator first, RandomAccessIterator last) {
for (RandomAccessIterator i = first + (last - first) / 2 - 1; i >= first; --i) {
heapify(first, last, i);
}
for (RandomAccessIterator i = last - 1; i > first; --i) {
std::swap(*first, *i);
heapify(first, i, first);
}
}
3.4 混合排序的实现
std::sort
在不同的场景下会选择不同的排序算法。如果数据规模较小,则切换到插入排序;如果数据较大且递归深度过大,则切换到堆排序。实际的 std::sort
通常会有一个阈值来决定何时切换到插入排序。
cpp
template <typename RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last) {
if (last - first <= INSERTION_SORT_THRESHOLD) {
insertion_sort(first, last); // 小规模使用插入排序
} else {
quick_sort(first, last); // 默认使用快速排序
}
}
std::sort
的优缺点总结
std::sort
是 C++ 标准库中最常用的排序算法,它的底层实现通常是基于 混合排序算法 ,结合了多种排序算法的优点,以确保在不同数据分布和规模下都能够提供高效的排序性能。以下是 std::sort
的优缺点总结:
优点
-
高效的平均性能
std::sort
在大多数情况下的时间复杂度为 O(n log n),特别是对于无序的大数据集。通常,std::sort
默认使用快速排序(QuickSort),在数据量大时能够提供非常高效的排序。
-
针对小数据集优化
- 对于数据量较小或已部分排序的数据,
std::sort
会使用 插入排序,插入排序在小数据集或接近有序的情况下具有较低的开销,能够比其他排序算法(如快速排序)更快。
- 对于数据量较小或已部分排序的数据,
-
避免最坏情况的性能退化
- 快速排序的最坏时间复杂度为 O(n²),但通过采用 三数取中法 (Median-of-Three)来选择基准元素,可以有效避免数据已经有序时发生最坏情况。此外,递归深度过深时,
std::sort
可能会转而使用 堆排序,保证最坏情况的时间复杂度始终为 O(n log n)。
- 快速排序的最坏时间复杂度为 O(n²),但通过采用 三数取中法 (Median-of-Three)来选择基准元素,可以有效避免数据已经有序时发生最坏情况。此外,递归深度过深时,
-
内存效率
std::sort
通常是 原地排序 (in-place sort),不需要额外的内存开销。与需要额外内存的排序算法(如归并排序)相比,std::sort
在内存消耗上更加高效。
-
稳定性(可选)
- 虽然
std::sort
本身不是稳定排序算法(即相等的元素可能会改变原来的顺序),但std::stable_sort
是稳定版本,适用于那些需要保留相等元素顺序的应用场景。
- 虽然
-
符合标准
std::sort
是 C++ 标准库的一部分,提供跨平台的兼容性。任何支持 C++ 标准库的编译器都实现了该算法,因此可以保证代码在不同平台间的可移植性。
缺点
-
最坏情况性能依然可能较差
- 即使通过三数取中法和堆排序的退化策略,
std::sort
的性能在某些极端情况下(例如大量重复元素或完全有序数据)仍然可能退化。对于非常特殊的输入数据,最坏情况下的性能可能接近 O(n²),尤其是在没有进行递归深度控制或选择基准元素不当时。
- 即使通过三数取中法和堆排序的退化策略,
-
不稳定排序
std::sort
本身是一个 不稳定的排序算法 ,即相等的元素在排序后可能会改变它们的相对顺序。如果排序的稳定性对你的应用至关重要,你需要使用std::stable_sort
,而后者的性能稍逊一筹,尤其是在大数据集的情况下。
-
需要选择阈值
- 在实际实现中,
std::sort
会根据数据大小来决定是否使用插入排序、快速排序或堆排序,这通常需要为阈值选择适当的数值。如果选择不当,可能会导致性能下降。例如,如果快速排序的阈值设置过低,可能会导致不必要的插入排序操作,反之,如果阈值过高,可能无法充分利用插入排序的小数据集优化。
- 在实际实现中,
-
递归栈深度
- 快速排序依赖递归实现,在递归调用的过程中,尤其是在数据规模非常大的时候,可能会面临 栈溢出 的风险。虽然许多实现通过尾递归优化或限制递归深度来避免这个问题,但在某些情况下,仍然可能因为过深的递归导致性能问题。
-
算法的实现复杂度
std::sort
作为一个混合排序算法,涉及多个算法(快速排序、堆排序、插入排序)的切换和优化,这使得实现比单一的排序算法更为复杂。如果对底层实现不熟悉,可能会增加理解和调试的难度。
4. 总结
std::sort
是 C++ 标准库中的高效排序算法,采用了混合排序策略,默认使用快速排序来排序大数据集,对于小数据集则使用插入排序,在需要时会退回到堆排序。std::sort
的实现还使用了多种优化策略,包括三数取中法、递归深度控制等,确保在不同情况下都能提供 O(n log n) 的平均时间复杂度。通过这些优化,std::sort
在大多数实际应用中表现出色。
如果你有兴趣了解具体的实现,可以查看 GCC 或 LLVM 标准库源代码,那里提供了更为详细的实现细节。
推荐:
- std::sort 源码讲解(深入源码) gcc13.2 c++17
- what-algorithms-are-used-in-c11-stdsort-in-different-stl-implementations
- std::sort
原理和源码讲解(深入源码