C++标准库排序与二分查找:算法基石与高效应用指南
2026/7/12 7:24:56 网站建设 项目流程

1. 项目概述:为什么C++标准库的排序与二分是算法基石

如果你正在学习C++,或者准备面试,那么“排序”和“二分查找”这两个词你一定不陌生。它们几乎是所有算法面试题的常客,也是实际开发中处理数据时最基础、最高频的操作。但很多朋友,尤其是初学者,常常陷入一个误区:花大量时间去手写冒泡排序、快速排序,或者小心翼翼地实现一个二分查找的循环,却忽略了C++标准库(STL)中已经为我们封装好的、经过千锤百炼的现成工具。这就像放着工厂里精密的电动螺丝刀不用,非要自己手工磨一把起子,精神可嘉,但效率堪忧。

这个教程的核心,就是想和你聊聊C++标准库里的sort和二分查找相关函数(lower_bound,upper_bound,binary_search等)。它们不是“黑盒子”,而是你应该像熟悉coutcin一样去熟悉的利器。掌握它们,你就能用几行代码解决过去需要几十行、并且容易出错的逻辑。更重要的是,理解它们的使用场景、边界条件和内部约定,是你写出高效、正确C++程序的关键一步。无论是处理一个简单的成绩单排名,还是在游戏里快速检索道具信息,抑或是解决LeetCode上那些复杂的算法题,这套工具组合拳都能让你事半功倍。

2. 排序函数std::sort的深度解析与实战

std::sort是C++<algorithm>头文件里的明星函数,它基于IntroSort(内省排序,混合了快速排序、堆排序和插入排序),在绝大多数情况下都能提供 O(N log N) 的优秀性能,并且是原地排序,不需要额外空间。

2.1 基础用法:让数据瞬间有序

最基本的用法就是对容器进行默认的升序排序。它接受两个迭代器,表示要排序的范围[first, last)

#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> nums = {5, 2, 8, 1, 9, 3}; // 默认升序排序 std::sort(nums.begin(), nums.end()); for (int num : nums) { std::cout << num << " "; } // 输出: 1 2 3 5 8 9 return 0; }

对于数组,用法同样简单:

int arr[] = {5, 2, 8, 1, 9, 3}; int n = sizeof(arr) / sizeof(arr[0]); std::sort(arr, arr + n); // 排序整个数组

注意std::sort要求迭代器是随机访问迭代器(Random Access Iterator)。这意味着std::vector,std::deque, 普通数组等可以完美使用,但std::liststd::forward_list不行,因为它们只提供双向或前向迭代器。对于链表,应使用其自带的sort()成员函数,如myList.sort()

2.2 自定义排序规则:掌控排序逻辑

std::sort的强大之处在于其第三个参数——一个比较函数(或函数对象、Lambda表达式)。这个函数决定了元素之间“小于”的关系。

1. 使用函数指针或函数对象:假设我们有一个Person结构体,想按年龄降序排序。

struct Person { std::string name; int age; }; bool compareByAgeDesc(const Person& a, const Person& b) { return a.age > b.age; // 降序:年龄大的“小于”年龄小的? } // 理解关键:sort根据比较函数返回的true来排列。如果a.age > b.age为true,意味着在排序视角下,a应该排在b前面。 int main() { std::vector<Person> people = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}}; std::sort(people.begin(), people.end(), compareByAgeDesc); // 排序后:Charlie(30), Alice(25), Bob(20) }

2. 使用Lambda表达式(现代C++推荐):Lambda让代码更紧凑,尤其适合一次性使用的比较逻辑。

std::vector<Person> people = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}}; // 按姓名升序排序 std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.name < b.name; }); // 按年龄升序,如果年龄相同则按姓名降序 std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { if (a.age != b.age) return a.age < b.age; return a.name > b.name; // 姓名降序 });

3. 使用标准库函数对象:对于简单的升降序,可以直接用std::greater<>std::less<>

#include <functional> // 需要包含此头文件以使用 greater, less std::vector<int> nums = {5, 2, 8, 1, 9, 3}; // 降序排序 std::sort(nums.begin(), nums.end(), std::greater<int>()); // 输出: 9 8 5 3 2 1

