C++ STL之排列组合与洗牌详解:从使用到底层,再到面试八股

C++ STL之排列组合与洗牌详解:从使用到底层,再到面试八股

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


一、用法速查

1.1 std::next_permutation / std::prev_permutation ------ 字典序排列

两个函数将序列原地变换为字典序的下一个/上一个排列。若存在则返回 true,若已是最后一个/第一个则返回 false 并将序列重置为最小/最大排列。

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

int main() {
    vector<int> v{1, 2, 3};

    // 生成所有全排列------1 2 3, 1 3 2, 2 1 3, 2 3 1, 3 1 2, 3 2 1
    do {
        for (int x : v) cout << x << " ";
        cout << "\n";
    } while (next_permutation(v.begin(), v.end()));

    // prev_permutation 用法相同,方向相反
    v = {3, 2, 1};
    do {
        for (int x : v) cout << x << " ";  // 3 2 1, 3 1 2, 2 3 1, 2 1 3, 1 3 2, 1 2 3
        cout << "\n";
    } while (prev_permutation(v.begin(), v.end()));
}

注意事项:

  • 要求输入序列已按目标顺序排列------生成全排列前必须先 sort 成升序再用 next_permutation,或用 sort(..., greater{}) 后配合 prev_permutation
  • 相等元素不会交换,所以 {1, 1, 2} 只会生成 3 种排列而非 6 种。

1.2 std::shuffle ------ 真随机洗牌(C++11)

用统一的随机数生成器(URNG)对序列执行均匀随机重排,O(n)。

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

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

    // 标准写法:random_device 获取种子,mt19937 做引擎
    shuffle(v.begin(), v.end(), mt19937{random_device{}()});

    for (int x : v) cout << x << " ";   // 每次运行结果不同
    cout << "\n";
}

关键点:

  • 第三个参数要求均匀随机位生成器(URNG) ,不满足 UniformRandomBitGenerator 要求的类型无法通过编译。
  • 每次 shuffle 的随机性完全取决于 URNG 的质量。

1.3 std::random_shuffle ------ 已被移除(C++14 弃用,C++17 移除)

cpp 复制代码
// C++11 中有两个重载:
// random_shuffle(first, last);                     // 使用 rand()
// random_shuffle(first, last, RandomFunc&&);       // 自定义随机函数

// 两个重载都在 C++14 被标记 deprecated,C++17 正式移除

移除原因:

  • 第一个重载固定使用 std::rand(),全局状态不可控、周期短(RAND_MAX 通常只有 32767)。
  • 第二个重载容易写出不均匀的随机函数(rand() % n 有模偏差)。
  • C++11 有了 <random> 标准库后,std::shuffle 是替代方案。

1.4 std::sample ------ 无放回采样(C++17)

从输入范围中均匀随机选取 k 个元素写入输出迭代器,不修改输入。返回类型为 OutputIt (指向输出范围末尾),而非 void

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

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

    // 从 pool 中无放回地随机抽取 3 个
    sample(pool.begin(), pool.end(), back_inserter(result), 3,
           mt19937{random_device{}()});

    for (int x : result) cout << x << " ";
    cout << "\n";
}

性能保证:O(n),只遍历输入一次。

1.5 快速查表

函数 作用 返回 复杂度 C++ 版本
next_permutation 字典序下一个排列 bool O(n) amortized C++98
prev_permutation 字典序上一个排列 bool O(n) amortized C++98
shuffle 均匀随机重排 void O(n) C++11
random_shuffle 随机重排(已移除) void O(n) C++98 → 弃用
sample 无放回采样 OutputIt O(n) C++17

二、底层原理

2.1 next_permutation 的四步算法

next_permutation 的核心思想是从右向左找第一个可以"增大"的位置,然后做最小幅度的调整。这是生成字典序下一个排列的经典算法,由 Donald Knuth 在 TAOCP 中给出:

  1. 从右向左 找到第一个相邻升序对 (i, j),其中 j = i+1*i < *j。如果找不到,说明序列已是最大排列(完全降序)。
  2. 从右向左 找到第一个大于 *i 的元素 *k(由于步骤 1 保证了 [j, last) 是降序的,这一步一定能找到)。
  3. 交换 *i*k(将字典序增加一步)。
  4. 反转 [j, last)(把后半段从降序变成升序,保证"最小增幅")。

