GESP 5级
二分查找
5级 · 基础算法与数据类型
📖 什么是二分查找?
假设你在一本按首字母排好序的电话簿里找"张三"。你不会从第一页开始翻——你会直接翻到中间,发现"李"在前面,然后翻到后半部分的中间……每次翻到中间,排除掉一半的不可能。
二分查找就是这个策略的算法版本:在一个有序数组中查找目标值,每次比较中间元素,把搜索范围缩小一半。
对比线性查找(一个一个找):
线性查找:100万个数,最坏比较 1000000 次
二分查找:100万个数,最多只需比较约 20 次!
(因为 2²⁰ ≈ 1000000,每次减半,20 次就只剩 1 个)
二分查找: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=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(溢出风险)
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. 重复直到找到最大的可行答案
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;
}
⚠️ 易错点
🚨 常见错误
- 死循环!:最常见的错误。如果
lo = mid而不是mid + 1,当 lo 和 hi 相邻时,mid 永远等于 lo,死循环!记住:lo = mid + 1、hi = mid - 1。 - mid 计算溢出:
(lo + hi) / 2当 lo + hi 超过 int 范围会溢出。用lo + (hi - lo) / 2更安全。 - while 条件写错:
while (lo < hi)和while (lo <= hi)是不同的!前者适合"找边界",后者适合"找具体值"。初学者建议统一用<=。 - 数组不是有序的:二分查找的前提是数组有序!如果数组无序,二分查找的结果是错误的。记得先排序。
- 找不到时忘记处理:循环结束后如果没找到目标,要返回 -1 或其他特殊值表示未找到。
- 边界处理:数组只有一个元素、空数组等特殊情况要考虑到。写完后用这些边界测试一下。
📝 练习建议
💡 怎么练?
- 手画搜索过程:给一个小数组,用纸笔模拟每一步 lo、hi、mid 的变化,直到找到(或找不到)目标。
- 背熟模板:把标准二分查找的代码背下来,做到不看参考就能默写。注意 mid 的计算和更新方向。
- 练习"找边界":在有重复元素的有序数组中,找第一个等于 target 的位置、最后一个等于 target 的位置。
- 练习二分答案:从简单题开始,比如"求满足条件的最小/最大值"类问题。
- 推荐题目:洛谷 P2249(查找)、LeetCode 34(找左右边界)、LeetCode 875(爱吃香蕉的珂珂,二分答案入门)。