前两篇我们已经把 C 语言转 C++ 刷题基础Hot100 里最常用的 STL 容器 过了一遍,这一篇就继续往下接,开始整理 STL 常用算法、迭代器、排序和二分工具

1. 这一篇主要学什么

前两篇你已经知道了:

  • 数组题常用 vector

  • 字符串题常用 string

  • 哈希常用 unordered_mapunordered_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_boundupper_boundbinary_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 里会反复见到。

你现在先别急着追求“全懂底层原理”,先做到:

看得懂、会改、会套、会用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