#mermaid-svg-ydmtEQLZuVAUrqFW{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-ydmtEQLZuVAUrqFW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ydmtEQLZuVAUrqFW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ydmtEQLZuVAUrqFW .error-icon{fill:#552222;}#mermaid-svg-ydmtEQLZuVAUrqFW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ydmtEQLZuVAUrqFW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ydmtEQLZuVAUrqFW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ydmtEQLZuVAUrqFW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ydmtEQLZuVAUrqFW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ydmtEQLZuVAUrqFW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ydmtEQLZuVAUrqFW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ydmtEQLZuVAUrqFW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ydmtEQLZuVAUrqFW .marker.cross{stroke:#333333;}#mermaid-svg-ydmtEQLZuVAUrqFW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ydmtEQLZuVAUrqFW p{margin:0;}#mermaid-svg-ydmtEQLZuVAUrqFW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ydmtEQLZuVAUrqFW .cluster-label text{fill:#333;}#mermaid-svg-ydmtEQLZuVAUrqFW .cluster-label span{color:#333;}#mermaid-svg-ydmtEQLZuVAUrqFW .cluster-label span p{background-color:transparent;}#mermaid-svg-ydmtEQLZuVAUrqFW .label text,#mermaid-svg-ydmtEQLZuVAUrqFW span{fill:#333;color:#333;}#mermaid-svg-ydmtEQLZuVAUrqFW .node rect,#mermaid-svg-ydmtEQLZuVAUrqFW .node circle,#mermaid-svg-ydmtEQLZuVAUrqFW .node ellipse,#mermaid-svg-ydmtEQLZuVAUrqFW .node polygon,#mermaid-svg-ydmtEQLZuVAUrqFW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ydmtEQLZuVAUrqFW .rough-node .label text,#mermaid-svg-ydmtEQLZuVAUrqFW .node .label text,#mermaid-svg-ydmtEQLZuVAUrqFW .image-shape .label,#mermaid-svg-ydmtEQLZuVAUrqFW .icon-shape .label{text-anchor:middle;}#mermaid-svg-ydmtEQLZuVAUrqFW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ydmtEQLZuVAUrqFW .rough-node .label,#mermaid-svg-ydmtEQLZuVAUrqFW .node .label,#mermaid-svg-ydmtEQLZuVAUrqFW .image-shape .label,#mermaid-svg-ydmtEQLZuVAUrqFW .icon-shape .label{text-align:center;}#mermaid-svg-ydmtEQLZuVAUrqFW .node.clickable{cursor:pointer;}#mermaid-svg-ydmtEQLZuVAUrqFW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ydmtEQLZuVAUrqFW .arrowheadPath{fill:#333333;}#mermaid-svg-ydmtEQLZuVAUrqFW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ydmtEQLZuVAUrqFW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ydmtEQLZuVAUrqFW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ydmtEQLZuVAUrqFW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ydmtEQLZuVAUrqFW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ydmtEQLZuVAUrqFW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ydmtEQLZuVAUrqFW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ydmtEQLZuVAUrqFW .cluster text{fill:#333;}#mermaid-svg-ydmtEQLZuVAUrqFW .cluster span{color:#333;}#mermaid-svg-ydmtEQLZuVAUrqFW 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-ydmtEQLZuVAUrqFW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ydmtEQLZuVAUrqFW rect.text{fill:none;stroke-width:0;}#mermaid-svg-ydmtEQLZuVAUrqFW .icon-shape,#mermaid-svg-ydmtEQLZuVAUrqFW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ydmtEQLZuVAUrqFW .icon-shape p,#mermaid-svg-ydmtEQLZuVAUrqFW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ydmtEQLZuVAUrqFW .icon-shape .label rect,#mermaid-svg-ydmtEQLZuVAUrqFW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ydmtEQLZuVAUrqFW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ydmtEQLZuVAUrqFW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ydmtEQLZuVAUrqFW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

从右向左找相邻升序对
找到?
已是最大排列

