你说得对。你原文的风格确实更加自然、接地气,特别适合写在个人博客或者刷题笔记里。我的润色版本在表达上更偏向“技术文档”风格,确实失去了你原来的那种轻松、直白的语气。
那我们可以来一个**“轻度润色+保持原汁原味”**的版本,在不改你叙述节奏的基础上,把逻辑再捋顺一点、表达再清晰一点,同时保留你写作时的那种语感。下面是我调整后的版本:
引言
刷了很多道二分查找类型的题目后,今天来系统总结一下各种变形,尤其是边界处理的技巧。
二分查找的题目类型
大致可以分为两类:
- 最多有一个目标值:比如「搜索插入位置」—— 要么找到目标值,要么返回它应该插入的位置(也就是第一个比它大的元素)。
- 可能有多个目标值:比如「查找目标值的左右边界」—— 找到一段连续的目标值的起始和结束位置。
单个目标值的标准二分查找
如果目标值最多只出现一次,那就可以直接用标准的二分模板来处理。常见的判断逻辑如下:
int left = 0;
int right = nums.size() - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (nums[mid] < target)
{
// 小于目标值,排除左半部分
left = mid + 1;
}
else if (nums[mid] > target)
{
// 大于目标值,排除右半部分
right = mid - 1;
}
else
{
// 找到了目标值
return mid;
}
}
那如果目标值不存在呢?
我们来观察一下最后循环结束时 left
和 right
的位置。
- 当
nums[mid] < target
时,我们移动left = mid + 1
,相当于把所有小于目标值的排除掉了。也就是说,left
会指向第一个大于等于目标值的位置。 - 当
nums[mid] > target
时,我们移动right = mid - 1
,把所有大于目标值的排除掉了。也就是说,right
会指向最后一个小于等于目标值的位置。
这就解释了为什么在 Leetcode 35 题「搜索插入位置」中,直接返回 left
就是目标值应该插入的位置。
多个目标值:查找左右边界
当目标值可能有多个时,就不能一看到等于目标值就直接返回了。比如下面这个数组:
nums = [1, 6, 6, 6, 6, 6, 9]
target = 6
如果我们用标准的二分查找,第一次碰到 nums[mid] == target
就返回,是无法得到目标值的左边界或右边界的。
1. 查找左边界
要找到第一个等于目标值的位置,我们需要改一下条件:
- 如果
nums[mid] < target
,说明目标值在右边,移动左边界; - 如果
nums[mid] >= target
,说明目标值可能在左边,也可能刚好是当前这个位置,先收缩右边界。
int left = 0;
int right = nums.size() - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1;
else
right = mid - 1;
}
// 此时 left 指向的是第一个大于等于 target 的位置
2. 查找右边界
- 如果
nums[mid] <= target
,说明目标值还可能在右边,移动左边界; - 如果
nums[mid] > target
,说明目标值只能在左边,收缩右边界。
int left = 0;
int right = nums.size() - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (nums[mid] <= target)
left = mid + 1;
else
right = mid - 1;
}
// 此时 right 指向的是最后一个小于等于 target 的位置
特别注意:还得判断目标值到底存不存在
有时候数组里可能根本没有目标值,所以不能直接返回 left
和 right
,而要先判断一下:
if (left == nums.size() || nums[left] != target)
return {-1, -1}; // 没找到目标值
小结
这次做了很多二分查找的题目,最大的收获就是:控制判断条件,其实就是在控制最后 left 和 right 的位置。理解这一点之后,就能灵活地写出找左边界、右边界,甚至是插入位置的各种代码了。