C++ STL之sort系列深度剖析:从使用到底层,再到面试八股

C++ STL之sort系列深度剖析:从使用到底层,再到面试八股

本文面向面试和日常开发,先讲调用,再讲原理,最后给口语化面试答案。


一、用法速查

1.1 std::sort ------ 最通用的排序

对 RandomAccessIterator 区间排序,默认升序(operator<),不稳定,平均 O(n log n)。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{5, 1, 9, 3, 7};

    // 默认升序
    sort(v.begin(), v.end());
    for (int x : v) cout << x << " ";   // 1 3 5 7 9
    cout << "\n";

    // greater 降序
    sort(v.begin(), v.end(), greater<int>());
    for (int x : v) cout << x << " ";   // 9 7 5 3 1
    cout << "\n";

    // Lambda 自定义
    vector<pair<int, int>> vp{{3, 1}, {1, 4}, {2, 2}};
    sort(vp.begin(), vp.end(), [](auto &a, auto &b) {
        return a.second < b.second;     // 按 pair 的 second 升序
    });
    for (auto &[a, b] : vp) cout << "(" << a << "," << b << ") ";
    cout << "\n";                       // (3,1) (2,2) (1,4)
}

1.2 std::stable_sort ------ 保持相等元素顺序

稳定归并排序,O(n log n),需要 O(n) 额外内存。当相等元素的原始顺序需要保留时使用。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>
using namespace std;

int main() {
    vector<pair<int, string>> v{{1, "a"}, {2, "b"}, {1, "c"}, {3, "d"}};

    // 按 first 稳定排序------相同 first 的 second 保持原顺序
    stable_sort(v.begin(), v.end(), [](auto &a, auto &b) {
        return a.first < b.first;
    });
    for (auto &[k, v] : v) cout << k << ":" << v << " ";
    cout << "\n";                       // 1:a 1:c 2:b 3:d
}

1.3 std::partial_sort ------ 只关心前 k 个有序

堆选择实现,只保证 [first, middle) 是有序的,[middle, last) 未指定顺序。O(n log k)。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{5, 1, 9, 3, 7, 2, 8, 4, 6};

    // 只排前 4 个
    partial_sort(v.begin(), v.begin() + 4, v.end());
    for (int x : v) cout << x << " ";   // 1 2 3 4 9 7 8 5 6
    cout << "\n";                       // 前 4 个有序,后面任意

    // 降序版------找前 4 个最大的
    partial_sort(v.begin(), v.begin() + 4, v.end(), greater<int>());
    for (int x : v) cout << x << " ";   // 9 8 7 6 ...
    cout << "\n";
}

1.4 std::nth_element ------ 只找第 k 大,不排其余

快速选择(introselect),O(n) 平均。保证第 k 个元素落在排序后的正确位置,左侧都 ≤ 它,右侧都 ≥ 它,但两侧内部无序。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{5, 1, 9, 3, 7, 2, 8, 4, 6};

    // 找第 4 小的元素(索引从 0 开始)
    nth_element(v.begin(), v.begin() + 3, v.end());
    cout << "第4小: " << v[3] << "\n";  // 4
    // 此时 v[0..2] ≤ 4,v[4..7] ≥ 4,但各自内部无序

    // 找中位数
    nth_element(v.begin(), v.begin() + v.size() / 2, v.end());
    cout << "中位数: " << v[v.size() / 2] << "\n";
}

1.5 std::partial_sort_copy ------ 排序结果写到新容器

不修改原容器,把前 k 个排序结果写入新容器。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> src{5, 1, 9, 3, 7, 2, 8, 4, 6};
    vector<int> dst(4);                      // 只取前 4 有序结果

    partial_sort_copy(src.begin(), src.end(), dst.begin(), dst.end());
    for (int x : dst) cout << x << " ";      // 1 2 3 4
    cout << "\n";

    // 降序版
    partial_sort_copy(src.begin(), src.end(), dst.begin(), dst.end(),
                      greater<int>());
    for (int x : dst) cout << x << " ";      // 9 8 7 6
    cout << "\n";
}

