GESP 5级

二分查找

5级 · 基础算法与数据类型

📖 什么是二分查找?

假设你在一本按首字母排好序的电话簿里找"张三"。你不会从第一页开始翻——你会直接翻到中间,发现"李"在前面,然后翻到后半部分的中间……每次翻到中间,排除掉一半的不可能。

二分查找就是这个策略的算法版本:在一个有序数组中查找目标值,每次比较中间元素,把搜索范围缩小一半。

对比线性查找(一个一个找):

线性查找:100万个数,最坏比较 1000000 次
二分查找:100万个数,最多只需比较约 20 次!
(因为 2²⁰ ≈ 1000000,每次减半,20 次就只剩 1 个)
🌟 为什么重要?
  • 效率极高:O(log n) 的时间复杂度,数据量翻倍只需多比一次。100万数据只需约 20 次比较。
  • 竞赛必备:几乎所有竞赛都会考二分,它是解决"在某个范围内找答案"类问题的万能钥匙。
  • 二分答案:不仅可以"在数组中查找",还可以"二分答案"——对答案进行二分搜索,验证答案是否可行。这是 GESP 5级的高频考点。
  • STL 中的二分:lower_bound、upper_bound 都是二分查找的变体。
📋 前置知识
  • ✅ 数组的基本操作(遍历、访问元素)
  • ✅ for / while 循环
  • ✅ 比较运算符(<、>、==、<=、>=)
  • ✅ 理解什么是"有序"(递增或递减排列)

🔍 手动模拟:二分查找过程

数组 [1, 3, 5, 7, 9, 11, 13, 15],目标值 7:

第1步:lo=0, hi=7, mid=3 → a[3]=7 = 目标!找到!
(运气好,一次就找到)

换个目标值 11:

数组:[1, 3, 5, 7, 9, 11, 13, 15]

第1步:lo=0, hi=7, mid=3 → a[3]=7 < 11
    7 比目标小,目标在右半边,lo = mid + 1 = 4

数组右半边:[_, _, _, _, 9, 11, 13, 15]

第2步:lo=4, hi=7, mid=5 → a[5]=11 = 目标!找到!

只用了 2 次比较!(而线性查找需要 6 次)

📝 标准模板

📐 二分查找三要素
1. 搜索区间:[lo, hi],初始 lo=0, hi=n-1
2. 循环条件:while (lo <= hi),区间不为空就继续
3. 更新规则:
   target == a[mid] → 找到了
   target > a[mid] → lo = mid + 1(去右半边找)
   target < a[mid] → hi = mid - 1(去左半边找)

⚠️ mid 计算:mid = lo + (hi - lo) / 2
不要写 lo + hi / 2(溢出风险)
💻 标准二分查找
#include <iostream>
#include <vector>
using namespace std;

// 在有序数组 a 中查找 target,返回下标;找不到返回 -1
int binarySearch(vector<int>& a, int target) {
    int lo = 0;             // 搜索区间的左端点
    int hi = a.size() - 1;  // 搜索区间的右端点

    while (lo <= hi) {  // 区间不为空,就继续找
        // 计算中间位置
        // 用 lo + (hi - lo) / 2 而不是 (lo + hi) / 2 是为了防止溢出
        int mid = lo + (hi - lo) / 2;

        if (a[mid] == target) {
            return mid;     // 找到了,返回下标
        } else if (a[mid] < target) {
            lo = mid + 1;   // 中间值太小,目标在右半边
        } else {
            hi = mid - 1;   // 中间值太大,目标在左半边
        }
    }

    return -1;  // 搜索区间为空,说明没找到
}

int main() {
    vector<int> arr = {1, 3, 5, 7, 9, 11, 13, 15};

    cout << binarySearch(arr, 7) << endl;   // 输出: 3(找到,下标3)
    cout << binarySearch(arr, 11) << endl;  // 输出: 5(找到,下标5)
    cout << binarySearch(arr, 10) << endl;  // 输出: -1(没找到)
    cout << binarySearch(arr, 1) << endl;   // 输出: 0(找到,下标0)
    cout << binarySearch(arr, 15) << endl;  // 输出: 7(找到,下标7)

    return 0;
}

