🎯 二分查找完全掌握手册 (五大场景 + 多种写法详解)
🎯 二分查找完全掌握手册 (五大场景 + 多种写法详解)
本笔记系统整理了 五大经典二分查找场景,并为每个场景提供了多种核心实现。无论你是初学者还是想巩固深化,都能从中获益。
我们将深入探讨两种主流的实现思路:
-
[l, r]闭区间写法:- 定义:搜索区间包含左右两个端点,循环条件为
while (l <= r)。 - 特点:逻辑直观,每次将搜索空间严格排除
mid后分为两部分 (l = mid + 1或r = mid - 1)。通常需要一个辅助变量ans来记录满足条件的潜在答案。
- 定义:搜索区间包含左右两个端点,循环条件为
-
[l, r)左闭右开写法:- 定义:搜索区间包含左端点但不包含右端点,循环条件为
while (l < r)。 - 特点:代码更紧凑,逻辑与 C++ STL (
lower_bound,upper_bound) 高度一致。当mid可能是答案时,我们会保留它 (r = mid),最终l(或r) 就是答案所在的位置。
- 定义:搜索区间包含左端点但不包含右端点,循环条件为
参考链接:
https://labuladong.online/algo/essential-technique/binary-search-framework/
🔑0 前置知识要点
🛡️0.1 防止整数溢出
计算 mid 时,强烈推荐使用 l + (r - l) / 2 的形式,可以有效避免 l + r 在数值较大时超出整型上限。
// 标准写法 (防溢出)int mid = l + ((r - l) / 2);
// 在某些场景 (如寻找右边界) 中,需要向上取整,防止死循环int mid = l + ((r - l + 1) / 2);🆚0.2 两种写法的核心区别
| 特性 | 写法 A:[l, r] (闭区间) | 写法 B:[l, r) (左闭右开) |
|---|---|---|
| 🔁 初始化 | l = 0, r = nums.size() - 1 | l = 0, r = nums.size() |
| 🛑 循环条件 | while (l <= r) | while (l < r) |
| ⏪ 向左收缩 | r = mid - 1 (mid 已检查, 排除) | r = mid (mid 可能是答案, 保留) |
| ⏩ 向右收缩 | l = mid + 1 (mid 已检查, 排除) | l = mid + 1 (mid 已检查, 排除) |
1️⃣ 1 场景一:精确查找 (Equal)
🎯 目标:在数组中找到一个值 nums[i] == target 的确切下标。
↩️ 返回:如果找到,返回其下标;若不存在,则返回 -1。
1.1 [l, r] 闭区间写法 (推荐)
思路解析:这是最标准的二分查找。在
[l, r]区间内寻找目标。找到mid后,如果nums[mid]就是target,则大功告成。如果nums[mid]小于target,说明目标在右侧,因此下一轮搜索[mid + 1, r];反之则搜索[l, mid - 1]。
int findEqual(vector<int>& nums, int target) { int l = 0, r = nums.size() - 1; while (l <= r) { int mid = l + (r - l) / 2;
if (nums[mid] == target) { return mid; // 找到直接返回 } else if (nums[mid] < target) { l = mid + 1; } else { r = mid - 1; } } return -1;}1.2 [l, r) 左闭右开写法
思路解析:在
[l, r)区间内寻找。当nums[mid] < target时,目标一定在[mid + 1, r)内。当nums[mid] > target时,目标可能在[l, mid)内。此写法在未找到目标时,循环终止后l和r会重合,但该位置不一定是目标,因此需要额外判断或直接返回-1。
int findEqual(vector<int>& nums, int target) { int l = 0, r = nums.size(); while (l < r) { int mid = l + (r - l) / 2;
if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { l = mid + 1; } else { r = mid; // 右边界变成 mid } } return -1; // 循环结束时 l == r,但未找到 target}2️⃣2 场景二:第一个 >= target 的位置 (Lower Bound)
2.1 情况一:找不到时返回 n (同 C++ STL)
🎯 目标:找到 最小 的下标 i,使得 nums[i] >= target。
↩️ 返回:若存在,返回该下标;若所有数都小于 target,返回数组大小 n。
2.1.1 [l, r] 闭区间写法
写法 1:ans 辅助变量法 思路解析:当
nums[mid] >= target时,mid是一个潜在答案。我们用ans记录它,然后向左半部分[l, mid - 1]进一步压缩,尝试寻找更靠左的好答案。
int lowerBound_v1(vector<int>& nums, int target) { int n = nums.size(); int l = 0, r = n - 1; int ans = n; // 默认返回 n (越界位置)
while (l <= r) { int mid = l + (r - l) / 2;
if (nums[mid] >= target) { ans = mid; // 记录当前位置 r = mid - 1; // 继续往左找 } else { l = mid + 1; } } return ans;}写法 2:混合写法 (闭区间初始化,开区间循环) 思路解析:这种写法
while (l < r)保证循环结束后l == r。如果nums[mid] >= target,说明答案在mid或其左侧,所以r = mid;否则答案在右侧,l = mid + 1。循环结束后,l指向的就是第一个>= target的候选位置,最后需要检查该位置是否真的满足条件。
int lowerBound_v2(const vector<int>& nums, int target) { int n = nums.size(); int l = 0, r = n - 1; // 注意这里 r 的初始化 while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] >= target) { r = mid; } else { l = mid + 1; } } // 循环结束时 l == r,需要检查 nums[l] 是否真的 >= target if (nums[l] < target) { return n; } return l;}2.1.2 [l, r) 左闭右开写法 (STL 原理)
思路解析:当
nums[mid] >= target时,mid可能是最终答案,所以我们将右边界收缩到r = mid保留mid。否则,mid太小,去右边找l = mid + 1。循环结束后,l指向的就是第一个>= target的位置。这是最优雅的lower_bound实现。
int lowerBound_v3(vector<int>& nums, int target) { int l = 0, r = nums.size();
while (l < r) { int mid = l + (r - l) / 2;
if (nums[mid] >= target) { r = mid; // 可能是答案,收缩右边界到 mid } else { l = mid + 1; } } return l; // 循环结束时 l == r}2.2 情况二:找不到时返回 -1
🎯 目标:找到 最小 的下标 i,使得 nums[i] == target(这里原笔记似乎意为找第一个等于,我们按此理解)。
↩️ 返回:若不存在,返回 -1。
2.2.1 [l, r] 闭区间写法
写法 1: ans 辅助变量法 + 验证 思路解析:先用
ans变量找到第一个>= target的位置,然后验证该位置的值是否恰好等于target。
int lowerBound_v1(vector<int>& nums, int target) { int n = nums.size(); int l = 0, r = n - 1; int ans = -1; // 默认返回 -1
while (l <= r) { int mid = l + (r - l) / 2;
if (nums[mid] >= target) { ans = mid; // 记录当前位置 r = mid - 1; // 继续往左找 } else { l = mid + 1; } } // ans 可能是 ≥ target 的第一个位置,需验证是否等于 target if (ans != -1 && ans < n && nums[ans] == target) { return ans; } return -1;}写法 2: 混合写法 + 验证 思路解析:同样是先找到候选位置
l,然后检查nums[l]是否等于target。
int lowerBound_v2(const vector<int>& nums, int target) { int n = nums.size(); int l = 0, r = n - 1; while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] >= target) { r = mid; } else { l = mid + 1; } } // 如果最后 nums[l] != target,说明应返回 -1 return nums[l] == target ? l : -1;}2.2.2 [l, r) 左闭右开写法
思路解析:先用标准的
lower_bound找到第一个>= target的位置l,然后检查该位置是否越界以及值是否等于target。
int lowerBound_v3(vector<int>& nums, int target) { int l = 0, r = nums.size(); while (l < r) { int mid = l + (r - l) / 2;
if (nums[mid] >= target) { r = mid; // 可能是答案,收缩右边界 } else { l = mid + 1; } } if (l < 0 || l >= nums.size()) { return -1; } return nums[l] == target ? l : -1;}3️⃣3 场景三:第一个 > target 的位置 (Upper Bound)
🎯 目标:找到 最小 的下标 i,使得 nums[i] > target。
↩️ 返回:若存在,返回该下标;若所有数都小于等于 target,返回数组大小 n。
3.1 [l, r] 闭区间写法
写法 1: ans 辅助变量法 思路解析:逻辑与
lower_bound完全相同,仅将判断条件从>=改为>。
int upperBound_v1(vector<int>& nums, int target) { int n = nums.size(); int l = 0, r = n - 1; int ans = n;
while (l <= r) { int mid = l + (r - l) / 2;
if (nums[mid] > target) { ans = mid; // 记录当前位置 r = mid - 1; // 继续往左找 } else { // nums[mid] <= target l = mid + 1; } } return ans;}写法 2: 混合写法 思路解析:与
lower_bound的混合写法类似,循环结束后l指向候选位置,最后需要根据nums[l]的值决定返回l还是l+1。
int upperBound_v2(vector<int>& nums, int target) { int n = nums.size(); if (n == 0) return 0;
int l = 0, r = n - 1; while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] > target) { r = mid; // 可能是答案 } else { // nums[mid] <= target l = mid + 1; } } // 循环结束 l==r,若 nums[l] > target,l 就是答案,否则答案是 l+1 return nums[l] > target ? l : l + 1;}3.2 [l, r) 左闭右开写法
思路解析:
upper_bound的标准优雅实现。判断条件变为nums[mid] > target时收缩右边界。
int upperBound_v3(vector<int>& nums, int target) { int l = 0, r = nums.size();
while (l < r) { int mid = l + (r - l) / 2;
if (nums[mid] > target) { r = mid; } else { l = mid + 1; } } return l;}4️⃣4 场景四:最后一个 <= target 的位置
🎯 目标:找到 最大 的下标 i,使得 nums[i] <= target。
↩️ 返回:若存在,返回该下标;若所有数都大于 target,返回 -1。
4.1 [l, r] 直接查找法
写法 1: ans 辅助变量法 思路解析:当
nums[mid] <= target时,mid是一个潜在答案。我们用ans记录它,然后向右半部分[mid + 1, r]寻找,尝试找到更靠右的好答案。
int lastLessEqual_v1(vector<int>& nums, int target) { int l = 0, r = nums.size() - 1; int ans = -1; // 默认为 -1
while (l <= r) { int mid = l + (r - l) / 2;
if (nums[mid] <= target) { ans = mid; // 记录位置 l = mid + 1; // 往右找更大的下标 } else { r = mid - 1; } } return ans;}写法 2: 改变 mid 计算方式 思路解析:这种写法
mid向上取整(r - l + 1) / 2,是为了防止当l = r - 1时mid = l,若此时l = mid会导致死循环。当nums[mid] <= target,答案在mid或其右侧,所以l = mid。
int lastLessEqual_v2(vector<int>& nums, int target) { int l = 0, r = nums.size() - 1; if (nums.empty()) return -1; while (l < r) { int mid = l + (r - l + 1) / 2; // 向上取整 if (nums[mid] <= target) { l = mid; // 可能是答案,向右收缩 [mid, r] } else { r = mid - 1; } } // 循环终止时 l == r,需最后判断 return (nums[l] <= target) ? l : -1;}4.2 [l, r) 转换法 (利用 Upper Bound)
💡 核心思想: 寻找 “最后一个
<= target的数”,等价于寻找 “第一个> target的数” 的 前一个位置。这是非常巧妙的转换。
int lastLessEqual_v3(vector<int>& nums, int target) { // 1. 先用 upper_bound 找到第一个 > target 的位置 l int l = 0, r = nums.size(); while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] > target) { r = mid; } else { l = mid + 1; } } // 2. 这个位置 l 的前一个位置 (l-1) 就是答案 // 如果 l=0 (所有数都 > target),返回 -1,逻辑正确 return l - 1;}5️⃣5 场景五:最后一个 < target 的位置
🎯 目标:找到 最大 的下标 i,使得 nums[i] < target。
↩️ 返回:若存在,返回该下标;若所有数都大于等于 target,返回 -1。
5.1 [l, r] 直接查找法
思路解析:与场景四非常相似,仅将条件从
<=改为<。满足nums[mid] < target时,记录答案并向右寻找。
int lastLess_v1(vector<int>& nums, int target) { int l = 0, r = nums.size() - 1; int ans = -1;
while (l <= r) { int mid = l + (r - l) / 2;
if (nums[mid] < target) { ans = mid; // 记录位置 l = mid + 1; // 往右找 } else { // nums[mid] >= target r = mid - 1; } } return ans;}5.2 [l, r) 转换法 (利用 Lower Bound)
💡 核心思想: 寻找 “最后一个
< target的数”,等价于寻找 “第一个>= target的数” 的 前一个位置。
int lastLess_v2(vector<int>& nums, int target) { // 1. 先用 lower_bound 找到第一个 >= target 的位置 l int l = 0, r = nums.size(); while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] >= target) { r = mid; } else { l = mid + 1; } } // 2. 这个位置 l 的前一个位置 (l-1) 就是答案 return l - 1;}📝6 总结速查表
| 场景 | 核心符号 | [l, r] 闭区间思路 (ans 记录法) | [l, r) 优雅思路 (返回 l 或 l-1) |
|---|---|---|---|
| 1. 精确查找 | == | 找到 target 直接 return mid | 找到 target 直接 return mid |
| 2. 第一个 ≥ (Lower Bound) | >= | ans = mid; r = mid - 1; (往左压缩) | 直接法: r = mid,返回 l |
| 3. 第一个 > (Upper Bound) | > | ans = mid; r = mid - 1; (往左压缩) | 直接法: r = mid,返回 l |
| 4. 最后一个 ≤ | <= | ans = mid; l = mid + 1; (往右贪心) | 转换法: 找 第一个 > 的位置,返回 l - 1 |
| 5. 最后一个 < | < | ans = mid; l = mid + 1; (往右贪心) | 转换法: 找 第一个 ≥ 的位置,返回 l - 1 |
🚀 记忆口诀
- 找第一个 (>=, >):满足条件时,答案可能在左边,所以
ans记录,r向左缩;或者用[l,r)写法,r = mid。- 找最后一个 (<, <=):满足条件时,答案可能在右边,所以
ans记录,l向右扩;或者优雅地转换为找 “第一个不满足条件的” 的前一个位置。
🛠️7 C++ STL 实现
C++ 标准库 <algorithm> 提供了两个强大的函数,lower_bound 与 upper_bound,完美实现了我们之前讨论的场景,且效率极高。它们都要求作用的区间是已排序的。
📌 7.1lower_bound
- 功能:查找第一个 不小于 (>=)
value的元素。 - 对应场景:场景二,找到第一个
>= target的位置。 - 函数签名:
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& value); - 参数:
first: 指向序列起始位置的迭代器。last: 指向序列结束位置之后的迭代器(构成[first, last)左闭右开区间)。value: 要查找的值。
- 返回值:返回一个指向目标的迭代器。如果所有元素都小于
value,则返回last。
示例代码
#include <algorithm> // 包含 lower_bound#include <iostream>#include <vector>using namespace std;int main() { vector<int> nums = {10, 20, 30, 30, 30, 40, 50}; int target = 30;
// 查找第一个 >= target 的元素 auto it = lower_bound(nums.begin(), nums.end(), target);
if (it != nums.end()) { // 直接使用迭代器减法计算下标(适用于随机访问迭代器) long long index = it - nums.begin(); cout << "第一个 >= " << target << " 的元素是: " << *it << endl; cout << "它的下标是: " << index << endl; // 输出下标 2 } else { cout << "没有找到 >= " << target << " 的元素。" << endl; } return 0;}📌7.2 upper_bound
- 功能:查找第一个 严格大于 (>)
value的元素。 - 对应场景:场景三,找到第一个
> target的位置。 - 函数签名:
ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T& value); - 参数:同
lower_bound。 - 返回值:返回一个指向目标的迭代器。如果所有元素都小于等于
value,则返回last。
示例代码
#include <algorithm>#include <iostream>#include <vector>using namespace std;int main() { vector<int> nums = {10, 20, 30, 30, 30, 40, 50}; int target = 30;
// 查找第一个 > 30 的元素 auto it = upper_bound(nums.begin(), nums.end(), target);
if (it != nums.end()) { long long index = it - nums.begin(); cout << "第一个 > " << target << " 的元素是: " << *it << endl; // 输出 40 cout << "它的下标是: " << index << endl; // 输出下标 5 } else { cout << "没有找到 > " << target << " 的元素。" << endl; } return 0;}支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!