CppCon 2018 学习:A Little Order! Delving into the STL sorting algorithms
记录一下一个编译器加密的算法
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <utility>
#include <array>
#include <iostream>
#include <string_view>
namespace detail {
// 编译期伪随机 key:每个字符对应不同 key
template <std::size_t N>
constexpr std::uint8_t key8() {return static_cast<std::uint8_t>((N * 31 + 57) ^ 0xAA);
}
} // namespace detail
// 主模板声明
template <typename CharT, std::size_t Size, typename KeySeq, typename IndexSeq>
class xor_string;
// 偏特化实现
template <typename CharT, std::size_t Size, std::uint8_t... Keys, std::size_t... Indices>
class xor_string<CharT, Size, std::integer_sequence<std::uint8_t, Keys...>,std::index_sequence<Indices...>> {std::array<CharT, Size> encrypted_{};mutable std::array<CharT, Size> decrypted_{};mutable bool decrypted = false;
public:// 修复:添加支持 integral_constant 的构造函数template <typename L, std::size_t S>constexpr xor_string(L l, std::integral_constant<std::size_t, S>,std::index_sequence<Indices...>): encrypted_{static_cast<CharT>(static_cast<std::uint8_t>(l()[Indices]) ^detail::key8<Indices>())...} {static_assert(Size > 0 && l()[Size - 1] == '\0', "String must be null-terminated.");static_assert(S == Size, "Size mismatch");}// 原有的构造函数:直接接受 key sequencetemplate <typename L>constexpr xor_string(L l, std::integer_sequence<std::uint8_t, Keys...>,std::index_sequence<Indices...>): encrypted_{static_cast<CharT>(static_cast<std::uint8_t>(l()[Indices]) ^ Keys)...} {static_assert(Size > 0 && l()[Size - 1] == '\0', "String must be null-terminated.");}// 解密函数const CharT* c_str() const {if (!decrypted) {// 使用编译期生成的 key 解密((decrypted_[Indices] = static_cast<CharT>(static_cast<std::uint8_t>(encrypted_[Indices]) ^ detail::key8<Indices>())),...);decrypted = true;}return decrypted_.data();}// 输出支持friend std::ostream& operator<<(std::ostream& os, const xor_string& xs) {return os << xs.c_str();}
};
// 类模板推导指引(CTAD):告诉编译器如何推导模板参数
template <class L, std::size_t Size, std::size_t... Indices>
xor_string(L, std::integral_constant<std::size_t, Size>, std::index_sequence<Indices...>)-> xor_string<std::remove_const_t<std::remove_reference_t<decltype(std::declval<L>()()[0])>>,Size, std::integer_sequence<std::uint8_t, detail::key8<Indices>()...>,std::index_sequence<Indices...>>;
// 宏封装:简化调用
#define XOR_LITERAL(str) \xor_string { \[] { return str; }, std::integral_constant<std::size_t, sizeof(str)>(), \std::make_index_sequence<sizeof(str)>() \}
int main() {// 使用修复后的构造函数constexpr auto secret = XOR_LITERAL("Hello, XOR World!");std::cout << "Decrypted: " << secret << "\n";// 也可以使用宏constexpr auto secret2 = XOR_LITERAL("Another secret!");std::cout << "Decrypted 2: " << secret2 << "\n";std::string_view sv(secret.c_str());std::cout << "Length: " << sv.size() << "\n";return 0;
}
语法结构解析:
template <class L, std::size_t Size, std::size_t... Indices>
xor_string(L, // 第一个参数:lambda(返回字符串常量)std::integral_constant<std::size_t, Size>, // 第二个参数:字符串大小std::index_sequence<Indices...> // 第三个参数:字符串索引序列(0, 1, ..., N-1)
)
-> xor_string<std::remove_const_t<std::remove_reference_t<decltype(std::declval<L>()()[0]) // 第一个字符的类型,一般是 char>>,Size, // 字符串大小std::integer_sequence<std::uint8_t, detail::key8<Indices>()...>, // 编译期 keystd::index_sequence<Indices...> // 字符索引序列
>;
举个具体例子
constexpr auto secret = XOR_LITERAL("Hello");
宏展开:
xor_string {[] { return "Hello"; }, // Lstd::integral_constant<std::size_t, 6>(), // Size = 6, includes '\0'std::make_index_sequence<6>() // Indices = 0, 1, 2, 3, 4, 5
};
推导等价于:
xor_string<char, // CharT = char6, // Size = 6std::integer_sequence<uint8_t, detail::key8<0>(), ..., detail::key8<5>()>,std::index_sequence<0, 1, 2, 3, 4, 5>
>
给定这个实例化:
xor_string<char, // 字符类型6, // 包含 '\0' 的字符串长度std::integer_sequence<uint8_t, detail::key8<0>(), ..., detail::key8<5>()>,std::index_sequence<0, 1, 2, 3, 4, 5>
>
这表示将一个 6 个字符的 null-terminated 字符串(比如 "Hello"
)加密,每个字符对应一个唯一的编译期 key。
示例:加密 “Hello”
先定义输入字符串:
constexpr const char* s = "Hello"; // 实际是 {'H', 'e', 'l', 'l', 'o', '\0'}
然后计算加密 key:
使用 detail::key8<N>() = ((N * 31 + 57) ^ 0xAA)
:
N | 公式 | key8() |
---|---|---|
0 | (0 * 31 + 57) ^ 0xAA | 0xAA ^ 57 = 0xF9 (249) |
1 | (1 * 31 + 57) ^ 0xAA | 0xAA ^ 88 = 0xE2 (226) |
2 | (2 * 31 + 57) ^ 0xAA | 0xAA ^ 119 = 0x99 (153) |
3 | (3 * 31 + 57) ^ 0xAA | 0xAA ^ 150 = 0x00 (0) |
4 | (4 * 31 + 57) ^ 0xAA | 0xAA ^ 181 = 0x33 (51) |
5 | (5 * 31 + 57) ^ 0xAA | 0xAA ^ 212 = 0x78 (120) |
加密过程:
使用:
encrypted_[i] = s[i] ^ key8<i>()
计算每个字符:
i | 字符 | ASCII | key8 | Encrypted (char) | 十六进制 |
---|---|---|---|---|---|
0 | ‘H’ | 72 | 249 | 72 ^ 249 = 177 | 0xB1 |
1 | ‘e’ | 101 | 226 | 101 ^ 226 = 135 | 0x87 |
2 | ‘l’ | 108 | 153 | 108 ^ 153 = 245 | 0xF5 |
3 | ‘l’ | 108 | 0 | 108 ^ 0 = 108 | 0x6C |
4 | ‘o’ | 111 | 51 | 111 ^ 51 = 92 | 0x5C |
5 | ‘\0’ | 0 | 120 | 0 ^ 120 = 120 | 0x78 |
因此,加密后的字符串是: |
encrypted_ = { '\xB1', '\x87', '\xF5', '\x6C', '\x5C', '\x78' };
解密过程:
在 c_str()
中用相同的 key 再异或一次(因为 XOR 是自反的):
decrypted_[i] = encrypted_[i] ^ key8<i>();
结果将是原始字符串:
decrypted_ = { 'H', 'e', 'l', 'l', 'o', '\0' };
结论总结
xor_string<char, 6, ...>
结果是一个类型,其行为是:
- 在编译期对固定字符串(如
"Hello"
)加密; - 在运行时首次访问时解密为原始内容;
- 避免明文字符串出现在
.rodata
只读段中,提高安全性; - 可用于
constexpr
场景,也适用于防止逆向工程分析。
quick-bench
https://quick-bench.com/
WSL 安装benchmark
sudo apt update
sudo apt install libbenchmark-dev
cmake_minimum_required(VERSION 3.10)
project(HelloTestableWorld)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Threads REQUIRED)
find_library(BENCHMARK_LIB benchmark)
# 添加你的源文件
set(SOURCESmain.cpp
)
# 添加可执行文件(主程序)
add_executable(test ${SOURCES})
target_link_libraries(test ${Boost_LIBRARIES})
target_link_libraries(test ${BENCHMARK_LIB} Threads::Threads)
#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>
#include <random>
static void SortAndFindMedian(benchmark::State& state) {std::vector<int> data(1'000'000);std::mt19937 rng(12345);std::uniform_int_distribution<int> dist(0, 1'000'000);for (auto& v : data) v = dist(rng);for (auto _ : state) {auto copy = data; // 复制数据std::sort(copy.begin(), copy.end());int median = copy[copy.size() / 2];benchmark::DoNotOptimize(median); // 防止编译器优化}
}
BENCHMARK(SortAndFindMedian);
BENCHMARK_MAIN();
Run on (20 X 2995.2 MHz CPU s)
CPU Caches:L1 Data 48 KiB (x10)L1 Instruction 32 KiB (x10)L2 Unified 1280 KiB (x10)L3 Unified 24576 KiB (x1)
Load Average: 0.18, 0.09, 0.02
***WARNING*** Library was built as DEBUG. Timings may be affected.
------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------
SortAndFindMedian 179664476 ns 179664525 ns 4
一、理解这几个排序相关算法
算法名 | 简述说明 | 时间复杂度(平均) | 是否稳定排序 | 适用场景 |
---|---|---|---|---|
std::sort | 最快的一般用途排序,非稳定,通常是 introsort(快排+堆排+插排) | O(n log n) | 否 | 快速排序,无需稳定性 |
std::stable_sort | 稳定排序,使用 merge sort 实现 | O(n log n) | 是 | 保留相等元素顺序,如按多个键排序 |
std::partial_sort | 仅将前 k 个最小元素排序 | O(n log k) | 否 | 找前 K 小(或大)元素 |
std::nth_element | 将第 n 小的元素放在正确位置,左边元素 ≤ 它,右边元素 ≥ 它,不排序其他元素 | O(n) 平均,O(n²) 最坏 | 否 | 选中位数、分位数,Top-K 等 |
二、基准测试(Benchmark)这四种算法性能
以下是一个 Google Benchmark 示例代码,比较四种算法在同样数据上的表现。
文件:sort_benchmark.cpp
#include <algorithm>
#include <benchmark/benchmark.h>
#include <random>
#include <vector>
constexpr size_t N = 1'000'000;
static std::vector<int> generate_data() {std::vector<int> data(N);std::mt19937 rng(12345);std::uniform_int_distribution<int> dist(0, 1'000'000);for (auto& v : data) v = dist(rng);return data;
}
static void BM_sort(benchmark::State& state) {auto base = generate_data();for (auto _ : state) {auto data = base;std::sort(data.begin(), data.end());benchmark::DoNotOptimize(data);}
}
BENCHMARK(BM_sort);
static void BM_stable_sort(benchmark::State& state) {auto base = generate_data();for (auto _ : state) {auto data = base;std::stable_sort(data.begin(), data.end());benchmark::DoNotOptimize(data);}
}
BENCHMARK(BM_stable_sort);
static void BM_partial_sort(benchmark::State& state) {auto base = generate_data();size_t k = N / 2;for (auto _ : state) {auto data = base;std::partial_sort(data.begin(), data.begin() + k, data.end());benchmark::DoNotOptimize(data[k]);}
}
BENCHMARK(BM_partial_sort);
static void BM_nth_element(benchmark::State& state) {auto base = generate_data();size_t k = N / 2;for (auto _ : state) {auto data = base;std::nth_element(data.begin(), data.begin() + k, data.end());benchmark::DoNotOptimize(data[k]);}
}
BENCHMARK(BM_nth_element);
BENCHMARK_MAIN();
三、构建步骤(本地 CMake)
1. CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(sort_benchmark)
set(CMAKE_CXX_STANDARD 17)
find_package(Threads REQUIRED)
find_library(BENCHMARK_LIB benchmark REQUIRED)
add_executable(sort_benchmark sort_benchmark.cpp)
target_link_libraries(sort_benchmark PRIVATE ${BENCHMARK_LIB} Threads::Threads)
2. 编译运行
mkdir build && cd build
cmake ..
make
./sort_benchmark
四、示例输出(大致)
Run on (20 X 2995.2 MHz CPU s)
CPU Caches:L1 Data 48 KiB (x10)L1 Instruction 32 KiB (x10)L2 Unified 1280 KiB (x10)L3 Unified 24576 KiB (x1)
Load Average: 0.07, 0.11, 0.07
***WARNING*** Library was built as DEBUG. Timings may be affected.
----------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------
BM_sort 174568089 ns 174567250 ns 4
BM_stable_sort 191823434 ns 191822600 ns 4
BM_partial_sort 272549634 ns 272541633 ns 3
BM_nth_element 20818890 ns 20818915 ns 33
注意:实际数值取决于你的机器、编译器优化等。
总结建议
目标 | 推荐使用 |
---|---|
完全排序 | std::sort |
完全排序 + 保序(稳定) | std::stable_sort |
只需前 K 小 / 大元素 | std::partial_sort |
只需找中位数、Top-K 值 | std::nth_element |
这个基准测试输出显示了四个不同排序算法的性能表现:
硬件信息简要
- CPU 核心数:20 个(运行频率 2995.2 MHz)
- 缓存信息:
- L1 数据缓存:48 KiB ×10(每个核心一个)
- L1 指令缓存:32 KiB ×10
- L2 缓存:1280 KiB ×10
- L3 缓存:24576 KiB(共用)
警告
Library was built as DEBUG
这意味着你使用的是 Debug 模式构建的库,运行时间会比 Release 模式 显著慢,不适合做最终性能结论。
基准测试结果(单位:纳秒)
算法 | 时间(Wall) | 时间(CPU) | 迭代次数 |
---|---|---|---|
BM_sort | 174,568,089 ns | 174,567,250 ns | 4 次 |
BM_stable_sort | 191,823,434 ns | 191,822,600 ns | 4 次 |
BM_partial_sort | 272,549,634 ns | 272,541,633 ns | 3 次 |
BM_nth_element | 20,818,890 ns | 20,818,915 ns | 33 次 |
理解每个测试项
sort
:标准排序(如std::sort
),性能最好。stable_sort
:稳定排序(如std::stable_sort
),稍慢,因其保证相等元素的顺序。partial_sort
:只对部分元素排序(如前 K 小),时间最长,可能输入规模或逻辑更复杂。nth_element
:用于找第 n 小元素(部分排序),最快,适合仅查找中位数或阈值等。
总结
- 当前测量是 Debug 模式,时间只可作为相对参考。
- 如果要做更准确的性能评估,请使用 Release 模式构建。
对 C++ 标准库中排序算法的复杂度说明。以下是系统性解读和对比分析,帮你更好地“理解”:
一、标准算法复杂度总结
算法 | 平均时间复杂度(假设容器大小为 N,子集大小为 k) | 适用场景 |
---|---|---|
std::sort | O(N·log(N)) | 完整排序 |
std::partial_sort | O(N·log(k)) | 排前 k 个最小/最大值(如排行榜、Top-k) |
std::nth_element | O(N)(平均) | 找第 k 小元素,如中位数 |
nth_element + sort | O(N + k·log(k)) | 找前 k 个并排序,如找 Top-k 并显示 |
二、复杂度推导说明
std::sort
- 直到 C++11:O(N·log(N))(平均情况)
- 自 C++11 起:算法实现被标准化,平均与最坏也为 O(N·log(N))
std::partial_sort
- 复杂度:O(N·log(k))
- 实现机制:用一个大小为 k 的堆处理 N 个元素
- 应用例:
- 排中位数时:k = N / 2
- 复杂度:O(N·log(N/2)) ≈ O(N·log(N))
- 排 Top 10 时:k = 10
- 复杂度:O(N·log(10)) ≈ O(N) (常数小)
- 排中位数时:k = N / 2
std::nth_element
- 复杂度:O(N)(平均),O(N²)(最坏,极少见)
- 仅保证第 k 个位置是排序后应在的位置,左右无序
- 应用:快速查中位数、分位数、阈值
nth_element + sort
(常用于优化 Top-k 排序)
- 复杂度:
nth_element
: O(N)sort
(k 个元素): O(k·log(k))- 总体:O(N + k·log(k))
- 比
partial_sort
更快(尤其当 k 很小时)
三、总结:应用建议
需求 | 推荐算法 | 复杂度分析 |
---|---|---|
排序所有元素 | std::sort | O(N·log(N)) |
查找中位数 | std::nth_element | O(N) |
Top-k 并排序 | nth_element + sort | O(N + k·log(k)) |
Top-k(顺序无关) | std::partial_sort | O(N·log(k)) |
排序排行榜(k=10) | partial_sort 或 nth_element+sort | O(N) 级别,后者更快 |
如果你在实现一个高性能排行榜、推荐系统、或数据采样模块,正确地选取这些算法将极大提高性能。 |
排序算法性能对比。聚焦于实际性能测量,并结合不同编译器和输入规模,展示了算法复杂度和实现之间的差异。以下是系统性解读:
一、比较实现(Implementations)
使用的工具
- Compiler Explorer:可用于查看各编译器(GCC、Clang、MSVC)对标准算法的生成汇编
- Wandbox:在线编译和运行对比多种 C++ 编译器输出
结论
“Same results for all implementations of the libc++.”
- 各平台(GCC、Clang)在使用
libc++
时,std::sort
,partial_sort
等表现一致。 - 标准库实现差异较小,但编译器优化、内联、调度等仍可能影响性能。
二、性能测量工具
常用性能评估工具:
- Google Benchmark
- 精准的微基准测试框架,适合评估
sort
、partial_sort
等
- 精准的微基准测试框架,适合评估
- Quick Bench
- 快速测试小段代码性能,http://quick-bench.com
三、输入规模对性能的影响
1. 变动子集大小(固定容器大小)
- 容器大小 = 1,000,000(N = 10⁶)
- 改变要排序的子集大小 k:
partial_sort
: O(N·log(k))nth_element + sort
: O(N + k·log(k))
- 小 k 时
nth_element
更快,大 k 时趋近于sort
2. 变动容器大小(固定子集大小)
- 固定 k = 100
- 改变容器整体大小 N
- 对于
partial_sort
: 仍是 O(N·log(k)) ≈ O(N) - 若 N 变大,处理时间增加线性
- 对于
3. 同时变动容器和子集大小
- 设定 k = N / 5
- 测试从小容器到大容器(比如 N 从 1,000 到 1,000,000)
- 观察:复杂度为 O(N·log(N/5)) ≈ O(N·log(N))
四、总结(“We have an actual difference!”)
结论指的是:不同算法在不同输入规模下确实有显著差异:
情况 | 最优选择 |
---|---|
k 很小(如 k = 10) | nth_element + sort 或 partial_sort (几乎是 O(N)) |
k 较大或接近 N | std::sort 更适合 |
快速找中位数或第 k 小元素 | nth_element 是最佳 |
如果你正在设计一个高性能系统,比如: |
- 推荐系统(Top-k 筛选)
- 大数据排序(仅关心前若干)
- 实时统计(如游戏排行榜)
理解这些输入规模与复杂度关系至关重要。
何时使用 std::partial_sort
?
为什么在某些情况下它比
nth_element + sort
更快,某些情况下更慢?
一、使用建议(What to use and when)
使用 std::partial_sort
:
即:当你只需要排序前 k
个元素,且 k ≪ N
(远小于容器大小),使用 partial_sort
更方便,并且在某些情况下性能更优。
使用 nth_element + sort
:
“Otherwise, use
nth_element + sort
.”
当:
k
不太小(例如接近 N)- 你希望最大限度压缩不必要的比较和内存移动
此组合往往性能更好。
二、为什么会这样?
1. std::partial_sort
内部实现原理:
- 一般基于 堆排序(
std::make_heap
、pop_heap
)或变种 - 它需要不断维护一个 大小为 k 的堆 来扫描整个容器,执行 N 次比较(每次 log(k))
复杂度:O(N·log(k))
当k
很小时 log(k) 很小,性能好
当k ≈ N
时变为 O(N·log(N)),就不如直接用std::sort
2. nth_element + sort
内部原理:
std::nth_element
使用 Quickselect(快速选择),平均 O(N) 时间定位前 k 个元素- 后续只对前
k
个元素排序:O(k·log(k))
整体复杂度:O(N + k·log(k))
优势在于: - 快速过滤出前
k
个元素 - 避免对整个容器进行排序或堆维护
总结选择指南
场景 | 推荐算法 |
---|---|
需要前 k 个排序结果,k 很小 | std::partial_sort |
需要前 k 个排序结果,k 较大 | nth_element + sort |
只要第 k 个元素,不排序 | std::nth_element |
排序整个容器 | std::sort |
std::sort
底层实现的代码,我将对其逐行添加中文注释并解释其整体逻辑和意义:
带中文注释的 __sort
实现(libstdc++ 中)
// 模板函数,接受一个随机访问迭代器范围 [first, last) 和一个比较函数 comp
template<typename RandIt, typename _Compare>
inline void
__sort(RandIt first, RandIt last, _Compare comp) {// 检查范围是否为空,不为空则执行排序逻辑if (first != last) {// 1⃣ 调用 introsort 主循环// 使用快速排序为主,限制递归深度避免最坏情况(使用堆排序兜底)// __lg 是 log2(底数为 2),深度限制设为 log2(N) * 2std::__introsort_loop(first, last,std::__lg(last - first) * 2,comp);// 2⃣ 用插入排序对小区间做最后优化// 通常 introsort 会在划分到很小段时停止,再由插入排序完成std::__final_insertion_sort(first, last, comp);}
}
分步解释
1. std::__introsort_loop(...)
- 内省排序(Introsort)主逻辑:
- 使用快速排序进行划分
- 如果递归深度超过 log(N) * 2,则转为堆排序,避免极端退化到 O(N²)
- 属于 深度受限的快速排序
__lg(last - first)
:- 是 log2(N),N 是元素个数
*2
是经验系数,允许一定深度但防止无限递归
2. std::__final_insertion_sort(...)
- 在划分结束后,有可能还剩下一些小范围子区间未排序
- 插入排序在小数据上效率很好,常用于最后“整理阶段”
- 通常是优化实际性能,不改变复杂度(仍是 O(N·logN))
总结:这是怎样的排序?
排序阶段 | 算法类型 | 作用说明 |
---|---|---|
初始划分 | 快速排序 | 快速排序子区间,高效 |
深度受限转备用 | 堆排序 | 保证最坏情况不超过 O(N·logN) |
小段优化 | 插入排序 | 提高实际运行性能,减少 cache miss |
所以 std::sort
是:
快速 + 稳定 + 实际高性能 的 混合排序算法
关于 Introsort(内省排序) 的原理讲解。以下是逐点解释和系统性理解:
一、问题背景
快速排序(Quicksort)
- 平均性能非常快:O(N·logN)
- 最坏情况退化为 O(N²),例如:
- 输入已经接近有序
- 每次分割极度不平衡
堆排序(Heapsort)
- 无论任何输入,始终是 O(N·logN)
- 常数因子大:相比快速排序在正常情况下慢 4~5 倍
- 原因包括较多的内存访问、cache 不友好等
二、Introsort 的设计哲学:两全其美!
Introsort(Internal introspective sort):
“使用快速排序进行排序,但监控其递归深度,一旦发现递归太深(= 可能退化),立即切换为堆排序。”
三、工作流程总结:
- 开始时用快速排序(Quicksort)
- 高效处理绝大多数情况
- 设定最大递归深度 = 2 × log₂(N)
- 这相当于预估一个合理的快速排序递归次数
- 如果递归超过此深度
- 表明当前输入可能导致快速排序退化
- 切换为 堆排序(Heapsort)
- 最后再用 插入排序 清理小范围残余(提升实际性能)
四、复杂度对比总结
算法 | 平均情况 | 最坏情况 | 常数因子 | 特点 |
---|---|---|---|---|
Quicksort | O(N·logN) | O(N²) | 非常小 | 快速但不稳定 |
Heapsort | O(N·logN) | O(N·logN) | 较大 | 稳定但慢 |
Introsort | O(N·logN) | O(N·logN) | 折中 | 快速 + 安全(默认选择) |
总结理解
Introsort = 快速排序 + 最坏情况保障(堆排序) + 小规模优化(插入排序)
它结合了各算法的优点,是 std::sort
的核心实现方式,是工业级通用排序的最佳实践。
如果你想继续深入:
- 研究
__introsort_loop
的具体实现 - 或看看
heap_sort
是如何被触发的
插入排序(Insertion Sort) 在 std::sort
中的角色和用途,尤其在 小规模子区间 的优化场景下。
以下是系统性解释:
一、插入排序简介
插入排序(Insertion Sort)复杂度:
情况 | 时间复杂度 |
---|---|
最好(已排序) | O(N) |
平均/最坏 | O(N²) |
所以插入排序不适合大规模数据,但在小范围内非常高效。
二、为什么在 std::sort
中使用 Insertion Sort?
“Over small subranges, Insertion Sort performs better than Quicksort.”
原因如下:
- 快速排序在小数据上常数因子大(递归、分割、多次交换)
- 插入排序没有递归开销,代码路径短
- 小规模数据往往已经部分有序(插入排序对有序数据特别快)
三、典型策略:小区间使用插入排序
“We sort until the size of subranges are < k.”
在 Introsort 实现中,通常设置一个阈值 k
(如 16):
- 如果当前子区间大小
< k
- 不再递归快速排序
- 留待最后由
__final_insertion_sort
来统一处理
这种“延迟排序 + 局部插入排序”是提升性能的关键技巧。
四、总结角色对比
算法 | 用途 |
---|---|
快速排序 | 主体排序,适合大规模数据,平均性能优异 |
堆排序 | 安全兜底,避免最坏情况 |
插入排序 | 小规模优化,最终完成局部排序 |
结论理解
插入排序虽为 O(N²),但由于其常数开销极小,在处理小数据时非常合适,尤其是当数据部分有序时。
这就是为什么 std::sort
最后总是调用:
std::__final_insertion_sort(...)
std::nth_element
的核心实现(源自 GCC 的 libstdc++
),这是一个非常重要的 选择算法(selection algorithm),用于在部分排序中查找“第 k 小元素”。
nth_element
实现(简化版本)
template<typename RandIt>
inline void
nth_element(RandIt first, RandIt nth, RandIt last)
{// 如果为空区间 或 nth 超出范围,则直接返回if (first == last || nth == last)return;// 核心调用:__introselect// 参数:first ~ last 的范围,目标位置 nth,最大递归深度,比较函数std::__introselect(first, nth, last,std::__lg(last - first) * 2,__gnu_cxx::__ops::__iter_less_iter());
}
一、std::nth_element
做了什么?
它重排元素,使得:
*nth
是范围[first, last)
中第k
小的元素(k = nth - first)- 左侧
[first, nth)
所有元素 ≤*nth
- 右侧
[nth + 1, last)
所有元素 ≥*nth
- 但左右两边并不是有序的
二、__introselect
是什么?
这与前面提到的 __introsort_loop
类似,是一种:
Introspective Selection(内省选择)算法
它的工作机制:
- 主要使用 Quickselect(快速选择):
- 类似快速排序,但只递归到包含
nth
的一侧 - 平均时间复杂度为 O(N)
- 类似快速排序,但只递归到包含
- 如果递归太深(超过
2 × log(N)
):- 则切换为 堆排序或 median-of-medians(取决于实现)
- 以避免最坏情况退化到 O(N²)
所以它结合了:
- 快速选择的高效
- 堆排序/median 的稳定
三、参数解读
参数 | 含义 |
---|---|
first , nth , last | 要处理的迭代器范围和目标位置 |
__lg(last - first) * 2 | 最大递归深度(避免 worst case) |
__iter_less_iter() | 默认比较器,使用 < 运算符 |
四、典型用法
std::vector<int> v = {9, 5, 7, 1, 3, 6};
std::nth_element(v.begin(), v.begin() + 2, v.end());
- 执行后:
v[2]
是整个序列中第 3 小的元素v[0]~v[1] ≤ v[2]
,v[3]~v[5] ≥ v[2]
- 但两侧是无序的
总结理解
特性 | 描述 |
---|---|
用途 | 找第 k 小元素(或中位数、Top-k) |
复杂度 | 平均 O(N),最坏 O(N·logN)(受控) |
排序结果 | 局部有序,仅保证位置 nth 的正确性 |
算法类型 | 内省选择(Introselect = Quickselect + 安全机制) |
关于 Quickselect 和 Introselect(内省选择) 的实现逻辑,它们是 std::nth_element
背后的核心算法。下面是对这些幻灯片内容的逐步解析和理解:
一、Quickselect 是什么?
它是 Quicksort 的“简化版”,用于找第 k 小(或大的)元素,属于 选择算法(Selection Algorithm)。
工作原理(与快速排序非常像):
- 选择一个 pivot(基准值)
- Partition(分区):把所有
< pivot
放左边,≥ pivot
放右边 - 检查 pivot 的最终位置:
- 如果正好在第
k
位(即pivot == nth
),就完成了! - 否则:
- 若
nth
落在左边 → 递归左边 - 若
nth
落在右边 → 递归右边
优势:
- 若
- 如果正好在第
- 每次递归只处理一侧,节省时间
- 平均时间复杂度是 O(N)
风险: - 最坏情况:pivot 总是最小或最大元素 → 时间退化为 O(N²)(极端不平衡)
二、Introselect:Quickselect + 安全机制
Introselect = Quickselect + 深度限制 + 备用安全算法(Heapselect)
行为逻辑:
- 使用 Quickselect 正常递归查找 nth 元素
- 设置最大递归深度 =
2 * log₂(N)
- 一旦超过递归深度 → 切换为 Heapsort(或另一种稳定算法)
- 保证时间复杂度仍为 O(N·logN),避免最坏情况退化
std::nth_element
就是基于这个策略实现的。
总结图示(可视化思维)
Quickselect flow:
Choose pivot|Partition|
Check pivot index|< nth ──> recurse right> nth ──> recurse left= nth ──> done
↘ if too deep, switch to heapsort (Introselect fallback)
你应理解的核心概念
算法 | 用途 | 平均复杂度 | 最坏复杂度 | 是否排序左右 |
---|---|---|---|---|
Quicksort | 排序所有元素 | O(N·logN) | O(N²) | 是 |
Quickselect | 找第 k 小元素(如中位数) | O(N) | O(N²) | 否,仅保证 nth |
Introselect | nth_element 背后算法 | O(N) | O(N·logN) | 否 |
Heapselect
的工作原理,它是 Introselect
(用于 std::nth_element
)中的 备用策略(fallback),当 Quickselect
递归太深时启用。以下是逐步讲解与总结:
一、Heapselect 是什么?
一种用于找第 k 小元素的算法,基于堆(heap)。
与 Quickselect
不同,它不依赖递归,而是通过维护一个 大小为 k 的最大堆 来动态保留当前前 k 小的元素。
二、Heapselect 的步骤详解
场景假设:找第 k
小元素
- 构建一个大小为 k 的最大堆(
std::priority_queue
可用)- 堆顶(最大值)是当前前 k 小元素中最大的
- 初始用前 k 个元素建立堆,O(k)
- 遍历剩下的元素(从第 k+1 个开始)
- 若当前元素 比堆顶更小:
- 弹出堆顶(最大的)
- 插入当前元素
- 若当前元素 比堆顶更小:
- 遍历完后,堆中包含前 k 小元素
- 堆顶即为 第 k 小元素(即 nth 元素)
三、复杂度分析
- 构建初始堆:O(k)
- 遍历 N-k 个元素,每次最多堆操作 log(k):
- 时间复杂度:O((N - k)·log(k)) ≈ O(N·log(k))
适用于:
- 时间复杂度:O((N - k)·log(k)) ≈ O(N·log(k))
- 不希望使用递归(如嵌入式系统)
- 在某些极端情况下 fallback 使用(如
Introselect
的递归超限)
四、总结行为对比
方法 | 描述 | 平均复杂度 | 最坏复杂度 |
---|---|---|---|
Quickselect | 快速递归分区,仅递归一边,适合大多数情况 | O(N) | O(N²) |
Heapselect | 使用大小为 k 的堆维护 top-k 元素 | O(N·log(k)) | O(N·log(k)) |
在 Introselect 中,Quickselect 递归太深时 → 自动调用 Heapselect,以保障最坏复杂度。 |
你的理解框架
std::nth_element
│
├── Quickselect (平均快,递归)
│ └── 超过 2*logN 层
│ ↓
└── Heapselect (稳定、无递归)
std::partial_sort
使用堆排序(Heapselect + Sort),但为什么它有时候比 nth_element + sort
(Quickselect + Sort)还快?不是说 Heapselect 慢吗?
这看起来矛盾,但其实是实现细节和数据特性共同作用的结果。下面逐步解释你贴的内容,并帮助你理解为什么这会发生。
一、你看到的源码(GCC std::partial_sort
实现)
template<typename RandIt, typename _Compare>
inline void
__partial_sort(RandIt first,RandIt middle,RandIt last,_Compare comp)
{// 1⃣ 用 Heapselect 找出前 k 个最小元素,放到 [first, middle)std::__heap_select(first, middle, last, comp);// 2⃣ 用堆排序对 [first, middle) 进行完全排序std::__sort_heap(first, middle, comp);
}
二、分析 std::partial_sort
的做法
步骤说明:
- Heapselect(
__heap_select
):- 从
[first, last)
中挑出k = middle - first
个最小元素 - 复杂度:
O(N·log(k))
- 从
- 堆排序(
__sort_heap
):- 对
[first, middle)
做完全堆排序 - 复杂度:
O(k·log(k))
- 对
总复杂度:
O ( N ⋅ log ( k ) + k ⋅ log ( k ) ) = O ( N ⋅ log ( k ) ) O(N \cdot \log(k) + k \cdot \log(k)) = O(N \cdot \log(k)) O(N⋅log(k)+k⋅log(k))=O(N⋅log(k))
三、nth_element + sort
的复杂度
步骤说明:
- Quickselect (
nth_element
):- 找出前 k 个最小元素,不排序
- 复杂度:平均 O(N)
- std::sort(k 元素):
- 复杂度:
O(k·log(k))
(快速排序)
- 复杂度:
总复杂度(平均):
O ( N + k ⋅ log ( k ) ) O(N + k \cdot \log(k)) O(N+k⋅log(k))
四、那为什么 partial_sort
有时候反而更快?
原因 1⃣:实际数据分布
- Quickselect 可能在某些输入上分区不平衡,即使平均是 O(N),实际运行时间可能略高
- Heapselect 是稳定的 O(N·log(k)),当
k
非常小(如 k = 10)时,log(k)
非常小,几乎近似线性
原因 2⃣:缓存效率 + 分支预测
- Heapselect 和堆排序对内存访问模式更稳定连续
- Quickselect 在分区时会产生不规则内存访问、递归、多分支,不利于 CPU 优化
原因 3⃣:实现优化
- libstdc++ 中的
__heap_select
和__sort_heap
是高度优化的内联函数 - 而
nth_element + sort
涉及两个函数调用,可能带来函数切换成本和更复杂逻辑判断
五、实战选择建议
情况 | 推荐方法 |
---|---|
k 非常小(如 top-10) | partial_sort |
k ≈ N/2 (如找中位数) | nth_element |
k 小,但你也要排序结果 | nth_element + sort 或 partial_sort ,两者都可以试试 |
性能极限优化/不确定哪个更快 | 两者都 benchmark,数据决定 |
总结
理论上
nth_element + sort
更快,但在小规模 top-k 或 对 CPU 友好数据分布场景中,partial_sort
的堆实现可能更稳定、更高效。
你问的这个问题,其实是算法分析中“理论复杂度 vs 实际表现”的经典案例
现在看到的是对 STL 中 std::partial_sort
与 std::nth_element
(背后使用 Heapselect、Quickselect 等)在性能表现、使用场景和设计哲学上的深入分析。
下面是这几页的内容的逐步理解整理:
一、Heapselect Benchmark(1,000,000 elements)
对 1,000,000 个元素做测试,改变要查找的第 k 个位置
观察结果:
- Heapselect 在 k 很小时性能表现很好
- 当 k 增大时:
- Heap 的大小增大
- 每次插入/替换堆顶的成本变高
- 总体复杂度上升为 O(N·log(k))
- 所以虽然是 O(N·log(k)),但在 小 k 时近似线性(非常快)
二、使用场景对比
std::nth_element
用于查找“第 k 小元素”,比如:
- 中位数(k = N/2)
- 分位点(前 10%、后 10%)
- 分界线(用于划分重要/不重要元素)
核心特性: - 使用 Introselect(Quickselect + fallback)
- 时间复杂度 O(N)(与 k 无关!)
- 不会排序(你自己 sort 一下 k 元素)
适用:任意位置的元素查找
std::partial_sort
用于获取前 k 小的元素,并且排序好
STL 设计者选择使用:
- Heapselect(O(N·log(k)))
- 因为最常见的用例是:
- k ≪ N,即你只要 top-10、top-100 等
- 堆方法对这种情况最稳、性能最好
- 若 k 变大,其复杂度也变大(不如 nth_element + sort)
适用:top-k 排序
三、设计哲学总结
“Despite STL being generic, implementers fine-tune algorithms for typical use cases.”
这句话体现了 C++ STL 的一个核心理念:
- STL 提供了通用接口(generic),比如
partial_sort
,nth_element
- 但其底层实现是根据真实世界的高频使用场景优化过的:
nth_element
:适合任意位置查找,选用 O(N) Quickselectpartial_sort
:典型用于 小规模 top-k 排序,选用 O(N·log(k)) Heapselect + Heapsort
总结你的理解重点
算法 | 核心用途 | 复杂度 | 最优使用场景 |
---|---|---|---|
std::nth_element | 找第 k 小的元素 | O(N) | 中位数、分位点 |
std::partial_sort | 取出并排序前 k 小元素 | O(N·log(k)) | Top-k 排序,k ≪ N |
Heapselect | 内部用于 partial_sort | O(N·log(k)) | 小 k 时性能非常好 |
排序算法总览
算法名称 | STL 函数名 | 时间复杂度 | 实现策略 | 典型使用场景 |
---|---|---|---|---|
Introsort | std::sort | O(N·logN) | Quicksort + Heapsort + Insertion Sort | 全排序,最常用的一般排序 |
Heapsort | std::sort (fallback) | O(N·logN) | 堆排序(递归深度过深时触发) | 保证最坏复杂度的安全机制 |
Insertion Sort | std::sort (尾部优化) | O(N²) | 插入排序(仅用于小段数据) | 小数据段优化,局部收尾 |
Partial Heapsort | std::partial_sort | O(N·log(k)) | Heapselect + 堆排序 | Top-k 排序(例如排行榜) |
Quickselect | std::nth_element | 平均 O(N),最坏 O(N²) | 简化的 Quicksort | 查找第 k 小元素、中位数、分位点 |
Introselect | std::nth_element (完整) | O(N),最坏 O(N·logN) | Quickselect + Heapsort fallback | 稳定查找任意位置的第 k 小元素 |
Heapselect | __heap_select | O(N·log(k)) | 保留 top-k 最大堆 | 用于 partial_sort 的前 k 筛选 |
细节对比
特性 | sort | partial_sort | nth_element |
---|---|---|---|
是否全排序 | 是 | 只排前 k 个 | 只定位第 k 小元素 |
返回结构是否有序 | 是 | 前 k 是有序的 | 无序,仅第 k 正确 |
是否稳定 | 否 (std::sort ) | 否 | 否 |
复杂度(最优) | O(N·logN) | O(N·log(k)) | O(N)(平均) |
数据规模推荐 | 任意 | k ≪ N | 任意 k,尤其是中位数等 |
实战推荐使用场景
应用目标 | 推荐算法组合 |
---|---|
排序整个容器 | std::sort (Introsort) |
获取前 10 名排行榜 | std::partial_sort |
查找中位数、分位点(无序) | std::nth_element |
获取前 k 小并排序 | nth_element + sort(k 部分) 或 partial_sort (k 小) |
数据量大,安全防最坏情况退化 | std::sort (会自动堆排序兜底) |
最后一句话总结
STL 排序算法虽然看起来很“泛型”,但它们内部使用了高度优化且精心设计的混合策略,能应对不同规模、不同 k 值的真实世界使用场景。
下面是你列出的 10 种排序算法的实现原理说明和对应的 C++ 风格伪代码,方便理解和参考。
1. Bubble Sort(冒泡排序)
原理:重复遍历数组,两两比较相邻元素,如果顺序错误则交换。每次遍历将最大元素“冒泡”到末尾。
void bubbleSort(vector<int>& arr) {int n = arr.size();for (int i = 0; i < n-1; i++) {for (int j = 0; j < n-i-1; j++) {if (arr[j] > arr[j+1])swap(arr[j], arr[j+1]);}}
}
2. Selection Sort(选择排序)
原理:每一轮选择剩余未排序区间的最小元素,放到当前已排序区间末尾。
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]);}
}
3. Insertion Sort(插入排序)
原理:将数组分为已排序和未排序两部分,逐个将未排序元素插入到已排序部分的正确位置。
void insertionSort(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;}
}
4. Quicksort(快速排序)
原理:选定基准元素,将数组划分成左右两部分,左边元素都小于基准,右边都大于基准,然后递归排序两部分。
int partition(vector<int>& arr, int low, int high) {int pivot = arr[high];int i = low - 1;for (int j = low; j < high; 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 p = partition(arr, low, high);quickSort(arr, low, p - 1);quickSort(arr, p + 1, high);}
}
5. Merge Sort(归并排序)
原理:递归将数组分成两半,分别排序后合并成有序数组。
void merge(vector<int>& arr, int left, int mid, int right) {vector<int> temp(right - left + 1);int i = left, j = mid + 1, k = 0;while (i <= mid && j <= right) {if (arr[i] <= arr[j]) temp[k++] = arr[i++];else temp[k++] = arr[j++];}while (i <= mid) temp[k++] = arr[i++];while (j <= right) temp[k++] = arr[j++];for (int x = 0; x < k; x++) arr[left + x] = temp[x];
}
void mergeSort(vector<int>& arr, int left, int right) {if (left < right) {int mid = left + (right - left) / 2;mergeSort(arr, left, mid);mergeSort(arr, mid + 1, right);merge(arr, left, mid, right);}
}
6. Odd-Even Sort(奇偶排序)
原理:交替进行奇数索引对和偶数索引对的元素比较和交换,直到数组有序。
void oddEvenSort(vector<int>& arr) {int n = arr.size();bool sorted = false;while (!sorted) {sorted = true;// Odd indexed pairsfor (int i = 1; i <= n - 2; i += 2) {if (arr[i] > arr[i + 1]) {swap(arr[i], arr[i + 1]);sorted = false;}}// Even indexed pairsfor (int i = 0; i <= n - 2; i += 2) {if (arr[i] > arr[i + 1]) {swap(arr[i], arr[i + 1]);sorted = false;}}}
}
7. Heapsort(堆排序)
原理:构建最大堆,不断将堆顶(最大值)与末尾元素交换,并重建堆。
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);}
}
8. Cocktail Sort(鸡尾酒排序)
原理:双向冒泡排序,先从左向右“冒泡”,再从右向左“冒泡”,缩小范围。
void cocktailSort(vector<int>& arr) {int n = arr.size();bool swapped = true;int start = 0, end = n - 1;while (swapped) {swapped = false;for (int i = start; i < end; i++) {if (arr[i] > arr[i+1]) {swap(arr[i], arr[i+1]);swapped = true;}}if (!swapped) break;swapped = false;end--;for (int i = end - 1; i >= start; i--) {if (arr[i] > arr[i+1]) {swap(arr[i], arr[i+1]);swapped = true;}}start++;}
}
9. Bitonic Mergesort(双调归并排序)
原理:适合并行,先构造递增和递减的 bitonic 序列,再合并成排序序列。
void bitonicMerge(vector<int>& arr, int low, int cnt, bool ascending) {if (cnt > 1) {int k = cnt / 2;for (int i = low; i < low + k; i++) {if ((arr[i] > arr[i + k]) == ascending) {swap(arr[i], arr[i + k]);}}bitonicMerge(arr, low, k, ascending);bitonicMerge(arr, low + k, k, ascending);}
}
void bitonicSort(vector<int>& arr, int low, int cnt, bool ascending) {if (cnt > 1) {int k = cnt / 2;bitonicSort(arr, low, k, true); // 升序bitonicSort(arr, low + k, k, false); // 降序bitonicMerge(arr, low, cnt, ascending);}
}
10. Introsort(混合排序)
原理:结合快速排序、堆排序和插入排序。递归深度超过阈值时切换堆排序避免快速排序最坏情况,底层小区间用插入排序。
void insertionSort(vector<int>& arr, int left, int right) {for (int i = left + 1; i <= right; i++) {int key = arr[i];int j = i - 1;while (j >= left && arr[j] > key) {arr[j+1] = arr[j];j--;}arr[j+1] = key;}
}
void heapify(vector<int>& arr, int n, int i) { /* 同上 */ }
void heapSort(vector<int>& arr, int left, int right) {int n = right - left + 1;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);}
}
int partition(vector<int>& arr, int low, int high) { /* 同上 */ }
void introsortUtil(vector<int>& arr, int low, int high, int depthLimit) {int size = high - low + 1;if (size < 16) {insertionSort(arr, low, high);return;}if (depthLimit == 0) {heapSort(arr, low, high);return;}int p = partition(arr, low, high);introsortUtil(arr, low, p - 1, depthLimit - 1);introsortUtil(arr, p + 1, high, depthLimit - 1);
}
void introsort(vector<int>& arr) {int depthLimit = 2 * log(arr.size());introsortUtil(arr, 0, arr.size() - 1, depthLimit);
}
相关动画可以参考
https://www.coderstool.com/bubble-sort