反转整个序列
从右找第一个大于 i 的元素 K
交换 i 和 K
反转 i 之后的区间
返回 true

举例 {1, 2, 3}{1, 3, 2} 的过程:

  • 从右往左,2 < 3,所以 i=1(指向2)j=2(指向3)
  • 从右往左找第一个大于 *i=2 的,找到 *k=3
  • 交换 23{1, 3, 2}
  • 反转 [j, last) → 此时 j 已到末尾,反转空区间 → {1, 3, 2}

每一步的 amortized O(1) 复杂度来源于:相邻排列之间只有常数个元素被移动。

2.2 prev_permutation ------ 对称操作

prev_permutationnext_permutation 的镜像操作,找的是字典序的上一个排列:

  1. 从右向左找第一个相邻降序对 (i, j),其中 *i > *j
  2. 从右向左找第一个小于 *i*k
  3. 交换 *i*k
  4. 反转 [j, last)(将后半段从升序变回降序)。

如果步骤 1 找不到降序对,说明已是第一排列(完全升序),返回 false 并反转成全降序。

2.3 Fisher-Yates / Knuth Shuffle

std::shuffle 的标准实现是 Fisher-Yates 洗牌算法(也称 Knuth Shuffle),从后往前遍历,每个位置与当前位置之前的随机位置交换:

cpp 复制代码
// 简化实现------std::shuffle 的等价逻辑
template<class RandomIt, class URNG>
void shuffle(RandomIt first, RandomIt last, URNG&& g) {
    for (auto i = last - 1; i > first; --i) {
        // 从 [first, i] 中均匀选取一个位置
        auto k = first + uniform_int_distribution<>(0, i - first)(g);
        iter_swap(i, k);
    }
}

#mermaid-svg-UULRdcRYmJdauIdE{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-UULRdcRYmJdauIdE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UULRdcRYmJdauIdE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UULRdcRYmJdauIdE .error-icon{fill:#552222;}#mermaid-svg-UULRdcRYmJdauIdE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UULRdcRYmJdauIdE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UULRdcRYmJdauIdE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UULRdcRYmJdauIdE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UULRdcRYmJdauIdE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UULRdcRYmJdauIdE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UULRdcRYmJdauIdE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UULRdcRYmJdauIdE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UULRdcRYmJdauIdE .marker.cross{stroke:#333333;}#mermaid-svg-UULRdcRYmJdauIdE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UULRdcRYmJdauIdE p{margin:0;}#mermaid-svg-UULRdcRYmJdauIdE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UULRdcRYmJdauIdE .cluster-label text{fill:#333;}#mermaid-svg-UULRdcRYmJdauIdE .cluster-label span{color:#333;}#mermaid-svg-UULRdcRYmJdauIdE .cluster-label span p{background-color:transparent;}#mermaid-svg-UULRdcRYmJdauIdE .label text,#mermaid-svg-UULRdcRYmJdauIdE span{fill:#333;color:#333;}#mermaid-svg-UULRdcRYmJdauIdE .node rect,#mermaid-svg-UULRdcRYmJdauIdE .node circle,#mermaid-svg-UULRdcRYmJdauIdE .node ellipse,#mermaid-svg-UULRdcRYmJdauIdE .node polygon,#mermaid-svg-UULRdcRYmJdauIdE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UULRdcRYmJdauIdE .rough-node .label text,#mermaid-svg-UULRdcRYmJdauIdE .node .label text,#mermaid-svg-UULRdcRYmJdauIdE .image-shape .label,#mermaid-svg-UULRdcRYmJdauIdE .icon-shape .label{text-anchor:middle;}#mermaid-svg-UULRdcRYmJdauIdE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UULRdcRYmJdauIdE .rough-node .label,#mermaid-svg-UULRdcRYmJdauIdE .node .label,#mermaid-svg-UULRdcRYmJdauIdE .image-shape .label,#mermaid-svg-UULRdcRYmJdauIdE .icon-shape .label{text-align:center;}#mermaid-svg-UULRdcRYmJdauIdE .node.clickable{cursor:pointer;}#mermaid-svg-UULRdcRYmJdauIdE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UULRdcRYmJdauIdE .arrowheadPath{fill:#333333;}#mermaid-svg-UULRdcRYmJdauIdE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UULRdcRYmJdauIdE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UULRdcRYmJdauIdE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UULRdcRYmJdauIdE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UULRdcRYmJdauIdE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UULRdcRYmJdauIdE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UULRdcRYmJdauIdE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UULRdcRYmJdauIdE .cluster text{fill:#333;}#mermaid-svg-UULRdcRYmJdauIdE .cluster span{color:#333;}#mermaid-svg-UULRdcRYmJdauIdE 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-UULRdcRYmJdauIdE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UULRdcRYmJdauIdE rect.text{fill:none;stroke-width:0;}#mermaid-svg-UULRdcRYmJdauIdE .icon-shape,#mermaid-svg-UULRdcRYmJdauIdE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UULRdcRYmJdauIdE .icon-shape p,#mermaid-svg-UULRdcRYmJdauIdE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UULRdcRYmJdauIdE .icon-shape .label rect,#mermaid-svg-UULRdcRYmJdauIdE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UULRdcRYmJdauIdE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UULRdcRYmJdauIdE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UULRdcRYmJdauIdE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

