当前位置: 首页 > news >正文

从一道LeetCode题出发,实战解析C++ set中lower_bound/upper_bound的四种经典用法

从一道LeetCode题出发,实战解析C++ set中lower_bound/upper_bound的四种经典用法

在算法面试和日常编程中,set容器的高效操作往往是解决问题的关键。lower_boundupper_bound这两个函数虽然简单,但它们的组合使用却能解决许多复杂问题。本文将从一个具体LeetCode题目入手,逐步拆解四种经典应用场景,帮助读者掌握这一强大工具。

1. 理解基础:分界线思维

lower_boundupper_bound的核心在于"分界线"概念。对于有序集合,这两个函数帮助我们快速定位虚拟的"分界线"位置:

  • lower_bound(val): 返回第一个不小于val的元素迭代器
  • upper_bound(val): 返回第一个大于val的元素迭代器

考虑一个简单的升序集合{1, 3, 5, 7, 9}

set<int> s = {1, 3, 5, 7, 9}; auto lb = s.lower_bound(4); // 指向5 auto ub = s.upper_bound(4); // 同样指向5

有趣的是,当集合为降序时行为会变化:

set<int, greater<int>> s = {9, 7, 5, 3, 1}; auto lb = s.lower_bound(4); // 指向3 auto ub = s.upper_bound(4); // 同样指向3

注意:在降序集合中,lower_bound返回的是第一个不大于val的元素,这与升序时的逻辑正好相反。

2. 应用一:确定插入位置

LeetCode题目示例: 352. 将数据流变为多个不相交区间

问题场景:我们需要维护一个动态增长的数值集合,并在任何时候都能输出当前所有数值组成的不相交区间。

解决方案:使用set存储所有数字,利用lower_bound快速定位插入位置:

class SummaryRanges { private: set<int> nums; public: void addNum(int val) { nums.insert(val); } vector<vector<int>> getIntervals() { vector<vector<int>> res; if(nums.empty()) return res; auto start = nums.begin(); auto end = start; ++end; while(end != nums.end()) { if(*end != *start + 1) { res.push_back({*start, *start}); start = end; } ++end; } res.push_back({*start, *start}); return res; } };

优化点:在插入时使用lower_bound检查相邻元素:

void addNum(int val) { auto it = nums.lower_bound(val); if(it != nums.end() && *it == val) return; // 已存在 // 检查是否可以合并到前一个区间 if(it != nums.begin()) { auto prev = it; --prev; if(*prev + 1 == val) { nums.erase(prev); } } // 检查是否可以合并到后一个区间 if(it != nums.end() && *it == val + 1) { nums.erase(it); } nums.insert(val); }

3. 应用二:查找值域范围

LeetCode题目示例: 220. 存在重复元素 III

问题场景:判断数组中是否存在两个索引i和j,使得abs(nums[i]-nums[j]) <= tabs(i-j) <= k

解决方案:使用滑动窗口结合set的查找功能:

bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) { set<long> window; for(int i = 0; i < nums.size(); ++i) { if(i > k) { window.erase(nums[i-k-1]); } long num = nums[i]; auto lb = window.lower_bound(num - t); if(lb != window.end() && *lb <= num + t) { return true; } window.insert(num); } return false; }

关键点

  1. 维护一个大小为k的滑动窗口
  2. 对于每个新元素,检查窗口中是否存在[num-t, num+t]范围内的值
  3. lower_bound(num-t)快速定位范围起点

4. 应用三:实现滑动窗口逻辑

LeetCode题目示例: 480. 滑动窗口中位数

问题场景:计算滑动窗口中的中位数,窗口每次移动一位。

解决方案:使用两个set分别维护窗口的前后部分:

