二分查找与二分答案算法详解(基于C++实现)

哈喽大家好!今天咱们来吃透算法面试和竞赛中的高频考点------二分算法,主要拆解「二分查找」和「二分答案」两大核心场景,全程基于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 核心原理与适用场景

适用场景(重点记):
  • 求「最大值最小」或「最小值最大」问题(如:木材加工、砍树、跳石头);

  • 难以直接计算答案,但能快速判断某个值是否满足条件;

  • 解空间具有单调性(满足二段性)。

核心步骤:
  1. 确定答案的取值范围(left:最小可能值,right:最大可能值);

  2. 编写check函数:判断当前mid值是否满足题目要求(可行解);

  3. 二分收缩范围:根据check结果,保留可行解区间,排除不可行区间;

  4. 循环结束,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 = midright = 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) / 2long long mid = (left + right) / 2(强制转换类型)。

4.4 误区4:二分答案忘记判断解的有效性

部分题目中,可能存在「没有可行解」的情况(如木材加工中,所有木材总长度&lt;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++算法干货~

相关推荐
瑞行AI1 小时前
一套数据格式框架搞定大模型微调和对齐训练
算法·语言模型
玛卡巴卡ldf1 小时前
【LeetCode 手撕算法】(动态规划)爬楼梯、杨辉三角、打家劫舍、完全平方数、零钱兑换、单词拆分、最长递增子序列、乘积最大子数组、分割等和子集
java·数据结构·算法·leetcode·动态规划·力扣
小短腿的代码世界1 小时前
Qt实时风控计算引擎:从订单校验到盈亏监控的完整架构设计与高性能实现
开发语言·qt
jake·tang1 小时前
深度解析 VESC 参数辨识源码:电阻、电感与磁链
arm开发·c++·嵌入式硬件·算法·数学建模·傅立叶分析
MaikieMaiky1 小时前
C++STL 系列(三):deque 容器详解与示例
开发语言·c++
图码1 小时前
矩阵边界遍历:顺时针与图案打印的两种高效解法
数据结构·python·线性代数·算法·青少年编程·矩阵·深度优先遍历
南境十里·墨染春水1 小时前
线程池学习(三) 实现固定线程池
开发语言·c++·学习
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章72-点-点距离
图像处理·人工智能·opencv·算法·计算机视觉
橘子海全栈攻城狮1 小时前
【最新源码】基于springboot的快递物流平台的设计与实现C102
java·开发语言·spring boot·后端·spring·web安全