1.6 std::is_sorted / std::is_sorted_until ------ 检查有序性

调试和保护性断言的首选工具。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v1{1, 3, 5, 7, 9};
    cout << is_sorted(v1.begin(), v1.end()) << "\n";       // 1 (true)

    vector<int> v2{1, 3, 5, 2, 9};
    cout << is_sorted(v2.begin(), v2.end()) << "\n";       // 0 (false)

    // 找到第一个破坏有序的位置
    auto it = is_sorted_until(v2.begin(), v2.end());
    cout << *it << "\n";                                   // 2(5→2 处失序)

    // 降序检查
    vector<int> v3{9, 7, 5, 3, 1};
    cout << is_sorted(v3.begin(), v3.end(), greater<int>()) << "\n";  // 1
}

1.7 C++20 ranges 版本

命名空间 std::ranges,支持投影(projection),无需显式传 begin/end。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{5, 1, 9, 3, 7};

    ranges::sort(v);
    for (int x : v) cout << x << " ";   // 1 3 5 7 9
    cout << "\n";

    // 投影------按 pair 的 second 排序
    vector<pair<int, int>> vp{{3, 1}, {1, 4}, {2, 2}};
    ranges::sort(vp, {}, &pair<int,int>::second);
    for (auto &[a, b] : vp) cout << "(" << a << "," << b << ") ";
    cout << "\n";                       // (3,1) (2,2) (1,4)

    // ranges 版本的其他 API
    ranges::stable_sort(v);
    ranges::partial_sort(v, v.begin() + 3);
    ranges::nth_element(v, v.begin() + 3);
    ranges::is_sorted(v);
}

1.8 快速查表

函数 作用 稳定 复杂度 内存
sort 全排序 O(n log n) O(log n) 栈
stable_sort 稳定全排序 O(n log n) O(n)
partial_sort 前 k 个有序 O(n log k) O(1)
nth_element 第 k 大元素 O(n) 平均 O(1)
partial_sort_copy 前 k 个写到新容器 O(n log k) O(k)
is_sorted 检查有序 --- O(n) O(1)
is_sorted_until 首个失序位置 --- O(n) O(1)

二、底层原理

2.1 introsort:std::sort 的骨架