vector<double> medianSlidingWindow(vector<int>& nums, int k) { multiset<int> left, right; vector<double> res; for(int i = 0; i < nums.size(); ++i) { // 插入新元素 if(left.empty() || nums[i] <= *left.rbegin()) { left.insert(nums[i]); } else { right.insert(nums[i]); } // 平衡两个集合 if(left.size() > right.size() + 1) { right.insert(*left.rbegin()); left.erase(--left.end()); } else if(right.size() > left.size()) { left.insert(*right.begin()); right.erase(right.begin()); } // 计算中位数 if(i >= k - 1) { if(k % 2 == 1) { res.push_back(*left.rbegin()); } else { res.push_back(((double)*left.rbegin() + *right.begin()) / 2); } // 移除最左边的元素 int to_remove = nums[i - k + 1]; if(to_remove <= *left.rbegin()) { left.erase(left.find(to_remove)); } else { right.erase(right.find(to_remove)); } } } return res; }

优化技巧

  • 使用multiset允许重复元素
  • 通过lower_bound快速定位要删除的元素
  • 保持两个集合的大小差不超过1

5. 应用四:处理有序集合的区间合并与分割

LeetCode题目示例: 715. Range模块

问题场景:设计一个数据结构,能够高效地添加、删除和查询数值区间。

解决方案:使用set存储区间的端点,利用lower_bound快速定位相关区间:

class RangeModule { private: set<pair<int, int>> ranges; public: void addRange(int left, int right) { auto it = ranges.lower_bound({left, right}); // 合并左侧可能重叠的区间 if(it != ranges.begin()) { --it; if(it->second < left) { ++it; } else { left = min(left, it->first); right = max(right, it->second); ranges.erase(it++); } } // 合并右侧可能重叠的区间 while(it != ranges.end() && it->first <= right) { right = max(right, it->second); ranges.erase(it++); } ranges.insert({left, right}); } bool queryRange(int left, int right) { auto it = ranges.lower_bound({left, INT_MIN}); if(it != ranges.begin() && (--it)->second >= right) { return true; } return false; } void removeRange(int left, int right) { auto it = ranges.lower_bound({left, right}); // 处理左侧可能被分割的区间 if(it != ranges.begin()) { --it; if(it->second <= left) { ++it; } else { auto old = *it; ranges.erase(it); if(old.first < left) { ranges.insert({old.first, left}); } if(old.second > right) { ranges.insert({right, old.second}); } it = ranges.lower_bound({left, right}); } } // 处理完全包含在移除范围内的区间 while(it != ranges.end() && it->first < right) { if(it->second <= right) { ranges.erase(it++); } else { auto old = *it; ranges.erase(it); ranges.insert({right, old.second}); break; } } } };

关键操作

  1. addRange: 使用lower_bound找到插入位置,合并重叠区间
  2. queryRange: 检查目标区间是否完全包含在某个现有区间内
  3. removeRange: 分割被部分移除的区间

6. 高级技巧与性能对比

在实际应用中,lower_boundupper_bound的性能表现优异,时间复杂度为O(log n)。以下是几种常见写法的性能对比:

操作时间复杂度适用场景
set.lower_boundO(log n)需要精确控制查找范围
std::lower_bound(set.begin(), set.end())O(n)不推荐,失去set优势
线性扫描O(n)小数据集或无序数据

代码优化示例

// 不推荐的写法 auto it = lower_bound(s.begin(), s.end(), val); // O(n) // 推荐的写法 auto it = s.lower_bound(val); // O(log n)

特殊场景处理

  1. 自定义比较函数:当使用自定义排序规则时,需要确保比较逻辑与lower_bound一致
struct Point { int x, y; bool operator<(const Point& other) const { return x < other.x || (x == other.x && y < other.y); } }; set<Point> points; auto it = points.lower_bound({3, INT_MIN}); // 查找x>=3的第一个点
  1. 多条件查询:结合多个lower_bound实现复杂查询
// 查找第一个满足a >= x && b >= y的元素 auto it = s.lower_bound({x, y}); while(it != s.end() && it->b < y) { ++it; }
  1. 边界条件处理:总是检查返回值是否为end()
auto it = s.lower_bound(val); if(it == s.end()) { // 处理未找到的情况 } else { // 处理找到的情况 }

掌握这些高级技巧后,lower_boundupper_bound将成为解决复杂算法问题的利器。在实际编码中,建议多思考"分界线"的概念,这能帮助更直观地理解这两个函数的行

http://www.jsqmd.com/news/1007152/

相关文章:

  • 2026年绵阳市PMP培训机构哪家好?官方授权R.E.P.报考指南 - 众智商学院课程中心
  • 终极Windows硬件信息伪装实战指南:免费开源工具完全解析
  • uiritoml:Python 里处理 TOML 的老牌工具
  • 桌面数字伙伴革命:DyberPet如何让你的电脑桌面活起来
  • ARM9平台SDRAM初始化与模式寄存器配置实战详解
  • QorIQ LS1046A安全引擎性能计数器实战解析与监控
  • 嵌入式通信协议设计:NXP ISF命令响应与流式传输详解
  • 终极指南:如何用MonitorControl彻底解决macOS外接显示器控制难题?
  • 手入门AI编程:依托口述开发搭建个人全栈博客一、入门AI编程的实战起点:用口述开发搭建博客
  • NHSE:动物森友会存档编辑器的终极指南与使用教程
  • 30米分辨率DEM数据实战:如何精准划定小流域边界并提取水系网络
  • ColabFold完整教程:3分钟学会免费蛋白质结构预测
  • 避坑指南:GEE计算大区域FVC时,如何解决‘像素超限’和保持10米分辨率?
  • 华新装修公司具备哪些资质
  • OpenModScan:开源Modbus主站工具的技术解析与工业协议测试实践
  • 嵌入式存储安全:SD卡硬件锁机制(CMD42)原理与实战
  • RESTful API设计原则通俗详解:资源、CRUD、状态码全套规范教程
  • Ollama如何安装到D盘
  • GPU 虚拟化与多租户算力治理云原生深度解析:MIG/MPS/Time-Slicing 技术对比、Kubernetes 资源配额与 AI 工作负载成本优化实战
  • pytest-xdist:把 pytest 测试分发到多核 CPU 执行
  • 别再只会做静态模型了!用Blender 3.0+的曲线修改器,5分钟搞定植物生长动画核心
  • 最大熵先验:贝叶斯建模中客观约束驱动的诚实起点
  • 工业安防技术解析:浙江区域防爆监控选型与技术要点
  • SniperDz 钓鱼即服务平台攻击链路与防御技术研究
  • i.MX21引脚复用与电源管理:嵌入式硬件设计的核心实践
  • 注意!乘坐飞机切勿携带这种“伪装”违禁品
  • 寄大件什么快递便宜?教你一招省一半运费 - 快递物流资讯
  • BilibiliDown:开源跨平台B站视频下载解决方案全解析
  • 深入解析UART发送FIFO中断抑制与自动波特率检测机制
  • 周志华《Machine Learning》学习笔记(11)--聚类