i = n-1
i > 0 ?
结束,完成洗牌
0, i 中均匀随机选 k
交换 vi 与 vk
i--

为什么是真正的均匀随机:

  • 对于 n 个元素,每个排列出现的概率恰好是 1/n!
  • 第 1 轮(最后一位):从 n 个中均匀选 1 个放末尾,概率 1/n
  • 第 2 轮(倒数第二位):从剩余 n-1 个中均匀选 1 个,概率 1/(n-1)
  • ... 最终联合概率 1/n × 1/(n-1) × ... × 1/1 = 1/n!

random_shuffle 的根本区别在于随机源的质量------shuffle 使用标准 <random> 库的 URNG,避免了 rand() 的周期短和模偏差问题。

2.4 std::sample 的实现------水塘抽样(Reservoir Sampling)

std::sample 在 C++17 标准中要求"每轮抽样的概率均匀"。当输入范围是 ForwardIterator 时,标准实现采用 水塘抽样(Reservoir Sampling) 算法,特别是 Algorithm R:

cpp 复制代码
// 简化实现------水塘抽样(Algorithm R)
template<class InputIt, class OutputIt, class URNG>
OutputIt sample(InputIt first, InputIt last, OutputIt out,
                size_t k, URNG&& g) {
    using dist_t = uniform_int_distribution<size_t>;

    vector<decltype(*first)> reservoir;
    reservoir.reserve(k);

    // 阶段1:填充水塘------取前 k 个元素
    auto it = first;
    for (size_t i = 0; i < k; ++i, ++it) {
        if (it == last) {           // 不足 k 个,全取返回
            return copy(reservoir.begin(), reservoir.end(), out);
        }
        reservoir.push_back(*it);
    }

    // 阶段2:替换------对第 i 个元素 (i >= k),以 k/(i+1) 的概率替换到水塘中
    for (size_t i = k; it != last; ++it, ++i) {
        auto j = dist_t{0, i}(g);   // [0, i] 均匀随机
        if (j < k) reservoir[j] = *it;  // 替换水塘中的第 j 个
    }

    return copy(reservoir.begin(), reservoir.end(), out);
}