🎯 二分答案:进阶用法

二分答案是二分查找的高级应用:不是在数组里找元素,而是对"答案"本身进行二分搜索。

典型问题:"有 N 头牛和 M 个牛栏,把牛分配到牛栏,要使相邻牛的最小间距最大,这个最大间距是多少?"

思路:答案的范围是 [1, 最大可能间距],我们对这个范围二分:

1. 猜一个间距 mid
2. 验证:用贪心看能否安排下所有牛(间距 ≥ mid)
3. 如果能安排 → 答案可能更大,搜索右半边
4. 如果不能安排 → 答案必须更小,搜索左半边
5. 重复直到找到最大的可行答案
💻 二分答案示例:猜数字游戏
#include <iostream>
using namespace std;

// 题目:猜一个 1~100 之间的数字,系统告诉你"大了"或"小了"
// 每次二分缩小范围,最快猜到答案

// 假设答案是 secret = 73
int secret = 73;

// 模拟系统返回的比较结果
// 返回 -1 表示猜小了,0 表示猜中了,1 表示猜大了
int guess(int mid) {
    if (mid == secret) return 0;   // 猜中了
    if (mid < secret) return -1;   // 猜小了
    return 1;                       // 猜大了
}

int main() {
    int lo = 1, hi = 100;   // 搜索范围:1 到 100
    int attempts = 0;        // 记录猜测次数

    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;  // 取中间值
        attempts++;

        int result = guess(mid);

        if (result == 0) {
            cout << "在第 " << attempts << " 次猜中了: " << mid << endl;
            break;
        } else if (result == -1) {
            cout << "第 " << attempts << " 次猜了 " << mid << " → 小了" << endl;
            lo = mid + 1;   // 答案在右半边
        } else {
            cout << "第 " << attempts << " 次猜了 " << mid << " → 大了" << endl;
            hi = mid - 1;   // 答案在左半边
        }
    }
    // 输出:
    // 第 1 次猜了 50 → 小了
    // 第 2 次猜了 75 → 大了
    // 第 3 次猜了 62 → 小了
    // 第 4 次猜了 68 → 小了
    // 第 5 次猜了 71 → 小了
    // 第 6 次猜了 73 → 猜中了: 73
    // 只用了 6 次就从 1~100 中找到了!

    return 0;
}

⚠️ 易错点

🚨 常见错误
  1. 死循环!:最常见的错误。如果 lo = mid 而不是 mid + 1,当 lo 和 hi 相邻时,mid 永远等于 lo,死循环!记住:lo = mid + 1、hi = mid - 1。
  2. mid 计算溢出:(lo + hi) / 2 当 lo + hi 超过 int 范围会溢出。用 lo + (hi - lo) / 2 更安全。
  3. while 条件写错:while (lo < hi) 和 while (lo <= hi) 是不同的!前者适合"找边界",后者适合"找具体值"。初学者建议统一用 <=。
  4. 数组不是有序的:二分查找的前提是数组有序!如果数组无序,二分查找的结果是错误的。记得先排序。
  5. 找不到时忘记处理:循环结束后如果没找到目标,要返回 -1 或其他特殊值表示未找到。
  6. 边界处理:数组只有一个元素、空数组等特殊情况要考虑到。写完后用这些边界测试一下。

📝 练习建议

💡 怎么练?
  1. 手画搜索过程:给一个小数组,用纸笔模拟每一步 lo、hi、mid 的变化,直到找到(或找不到)目标。
  2. 背熟模板:把标准二分查找的代码背下来,做到不看参考就能默写。注意 mid 的计算和更新方向。
  3. 练习"找边界":在有重复元素的有序数组中,找第一个等于 target 的位置、最后一个等于 target 的位置。
  4. 练习二分答案:从简单题开始,比如"求满足条件的最小/最大值"类问题。
  5. 推荐题目:洛谷 P2249(查找)、LeetCode 34(找左右边界)、LeetCode 875(爱吃香蕉的珂珂,二分答案入门)。