当前位置: 首页 > news >正文

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) ^ 0xAA0xAA ^ 57 = 0xF9 (249)
1(1 * 31 + 57) ^ 0xAA0xAA ^ 88 = 0xE2 (226)
2(2 * 31 + 57) ^ 0xAA0xAA ^ 119 = 0x99 (153)
3(3 * 31 + 57) ^ 0xAA0xAA ^ 150 = 0x00 (0)
4(4 * 31 + 57) ^ 0xAA0xAA ^ 181 = 0x33 (51)
5(5 * 31 + 57) ^ 0xAA0xAA ^ 212 = 0x78 (120)

加密过程:

使用:

encrypted_[i] = s[i] ^ key8<i>()

计算每个字符:

i字符ASCIIkey8Encrypted (char)十六进制
0‘H’7224972 ^ 249 = 1770xB1
1‘e’101226101 ^ 226 = 1350x87
2‘l’108153108 ^ 153 = 2450xF5
3‘l’1080108 ^ 0 = 1080x6C
4‘o’11151111 ^ 51 = 920x5C
5‘\0’01200 ^ 120 = 1200x78
因此,加密后的字符串是:
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_sort174,568,089 ns174,567,250 ns4 次
BM_stable_sort191,823,434 ns191,822,600 ns4 次
BM_partial_sort272,549,634 ns272,541,633 ns3 次
BM_nth_element20,818,890 ns20,818,915 ns33 次

理解每个测试项

  • sort:标准排序(如 std::sort),性能最好。
  • stable_sort:稳定排序(如 std::stable_sort),稍慢,因其保证相等元素的顺序。
  • partial_sort:只对部分元素排序(如前 K 小),时间最长,可能输入规模或逻辑更复杂。
  • nth_element:用于找第 n 小元素(部分排序),最快,适合仅查找中位数或阈值等。

总结

  • 当前测量是 Debug 模式,时间只可作为相对参考
  • 如果要做更准确的性能评估,请使用 Release 模式构建。

对 C++ 标准库中排序算法的复杂度说明。以下是系统性解读和对比分析,帮你更好地“理解”:

一、标准算法复杂度总结

算法平均时间复杂度(假设容器大小为 N,子集大小为 k)适用场景
std::sortO(N·log(N))完整排序
std::partial_sortO(N·log(k))排前 k 个最小/最大值(如排行榜、Top-k)
std::nth_elementO(N)(平均)找第 k 小元素,如中位数
nth_element + sortO(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) (常数小)

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::sortO(N·log(N))
查找中位数std::nth_elementO(N)
Top-k 并排序nth_element + sortO(N + k·log(k))
Top-k(顺序无关)std::partial_sortO(N·log(k))
排序排行榜(k=10)partial_sortnth_element+sortO(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
    • 精准的微基准测试框架,适合评估 sortpartial_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 + sortpartial_sort(几乎是 O(N))
k 较大或接近 Nstd::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_heappop_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):

“使用快速排序进行排序,但监控其递归深度,一旦发现递归太深(= 可能退化),立即切换为堆排序。”

三、工作流程总结:

  1. 开始时用快速排序(Quicksort)
  • 高效处理绝大多数情况
  1. 设定最大递归深度 = 2 × log₂(N)
  • 这相当于预估一个合理的快速排序递归次数
  1. 如果递归超过此深度
  • 表明当前输入可能导致快速排序退化
  • 切换为 堆排序(Heapsort)
  1. 最后再用 插入排序 清理小范围残余(提升实际性能)

四、复杂度对比总结

算法平均情况最坏情况常数因子特点
QuicksortO(N·logN)O(N²)非常小快速但不稳定
HeapsortO(N·logN)O(N·logN)较大稳定但慢
IntrosortO(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.”

原因如下:

  1. 快速排序在小数据上常数因子大(递归、分割、多次交换)
  2. 插入排序没有递归开销,代码路径短
  3. 小规模数据往往已经部分有序(插入排序对有序数据特别快)

三、典型策略:小区间使用插入排序

“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 + 安全机制)

关于 QuickselectIntroselect(内省选择) 的实现逻辑,它们是 std::nth_element 背后的核心算法。下面是对这些幻灯片内容的逐步解析和理解:

一、Quickselect 是什么?

它是 Quicksort 的“简化版”,用于找第 k 小(或大的)元素,属于 选择算法(Selection Algorithm)

工作原理(与快速排序非常像):

  1. 选择一个 pivot(基准值)
  2. Partition(分区):把所有 < pivot 放左边,≥ pivot 放右边
  3. 检查 pivot 的最终位置:
    • 如果正好在第 k 位(即 pivot == nth),就完成了!
    • 否则:
      • nth 落在左边 → 递归左边
      • nth 落在右边 → 递归右边
        优势:
  • 每次递归只处理一侧,节省时间
  • 平均时间复杂度是 O(N)
    风险:
  • 最坏情况:pivot 总是最小或最大元素 → 时间退化为 O(N²)(极端不平衡)

二、Introselect:Quickselect + 安全机制

Introselect = Quickselect + 深度限制 + 备用安全算法(Heapselect)

行为逻辑:

  1. 使用 Quickselect 正常递归查找 nth 元素
  2. 设置最大递归深度 = 2 * log₂(N)
  3. 一旦超过递归深度 → 切换为 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
Introselectnth_element 背后算法O(N)O(N·logN)