2.3 核心原理与避坑指南

比较函数的严格弱序要求:这是使用自定义排序时最容易出错的地方。比较函数comp(a, b)必须满足严格弱序,即:

  1. 非自反性comp(a, a)必须为false
  2. 非对称性:如果comp(a, b)true,则comp(b, a)必须为false
  3. 可传递性:如果comp(a, b)truecomp(b, c)true,则comp(a, c)必须为true
  4. 等价的可传递性:如果!comp(a, b) && !comp(b, a)(即a和b“等价”),并且!comp(b, c) && !comp(c, b),那么!comp(a, c) && !comp(c, a)也必须成立。

违反这些规则(例如,在比较函数中写return a.age <= b.age;),会导致未定义行为,程序可能崩溃或产生错误结果。黄金法则:在比较函数中,只使用<>进行明确的大小判断,避免使用<=>=

排序稳定性:std::sort不保证是稳定排序。稳定排序意味着相等元素的相对顺序在排序后保持不变。如果你需要稳定排序,请使用std::stable_sort,它的接口和sort完全一样,但通常稍慢一些。

std::vector<std::pair<int, char>> data = {{1, 'a'}, {2, 'b'}, {1, 'c'}, {3, 'd'}}; // 按pair的第一个元素(int)排序 std::stable_sort(data.begin(), data.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); // 排序后,{1, 'a'} 和 {1, 'c'} 的相对顺序保持不变。

性能与选择:

  • 对于小型数据集(例如,元素数量少于几十个),std::sort内部可能会切换到插入排序,因为对于几乎有序或非常小的序列,插入排序的常数因子更小。
  • 如果序列已经基本有序,考虑使用std::inplace_merge或直接使用std::stable_sort,后者对部分有序序列可能更友好。
  • 只需要对序列的前N个元素排序?使用std::partial_sort。它会把最小的N个元素放到序列开头并排好序,其余元素顺序未定义但都在它们之后,时间复杂度接近 O(N log K),其中K是你需要的部分大小。

3. 二分查找函数族:在有序世界中快速定位

二分查找的前提是序列必须已经按照你将要使用的比较规则排好序。如果对未排序的序列使用二分查找,结果是未定义的。C++提供了多个二分查找函数,各有侧重。

3.1std::binary_search:最简单的存在性检查

这个函数只回答一个问题:“值存不存在?” 它返回一个bool

#include <algorithm> #include <vector> #include <iostream> int main() { std::vector<int> vec = {1, 3, 5, 7, 9}; bool found = std::binary_search(vec.begin(), vec.end(), 5); std::cout << std::boolalpha << found << std::endl; // 输出: true found = std::binary_search(vec.begin(), vec.end(), 6); std::cout << found << std::endl; // 输出: false return 0; }

它很简单,但信息量也最少。你只知道有没有,不知道在哪里,也不知道如果有多个相同元素,找到的是哪一个。

3.2std::lower_boundstd::upper_bound:获取精确位置

这两个函数是二分查找的精髓,它们返回的是迭代器(位置)。

  • std::lower_bound(first, last, value):返回第一个不小于(即大于或等于)value的元素的迭代器。如果所有元素都小于value,则返回last
  • std::upper_bound(first, last, value):返回第一个大于value的元素的迭代器。如果所有元素都不大于value,则返回last

理解它们最直观的方式是想象将value插入到这个有序序列中,lower_bound告诉你它应该被插入的起始位置(保持有序),upper_bound则告诉你它应该被插入的结束位置之后。

std::vector<int> vec = {1, 2, 2, 3, 4, 4, 4, 5}; auto low = std::lower_bound(vec.begin(), vec.end(), 4); auto up = std::upper_bound(vec.begin(), vec.end(), 4); // low 指向第一个4(索引4),up指向第一个5(索引7) std::cout << "lower_bound at position: " << (low - vec.begin()) << std::endl; // 输出 4 std::cout << "upper_bound at position: " << (up - vec.begin()) << std::endl; // 输出 7 // 那么,序列中值为4的元素范围就是 [low, up) std::cout << "Number of 4s: " << (up - low) << std::endl; // 输出 3

一个极其常见的用法:在有序数组中查找插入位置。比如,你要维护一个动态的有序列表,每次新增一个元素。

