CppCon 2018 学习:A Little Order! Delving into the STL sorting algorithms

发布于:2025-07-02 ⋅ 阅读:(23) ⋅ 点赞:(0)

记录一下一个编译器加密的算法

#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 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。

示例:加密 “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(SOURCES
    main.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) (常数小)

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_sortnth_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
    • 精准的微基准测试框架,适合评估 sortpartial_sort
  • Quick Bench

三、输入规模对性能的影响

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 较大或接近 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_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) * 2
    std::__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. 最后再用 插入排序 清理小范围残余(提升实际性能)

四、复杂度对比总结

算法 平均情况 最坏情况 常数因子 特点
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.”

原因如下:

  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
Introselect nth_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_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 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;
            }
        }
    }
}

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


网站公告

今日签到

点亮在社区的每一天
去签到