std::sort 的默认实现是 introsort(introspective sort),由 David Musser 在 1997 年提出。它不是一个单一的排序算法,而是一个算法选择策略------根据区间大小和递归深度,在三种算法之间动态切换。
#mermaid-svg-p7oibiyQ0deRk2ua{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p7oibiyQ0deRk2ua .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p7oibiyQ0deRk2ua .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p7oibiyQ0deRk2ua .error-icon{fill:#552222;}#mermaid-svg-p7oibiyQ0deRk2ua .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p7oibiyQ0deRk2ua .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p7oibiyQ0deRk2ua .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p7oibiyQ0deRk2ua .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p7oibiyQ0deRk2ua .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p7oibiyQ0deRk2ua .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p7oibiyQ0deRk2ua .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p7oibiyQ0deRk2ua .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p7oibiyQ0deRk2ua .marker.cross{stroke:#333333;}#mermaid-svg-p7oibiyQ0deRk2ua svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p7oibiyQ0deRk2ua p{margin:0;}#mermaid-svg-p7oibiyQ0deRk2ua .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p7oibiyQ0deRk2ua .cluster-label text{fill:#333;}#mermaid-svg-p7oibiyQ0deRk2ua .cluster-label span{color:#333;}#mermaid-svg-p7oibiyQ0deRk2ua .cluster-label span p{background-color:transparent;}#mermaid-svg-p7oibiyQ0deRk2ua .label text,#mermaid-svg-p7oibiyQ0deRk2ua span{fill:#333;color:#333;}#mermaid-svg-p7oibiyQ0deRk2ua .node rect,#mermaid-svg-p7oibiyQ0deRk2ua .node circle,#mermaid-svg-p7oibiyQ0deRk2ua .node ellipse,#mermaid-svg-p7oibiyQ0deRk2ua .node polygon,#mermaid-svg-p7oibiyQ0deRk2ua .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p7oibiyQ0deRk2ua .rough-node .label text,#mermaid-svg-p7oibiyQ0deRk2ua .node .label text,#mermaid-svg-p7oibiyQ0deRk2ua .image-shape .label,#mermaid-svg-p7oibiyQ0deRk2ua .icon-shape .label{text-anchor:middle;}#mermaid-svg-p7oibiyQ0deRk2ua .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p7oibiyQ0deRk2ua .rough-node .label,#mermaid-svg-p7oibiyQ0deRk2ua .node .label,#mermaid-svg-p7oibiyQ0deRk2ua .image-shape .label,#mermaid-svg-p7oibiyQ0deRk2ua .icon-shape .label{text-align:center;}#mermaid-svg-p7oibiyQ0deRk2ua .node.clickable{cursor:pointer;}#mermaid-svg-p7oibiyQ0deRk2ua .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p7oibiyQ0deRk2ua .arrowheadPath{fill:#333333;}#mermaid-svg-p7oibiyQ0deRk2ua .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p7oibiyQ0deRk2ua .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p7oibiyQ0deRk2ua .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p7oibiyQ0deRk2ua .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p7oibiyQ0deRk2ua .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p7oibiyQ0deRk2ua .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p7oibiyQ0deRk2ua .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p7oibiyQ0deRk2ua .cluster text{fill:#333;}#mermaid-svg-p7oibiyQ0deRk2ua .cluster span{color:#333;}#mermaid-svg-p7oibiyQ0deRk2ua div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p7oibiyQ0deRk2ua .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p7oibiyQ0deRk2ua rect.text{fill:none;stroke-width:0;}#mermaid-svg-p7oibiyQ0deRk2ua .icon-shape,#mermaid-svg-p7oibiyQ0deRk2ua .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p7oibiyQ0deRk2ua .icon-shape p,#mermaid-svg-p7oibiyQ0deRk2ua .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p7oibiyQ0deRk2ua .icon-shape .label rect,#mermaid-svg-p7oibiyQ0deRk2ua .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p7oibiyQ0deRk2ua .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p7oibiyQ0deRk2ua .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p7oibiyQ0deRk2ua :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



sort 排序开始
区间 ≤ 16?
插入排序

小常数 O(k²)
递归深度 > 2×⌊log₂n⌋?
堆排序

O(n log n) 保底
快速排序

median-of-three 选 pivot
分割为左右子区间

关键设计理念:

  • 快速排序平均最快,但最坏退化为 O(n²)(已排序或全相等输入 + 固定选第一个 pivot)。
  • introsort 通过追踪递归深度来检测退化:当深度超过 2 × ⌊log₂n⌋ 时,切换到 堆排序------O(n log n) 保底,无退化可能。
  • 小范围短路 :当区间大小 ≤ 阈值(gcc 用 16,MSVC 用 32)时,切换为 插入排序。插入排序对几乎有序的小区间非常快,且无递归和函数调用开销。