std::vector<int> sorted_list = {10, 20, 30, 40}; int new_value = 25; // 找到插入位置,保持升序 auto insert_pos = std::lower_bound(sorted_list.begin(), sorted_list.end(), new_value); sorted_list.insert(insert_pos, new_value); // 现在 sorted_list 是 {10, 20, 25, 30, 40}

3.3std::equal_range:一键获取相等范围

如果你既需要lower_bound又需要upper_bound,那就用std::equal_range。它返回一个pair,包含lower_boundupper_bound的结果。

std::vector<int> vec = {1, 2, 2, 3, 4, 4, 4, 5}; auto range = std::equal_range(vec.begin(), vec.end(), 4); // range.first 等同于 lower_bound 的结果 // range.second 等同于 upper_bound 的结果 std::cout << "Range of 4: [" << (range.first - vec.begin()) << ", " << (range.second - vec.begin()) << ")" << std::endl; // 输出: Range of 4: [4, 7)

3.4 自定义比较规则的二分查找

sort一样,二分查找函数也可以接受一个自定义的比较函数或对象。关键点:你传入的比较规则,必须和序列排序时所使用的规则一致!否则结果错误。

std::vector<Person> people = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}}; // 假设我们已按年龄升序排序 std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.age < b.age; }); // 现在要查找年龄为25的人 Person target{"", 25}; auto it = std::lower_bound(people.begin(), people.end(), target, [](const Person& a, const Person& b) { return a.age < b.age; }); // 注意:比较函数只用了age成员,与排序规则一致。 if (it != people.end() && it->age == target.age) { std::cout << "Found: " << it->name << std::endl; }

4. 排序与二分查找的联合实战:解决经典问题

理论说再多,不如动手练。我们来看几个结合排序和二分的典型应用场景。

4.1 案例一:统计分数段人数

假设你有一组学生的考试成绩(0-100),需要快速统计出落在[60, 80]这个分数区间内的人数。

思路:先排序。然后,分数>=60的第一个位置是lower_bound(60),分数>80的第一个位置是upper_bound(80)。区间内的人数就是这两个位置之差。

std::vector<int> scores = {85, 92, 76, 60, 55, 88, 72, 61, 59, 80, 90, 65}; std::sort(scores.begin(), scores.end()); // 升序排序 int lower_score = 60; int upper_score = 80; // 注意:要找 >=60 和 >80 的位置 auto low_it = std::lower_bound(scores.begin(), scores.end(), lower_score); auto up_it = std::upper_bound(scores.begin(), scores.end(), upper_score); int count_in_range = std::distance(low_it, up_it); // 或者 up_it - low_it std::cout << "Number of scores in [60, 80]: " << count_in_range << std::endl;

实操心得:这里最容易混淆的是边界。lower_bound(60)找的是第一个>=60的,upper_bound(80)找的是第一个>80的。所以[low_it, up_it)这个左闭右开区间,正好对应数学上的[60, 80]闭区间。如果你想找(60, 80],就应该用upper_bound(60)upper_bound(80)

4.2 案例二:在二维空间或自定义结构中的查找

问题:有一系列带有时间戳的事件{timestamp, event_id},已按时间戳升序排序。现在给定一个时间点T,找到时间戳不晚于T的最后一个事件。

思路:这其实是upper_bound的变种。upper_bound(T)返回第一个时间戳大于T的事件。那么它前面的那个事件,就是最后一个时间戳小于等于T的事件。

struct Event { long long timestamp; int event_id; }; std::vector<Event> events = {{1000, 1}, {2000, 2}, {2000, 3}, {3000, 4}, {4000, 5}}; // 假设已按 timestamp 升序排序 long long query_time = 2000; // 使用 upper_bound 找到第一个 timestamp > query_time 的事件 auto it = std::upper_bound(events.begin(), events.end(), query_time, [](long long t, const Event& e) { // 注意参数顺序!这是 upper_bound 的重载形式。 // 比较的是:查询值t 是否 < 元素e.timestamp return t < e.timestamp; }); // it 指向 timestamp=3000 的事件 if (it != events.begin()) { auto last_event_before_or_at_T = *(it - 1); std::cout << "Last event at or before " << query_time << ": ID=" << last_event_before_or_at_T.event_id << ", TS=" << last_event_before_or_at_T.timestamp << std::endl; } else { std::cout << "No event at or before " << query_time << std::endl; }

