【C++ 面试真题】20. 聊聊 C++ 的标准库常用算法

【C++ 面试真题】聊聊 C++ 的标准库常用算法

"介绍一下标准库的算法"是标准库篇的经典开场题。背得出"sort 排序、find 查找"只是及格,真考你的是"心里有没有分类地图、sort 的时间复杂度和内部实现、自己的类型怎么交给 std::sort、比较器写错会怎样、accumulate 的初值藏着什么坑、copy 到空容器为什么崩"。本文先总览分类,再从排序开始一路深入。


一、开场:标准库的算法有哪些?

❓ 介绍一下标准库的算法?

✅ 就两个头文件:<algorithm> 为主,<numeric> 补数值族 ,共十个分类。按常用程度从高到低报------排序最常用放首位,冷门的集合、堆、排列垫后:

类别 代表算法
排序 Sorting sort、stable_sort、partial_sort、nth_element
非修改序列 Non-modifying find、find_if、count_if、for_each、any_of、search
修改序列 Modifying copy、move、transform、remove、unique、replace、reverse
二分查找 Binary search lower_bound、upper_bound、binary_search、equal_range
分区 Partition partition、stable_partition、partition_point
最值 Min/Max max_element、minmax_element、clamp
集合 Set(有序) merge、set_union、set_intersection
堆 Heap make_heap、push_heap、pop_heap
排列 Permutations next_permutation、prev_permutation
数值 <numeric> accumulate、iota、inner_product、reduce

回答思路:先报分类、每类点两三个代表------面试官立刻知道你心里有地图;他多半会挑一类深入,最常挑的是排序。

这些算法共享同一个设计:只认迭代器区间 [first, last),不认容器 ------sort(v.begin(), v.end())sort(a, a + n) 通吃;反过来,迭代器类别不够就用不了:list 只有双向迭代器,std::sort 直接编译报错,得用成员 l.sort()


二、从排序深入:复杂度与内部实现

❓ std::sort 的时间复杂度是多少?

平均、最坏都是 O(n log n) 。内部不是纯快排,是内省排序(introsort)

  • 快排打底------平均最快;
  • 递归太深(快排退化成 O(n²) 的风险)→ 切堆排序------最坏情况有保障;
  • 区间小到一定程度 → 改插入排序------小区间常数开销最小。

三个算法各取所长,就是"内省"的含义。注意 sort 不稳定 ------相等元素可能换相对位置;要保序用 stable_sort(归并实现)。

同族还有两个场景特化:

算法 特点 场景
partial_sort 只要前 k 个有序,O(n log k) Top-K
nth_element 第 k 名站对位置即可,平均 O(n) 找中位数/第 k 大

三、自定义类型怎么用 std::sort?

❓ 我自己写的 struct 想排序,怎么写?

✅ 三种方式,按需选:

方式一:给类型重载 operator<------定义"天然顺序":

cpp 复制代码
struct P {
    string name;
    int score;
    bool operator<(const P& o) const {
        return score < o.score;
    }
};
sort(v.begin(), v.end());  // 直接排

方式二:调用时传比较器(lambda)------不动类型,最灵活:

cpp 复制代码
sort(v.begin(), v.end(),
     [](const P& a, const P& b) {
         return a.score > b.score;
     });

方式三:现成仿函数------简单场景一个词搞定:

cpp 复制代码
sort(v.begin(), v.end(),
     greater<int>{});   // 降序

多字段排序就是 if 链------先比第一关键字,相等再比下一个:

cpp 复制代码
[](const P& a, const P& b) {
    if (a.score != b.score)
        return a.score > b.score;
    return a.name < b.name;
}

⚠️ 比较器必须严格弱序 ------只能写 <,写 <=(相等时也返回 true)会让 sort 越界崩溃,未定义行为。这是自定义排序最高频的翻车点。
💡 方式一和方式二怎么选:operator< 表达"这个类型默认 怎么排"(语义上唯一、天然的顺序);lambda 表达"这一次想怎么排"的临时顺序。业务顺序常变的场景,别把每种顺序都塞进 operator<。