三种算法的组合使 introsort 在平均性能和最坏保证之间达到了业界公认的最佳平衡。
#mermaid-svg-xKJMyr9ZXP1vEZwp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xKJMyr9ZXP1vEZwp .error-icon{fill:#552222;}#mermaid-svg-xKJMyr9ZXP1vEZwp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xKJMyr9ZXP1vEZwp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .marker.cross{stroke:#333333;}#mermaid-svg-xKJMyr9ZXP1vEZwp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xKJMyr9ZXP1vEZwp p{margin:0;}#mermaid-svg-xKJMyr9ZXP1vEZwp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .cluster-label text{fill:#333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .cluster-label span{color:#333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .cluster-label span p{background-color:transparent;}#mermaid-svg-xKJMyr9ZXP1vEZwp .label text,#mermaid-svg-xKJMyr9ZXP1vEZwp span{fill:#333;color:#333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .node rect,#mermaid-svg-xKJMyr9ZXP1vEZwp .node circle,#mermaid-svg-xKJMyr9ZXP1vEZwp .node ellipse,#mermaid-svg-xKJMyr9ZXP1vEZwp .node polygon,#mermaid-svg-xKJMyr9ZXP1vEZwp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xKJMyr9ZXP1vEZwp .rough-node .label text,#mermaid-svg-xKJMyr9ZXP1vEZwp .node .label text,#mermaid-svg-xKJMyr9ZXP1vEZwp .image-shape .label,#mermaid-svg-xKJMyr9ZXP1vEZwp .icon-shape .label{text-anchor:middle;}#mermaid-svg-xKJMyr9ZXP1vEZwp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xKJMyr9ZXP1vEZwp .rough-node .label,#mermaid-svg-xKJMyr9ZXP1vEZwp .node .label,#mermaid-svg-xKJMyr9ZXP1vEZwp .image-shape .label,#mermaid-svg-xKJMyr9ZXP1vEZwp .icon-shape .label{text-align:center;}#mermaid-svg-xKJMyr9ZXP1vEZwp .node.clickable{cursor:pointer;}#mermaid-svg-xKJMyr9ZXP1vEZwp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .arrowheadPath{fill:#333333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xKJMyr9ZXP1vEZwp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xKJMyr9ZXP1vEZwp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xKJMyr9ZXP1vEZwp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xKJMyr9ZXP1vEZwp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xKJMyr9ZXP1vEZwp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xKJMyr9ZXP1vEZwp .cluster text{fill:#333;}#mermaid-svg-xKJMyr9ZXP1vEZwp .cluster span{color:#333;}#mermaid-svg-xKJMyr9ZXP1vEZwp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xKJMyr9ZXP1vEZwp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xKJMyr9ZXP1vEZwp rect.text{fill:none;stroke-width:0;}#mermaid-svg-xKJMyr9ZXP1vEZwp .icon-shape,#mermaid-svg-xKJMyr9ZXP1vEZwp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xKJMyr9ZXP1vEZwp .icon-shape p,#mermaid-svg-xKJMyr9ZXP1vEZwp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xKJMyr9ZXP1vEZwp .icon-shape .label rect,#mermaid-svg-xKJMyr9ZXP1vEZwp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xKJMyr9ZXP1vEZwp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xKJMyr9ZXP1vEZwp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xKJMyr9ZXP1vEZwp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





sort 排序
需要稳定?
stable_sort

归并排序
只看前 k 个?
partial_sort

堆选择 O(n log k)
只看第 k 个?
nth_element

快速选择 O(n)
sort

introsort O(n log n)

2.2 stable_sort 的归并本质

std::stable_sort 基于自底向上归并(bottom-up merge sort):

  1. 如果有足够额外内存(n × sizeof(T)),分配临时缓冲区,用迭代式归并(相邻子区间逐轮合并),O(n log n)。
  2. 如果内存不足,退化到 O(n log² n) 原地归并------通过旋转(rotate)来实现原地合并,速度下降但依然稳定。

稳定性的代价:

  • 内存开销 O(n) 而非 O(log n)
  • 常数远大于不稳定排序
  • 对非平凡类型(stringvector 等)的移动和复制成本敏感

工程选择 :默认用 stable_sort 是有成本的。当你不需要稳定时,sort 更快,且对缓存更友好。

2.3 partial_sort 的堆实现

cpp 复制代码
// 简化的实现逻辑
template<class RandomIt, class Compare>
void partial_sort(RandomIt first, RandomIt middle, RandomIt last, Compare comp) {
    // 阶段1:前 k 个元素建最大堆
    make_heap(first, middle, comp);
    // 阶段2:遍历剩余元素,若比堆顶小则替换后调整
    for (auto it = middle; it != last; ++it) {
        if (comp(*it, *first)) {         // *it < 堆顶
            pop_heap(first, middle, comp); // 堆顶移到 middle-1
            iter_swap(middle - 1, it);
            push_heap(first, middle, comp);
        }
    }
    // 阶段3:对前 k 个做堆排序(让它们最终有序)
    sort_heap(first, middle, comp);
}
  • 建堆 O(k)
  • 遍历剩余元素,每个 O(log k) 替换堆顶 → 合计 O(n log k)
  • 最终堆排序 O(k log k)