注意事项:这里使用了upper_bound的一个重载版本,它接受一个将元素进行比较的函数。其语义是:寻找第一个使得comp(value, element)false的元素。对于升序序列,comp通常是<。所以upper_bound(value)等价于寻找第一个使得value < elementfalse的元素,即第一个element >= value?不对,仔细想:我们要找的是第一个element > value的。这个重载的写法[](value, element){ return value < element; },当value < elementtrue时,说明value小于当前element,循环还会继续向左?实际上,标准库的实现保证了它返回的是第一个使得比较条件为false的位置。对于upper_bound,这个条件就是value < element。所以它返回的是第一个value < elementfalse的位置,也就是第一个element <= valuefalse的位置,即第一个element > value的位置。没错。这个参数顺序容易搞反,务必小心。一个更稳妥的记忆方法是:当比较函数用于二分查找时,它应该和排序时的比较函数具有相同的“序”。如果你排序用的是[](const Event& a, const Event& b){ return a.timestamp < b.timestamp; },那么二分查找时,如果直接传Event对象,比较函数签名应为bool comp(const Event& a, const Event& b)。如果像上面那样用值来比较,签名是bool comp(const T& value, const Event& elem),并且逻辑要与排序一致(即判断value是否应该排在elem前面)。

4.3 案例三:实现“二分答案”算法框架

“二分答案”是算法竞赛和面试中一种非常重要的技巧,用于求解满足某种条件的最大/最小值问题。其核心就是利用二分查找来猜测答案,并用一个检验函数来验证猜测是否可行。

问题模板:存在一个单调的可行解序列,我们需要找到其中满足条件的边界值。

框架代码

// 假设我们有一个检验函数 check(mid),当答案候选值为 mid 时,返回 true 表示可行,false 表示不可行。 // 我们在 [left, right] 区间内寻找最大的可行解。 int binary_search_answer(int left, int right) { int ans = left; // 记录答案 while (left <= right) { int mid = left + (right - left) / 2; // 防止溢出 if (check(mid)) { // mid 可行,说明答案至少是 mid,尝试更大的值 ans = mid; left = mid + 1; } else { // mid 不可行,答案应该更小 right = mid - 1; } } return ans; } // 寻找最小的可行解(例如,第一个满足条件的值) int binary_search_answer_min(int left, int right) { int ans = -1; // 先用-1表示未找到 while (left <= right) { int mid = left + (right - left) / 2; if (check(mid)) { // mid 可行,可能还有更小的可行解,向左收缩 ans = mid; right = mid - 1; } else { // mid 不可行,答案应该更大 left = mid + 1; } } return ans; }

实例:在有序数组中查找目标值的插入位置(LeetCode 35)这其实就是lower_bound的功能,但我们可以用二分答案的思想手写一遍。

