C++刷 LeetCode Hot100 笔记(三)刷题常用的 STL 函数与语法补充:sort、reverse、max/min、auto、迭代器、lambda、字符串处理
前两篇我们已经把 C 语言转 C++ 刷题基础 和 Hot100 里最常用的 STL 容器 过了一遍,这一篇就继续往下接,开始整理 STL 常用算法、迭代器、排序和二分工具。
1. 这一篇主要学什么
前两篇你已经知道了:
-
数组题常用
vector -
字符串题常用
string -
哈希常用
unordered_map和unordered_set -
BFS 常用
queue -
栈题常用
stack
但真正开始刷题以后,你很快就会发现:
光有容器还不够,还得会“对容器动手”。
比如你经常会遇到这些操作:
-
给数组排序
-
给区间按左端点排序
-
找某个数在不在数组里
-
找第一个大于等于 target 的位置
-
数组去重
-
翻转字符串
-
找最大值最小值
这些操作,C++ STL 其实都给你准备好了。
所以这一篇,你要掌握的核心就是:
会用 STL 算法处理容器。
2. STL 算法到底是什么
你可以把 STL 分成两块理解:
2.1 容器
用来存数据。
比如:
vector<int> nums;
string s;
unordered_map<int, int> mp;
2.2 算法
用来处理这些数据。
比如:
sort(nums.begin(), nums.end());
reverse(nums.begin(), nums.end());
find(nums.begin(), nums.end(), 3);
你可以把它理解成:
-
容器 = 装东西的盒子
-
算法 = 操作盒子里东西的工具
刷题时,这两样是配套出现的。
3. begin() 和 end() 到底是什么
这个东西你刷题会天天见到。
比如:
sort(nums.begin(), nums.end());
很多 C 语言选手一开始看到这个会懵。
3.1 先不用死磕底层定义
前期你就把它记成一句话:
-
begin():起点 -
end():终点的后一个位置
也就是说:
nums.begin(), nums.end()
表示的就是:
整个 nums 这段范围
所以:
sort(nums.begin(), nums.end());
意思就是:
把 nums 整个数组排个序。
4. sort:刷题出场率极高的排序函数
4.1 最基本写法
vector<int> nums = {4, 2, 1, 3};
sort(nums.begin(), nums.end());
for (int x : nums) {
cout << x << " ";
}
cout << "\n";
输出:
1 2 3 4
默认是 从小到大排序。
4.2 降序排序
vector<int> nums = {4, 2, 1, 3};
sort(nums.begin(), nums.end(), greater<int>());
for (int x : nums) {
cout << x << " ";
}
cout << "\n";
输出:
4 3 2 1
记忆:
-
默认:升序
-
greater<int>():降序
4.3 sort 最常用在哪些题里
Hot100 里,排序特别常见。
常见场景:
-
合并区间
-
三数之和
-
排序数组
-
找重复元素
-
贪心题
-
二分查找前的预处理
-
双指针前的预处理
很多题你第一步就是:
sort(nums.begin(), nums.end());
4.4 pair 排序规则
如果你排序的是 pair<int, int>,默认规则是:
-
先比
first -
first相同,再比second
例子:
vector<pair<int, int>> vp = {{3, 2}, {1, 5}, {1, 2}, {2, 4}};
sort(vp.begin(), vp.end());
for (auto &p : vp) {
cout << p.first << " " << p.second << "\n";
}
输出:
1 2
1 5
2 4
3 2
这个规则在区间题、坐标题里很常用。
4.5 二维数组排序
Hot100 里还有一种很常见的情况:
vector<vector<int>> intervals;
比如区间题:
intervals = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
这时候直接排序:
sort(intervals.begin(), intervals.end());
默认会按每个小数组的字典序排序。
也就是说:
-
先比
intervals[i][0] -
一样再比
intervals[i][1]
这对很多区间题已经够用了。
5. 自定义排序:前期最该学会的进阶点
很多题不是简单升序就够了。
比如:
-
按区间左端点排序
-
按字符串长度排序
-
按某个二维数组的第二列排序
-
按 pair 的 second 排序
这时候就要用 自定义排序。
5.1 最常见写法:lambda
先直接看格式:
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
这个意思是:
如果你觉得 a 应该排在 b 前面,就返回 true。
上面这段代码就是按降序排序。
5.2 按 pair 的 second 排序
vector<pair<int, int>> vp = {{1, 3}, {2, 1}, {4, 2}};
sort(vp.begin(), vp.end(), [](pair<int, int>& a, pair<int, int>& b) {
return a.second < b.second;
});
排序后:
2 1
4 2
1 3
5.3 按二维数组第一列排序
vector<vector<int>> intervals = {{5, 6}, {1, 3}, {2, 4}};
sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b) {
return a[0] < b[0];
});
这就是区间题最经典的写法之一。
5.4 自定义排序你只需要先记住一句话
return a ??? b;
表示:
如果返回 true,就说明 a 应该放前面。
比如:
return a < b;
升序。
return a > b;
降序。
5.5 前期不用把 lambda 学很深
你现在只要会看、会抄、会改就够了。
先记住这几个高频模板。
模板 1:普通降序
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
模板 2:pair 按 second 升序
sort(vp.begin(), vp.end(), [](pair<int, int>& a, pair<int, int>& b) {
return a.second < b.second;
});
模板 3:区间按左端点升序
sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b) {
return a[0] < b[0];
});
先把这三个记住,够你用一阵子了。
6. reverse:翻转
这个也超级常见。
vector<int> nums = {1, 2, 3, 4, 5};
reverse(nums.begin(), nums.end());
for (int x : nums) {
cout << x << " ";
}
cout << "\n";
输出:
5 4 3 2 1
字符串也能翻转:
string s = "hello";
reverse(s.begin(), s.end());
cout << s << "\n";
输出:
olleh
6.1 reverse 常出现在哪
-
回文题
-
字符串翻转
-
数组翻转
-
双指针题
-
下一个排列相关题
7. swap:交换两个值
这个很简单,但特别常用。
int a = 10, b = 20;
swap(a, b);
cout << a << " " << b << "\n";
输出:
20 10
也可以配合数组下标:
vector<int> nums = {1, 2, 3};
swap(nums[0], nums[2]);
结果变成:
3 2 1
这个在双指针题里出现率很高。
8. max 和 min:取最大值最小值
8.1 两个数里取最大最小
int a = 3, b = 7;
cout << max(a, b) << "\n"; // 7
cout << min(a, b) << "\n"; // 3
8.2 三个数以上怎么办
你可以嵌套写:
int a = 3, b = 7, c = 5;
cout << max(a, max(b, c)) << "\n";
8.3 刷题里常用在哪
-
动态规划状态转移
-
区间合并
-
维护窗口最值
-
更新答案
比如:
ans = max(ans, cur);
这句你后面会看见无数次。
9. find:查找元素
9.1 在数组里找某个值
vector<int> nums = {1, 2, 3, 4, 5};
auto it = find(nums.begin(), nums.end(), 3);
if (it != nums.end()) {
cout << "找到了\n";
} else {
cout << "没找到\n";
}
解释:
-
找到就返回对应位置
-
找不到就返回
end()
所以固定判断方式就是:
if (find(nums.begin(), nums.end(), x) != nums.end())
9.2 在字符串里找字符
string s = "hello";
auto it = find(s.begin(), s.end(), 'e');
if (it != s.end()) {
cout << "找到了\n";
}
9.3 find 的缺点
它本质上是 顺序查找,时间复杂度是 O(n)。
所以:
-
数组不大,能用
-
只是偶尔查一次,能用
-
需要频繁查找,不如哈希表
也就是说:
看到“频繁查询是否存在”
优先想:
unordered_set
unordered_map
不要老想着 find()。
10. count:统计某个值出现几次
vector<int> nums = {1, 2, 2, 3, 2};
cout << count(nums.begin(), nums.end(), 2) << "\n";
输出:
3
字符串也可以:
string s = "abacaba";
cout << count(s.begin(), s.end(), 'a') << "\n";
输出:
4
10.1 count 的注意点
它也是 O(n)。
所以:
-
只统计一次,没问题
-
要频繁统计,还是哈希表更合适
比如字符统计题,一般还是:
unordered_map<char, int> cnt;
for (char c : s) cnt[c]++;
11. binary_search:判断一个数在不在有序数组里
这个函数名字就很直白:
二分查找
但它有一个非常重要的前提:
数组必须有序
11.1 用法
vector<int> nums = {1, 3, 5, 7, 9};
if (binary_search(nums.begin(), nums.end(), 5)) {
cout << "存在\n";
} else {
cout << "不存在\n";
}
返回值是 bool:
-
找到返回
true -
找不到返回
false
11.2 注意点
这个函数只能告诉你:
某个值在不在
但它不能直接告诉你:
-
第一个大于等于它的位置
-
第一个大于它的位置
-
它具体在哪个下标
这些更细的需求,要用:
-
lower_bound -
upper_bound
12. lower_bound:第一个大于等于 target 的位置
这个是 Hot100 和面试里特别重要的函数。
12.1 先记结论
lower_bound(begin, end, target)
返回的是:
第一个 >= target 的位置
12.2 例子
vector<int> nums = {1, 3, 3, 5, 7};
auto it = lower_bound(nums.begin(), nums.end(), 3);
cout << (it - nums.begin()) << "\n";
输出:
1
因为下标 1 的位置就是第一个 >= 3 的数。
再看一个:
vector<int> nums = {1, 3, 3, 5, 7};
auto it = lower_bound(nums.begin(), nums.end(), 4);
cout << (it - nums.begin()) << "\n";
输出:
3
因为下标 3 的值是 5,它是第一个 >= 4 的数。
12.3 为什么返回值可以减 begin()
因为它返回的是“位置”,你减掉起点,就能得到下标。
int pos = lower_bound(nums.begin(), nums.end(), target) - nums.begin();
这个写法非常常见。
12.4 它的典型用途
-
查找插入位置
-
二分答案
-
有序数组定位
-
查找左边界
-
去重后定位
-
LIS 最长递增子序列优化写法里也会出现
13. upper_bound:第一个大于 target 的位置
13.1 先记结论
upper_bound(begin, end, target)
返回的是:
第一个 > target 的位置
13.2 例子
vector<int> nums = {1, 3, 3, 5, 7};
auto it = upper_bound(nums.begin(), nums.end(), 3);
cout << (it - nums.begin()) << "\n";
输出:
3
因为下标 3 的 5,才是第一个 > 3 的数。
13.3 lower_bound 和 upper_bound 的区别
这个你一定要搞清楚。
lower_bound
第一个 >= target
upper_bound
第一个 > target
别看只差一个等号,刷题里区别非常大。
13.4 最好直接背例子
数组:
1 3 3 5 7
找 3:
-
lower_bound指向第一个 3 -
upper_bound指向 5
这个画面记住,后面就不容易混。
14. 二分查找最容易犯的错误
错误 1:忘了先排序
vector<int> nums = {5, 1, 3, 2};
lower_bound(nums.begin(), nums.end(), 3); // 错
因为 lower_bound、upper_bound、binary_search 都要求有序。
所以很多时候要先:
sort(nums.begin(), nums.end());
错误 2:把 it 当下标直接用
auto it = lower_bound(nums.begin(), nums.end(), 3);
cout << it << "\n"; // 不对
如果你想要下标,一般要写:
int pos = it - nums.begin();
错误 3:没判断是否越界
auto it = lower_bound(nums.begin(), nums.end(), target);
cout << *it << "\n";
如果 target 比所有数都大,it 可能等于 nums.end(),这时候不能直接解引用。
稳一点的写法:
auto it = lower_bound(nums.begin(), nums.end(), target);
if (it != nums.end()) {
cout << *it << "\n";
}
15. unique:去掉相邻重复元素
这个函数特别容易被初学者误解。
15.1 它不是“直接删掉重复元素”
它做的事情其实是:
把相邻重复元素挪到后面去,然后返回新的结尾位置。
所以它一般要和 erase() 配合使用。
15.2 正确去重写法
vector<int> nums = {1, 1, 2, 2, 3, 3, 3};
nums.erase(unique(nums.begin(), nums.end()), nums.end());
for (int x : nums) {
cout << x << " ";
}
cout << "\n";
输出:
1 2 3
15.3 一个非常关键的点
unique() 只能去掉 相邻重复。
所以如果数组本来不是有序的,通常要先排序。
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
这个组合很经典。
15.4 这个组合经常在哪出现
-
数组去重
-
区间预处理
-
离散化
-
一些搜索题的剪枝预处理
16. erase:删除元素
16.1 删除某个位置
vector<int> nums = {1, 2, 3, 4};
nums.erase(nums.begin() + 1);
结果变成:
1 3 4
16.2 删除一段区间
vector<int> nums = {1, 2, 3, 4, 5};
nums.erase(nums.begin() + 1, nums.begin() + 4);
删除的是:
2 3 4
剩下:
1 5
16.3 erase 的注意点
vector 中间删除元素,后面的元素要往前挪,所以不是 O(1)。
也就是说:
-
尾删快
-
中间删不快
这点后面学双指针时你会更有感觉。
17. 迭代器你目前要掌握到什么程度
“迭代器”这个词听起来很唬人,其实你现阶段真不用学太复杂。
前期你只要做到:
17.1 知道这几个返回的往往是迭代器
-
find -
lower_bound -
upper_bound
17.2 会写这两种判断
判断有没有找到:
if (it != nums.end())
转成下标:
int pos = it - nums.begin();
先会这两个就够了。
18. 常见刷题模板
18.1 排序模板
sort(nums.begin(), nums.end());
18.2 降序模板
sort(nums.begin(), nums.end(), greater<int>());
18.3 自定义排序模板
sort(v.begin(), v.end(), [](auto& a, auto& b) {
return a[0] < b[0];
});
前期你也可以写得更明确一点:
sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b) {
return a[0] < b[0];
});
18.4 反转模板
reverse(nums.begin(), nums.end());
18.5 二分查找是否存在
if (binary_search(nums.begin(), nums.end(), target)) {
}
18.6 找第一个大于等于 target 的位置
int pos = lower_bound(nums.begin(), nums.end(), target) - nums.begin();
18.7 找第一个大于 target 的位置
int pos = upper_bound(nums.begin(), nums.end(), target) - nums.begin();
18.8 排序后去重模板
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
这个模板直接背下来。
19. Hot100 里这些函数对应什么思路
这部分你要慢慢建立“题目 -> 工具”的反应。
19.1 看到“先排序再处理”
先想:
sort()
典型:
-
三数之和
-
合并区间
-
组合类去重
-
贪心
19.2 看到“反转字符串 / 反转数组”
先想:
reverse()
19.3 看到“有序数组里查某个值”
先想:
binary_search()
19.4 看到“找左边界 / 插入位置”
先想:
lower_bound()
19.5 看到“找右边界”
先想:
upper_bound()
19.6 看到“排序后去重”
先想:
sort + unique + erase
20. 几个很容易混的点
20.1 find() 和 unordered_set::find() 不一样
这个初学者很容易搞混。
普通 find()
find(nums.begin(), nums.end(), x)
本质上是顺序查找,O(n)。
哈希表里的 find()
st.find(x)
mp.find(x)
平均是 O(1)。
所以刷题时要有这个意识:
-
普通容器里找:可能慢
-
哈希表里找:更快
20.2 sort() 和 stable_sort()
前期你几乎都用 sort() 就够了。
stable_sort() 是稳定排序,进阶再说。
前期不要给自己加负担。
20.3 lower_bound() 不是“找等于”
它找的是:
第一个大于等于
这个一定要背熟。
21. 现阶段你必须掌握到什么程度
你现在不需要把 STL 算法全部学完。
但这一篇学完,至少要做到:
必会 1:sort
-
会升序
-
会降序
-
会给 pair / 二维数组排序
必会 2:reverse
-
会翻转数组
-
会翻转字符串
必会 3:max / min
-
会更新答案
-
会比较两个值
必会 4:binary_search
-
知道只能用于有序数组
-
会判断元素在不在
必会 5:lower_bound / upper_bound
-
知道返回的是位置
-
会转下标
-
知道一个是
>= -
一个是
>
必会 6:sort + unique + erase
-
会数组去重
这几项会了,你刷 Hot100 会顺很多。
22. 给自己背的口诀
可以直接记:
排序用 sort
翻转用 reverse
找左边界用 lower_bound
找右边界用 upper_bound
排序后去重用 unique + erase
再记一句:
二分函数只能用在有序数组上
再记一句:
lower_bound 是大于等于,upper_bound 是严格大于
23. 本篇总结
这一篇你最该建立起来的感觉就是:
刷题不是所有东西都自己手写,很多操作 STL 已经给你做好了。
你现在最常用的几个 STL 算法就是:
-
sort -
reverse -
swap -
max -
min -
find -
binary_search -
lower_bound -
upper_bound -
unique -
erase
其中最核心的又是这几个:
-
sort -
lower_bound -
upper_bound -
sort + unique + erase
这几个东西,在 Hot100 里会反复见到。
你现在先别急着追求“全懂底层原理”,先做到:
看得懂、会改、会套、会用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)