Heapselect 的工作原理,它是 Introselect(用于 std::nth_element)中的 备用策略(fallback),当 Quickselect 递归太深时启用。以下是逐步讲解与总结:

一、Heapselect 是什么?

一种用于找第 k 小元素的算法,基于堆(heap)。

Quickselect 不同,它不依赖递归,而是通过维护一个 大小为 k 的最大堆 来动态保留当前前 k 小的元素。

二、Heapselect 的步骤详解

场景假设:找第 k 小元素

  1. 构建一个大小为 k 的最大堆std::priority_queue 可用)
    • 堆顶(最大值)是当前前 k 小元素中最大的
    • 初始用前 k 个元素建立堆,O(k)
  2. 遍历剩下的元素(从第 k+1 个开始)
    • 若当前元素 比堆顶更小
      • 弹出堆顶(最大的)
      • 插入当前元素
  3. 遍历完后,堆中包含前 k 小元素
    • 堆顶即为 第 k 小元素(即 nth 元素)

三、复杂度分析

  • 构建初始堆:O(k)
  • 遍历 N-k 个元素,每次最多堆操作 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 的做法

步骤说明:

  1. Heapselect__heap_select):
    • [first, last) 中挑出 k = middle - first 个最小元素
    • 复杂度:O(N·log(k))
  2. 堆排序__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(Nlog(k)+klog(k))=O(Nlog(k))

三、nth_element + sort 的复杂度

步骤说明:

  1. Quickselect (nth_element)
    • 找出前 k 个最小元素,不排序
    • 复杂度:平均 O(N)
  2. std::sort(k 元素)
    • 复杂度:O(k·log(k))(快速排序)

总复杂度(平均):

O ( N + k ⋅ log ⁡ ( k ) ) O(N + k \cdot \log(k)) O(N+klog(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 + sortpartial_sort,两者都可以试试
性能极限优化/不确定哪个更快两者都 benchmark,数据决定

总结

理论上 nth_element + sort 更快,但在小规模 top-k对 CPU 友好数据分布场景中,partial_sort 的堆实现可能更稳定、更高效

你问的这个问题,其实是算法分析中“理论复杂度 vs 实际表现”的经典案例

现在看到的是对 STL 中 std::partial_sortstd::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) Quickselect
    • partial_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_sortO(N·log(k))小 k 时性能非常好

排序算法总览

算法名称STL 函数名时间复杂度实现策略典型使用场景
Introsortstd::sortO(N·logN)Quicksort + Heapsort + Insertion Sort全排序,最常用的一般排序
Heapsortstd::sort(fallback)O(N·logN)堆排序(递归深度过深时触发)保证最坏复杂度的安全机制
Insertion Sortstd::sort(尾部优化)O(N²)插入排序(仅用于小段数据)小数据段优化,局部收尾
Partial Heapsortstd::partial_sortO(N·log(k))Heapselect + 堆排序Top-k 排序(例如排行榜)
Quickselectstd::nth_element平均 O(N),最坏 O(N²)简化的 Quicksort查找第 k 小元素、中位数、分位点
Introselectstd::nth_element(完整)O(N),最坏 O(N·logN)Quickselect + Heapsort fallback稳定查找任意位置的第 k 小元素
Heapselect__heap_selectO(N·log(k))保留 top-k 最大堆用于 partial_sort 的前 k 筛选

细节对比

特性sortpartial_sortnth_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

http://www.dtcms.com/a/263058.html

相关文章:

  • MySQL索引原理-主键索引与普通索引
  • 【软考高项论文】论信息系统项目的干系人管理
  • ACT-R 7.28
  • pbootcms程序运行异常: Modulo by zero,位置:/www/wwwroot/****/core/function/helper.php
  • 链表题解——设计链表【LeetCode】
  • langchain从入门到精通(二十四)——RAG优化策略(二)多查询结果融合策略及 RRF
  • [特殊字符]️ Hyperf框架的数据库查询(ORM组件)
  • iOS 接口频繁请求导致流量激增?抓包分析定位与修复全流程
  • Reactor重试操作
  • 十大排序算法汇总
  • 2025年06月30日Github流行趋势
  • 创客匠人解析强 IP 时代创始人 IP 打造的底层逻辑与破局之道
  • Java开发新变革!飞算JavaAI深度剖析与实战指南
  • 一文讲清楚React中类组件与函数组件的区别与联系
  • 手机屏暗点缺陷修复及相关液晶线路激光修复原理
  • 类图+案例+代码详解:软件设计模式----生成器模式(建造者模式)
  • Franka机器人赋能RoboCulture研究,打造生物实验室智能解决方案
  • Vue防抖节流
  • 最新版 JT/T808 终端模拟器,协议功能验证、平台对接测试、数据交互调试
  • Spring Cloud Bus 和 Spring Cloud Stream
  • HarmonyOS NEXT仓颉开发语言实战案例:外卖App
  • NAT 类型及 P2P 穿透
  • 人工智能和云计算对金融未来的影响
  • Docker 入门教程(九):容器网络与通信机制
  • Qt 前端开发
  • (3)pytest的setup/teardown
  • 文心大模型 4.5 系列开源首发:技术深度解析与应用指南
  • Python 数据分析与可视化 Day 12 - 建模前准备与数据集拆分
  • 【C语言 | 字符串处理】sscanf 用法(星号*、集合%[]等)详细介绍、使用例子源码
  • 嵌入式SoC多线程架构迁移多进程架构开发技巧