int searchInsert(std::vector<int>& nums, int target) { int left = 0, right = nums.size() - 1; int ans = nums.size(); // 默认插入位置在末尾 while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] >= target) { // mid位置的值 >= target,说明插入位置可能在mid或左边 ans = mid; right = mid - 1; // 向左搜索更小的可能位置 } else { // nums[mid] < target,插入位置一定在右边 left = mid + 1; } } return ans; } // 这个函数返回的就是 lower_bound 的结果。

5. 进阶技巧、性能考量与常见陷阱

当你熟练使用这些函数后,可以关注一些进阶用法和性能细节。

5.1 与特定容器的协同

std::set/map的成员函数:std::setstd::map本身是有序的,它们提供了自己的lower_bound,upper_bound,equal_range成员函数。务必使用成员函数版本,而不是<algorithm>里的全局函数。因为成员函数利用红黑树的结构,时间复杂度是 O(log N),而全局函数接受迭代器,对于set/map的非随机访问迭代器,复杂度会退化为 O(N)。

std::set<int> mySet = {1, 3, 5, 7, 9}; auto it = mySet.lower_bound(4); // 正确:使用成员函数,O(log N) // auto it = std::lower_bound(mySet.begin(), mySet.end(), 4); // 错误:全局函数,O(N)

std::vector的排序优化:如果需要对一个std::vector频繁进行“插入少量数据-排序-查找”的操作,可以考虑使用std::lower_bound找到插入位置后使用vector::insert,但这在中间插入是 O(N) 操作。如果插入非常频繁,可能std::setstd::multiset是更好的选择,尽管其元素访问不如vector连续。

5.2 性能对比与选择

  • std::sort: 平均 O(N log N),最坏情况 O(N log N)(IntroSort保证)。对于内置类型(如int,double),通常非常快,因为它可能使用特化版本的排序。
  • std::stable_sort: 也是 O(N log N),但常数因子更大,因为它需要额外空间来保证稳定性。只有在需要保持相等元素原始顺序时才使用它。
  • 二分查找族: O(log N)。这是它们最大的优势。一旦数据排好序,多次查找的成本极低。

一个经验法则:如果你的数据是静态的(一次性构建,多次查询),那么先排序再二分查找是黄金组合。如果你的数据是动态的(频繁插入删除),那么使用std::set,std::map,std::multiset,std::multimap这类有序关联容器可能更合适,它们能自动维护顺序,插入删除和查找都是 O(log N)。

5.3 你必须避开的陷阱

  1. 未排序使用二分查找:这是最致命的错误。对未排序序列使用binary_search,lower_bound等,结果是未定义的,可能返回错误结果而不报错。

    std::vector<int> vec = {9, 3, 5, 1, 7}; bool wrong = std::binary_search(vec.begin(), vec.end(), 5); // 未定义行为!
  2. 比较函数不一致:排序用的比较规则是A,二分查找用的比较规则是B,且AB定义的序不同。这会导致二分查找在错误的“有序”假设下工作,结果自然错误。

  3. 迭代器失效:在vectordeque上使用sort或二分查找不会导致迭代器失效(因为它们不改变容器结构)。但是,如果你在查找之后,基于返回的迭代器进行了插入或删除操作,那么之后再次使用旧的迭代器就是危险的。

  4. 浮点数的比较:对浮点数(float, double)进行排序和二分查找要格外小心。由于精度问题,两个数学上相等的浮点数在计算机中可能略有差异。在自定义比较函数时,避免直接使用==,而应考虑使用一个误差范围(epsilon)。

    bool compareDouble(double a, double b) { const double eps = 1e-9; return a < b - eps; // 只有当a明显小于b时,才认为a<b } // 排序和二分查找都使用这个 compareDouble 函数
  5. lower_boundupper_bound的返回值判断:拿到返回的迭代器it后,不要直接解引用。必须先判断it != container.end(),否则解引用end()迭代器是未定义行为。

    auto it = std::lower_bound(vec.begin(), vec.end(), value); if (it != vec.end() && *it == value) { // 找到了确切的值 } else { // 没找到,it指向第一个>=value的位置,可能是插入点 }

6. 从理论到实践:一个综合应用示例

让我们设计一个简单的“学生成绩管理系统”中的核心查询功能,综合运用所学。

需求:有一批学生记录(student_id, name, score)。我们需要支持:

  1. 按成绩从高到低排序并输出。
  2. 查询某个分数段[low, high]内的所有学生。
  3. 快速查找某个学号的学生(假设学号在录入后不变,但记录未按学号排序)。
#include <iostream> #include <vector> #include <algorithm> #include <string> struct Student { int id; std::string name; int score; }; class GradeManager { private: std::vector<Student> students; // 维护一个按id排序的索引,用于按id快速查找 std::vector<const Student*> id_index; void buildIdIndex() { id_index.clear(); for (const auto& stu : students) { id_index.push_back(&stu); } std::sort(id_index.begin(), id_index.end(), [](const Student* a, const Student* b) { return a->id < b->id; }); } public: void addStudent(int id, const std::string& name, int score) { students.push_back({id, name, score}); // 添加后重建索引(简单实现,频繁添加时效率低,可优化) buildIdIndex(); } // 1. 按成绩降序输出 void printByScoreDesc() { // 注意:这里排序会改变students原始顺序。如果不希望改变,可以排序一个副本或索引。 std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) { return a.score > b.score; }); std::cout << "Rank by score (Descending):\n"; for (const auto& stu : students) { std::cout << "ID: " << stu.id << ", Name: " << stu.name << ", Score: " << stu.score << std::endl; } std::cout << "----------\n"; } // 2. 查询分数段内的学生 std::vector<Student> queryByScoreRange(int low, int high) { // 先按成绩升序排序(如果之前不是这个顺序) std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) { return a.score < b.score; }); // 找到第一个分数 >= low 的学生 auto low_it = std::lower_bound(students.begin(), students.end(), low, [](const Student& stu, int val) { return stu.score < val; }); // 找到第一个分数 > high 的学生 auto high_it = std::upper_bound(students.begin(), students.end(), high, [](int val, const Student& stu) { return val < stu.score; }); return std::vector<Student>(low_it, high_it); } // 3. 按学号查找学生(使用二分查找在索引中) const Student* findById(int id) { // 在 id_index (已按id排序的指针数组) 中二分查找 auto comp = [](const Student* ptr, int val) { return ptr->id < val; }; auto it = std::lower_bound(id_index.begin(), id_index.end(), id, comp); if (it != id_index.end() && (*it)->id == id) { return *it; // 找到 } return nullptr; // 未找到 } }; int main() { GradeManager mgr; mgr.addStudent(1001, "Alice", 85); mgr.addStudent(1002, "Bob", 92); mgr.addStudent(1003, "Charlie", 76); mgr.addStudent(1004, "David", 60); mgr.printByScoreDesc(); auto students_in_range = mgr.queryByScoreRange(70, 90); std::cout << "Students with score between 70 and 90:\n"; for (const auto& stu : students_in_range) { std::cout << stu.name << " (" << stu.score << ")\n"; } const Student* stu = mgr.findById(1003); if (stu) { std::cout << "Found student by ID 1003: " << stu->name << std::endl; } return 0; }

这个例子展示了几个关键点:

  1. 多维度排序:我们根据不同的查询需求(按成绩、按学号),对数据进行了不同方式的排序或建立索引。
  2. 二分查找的灵活应用:在queryByScoreRange中,我们结合lower_boundupper_bound高效地获取了一个区间范围。
  3. 空间换时间:为了支持按学号快速查找,我们维护了一个按学号排序的指针索引id_index。这样,按学号查找的时间复杂度是 O(log N),而不用每次都对整个students向量排序。代价是需要额外的空间和更新索引的开销。
  4. 自定义比较函数的使用:注意在lower_boundupper_bound中,我们使用了不同的参数顺序和Lambda表达式来适应查找“值”与“元素”的比较。

7. 总结与个人体会

走完这一趟,你应该能感受到,C++标准库提供的排序和二分查找远不止是几个简单的函数调用。它们背后是严谨的算法实现和接口设计。我个人的经验是,在绝大多数情况下,你都应该优先使用这些标准组件,而不是自己从头实现。它们的正确性和效率经过了全球开发者数十年的检验。

在实际项目中,我踩过最多的坑就是“比较函数不一致”和“未排序就二分”。尤其是当数据结构比较复杂时,很容易在排序时用A字段,查找时却用B字段的逻辑。我的建议是,将比较逻辑封装成函数或函数对象,并在排序和查找时复用同一个比较器,这样可以最大程度避免错误。

另一个深刻的体会是关于“选择”。sortstable_sort之间,lower_boundupper_bound之间,甚至是用二分查找还是用std::unordered_map(哈希表)进行查找,都取决于具体场景。对于静态数据、范围查询,排序+二分是无敌的。对于动态数据、精确键值查找,关联容器可能更合适。理解这些工具的特性和成本,才能在面对问题时做出最合适的选择。

最后,别忘了实践。找一些在线判题网站(如 LeetCode)上的简单题目,比如 “35. 搜索插入位置”、“34. 在排序数组中查找元素的第一个和最后一个位置”,用标准库函数去解决它们,再尝试自己手写二分循环实现,对比一下代码的简洁度和正确率。你会发现,熟练掌握sortlower_bound这些函数,绝对是提升你C++编码效率和可靠性的利器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询