22、STL算法:排序算法、查找算法、数值算法、修改序列算法、非修改序列算法

STL算法,说白了就是C++标准库给你准备好的一整套"操作数据的工具包"。我个人觉得,这是STL里最值钱的部分。你想想看,不用自己手写冒泡排序、不用自己实现二分查找,直接调函数就完事了。我刚开始学的时候,总觉得这些算法可有可无,直到有一次项目里要处理几十万条数据,自己写的排序跑了一分多钟,换成std::sort后秒出结果……嗯,从那以后我再也不敢小看STL算法了。

核心要点:STL算法分为五大类——排序、查找、数值、修改序列、非修改序列。掌握它们,你的C++水平直接上一个台阶。

22.1 排序算法

排序是日常开发里最常用的操作。STL提供了好几种排序算法,各有各的适用场景。

算法 功能 时间复杂度 我常用的场景
sort 快速排序(平均情况) O(n log n) 绝大多数排序需求
stable_sort 稳定排序 O(n log n) 需要保持相等元素原始顺序时
partial_sort 部分排序 O(n log k) 只关心前几名时
nth_element 第n大元素 O(n) 找中位数或分位数
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> data = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    
    // 完整排序
    std::sort(data.begin(), data.end());
    
    // 稳定排序
    std::stable_sort(data.begin(), data.end());
    
    // 只排前3个
    std::partial_sort(data.begin(), data.begin() + 3, data.end());
    
    // 找第5大的元素
    std::nth_element(data.begin(), data.begin() + 4, data.end());
    std::cout << "第5大的元素是: " << data[4] << std::endl;
    
    return 0;
}

我的经验:项目中90%的情况用std::sort就够了。但如果你要对一个已经基本有序的数组排序,std::stable_sort有时反而更快。我曾经在日志排序时遇到过这种情况,换成稳定排序后性能提升了30%。

22.2 查找算法

查找算法帮你快速定位数据。STL里最常用的就是二分查找和线性查找。

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> sorted = {1, 3, 5, 7, 9, 11, 13};
    
    // 二分查找(要求有序)
    bool found = std::binary_search(sorted.begin(), sorted.end(), 7);
    
    // 返回第一个不小于7的位置
    auto it = std::lower_bound(sorted.begin(), sorted.end(), 7);
    
    // 返回第一个大于7的位置
    auto it2 = std::upper_bound(sorted.begin(), sorted.end(), 7);
    
    // 返回所有等于7的范围
    auto range = std::equal_range(sorted.begin(), sorted.end(), 7);
    
    return 0;
}

注意:binary_searchlower_boundupper_bound都要求数据是有序的。我曾经在项目里忘了排序就直接用binary_search,结果查了半天找不到数据,排查了好久才发现是顺序问题。嗯,这个坑我替你们踩过了。

22.3 数值算法

数值算法在<numeric>头文件里,专门处理数学计算。说实话,以前我都是自己写循环累加,后来才发现STL早就准备好了。

#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
    
    // 累加
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    
    // 内积
    std::vector<int> a = {1, 2, 3};
    std::vector<int> b = {4, 5, 6};
    int dot = std::inner_product(a.begin(), a.end(), b.begin(), 0);
    
    // 部分和
    std::vector<int> partial(nums.size());
    std::partial_sum(nums.begin(), nums.end(), partial.begin());
    
    // 相邻差
    std::vector<int> diff(nums.size());
    std::adjacent_difference(nums.begin(), nums.end(), diff.begin());
    
    return 0;
}

避坑指南:accumulate时,第三个参数的类型决定了返回类型。如果你写0,返回int;如果写0.0,返回double。我有个同事因为没注意这个,算浮点数累加时精度丢得一塌糊涂。

22.4 修改序列算法

这类算法会改变容器里的内容。最常用的有copytransformreplacefill等。

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst(5);
    
    // 复制
    std::copy(src.begin(), src.end(), dst.begin());
    
    // 变换(每个元素乘以2)
    std::transform(src.begin(), src.end(), src.begin(),
                   [](int x) { return x * 2; });
    
    // 替换所有3为99
    std::replace(src.begin(), src.end(), 3, 99);
    
    // 填充
    std::fill(dst.begin(), dst.end(), 0);
    
    return 0;
}

重点:transform是我个人最喜欢的算法之一。它可以把一个函数应用到每个元素上,生成新的序列。我在做图像处理时,经常用它来做像素级别的变换,比手写循环简洁多了。

22.5 非修改序列算法

这类算法只读不写,不会改变容器内容。包括findcountfor_eachall_of等。

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 3, 6, 7};
    
    // 查找第一个3
    auto it = std::find(data.begin(), data.end(), 3);
    
    // 统计3出现的次数
    int cnt = std::count(data.begin(), data.end(), 3);
    
    // 遍历打印
    std::for_each(data.begin(), data.end(),
                  [](int x) { std::cout << x << " "; });
    
    // 检查是否所有元素都大于0
    bool all_positive = std::all_of(data.begin(), data.end(),
                                    [](int x) { return x > 0; });
    
    return 0;
}

我的习惯:检查容器是否包含某个元素时,我一般用std::find而不是自己写循环。代码更短,而且可读性更好。你想想看,if (std::find(v.begin(), v.end(), x) != v.end()) 一眼就能看出是在查找,比手写循环清晰多了。

22.6 知识体系总览

下面这张图帮你理清STL算法的整体脉络。我建议你把它存下来,写代码时对照着看。

STL算法 排序算法 sort, stable_sort partial_sort nth_element 查找算法 binary_search lower_bound equal_range 数值算法 accumulate inner_product partial_sum 修改序列 copy, transform replace, fill remove, unique 非修改序列 find, count for_each all_of, any_of STL算法五大分类:排序、查找、数值、修改序列、非修改序列 每个分类下都有多个实用算法,按需选用

22.7 实战建议

学完这些算法,我建议你记住一个原则:能用STL算法就别自己写循环。原因有三:

  • 性能有保障——STL算法的实现经过高度优化,比自己手写的循环快得多
  • 代码更简洁——一行算法调用顶得上十几行手写代码
  • 可读性更好——std::sort一眼就知道是排序,而手写快排还得看半天

最后提醒:使用算法前一定要确认迭代器范围是否正确。我曾经在项目里传错了end迭代器,结果算法跑到了别的容器里,数据全乱了。调试了一下午才发现是迭代器越界。嗯,迭代器范围检查,写代码时多看一眼,能省你半天时间。

公众号:蓝海资料掘金营,微信deep3321