二分查找是算法笔试、面试里最基础也高频考察的算法。它依托有序数组,通过不断折半收缩搜索区间,把线性查找 (O(n)) 的复杂度优化到 (O(log n)) 。但二分有多种变体,很多同学容易混淆**「基础二分」「找左边界」「找右边界」**的写法,一不小心就死循环或者边界出错。我们现在利用四道 LeetCode 经典二分例题,从思路、模拟、代码、复杂度完整梳理,区分不同二分模板,方便理解和背诵。
目录
[1. 题目](#1. 题目)
[2. 核心思路](#2. 核心思路)
[3. 举例模拟](#3. 举例模拟)
[4. 代码参考](#4. 代码参考)
[5. 复杂度](#5. 复杂度)
[1. 题目](#1. 题目)
[2. 核心思路](#2. 核心思路)
[3. 举例模拟](#3. 举例模拟)
[4. 代码参考](#4. 代码参考)
[5. 复杂度](#5. 复杂度)
[1. 题目](#1. 题目)
[2. 核心思路](#2. 核心思路)
[3. 举例模拟](#3. 举例模拟)
[4. 代码参考](#4. 代码参考)
[5. 复杂度](#5. 复杂度)
[1. 题目](#1. 题目)
[2. 核心思路](#2. 核心思路)
[3. 举例模拟](#3. 举例模拟)
[4. 代码参考](#4. 代码参考)
[5. 复杂度](#5. 复杂度)
一、二分查找
1. 题目
2. 核心思路
二分查找,针对有序升序数组,不断缩小查找区间:
- 定义左右边界
left、right,代表当前查找范围[left, right]; - 取中间位置
mid,拿nums[mid]和目标target对比:- 相等:直接找到,返回下标
mid; nums[mid] > target:目标一定在左半区间,收缩右边界right = mid - 1;nums[mid] < target:目标一定在右半区间,收缩左边界left = mid + 1;
- 相等:直接找到,返回下标
- 当
left > right,区间为空,说明不存在目标,返回-1。
前提:数组单调升序、无重复要求。
3. 举例模拟
示例:nums = [-1,0,3,5,9,12],target = 9 初始:left=0, right=5
第 1 轮:mid=(0+5)/2=2,nums[2]=3 < 9 → left=3
第 2 轮:mid=(3+5)/2=4,nums[4]=9 == 9 → 返回 4,结束
再模拟找不到的情况:target=2 初始:left=0, right=5
mid=2,nums2=3>2 → right=1
mid=0,nums0=-1<2 → left=1
mid=1,nums 1=0<2 → left=2 此时 left=2 > right=1,循环退出,返回 -1
4. 代码参考
cpp
class Solution {
public:
int search(vector<int>& nums, int target)
{
int left=0,right=nums.size()-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target)
{
return mid;
}
else if(nums[mid]>target)
{
right=mid-1;
}
else
{
left=mid+1;
}
}
return -1;
}
};
5. 复杂度
时间复杂度:(O(log n)),每次区间折半,n 是数组长度
空间复杂度:(O(1)),只使用常数额外变量,原地二分
二、在排序数组中查找元素的第一个和最后一个位置
1. 题目
2. 核心思路
这道题是二分查找左右边界(在有序数组里找目标值的起始、终止下标),不能只找一个位置。
- 数组升序有序,相等元素集中在一起,分两次二分:
- 第一次二分:找左边界 ,向下取整
mid = left + (right-left)/2。nums[mid] < target→ 左边界在右边left = mid+1;否则right=mid,不断往左压缩,锁定第一个等于 target 的位置。 - 校验:如果最后停下来的位置值不等于 target,说明数组不存在 target,直接返回
[-1,-1]。 - 第二次二分:找右边界 ,向上取整
mid = left + (right-left+1)/2,防止死循环。nums[mid] <= target→ 右边界在右边left=mid;否则right=mid-1,往右压缩,锁定最后一个等于 target 的位置。
- 第一次二分:找左边界 ,向下取整
- 两次二分结束,返回左边界和右边界下标。
3. 举例模拟
nums = [5,7,7,8,8,10], target = 8
**找左边界:**left=0, right=5,mid = 0+(5-0)/2 = 2,nums 2=7 <8 → left=3 现在 left=3,right=5 mid = 3+(5-3)/2 =4,nums 4=8 ≥8 → right=4 现在 left=3,right=4 mid=3+(4-3)/2=3,nums 3=8≥8 → right=3 循环结束 left=3,nums 3=8,有效,begin=3
**找右边界:**left=3,right=5 mid=3+(5-3+1)/2 = 4,nums 4=8 ≤8 → left=4 left=4,right=5 mid=4+(5-4+1)/2=5,nums 5=10>8 → right=4 循环结束 right=4
结果: [3,4]
4. 代码参考
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target)
{
if(nums.size()==0)
{
return {-1,-1};
}
int left=0,right=nums.size()-1;
//查找左端点
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]<target)
{
left=mid+1;
}
else
{
right=mid;
}
}
//没找到左端点就是不存在
if(nums[left]!=target)
{
return {-1,-1};
}
//标记左端点
int begin=left;
right=nums.size()-1;
//查找右端点
while(left<right)
{
int mid=left+(right-left+1)/2;
if(nums[mid]<=target)
{
left=mid;
}
else
{
right=mid-1;
}
}
return {begin,right};
}
};
5. 复杂度
时间复杂度:(O(log n)),两次二分查找,每次都是对数级,n 是数组长度
空间复杂度:(O(1)),只用常数变量,没有额外开辟数组
三、X的平方根
1. 题目
2. 核心思路
- 我们要找最大的满足 (mid^2 <= x) 的整数 mid ,属于二分找右边界模型,所以 mid 向上取整:
mid = left + (right-left+1)/2,避免死循环。 - 用
long long mid防止mid*mid溢出(int 相乘容易越界)。 - 判断:
- 如果 (mid^2 < =x):mid 是候选,答案在 mid 及右侧 →
left = mid - 如果 (mid^2 > x):mid 太大,答案在左侧 →
right = mid -1
- 如果 (mid^2 < =x):mid 是候选,答案在 mid 及右侧 →
- 循环结束
left == right,就是答案。 - 特判:(x<=0) 返回 0。
3. 举例模拟
(x=8),目标答案是 2(向下取整) 初始:left=1,right=8
- mid = 1+(8-1+1)/2 = 4,(4*=16>8) → right=3 left=1, right=3
- mid =1+(3-1+1)/2=2,(2*2=4\le8) → left=2 left=2, right=3
- mid =2+(3-2+1)/2=3,(3*3=9>8) → right=2 循环结束 left=right=2,返回 2
4. 代码参考
cpp
class Solution {
public:
int mySqrt(int x)
{
if(x<=0)
{
return 0;
}
int left=1,right=x;
while(left<right)
{
long long mid=left+(right-left+1)/2;
if(mid*mid <= x)
{
left=mid;
}
else
{
right=mid-1;
}
}
return left;
}
};
5. 复杂度
时间复杂度:(O(log x)),二分查找
空间复杂度:(O(1)),仅常数变量
四、搜索插入位置
1. 题目
2. 核心思路
要找第一个大于等于 target 的位置(左边界二分模板,向下取整 mid)。
- 两个前置 if 用来处理插在最前面、插在最后的边界;
- 二分区间
[0, nums.size()-1]; mid = left + (right-left)/2(向下取整)nums[mid] < target:mid 太小,答案在 mid 右边 →left = mid + 1nums[mid] >= target:mid 是候选,答案在 mid 及左边 →right = mid
- 循环结束
left == right,就是插入位置。
3. 举例模拟
样例:nums=[1,3,5,6], target=2 初始 left=0, right=3
- mid=0+(3-0)/2=1,nums1=3 ≥2 → right=1
- mid=0+(1-0)/2=0,nums 0=1 <2 → left=1 循环结束 left=1,返回 1
样例:nums=[1,3,3,5], target=3 left=0,right=3
- mid=1,nums1=3≥3 → right=1
- mid=0,nums 0=1<3 → left=1 返回 1 (正确,第一个 3 的下标)
4. 代码参考
cpp
class Solution {
public:
int searchInsert(vector<int>& nums, int target)
{
if(target<nums[0])
{
return 0;
}
if(target>nums[nums.size()-1])
{
return nums.size();
}
int left=0,right=nums.size()-1;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]<target)
{
left=mid+1;
}
else
{
right=mid;
}
}
return left;
}
};
5. 复杂度
时间复杂度:(O(log n))
空间复杂度:(O(1))
五、总结
这四道题目覆盖了二分的三类经典写法:
while(left <= right):基础二分,找到直接 return,适合数组无重复、只需要判断是否存在的场景;while(left < right)、mid 向下取整:左边界模板,用来找第一个满足条件的位置,对应 searchInsert、searchRange 左端点;while(left < right)、mid 向上取整:右边界模板,用来找最后一个满足条件的位置,对应 mySqrt、searchRange 右端点。
mid 的计算方式决定循环会不会死循环,区间开闭条件、边界收缩方向一定要配套。记住模板不要混用,遇到有序数组查找问题,先判断我们要找**「精确值」、「左边界」还是「右边界」**,再选择对应的二分框架。二分看似简单,但边界是细节重灾区,多模拟样例、理清区间含义,才能稳定写对。