❓ std::find 是什么复杂度?什么时候不该用它?

find线性 O(n) ------从头到尾逐个 ==。数据无序时它就是唯一选择,但有序数据用它纯属浪费:

  • 有序 + 随机访问binary_search(存在性,bool)/ lower_bound(找位置),O(log n);
  • 条件查找find_if 配谓词,配 any_of / all_of / none_of / count_if 一家人。

⚠️ 最贵的事故:对关联容器用 std::findstd::find 把红黑树当线性表扫,O(n) 直接干掉人家 O(log n) 的优势:

cpp 复制代码
map<string, int> m;
auto it = m.find("k");
// ✅ 成员:红黑树 O(log n)
// std::find(m.begin(),
//     m.end(), "k");
// ❌ 自由:线性 O(n)

💡 规则:有成员版本的容器,永远优先成员版本(map/set/unordered/string 的 find、sort 等),自由算法留给序列容器。


五、搬运与整理:copy / transform / remove / unique

❓ copy 到空容器为什么崩?

✅ 因为 copy 假设目标区间已有足够空间,它只管往目标迭代器指的位置赋值------空容器一赋值就越界:

cpp 复制代码
vector<int> src{1, 2, 3}, dst;
// copy(src.begin(), src.end(),
//      dst.begin());   // ❌ 崩
copy(src.begin(), src.end(),
     back_inserter(dst));   // ✅

back_inserter 是插入迭代器:把"赋值"翻译成 push_back,边构造边长。目标大小不确定时一律用它。

transform 是"带变换的 copy",原地翻倍或搬运皆可:

cpp 复制代码
transform(v.begin(), v.end(),
          v.begin(),
          [](int x) { return x * 2; });

copy 的移动版 :元素大、源容器不再需要时,把 copy 换成 move 算法------逐元素走移动赋值,偷指针不复制内容:

cpp 复制代码
vector<string> src{/* 很大 */};
vector<string> dst(
    src.size());
move(src.begin(), src.end(),
     dst.begin());
// src 元素已变空壳

remove / unique 家族只搬不删(算法够不着容器的 size,真正删除要靠 erase 配合):

cpp 复制代码
// 排序 + 去重三件套
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()),
        v.end());
// unique 只合并"相邻"重复
// 所以必须先 sort

六、数值族:accumulate / iota / reduce

❓ accumulate 有什么坑?

accumulate<numeric>(不在 <algorithm>),核心规则一句话:初值决定累加类型

cpp 复制代码
vector<int> v{1, 2, 3};
int s1 = accumulate(
    v.begin(), v.end(), 0);   // int
// ⚠️ 初值 0 是 int:
// 大数求和会溢出
long long s2 = accumulate(
    v.begin(), v.end(), 0LL); // ✅
double avg = accumulate(
    v.begin(), v.end(), 0.0)
    / v.size();               // ✅

它还能做"通用折叠"------换个运算就是连乘、拼串:

cpp 复制代码
string joined = accumulate(
    words.begin(), words.end(),
    string{},
    [](a, b) { return a + b; });

同族:iota [C++11] 填充 0,1,2,...;reduce [C++17] 可并行版,不保证求和顺序------浮点加法不满足结合律,对精度敏感就老实用 accumulate。

找最大最小也别手写循环:标量比较用 max / min;区间找用 max_element / minmax_element------一次遍历同时拿最大和最小,返回的是迭代器(位置),要解引用取值:

cpp 复制代码
auto it = max_element(
    v.begin(), v.end());
int best = *it;   // 解引用取值
auto [lo, hi] = minmax_element(
    v.begin(), v.end());

七、面试高频追问

❓ Q1:stable_sort 和 sort 怎么选?