#mermaid-svg-BOKmlCcUvurQoT8F{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-BOKmlCcUvurQoT8F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BOKmlCcUvurQoT8F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BOKmlCcUvurQoT8F .error-icon{fill:#552222;}#mermaid-svg-BOKmlCcUvurQoT8F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BOKmlCcUvurQoT8F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BOKmlCcUvurQoT8F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BOKmlCcUvurQoT8F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BOKmlCcUvurQoT8F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BOKmlCcUvurQoT8F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BOKmlCcUvurQoT8F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BOKmlCcUvurQoT8F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BOKmlCcUvurQoT8F .marker.cross{stroke:#333333;}#mermaid-svg-BOKmlCcUvurQoT8F svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BOKmlCcUvurQoT8F p{margin:0;}#mermaid-svg-BOKmlCcUvurQoT8F .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BOKmlCcUvurQoT8F .cluster-label text{fill:#333;}#mermaid-svg-BOKmlCcUvurQoT8F .cluster-label span{color:#333;}#mermaid-svg-BOKmlCcUvurQoT8F .cluster-label span p{background-color:transparent;}#mermaid-svg-BOKmlCcUvurQoT8F .label text,#mermaid-svg-BOKmlCcUvurQoT8F span{fill:#333;color:#333;}#mermaid-svg-BOKmlCcUvurQoT8F .node rect,#mermaid-svg-BOKmlCcUvurQoT8F .node circle,#mermaid-svg-BOKmlCcUvurQoT8F .node ellipse,#mermaid-svg-BOKmlCcUvurQoT8F .node polygon,#mermaid-svg-BOKmlCcUvurQoT8F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BOKmlCcUvurQoT8F .rough-node .label text,#mermaid-svg-BOKmlCcUvurQoT8F .node .label text,#mermaid-svg-BOKmlCcUvurQoT8F .image-shape .label,#mermaid-svg-BOKmlCcUvurQoT8F .icon-shape .label{text-anchor:middle;}#mermaid-svg-BOKmlCcUvurQoT8F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BOKmlCcUvurQoT8F .rough-node .label,#mermaid-svg-BOKmlCcUvurQoT8F .node .label,#mermaid-svg-BOKmlCcUvurQoT8F .image-shape .label,#mermaid-svg-BOKmlCcUvurQoT8F .icon-shape .label{text-align:center;}#mermaid-svg-BOKmlCcUvurQoT8F .node.clickable{cursor:pointer;}#mermaid-svg-BOKmlCcUvurQoT8F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BOKmlCcUvurQoT8F .arrowheadPath{fill:#333333;}#mermaid-svg-BOKmlCcUvurQoT8F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BOKmlCcUvurQoT8F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BOKmlCcUvurQoT8F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BOKmlCcUvurQoT8F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BOKmlCcUvurQoT8F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BOKmlCcUvurQoT8F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BOKmlCcUvurQoT8F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BOKmlCcUvurQoT8F .cluster text{fill:#333;}#mermaid-svg-BOKmlCcUvurQoT8F .cluster span{color:#333;}#mermaid-svg-BOKmlCcUvurQoT8F 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-BOKmlCcUvurQoT8F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BOKmlCcUvurQoT8F rect.text{fill:none;stroke-width:0;}#mermaid-svg-BOKmlCcUvurQoT8F .icon-shape,#mermaid-svg-BOKmlCcUvurQoT8F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BOKmlCcUvurQoT8F .icon-shape p,#mermaid-svg-BOKmlCcUvurQoT8F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BOKmlCcUvurQoT8F .icon-shape .label rect,#mermaid-svg-BOKmlCcUvurQoT8F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BOKmlCcUvurQoT8F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BOKmlCcUvurQoT8F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BOKmlCcUvurQoT8F :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否,第 i 个


前 k 个直接入水塘
i = k
遍历到末尾?
输出水塘

返回 out
随机 j ∈ 0, i
j < k ?
替换水塘中第 j 个
跳过
i++

关键设计:

  • O(n) 时间复杂度:只遍历输入一次,每个元素决定是否进入水塘。
  • O(k) 额外空间:只需要一个大小 k 的缓存区。
  • 均匀性证明 :对于输入流中的任意位置 i(从 0 计数),它最终留在水塘中的概率是 k / (i+1) × (1 - 1/(i+2)) × ... = k/n
  • 如果传入的是 RandomAccessIterator,某些实现会用更高效的 Selection Sampling,不需要 O(k) 额外空间。

三、面试题 + 口语化答案

Q1:手写 next_permutation

"标准库实现可以分为四步。先解释思路:找第一个升序对、找交换位置、交换、反转后半。面试时可以写简化版,注意处理返回 false 的情况时要逆序还原。

