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 中给出:
- 从右向左 找到第一个相邻升序对
(i, j),其中j = i+1且*i < *j。如果找不到,说明序列已是最大排列(完全降序)。 - 从右向左 找到第一个大于
*i的元素*k(由于步骤 1 保证了[j, last)是降序的,这一步一定能找到)。 - 交换
*i和*k(将字典序增加一步)。 - 反转
[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。 - 交换
2和3→{1, 3, 2}。 - 反转
[j, last)→ 此时 j 已到末尾,反转空区间 →{1, 3, 2}。
每一步的 amortized O(1) 复杂度来源于:相邻排列之间只有常数个元素被移动。
2.2 prev_permutation ------ 对称操作
prev_permutation 是 next_permutation 的镜像操作,找的是字典序的上一个排列:
- 从右向左找第一个相邻降序对
(i, j),其中*i > *j。 - 从右向左找第一个小于
*i的*k。 - 交换
*i和*k。 - 反转
[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 的水塘抽样则解决了流式数据中均匀采样的问题------三者加起来覆盖了排列、洗牌、采样三个最常见的随机化操作场景。