2.4 nth_element 的快速选择(introselect)

std::nth_element 默认实现基于 introselect------introsort 的选择算法版本:

  1. 用 median-of-three 选择 pivot(头、中、尾三个位置的中间值),在绝大多数情况下避免不平衡划分。
  2. 递归只在包含目标元素的那一侧进行,另一侧丢弃------单向收敛,平均 O(n)。
  3. 当递归深度退化时,切换到堆排序的选择变体,O(n log n) 保底。

partial_sort 的区别:

  • nth_element 只保证第 k 个位置正确,不保证其他元素有序
  • partial_sort 保证前 k 个全部有序

前者做的工作少,常数和渐进复杂度都更优。

2.5 严格弱序(Strict Weak Ordering)

C++ 标准要求所有排序比较器满足严格弱序------四条规定:

  1. 非自反性comp(a, a) == false
  2. 反对称性 :如果 comp(a, b) == true,则 comp(b, a) == false
  3. 传递性 :如果 comp(a, b) && comp(b, c),则 comp(a, c) == true
  4. 等价的传递性 :如果 !comp(a,b) && !comp(b,a)(等价),且 !comp(b,c) && !comp(c,b),则 !comp(a,c) && !comp(c,a)
cpp 复制代码
// 错误案例:不满足严格弱序
auto bad_cmp = [](int a, int b) {
    return a * a < b * b;   // 按绝对值比较?a=3, b=-3 时互为等价,但 a=-3, c=3 时!comp(a,b), !comp(c,b)...
    // 问题在于等价的传递性:-3 和 3 等价,3 和 -3 等价,但 -3 和 -3 也等价------看起来没问题,
    // 但如果 comp 内部有副作用或浮点精度问题,行为可能 UB。
};

// 更常见的 UB 源:用 <= 实现
auto wrong_cmp = [](int a, int b) { return a <= b; };
// comp(a,a) = true,违反非自反性,sort 的行为未定义

违反严格弱序不会产生编译错误------它直接导致 未定义行为:排序结果错误、死循环甚至崩溃。

2.6 MSVC 的调试检查

MSVC STL 在 debug 模式下(_ITERATOR_DEBUG_LEVEL 为 2)增加了多项比较器合法性检查:

  • _DEBUG_POINTER:确保迭代器可解引用
  • 对堆操作验证堆结构有效性
  • 对比较器进行对称性验证 :如果 comp(a, b) 成立,则检查 comp(b, a) 必须不成立------能在运行时捕捉到上一节提到的 <= 类错误
cpp 复制代码
// MSVC debug 模式下的内部检查示意
_Validate_compare(_Comp_pred _Pred, _Ty& _Left, _Ty& _Right) {
    // 检查 comp(a, a) == false
    // 检查 comp(a, b) 和 comp(b, a) 不能同时为 true
}

gcc libstdc++ 的 debug 模式(-D_GLIBCXX_DEBUG)同样提供类似检查,但 MSVC 的堆验证(_IS_VALID_HEAP)最为严格。

2.7 为什么 sort 不是稳定的

introsort 选快速排序做主算法就是因为它不稳定------快速排序在 partition 阶段会把相等的元素分到左右两侧,顺序无法保证。要做到稳定,必须用归并,而归并需要 O(n) 额外内存。C++ 标准委员会把稳定性决策权交给了开发者:需要稳定用 stable_sort,追求性能用 sort


三、面试题 + 口语化答案

Q1:std::sort 稳定吗?怎么实现稳定排序?

