哈喽大家好!今天咱们来吃透算法面试和竞赛中的高频考点------二分算法,主要拆解「二分查找」和「二分答案」两大核心场景,全程基于C++实现,从原理到模板,从例题到避坑,新手也能轻松上手,老手也能查漏补缺~
先抛个核心结论:二分算法的本质是「减而治之」,利用解空间的「二段性」,每次将搜索范围减半,把O(n)的时间复杂度压缩到O(log n),不管是有序数组查找,还是最值优化问题,都能高效解决。
一、基础铺垫:二分的核心前提与通用技巧
在讲具体算法前,先明确两个关键要点,避免后续踩坑:
1.1 二分的核心前提
二分能生效的关键的是「解空间具有二段性」:存在一个分界点,使得分界点一侧的所有元素都满足某个条件,另一侧都不满足(或反之)。没有二段性的问题,无法直接用二分解决。
比如:有序数组找目标值(分界点是目标值,左侧≤目标值,右侧≥目标值);木材加工问题(分界点是最大可切割长度,左侧能切出足够段数,右侧不能)。
1.2 通用避坑技巧(C++专属)
-
mid计算:优先用
mid = left + (right - left) / 2,避免(left + right) / 2因left和right过大导致int溢出;也可写成mid = left + ((right - left) >> 1)(位运算效率更高)。 -
边界处理:二分的核心难点的是边界收缩,记住「模板化思维」,不要临时推导,避免死循环,下文会给出固定模板。
-
类型选择:涉及mid平方、乘积等操作时,优先用long long,防止溢出(比如LeetCode 69题求平方根)。
二、二分查找(Binary Search):有序数组的精准定位
二分查找是最基础的二分场景,核心是「在有序数组中查找目标值的位置」,常见变种有:找第一个≥目标值、最后一个≤目标值、判断目标值是否存在等,我们重点讲两种通用模板,覆盖所有场景。
2.1 模板1:左闭右闭区间(最直观,适合精准查找)
区间定义:left和right都包含在搜索范围内([left, right]),循环条件为left ≤ right,边界收缩时需同步调整left和right。
适用场景:判断目标值是否存在、找目标值的精确位置。
cpp
#include <iostream>
#include <vector>
using namespace std;
// 二分查找:左闭右闭区间,返回目标值下标,不存在返回-1
int binarySearch(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 右边界初始化为数组最后一个下标(闭区间)
while (left ≤ right) { // 区间不为空就继续搜索
int mid = left + (right - left) / 2; // 避免溢出
if (nums[mid] == target) {
return mid; // 找到目标,直接返回下标
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右半区,左边界右移(排除mid)
} else {
right = mid - 1; // 目标在左半区,右边界左移(排除mid)
}
}
return -1; // 循环结束,未找到目标
}
// 测试示例
int main() {
vector<int> nums = {1, 3, 5, 7, 9, 11, 13};
int target = 7;
int res = binarySearch(nums, target);
if (res != -1) {
cout << "目标值 " << target << " 的下标为:" << res << endl;
} else {
cout << "未找到目标值 " << target << endl;
}
return 0;
}
运行结果:目标值 7 的下标为:3
2.2 模板2:左闭右开区间(边界更统一,适合找边界)
区间定义:left包含在搜索范围内,right不包含([left, right)),循环条件为 left < right,边界收缩时只调整一侧。
适用场景:找第一个≥目标值、第一个>目标值、最后一个≤目标值的位置。
cpp
#include <iostream>
#include <vector>
using namespace std;
// 二分查找:左闭右开区间,找第一个≥target的元素下标(常用变种)
int binarySearchLeftBound(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 右边界初始化为数组长度(开区间,不包含最后一个元素)
while (left < right) { // 区间不为空就继续搜索
int mid = left + (right - left) / 2;
if (nums[mid] ≥ target) {
right = mid; // 满足条件,收缩右边界(保留mid,继续向左找)
} else {
left = mid + 1; // 不满足条件,左边界右移(排除mid)
}
}
// 循环结束后left == right,就是第一个≥target的下标
return left;
}
// 测试示例
int main() {
vector<int> nums = {1, 3, 5, 7, 9, 11, 13};
int target = 6;
int res = binarySearchLeftBound(nums, target);
cout << "第一个≥" << target << " 的元素下标为:" << res << ",元素值为:" << nums[res] << endl;
return 0;
}
运行结果:第一个≥6 的元素下标为:3,元素值为:7
2.3 实战例题(LeetCode 35. 搜索插入位置)
题目描述:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
思路:直接复用「左闭右开模板」,找第一个≥target的位置,就是插入位置,代码如下:
cpp
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] ≥ target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
};
复杂度分析:时间O(log n),空间O(1),高效解决问题。
三、二分答案:最值问题的"克星"
二分答案是二分算法的高级应用,核心是「将求最优解的问题,转化为判断某个值是否为可行解」,通过二分可行解空间,找到最优解。很多看似复杂的最值问题,用二分答案能轻松破局。
3.1 核心原理与适用场景
适用场景(重点记):
-
求「最大值最小」或「最小值最大」问题(如:木材加工、砍树、跳石头);
-
难以直接计算答案,但能快速判断某个值是否满足条件;
-
解空间具有单调性(满足二段性)。
核心步骤:
-
确定答案的取值范围(left:最小可能值,right:最大可能值);
-
编写check函数:判断当前mid值是否满足题目要求(可行解);
-
二分收缩范围:根据check结果,保留可行解区间,排除不可行区间;
-
循环结束,left(或right)即为最优解。
3.2 实战例题(洛谷 P2440 木材加工)
题目描述:有n根木材,长度分别为a1,a2,...,an,现在要把它们切成k段长度相同的木材,求每段木材的最大可能长度(木材长度为整数)。
思路:
-
答案范围:left=1(最小可能长度),right=最大木材长度(最大可能长度);
-
check函数:判断以mid为每段长度,能否切出至少k段木材;
-
二分逻辑:若能切出k段(check(mid)=true),说明可以尝试更长的长度,收缩左边界;若不能,收缩右边界。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// check函数:判断以mid为每段长度,能否切出至少k段
bool check(vector<int>& woods, int mid, int k) {
int count = 0; // 统计能切出的段数
for (int wood : woods) {
count += wood / mid; // 每根木材能切出的段数
if (count ≥ k) { // 提前退出,优化效率
return true;
}
}
return count ≥ k;
}
// 二分答案求最大长度
int maxWoodLength(vector<int>& woods, int k) {
int left = 1;
int right = *max_element(woods.begin(), woods.end()); // 最大木材长度
int ans = 0; // 存储最优解
while (left ≤ right) {
int mid = left + (right - left) / 2;
if (check(woods, mid, k)) {
ans = mid; // 记录可行解,尝试更大的长度
left = mid + 1;
} else {
right = mid - 1; // 不可行,尝试更小的长度
}
}
return ans;
}
// 测试示例
int main() {
int n, k;
cin >> n >> k;
vector<int> woods(n);
for (int i = 0; i < n; i++) {
cin >> woods[i];
}
int res = maxWoodLength(woods, k);
cout << "每段木材的最大可能长度为:" << res << endl;
return 0;
}
测试输入:4 5,木材长度 4 4 4 4,运行结果:每段木材的最大可能长度为:3(4根木材各切1段3,共4段,不够5段?修正:输入改为5 5,木材长度4 4 4 4 4,结果为4)。
3.3 关键注意点(二分答案必看)
-
check函数是核心:必须准确判断mid是否满足条件,逻辑错误会导致答案偏差;
-
边界初始化:left和right必须覆盖所有可能的解,不能漏判(如木材加工中left不能为0,避免除0错误);
-
最优解保存:二分过程中要记录可行解,避免循环结束后丢失正确答案(如上述例题中的ans变量)。
四、常见误区与避坑指南(重中之重)
4.1 误区1:认为二分只能用于有序数组
错误!二分的核心是「二段性」,不是「有序」。有序数组只是满足二段性的一种情况,二分答案场景中,解空间的单调性才是关键,与数组是否有序无关(如木材加工问题,木材长度可以无序)。
4.2 误区2:边界处理不当导致死循环
常见错误:
-
左闭右闭区间中,用
left = mid或right = mid,导致循环无法退出; -
左闭右开区间中,循环条件用
left ≤ right,导致越界。
解决方法:牢记两套模板,严格按照模板收缩边界,不临时修改逻辑。
4.3 误区3:mid计算溢出
错误写法:int mid = (left + right) / 2,当left和right接近2^31-1时,left+right会溢出,导致mid计算错误。
正确写法:int mid = left + (right - left) / 2 或 long long mid = (left + right) / 2(强制转换类型)。
4.4 误区4:二分答案忘记判断解的有效性
部分题目中,可能存在「没有可行解」的情况(如木材加工中,所有木材总长度<k),此时需要在最后判断ans是否有效,避免返回错误答案。
五、总结与练习推荐
5.1 核心总结
-
二分查找:针对有序数组,核心是「精准定位」,记住两套模板(左闭右闭、左闭右开),覆盖所有查找场景;
-
二分答案:针对最值问题,核心是「转化为判断问题」,重点是设计check函数和确定解空间;
-
共性:都利用二段性缩小区间,时间复杂度O(log n),是高效算法的核心思想之一。
5.2 练习推荐(从易到难)
-
二分查找:LeetCode 704. 二分查找、LeetCode 35. 搜索插入位置、LeetCode 69. x的平方根;
-
二分答案:洛谷 P2440 木材加工、洛谷 P1873 砍树、LeetCode 410. 分割数组的最大值;
-
进阶:LeetCode 33. 搜索旋转排序数组(二分查找变种)、洛谷 P2678 跳石头(二分答案进阶)。
最后,二分算法的核心是「模板化+多练」,刚开始写可能会卡边界,但只要熟练掌握两套模板,多做几道例题,就能彻底吃透,再也不怕面试和竞赛中的二分问题啦!
如果觉得本文有用,欢迎点赞收藏,关注我,后续更新更多C++算法干货~