✅ 看相等元素要不要保持原相对顺序。先按 A 字段排、再按 B 字段排想让同 B 的保持 A 序,第二次必须 stable_sort;单次排序不关心相等元素的位次,用更快的 sort。

❓ Q2:partial_sort 和 nth_element 都能做 Top-K,怎么选?

✅ 要前 k 名排好序用 partial_sort(O(n log k));只要"第 k 大是多少"、不要求前 k 内部有序,nth_element 更快(平均 O(n))。

❓ Q3:为什么 remove 不真正删除元素?

✅ 算法手里只有迭代器,够不着容器的 size------缩短容器只能由容器自己(erase)完成。remove 只把要留的元素前移、返回新逻辑终点,删除交给 erase-remove 惯用法。

❓ Q4:for_each 和 range-for 怎么选?

✅ 单纯遍历用 range-for,意图直白;需要把"遍历逻辑"封装成可复用的一等公民(命名仿函数、多处复用、作为参数传递)时用 for_each。日常代码 range-for 占九成。

❓ Q5:算法能并行跑吗?

[C++17] 起多数算法加了执行策略重载:sort(std::execution::par, first, last) 由运行时多线程执行。代价是元素级并行------比较器、谓词必须是线程安全的纯函数。

❓ Q6:lower_bound 和 find 都能"找到位置",用哪个?

✅ 看数据是否有序:有序用 lower_bound(O(log n),返回第一个 ≥ 值的位置);无序只能 find(O(n))。给无序数据用 lower_bound 是逻辑错误------结果没有意义。

❓ Q7:max 和 max_element 有什么区别?

max 比较两个值 ,返回较大者(值语义,还能 max({a, b, c}) 比一串初始化列表);max_element区间 里扫描,返回最大元素的迭代器,要解引用取值、判空区间。一个是比两数,一个是扫区间。


八、总结速查表

考点 一句话结论
算法分类 排序/非修改/修改/二分/分区/最值/集合/堆/排列 + 数值
设计 只认迭代器区间,类别不够编译报错
sort 内省排序,最坏也 O(n log n),不稳定
自定义排序 operator< / lambda / 仿函数三选一
比较器 必须严格弱序,写 <= 是 UB
Top-K 有序要 partial_sort,无序 nth_element
成员 vs 自由 关联容器优先成员 find
copy 空容器 用 back_inserter
去重 sort + unique + erase
accumulate 初值决定类型,防溢出
reduce C++17 可并行,不保顺序

一句话回顾

回答"标准库算法有哪些"先亮分类地图(排序、查找、搬运、数值......每类点两个代表);深入排序时记三条------内省排序最坏也有 O(n log n)自定义类型靠 operator< 或 lambda比较器必须严格弱序;再往下:关联容器用成员 find、copy 空容器配 back_inserter、accumulate 初值决定类型。

如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们继续标准库篇,聊 std::function------它是怎么统一各种可调用物的,和 C 函数指针、虚函数回调相比强在哪,敬请关注 👋

相关推荐
黄敬峰1 小时前
TypeScript 工具类型一次讲透:Pick、Omit、Partial、Record 逐个击破
面试
m0_547486661 小时前
《数据结构与算法(Python语言版)》全套PPT课件2026
数据结构·算法
个 人 练 习 生2 小时前
数据结构入门:算法复杂度
开发语言·数据结构·经验分享·学习·程序人生·算法
一只叫煤球的猫2 小时前
Spring AI 2.0 源码解析(二):Starter 如何自动装配 ChatClient ?
后端·面试·langchain
白色冰激凌2 小时前
[SECS/GEM研究] (六)SML与数据序列化
c++·secs/gem
Ricardo-Yang2 小时前
无人机单目深度估计测试:ZipDepth 与 AerialMetric
人工智能·算法·机器学习·计算机视觉·无人机
_小柏_2 小时前
总结下最近面试出现的问题
c++·面试
Nil2082 小时前
golang解决单词转换
算法
我是慎独2 小时前
VulkanSceneGraph学习教程(十一)
c++·学习