"不稳定。std::sort 默认用 introsort,快速排序的 partition 会把相等元素拆到两侧。需要稳定用 std::stable_sort------它基于归并排序,保证相等元素保持原序,代价是需要 O(n) 额外内存。如果内存不够,会退化成 O(n log² n) 的原地归并。"

Q2:怎么用 std::sort 降序排序?

"三种方式:传 std::greater<int>() 作为比较器、传 Lambda [](int a, int b) { return a > b; }、或者 C++20 的 ranges::sort(v, ranges::greater{})。更通用的做法是定义自己的比较函数传进去。"

Q3:nth_element 和 partial_sort 有什么区别?

"nth_element 只保证第 k 个元素在排好序后的正确位置上,前后的元素只保证比它小或大但不排序------O(n) 平均。partial_sort 保证前 k 个元素完全有序------O(n log k)。如果只需要知道第几名,用 nth_element;如果要把前几名展示出来,用 partial_sort。"

Q4:sort 的默认比较器为什么是 less 而不是 greater?

"所有 STL 容器和算法的默认比较器都是 std::less,这遵循了最小意外原则------sort 的默认行为是升序,和 operator< 的语义一致。mapsetpriority_queue 也都默认是 less,形成一致的约定:less = 升序(最小元素在顶部)。突然改成 greater 会让使用者困惑。"

Q5:稳定性到底要付出什么代价?

"两个层面。内存:stable_sort 需要分配 O(n) 的临时缓冲区来存搬运的元素,而 sort 只需 O(log n) 的栈空间。时间:稳定排序的常数显著更大------归并的每轮需要额外的元素复制,缓存不友好。对 int 排序可能慢 20--50%,对 string 等非平凡类型更严重。工程上的黄金法则是:默认用 sort,只有当稳定性有业务意义时(比如先排年级再排班级,需要保持相对位置)才用 stable_sort。"

Q6:什么场景下 sort 会退化到 O(n²)?

"原始快速排序会在已排序或逆序输入配合固定选第一个 pivot 时退化为 O(n²)。但现代 STL 的 sort 用了 introsort------它监控递归深度,超过 2 × ⌊log₂n⌋ 时切换到堆排序。所以 sort 不可能退化到 O(n²)。如果你的程序里 sort 变慢了,更可能是比较器太重或拷贝代价太大,而不是算法退化。"

Q7:is_sorted 在实际工程中有什么用?

"两个价值。第一,调试断言:在关键排序操作后用 assert(is_sorted(...)) 做运行时验证,快速发现比较器中的逻辑错误或迭代器越界。第二,短路优化:在已排序数据上重复排序是完全不必要的,可以用 is_sorted 跳过。C++20 ranges 里还有 ranges::is_sorted,配合投影可以在复杂结构上做更灵活的检查。"

Q8:partial_sort 底层为什么不直接用快排然后只取前 k 个?

"快排全排要 O(n log n)------如果只想要前 k 个有序的,O(n log k) 比 O(n log n) 快得多,尤其当 k 远小于 n 时差距明显。partial_sort 用 make_heap 加剩余元素逐一替换堆顶的方式,做到了 O(n log k),不需要对全部元素排序。而且如果 k 比较小(比如取前 10 名),它在 n 很大时比快排快一个数量级。"

Q9:严格弱序的等价传递性为什么重要?

"因为 STL 排序算法内部常依赖等价关系来做优化。比如 lower_boundupper_bound 二分查找就用等价关系确定插入位置。如果等价不传递------a 等价于 b,b 等价于 c,但 a 不等价于 c------排序结果可能错乱,甚至导致 equal_range 返回空区间。更隐蔽的是,这种错误在测试中很难暴露,往往要在特定输入组合下才触发。"


一句话总结std::sort 不是单纯的快排,而是 introsort 的多算法组合(快速排序 + 堆排序 + 插入排序),保证了 O(n log n) 的最坏性能;围绕它展开的 stable_sort(稳定)、partial_sort(前 k 有序)、nth_element(第 k 正确)各有场景和代价------理解它们的算法本质和稳定性取舍,是面试和生产中用好 sort 系列的关键。