cpp 复制代码
bool next_permutation(vector<int>& v) {
    int n = v.size();
    int i = n - 2;
    // 1. 从右找第一个 *i < *j
    while (i >= 0 && v[i] >= v[i + 1]) --i;
    if (i < 0) {
        reverse(v.begin(), v.end());
        return false;
    }
    // 2. 从右找第一个 > *i 的 *k
    int k = n - 1;
    while (v[k] <= v[i]) --k;
    // 3. 交换
    swap(v[i], v[k]);
    // 4. 反转后半
    reverse(v.begin() + i + 1, v.end());
    return true;
}

注意严格大于和大于等于的区分------处理重复元素时要用 >= / <=,否则会跳过或漏掉一些排列。"

Q2:全排列的时间复杂度是多少?

"n! 种排列,调用 n! 次 next_permutation,每次分摊 O(1),所以生成全部排列的时间是 O(n!)。空间 O(1),在输入序列上原地操作,不需要额外内存来存排列。这个复杂度是硬性的------n = 12 时 4.79 亿次,已经跑不完了,所以真写全排列的时候数据量不会太大。"

Q3:shuffle 和 random_shuffle 有什么区别?

"random_shuffle 被移除了,两个核心缺陷:一是默认用 rand(),RAND_MAX 通常 32767,序列稍长就会不均匀;二是模运算 rand() % n 在 n 不是 RAND_MAX 的约数时有模偏差。shuffle 用 <random> 库的 mt19937 配合 uniform_int_distribution,周期 2^19937-1,且分布模板直接给出均匀整数------这两个问题同时解决。所以 C++17 起只有 shuffle。"

Q4:随机数引擎怎么选?mt19937 还是 default_random_engine?

"面试问到就说 mt19937。它是 Mersenne Twister 算法,周期 2^19937-1,质量经过充分验证,是通用场景的首选。default_random_engine 是实现的别名------gcc 用 minstd_rand0,clang 用 mt19937,MSVC 用 mt19937。不同编译器行为不同,不可移植。跨平台要确定性的场景必须显式指定引擎类型,不要用 default_random_engine。"

Q5:std::sample 的时间复杂度和空间复杂度?返回类型是什么?

"时间 O(n),只遍历一次输入序列。空间 O(k),需要一个大小为 k 的水塘缓冲区。注意 sample 的返回类型是 OutputIt(指向输出范围末尾),不是 void------很多面试者会记错这一点。如果输入是 RandomAccessIterator,某些实现可以用 Selection Sampling 做到 O(k) 额外空间,但时间同样是 O(n)。采样数量 k 不能超过输入大小,超过时 sample 不会报错,会直接取完所有元素。"

Q6:水塘抽样(Reservoir Sampling)的原理是什么?

"水塘抽样解决的是'未知长度的数据流中均匀取 k 个'的问题。前 k 个元素直接进池子;对第 i(i ≥ k)个元素,以 k/(i+1) 的概率替换水塘中的一个随机位置。数学归纳可以证明每个元素最终留在池子里的概率都是 k/n。这个算法的精妙之处在于它只需要 O(n) 的遍历和 O(k) 的内存,且事先不需要知道 n。"

Q7:输入有重复元素时 next_permutation 怎么工作?

"它不会生成重复排列。比较时使用 *i < *j*i < *k 的严格语义------注意不是 <=。所以 {1, 1, 2} 用 do-while 循环只输出 3 种排列而不是 6 种。这是设计上的有意选择:按字典序,重复元素在排列中的相对位置固定,减少了不必要的排列数量。如果需要全排列包含重复元素,可以先转换成带 index 的 pair 再排列。"

Q8:prev_permutation 怎么从对称性理解?

"和 next_permutation 是镜像操作。next 找升序对、交换较大元素、反转降序段为升序;prev 找降序对、交换较小元素、反转升序段为降序。从代码结构上看,把 next 的所有比较符取反(<>><)就是 prev。实际上标准库实现经常把两个函数做对称实现,内部只差一个比较器的方向选择。"


一句话总结next_permutation 的四步找升序对→交换→反转算法和 shuffle 背后的 Fisher-Yates 洗牌是两大经典原地算法,分别对应"有序生成"和"无序打乱"两个方向;std::sample 的水塘抽样则解决了流式数据中均匀采样的问题------三者加起来覆盖了排列、洗牌、采样三个最常见的随机化操作场景。