#include
#include
#include
#include
#include
#include
#include
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 sequence
template <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>()...>, // 编译期 key
std::index_sequence<Indices...> // 字符索引序列
>;
constexpr auto secret = XOR_LITERAL("Hello");
xor_string {
[] { return "Hello"; }, // L
std::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 = char
6, // Size = 6
std::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。
先定义输入字符串:
constexpr const char* s = "Hello"; // 实际是 {'H', 'e', 'l', 'l', 'o', '\0'}
使用 detail::key8
:
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
结果是一个类型,其行为是:
"Hello"
)加密;.rodata
只读段中,提高安全性;constexpr
场景,也适用于防止逆向工程分析。https://quick-bench.com/
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(SOURCES
main.cpp
)
# 添加可执行文件(主程序)
add_executable(test ${SOURCES})
target_link_libraries(test ${Boost_LIBRARIES})
target_link_libraries(test ${BENCHMARK_LIB} Threads::Threads)
#include
#include
#include
#include
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 等 |
以下是一个 Google Benchmark 示例代码,比较四种算法在同样数据上的表现。
sort_benchmark.cpp
#include
#include
#include
#include
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();
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)
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 |
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 小元素(部分排序),最快,适合仅查找中位数或阈值等。算法 | 平均时间复杂度(假设容器大小为 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
std::partial_sort
std::nth_element
nth_element + sort
(常用于优化 Top-k 排序)nth_element
: O(N)sort
(k 个元素): O(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) 级别,后者更快 |
如果你在实现一个高性能排行榜、推荐系统、或数据采样模块,正确地选取这些算法将极大提高性能。 |
“Same results for all implementations of the libc++.”
libc++
时,std::sort
, partial_sort
等表现一致。sort
、partial_sort
等partial_sort
: O(N·log(k))nth_element + sort
: O(N + k·log(k))nth_element
更快,大 k 时趋近于 sort
partial_sort
: 仍是 O(N·log(k)) ≈ O(N)结论指的是:不同算法在不同输入规模下确实有显著差异:
情况 | 最优选择 |
---|---|
k 很小(如 k = 10) | nth_element + sort 或 partial_sort (几乎是 O(N)) |
k 较大或接近 N | std::sort 更适合 |
快速找中位数或第 k 小元素 | nth_element 是最佳 |
如果你正在设计一个高性能系统,比如: |
std::partial_sort
?为什么在某些情况下它比
nth_element + sort
更快,某些情况下更慢?
std::partial_sort
:即:当你只需要排序前 k
个元素,且 k ≪ N
(远小于容器大小),使用 partial_sort
更方便,并且在某些情况下性能更优。
nth_element + sort
:“Otherwise, use
nth_element + sort
.”
当:
k
不太小(例如接近 N)std::partial_sort
内部实现原理:std::make_heap
、pop_heap
)或变种O(N·log(k))
k
很小时 log(k) 很小,性能好k ≈ N
时变为 O(N·log(N)),就不如直接用 std::sort
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) * 2
std::__introsort_loop(first, last,
std::__lg(last - first) * 2,
comp);
// 2⃣ 用插入排序对小区间做最后优化
// 通常 introsort 会在划分到很小段时停止,再由插入排序完成
std::__final_insertion_sort(first, last, comp);
}
}
std::__introsort_loop(...)
__lg(last - first)
:
*2
是经验系数,允许一定深度但防止无限递归std::__final_insertion_sort(...)
排序阶段 | 算法类型 | 作用说明 |
---|---|---|
初始划分 | 快速排序 | 快速排序子区间,高效 |
深度受限转备用 | 堆排序 | 保证最坏情况不超过 O(N·logN) |
小段优化 | 插入排序 | 提高实际运行性能,减少 cache miss |
std::sort
是:快速 + 稳定 + 实际高性能 的 混合排序算法
“使用快速排序进行排序,但监控其递归深度,一旦发现递归太深(= 可能退化),立即切换为堆排序。”
算法 | 平均情况 | 最坏情况 | 常数因子 | 特点 |
---|---|---|---|---|
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
是如何被触发的std::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(内省选择)算法
nth
的一侧2 × log(N)
):
参数 | 含义 |
---|---|
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 + 安全机制) |
std::nth_element
背后的核心算法。下面是对这些幻灯片内容的逐步解析和理解:它是 Quicksort 的“简化版”,用于找第 k 小(或大的)元素,属于 选择算法(Selection Algorithm)。
< pivot
放左边,≥ pivot
放右边k
位(即 pivot == nth
),就完成了!nth
落在左边 → 递归左边nth
落在右边 → 递归右边Introselect = Quickselect + 深度限制 + 备用安全算法(Heapselect)
2 * log₂(N)
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
递归太深时启用。以下是逐步讲解与总结:一种用于找第 k 小元素的算法,基于堆(heap)。
与 Quickselect
不同,它不依赖递归,而是通过维护一个 大小为 k 的最大堆 来动态保留当前前 k 小的元素。
k
小元素std::priority_queue
可用)
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 慢吗?这看起来矛盾,但其实是实现细节和数据特性共同作用的结果。下面逐步解释你贴的内容,并帮助你理解为什么这会发生。
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
的做法__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
的复杂度nth_element
):
O(k·log(k))
(快速排序)O ( N + k ⋅ log ( k ) ) O(N + k \cdot \log(k)) O(N+k⋅log(k))
partial_sort
有时候反而更快?k
非常小(如 k = 10)时,log(k)
非常小,几乎近似线性__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 实际表现”的经典案例
std::partial_sort
与 std::nth_element
(背后使用 Heapselect、Quickselect 等)在性能表现、使用场景和设计哲学上的深入分析。下面是这几页的内容的逐步理解整理:
对 1,000,000 个元素做测试,改变要查找的第 k 个位置
std::nth_element
用于查找“第 k 小元素”,比如:
std::partial_sort
用于获取前 k 小的元素,并且排序好
STL 设计者选择使用:
“Despite STL being generic, implementers fine-tune algorithms for typical use cases.”
这句话体现了 C++ STL 的一个核心理念:
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 值的真实世界使用场景。
原理:重复遍历数组,两两比较相邻元素,如果顺序错误则交换。每次遍历将最大元素“冒泡”到末尾。
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]);
}
}
}
原理:每一轮选择剩余未排序区间的最小元素,放到当前已排序区间末尾。
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]);
}
}
原理:将数组分为已排序和未排序两部分,逐个将未排序元素插入到已排序部分的正确位置。
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;
}
}
原理:选定基准元素,将数组划分成左右两部分,左边元素都小于基准,右边都大于基准,然后递归排序两部分。
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);
}
}
原理:递归将数组分成两半,分别排序后合并成有序数组。
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);
}
}
原理:交替进行奇数索引对和偶数索引对的元素比较和交换,直到数组有序。
void oddEvenSort(vector<int>& arr) {
int n = arr.size();
bool sorted = false;
while (!sorted) {
sorted = true;
// Odd indexed pairs
for (int i = 1; i <= n - 2; i += 2) {
if (arr[i] > arr[i + 1]) {
swap(arr[i], arr[i + 1]);
sorted = false;
}
}
// Even indexed pairs
for (int i = 0; i <= n - 2; i += 2) {
if (arr[i] > arr[i + 1]) {
swap(arr[i], arr[i + 1]);
sorted = false;
}
}
}
}
原理:构建最大堆,不断将堆顶(最大值)与末尾元素交换,并重建堆。
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);
}
}
原理:双向冒泡排序,先从左向右“冒泡”,再从右向左“冒泡”,缩小范围。
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++;
}
}
原理:适合并行,先构造递增和递减的 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);
}
}
原理:结合快速排序、堆排序和插入排序。递归深度超过阈值时切换堆排序避免快速排序最坏情况,底层小区间用插入排序。
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