须知
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力! 👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力! 🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++算法感兴趣的朋友,让我们一起进步!
二分查找(Binary Search)是一种经典的算法,广泛应用于计算机科学中,尤其在处理有序数据时。其重要性体现在以下几个方面:
二分查找是一种在 有序数组 中查找目标值的算法。通过每次将查找范围缩小一半,逐步逼近目标值的位置。
定义核心:
核心思想可以概括为“分而治之”:
二分查找的基本步骤
left
和 right
,初始为数组的起点和终点。mid = left + (right - left) / 2
计算中间索引,避免溢出。right = mid - 1
。left = mid + 1
。left > right
时,说明目标值不存在,结束搜索。用于在 有序数组 中查找某个目标值。
int binarySearch(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) { // 搜索区间为 [left, right]
int mid = left + (right - left) / 2; // 避免溢出
if (nums[mid] == target) {
return mid; // 找到目标值,返回索引
} else if (nums[mid] < target) {
left = mid + 1; // 缩小到右半部分
} else {
right = mid - 1; // 缩小到左半部分
}
}
return -1; // 未找到目标值
}
特点:
left
>
right
时,说明目标值不存在。查找目标值的 最左位置。
int lowerBound(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) { // 搜索区间为 [left, right)
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1; // 缩小到右半部分
} else {
right = mid; // 缩小到左半部分
}
}
return left; // 返回左边界
}
查找目标值的 最右位置。
int upperBound(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) { // 搜索区间为 [left, right)
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1; // 缩小到右半部分
} else {
right = mid; // 缩小到左半部分
}
}
return left - 1; // 返回右边界索引
}
特点:
同时查找目标值的左、右边界(适用于数组中目标值重复出现)。
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
// 查找左边界
int start = -1, end = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
if (left < nums.size() && nums[left] == target) {
start = left;
} else {
return {-1, -1}; // 目标值不存在
}
// 查找右边界
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 - 1;
}
}
end = right;
return {start, end};
}
特点:
[start, end]
。{-1, -1}。
适用于需要在范围内找到满足某种条件的最优解。
int binaryAnswer(int low, int high, function<bool(int)> condition) {
while (low < high) {
int mid = low + (high - low) / 2;
if (condition(mid)) {
high = mid; // 满足条件,收缩右边界
} else {
low = mid + 1; // 不满足条件,收缩左边界
}
}
return low; // 返回最小满足条件的值
}
特点:
condition(mid)
判断是否满足目标。right
:
[left, right]
区间通常用于包含上下界查找。[left, right)
区间通常用于统一逻辑,更适合下界查找。mid
计算防止溢出:
mid = left + (right - left) / 2
。while (left <= right)
和 while (left < right)
需区别对待。题目描述:
3.1 算法思路:
核心思想
利用 二分查找算法,在有序数组中查找目标值。每次通过比较中间值与目标值的大小,缩小搜索范围,直到找到目标值或搜索范围为空。
left
初始化为数组起点 0
。right
初始化为数组终点 nums.size() - 1
。left <= right
时: mid = left + (right - left) / 2
(避免整数溢出)。nums[mid] == target
,直接返回索引 mid
。nums[mid] < target
,目标值在右半部分,将 left
更新为 mid + 1
。nums[mid] > target
,目标值在左半部分,将 right
更新为 mid - 1
。-1
。class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) { // 搜索区间为 [left, right]
int mid = left + (right - left) / 2; // 计算中点,防止溢出
if (nums[mid] == target) {
return mid; // 找到目标值,返回索引
} else if (nums[mid] < target) {
left = mid + 1; // 目标值在右半部分
} else {
right = mid - 1; // 目标值在左半部分
}
}
return -1; // 未找到目标值
}
};
示例 1:
输入:nums = [-1, 0, 3, 5, 9, 12], target = 9
输出:4
执行过程:
left = 0
, right = 5
。mid = (0 + 5) / 2 = 2
,nums[mid] = 3 < 9
,更新 left = 3
。mid = (3 + 5) / 2 = 4
,nums[mid] = 9 == 9
,返回索引 4
。边界条件分析
nums
为空,right = nums.size() - 1 = -1
,循环直接跳过,返回 -1
。left > right
,返回 -1
。暴力解法
暴力解法的核心是直接遍历数组 nums
中的每一个元素,逐一与目标值 target
进行比较,直到找到目标值或遍历结束。
-1
表示目标值不存在。class Solution {
public:
int search(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == target) {
return i; // 找到目标值,返回索引
}
}
return -1; // 未找到目标值
}
};
时间复杂度:
空间复杂度:
暴力解法的优缺点
优点:
缺点:
题目链接:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
题目描述:
4.1 算法思路:
这道题的核心是利用 二分查找 在排序数组中分别找到目标值的左右边界。
1. 核心思想
target
的元素的索引)。
target
的元素的索引)。
[-1, -1]
。
2. 使用两次二分查找
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
// 查找左边界
int left = 0, right = nums.size() - 1;
int leftIndex = -1, rightIndex = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
if (left < nums.size() && nums[left] == target) {
leftIndex = left;
} else {
return {-1, -1}; // 如果左边界不存在,直接返回
}
// 查找右边界
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 - 1;
}
}
if (right >= 0 && nums[right] == target) {
rightIndex = right;
}
return {leftIndex, rightIndex};
}
};
详细步骤
left = 0
,right = nums.size() - 1
。target
的位置: nums[mid] < target
,则目标值在右侧,调整 left = mid + 1
。nums[mid] >= target
,则调整 right = mid - 1
。nums[left]
是否等于 target
,如果相等,返回 left
,否则返回 -1
。left = 0
,right = nums.size() - 1
。target
的位置: nums[mid] > target
,则目标值在左侧,调整 right = mid - 1
。nums[mid] <= target
,则调整 left = mid + 1
。nums[right]
是否等于 target
,如果相等,返回 right
,否则返回 -1
。示例解析
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
执行过程:
left = 0, right = 5
。mid = 2
,nums[2] = 7 < 8
,更新 left = 3
。mid = 4
,nums[4] = 8
,更新 right = 3
。mid = 3
,nums[3] = 8
,更新 right = 2
。3
。left = 0, right = 5
。mid = 2
,nums[2] = 7 < 8
,更新 left = 3
。mid = 4
,nums[4] = 8
,更新 left = 5
。mid = 5
,nums[5] = 10 > 8
,更新 right = 4
。4
。输出:
[3,4]
[-1, -1]
。题目链接:35. 搜索插入位置 - 力扣(LeetCode)
题目描述:
5.1 算法思路:
核心思想:二分查找
由于数组是升序排序的,因此可以使用二分查找在 O(logn) 时间复杂度内找到目标值的插入位置。
class Solution
{
public:
int searchInsert(vector<int>& nums, int target)
{
// 定义左右边界
int left = 0, right = nums.size() - 1;
// 特殊情况:如果目标值大于数组中所有元素,则插入到数组末尾
if (nums[right] < target)
return right + 1;
// 二分查找
while (left < right)
{
// 计算中间位置,避免溢出
int mid = left + (right - left) / 2;
// 如果中间值小于目标值,目标值在右半部分
if (nums[mid] < target)
left = mid + 1; // 调整左边界
else
// 如果中间值大于等于目标值,目标值可能在左半部分
right = mid; // 调整右边界
}
// 最终 left == right,返回插入位置
return left;
}
};
nums[right] < target
):
while (left < right)
循环,通过不断缩小区间找到目标值或插入位置。mid = left + (right - left) / 2
计算中点,避免 left + right
的溢出问题。nums[mid] < target
时,说明目标值在右半部分,更新左边界 left = mid + 1
。nums[mid] == target
,更新右边界 right = mid
。left == right
,此时 left
即为目标值所在位置或插入位置。暴力解法的核心是 直接遍历数组:
target
进行比较。这种方法时间复杂度是 O(n),适合小规模数组。
class Solution
{
public:
int searchInsert(vector<int>& nums, int target)
{
for (int i = 0; i < nums.size(); i++)
{
// 如果找到第一个大于或等于目标值的位置,返回索引
if (nums[i] >= target)
return i;
}
// 如果目标值比所有元素都大,返回数组长度
return nums.size();
}
};
示例 1:
输入:nums = [1, 3, 5, 6], target = 5 输出:2
执行过程:
nums[0] = 1
,1 < 5
,继续。nums[1] = 3
,3 < 5
,继续。nums[2] = 5
,5 >= 5
,返回索引 2
。时间复杂度:
0
,时间复杂度为 O(1)。空间复杂度:
优缺点分析
优点:
缺点:
left
即为插入位置。题目链接:69. x 的平方根 - 力扣(LeetCode)
题目描述:
我们要找到满足以下条件的最大整数 y
:
利用 二分查找 来寻找平方根:
[1, x]
(因为 x\sqrt{x}x 最大不会超过 x
本身)。
mid
,计算 mid2 与 x
比较:
left = mid
。right = mid - 1
。left
指向平方根的整数部分。
class Solution
{
public:
int mySqrt(int x)
{
if (x < 1) return 0; // 特殊情况处理
int left = 1, right = x; // 定义二分查找的边界
while (left < right)
{
long long mid = left + (right - left + 1) / 2; // 向上取整计算中点
if (mid * mid > x)
right = mid - 1; // 排除右半部分
else
left = mid; // 更新左边界,保留 mid
}
return left; // 最终返回平方根的整数部分
}
};
版本2:
class Solution
{
public:
int mySqrt(int x)
{
if (x == 0) return 0; // 直接处理特殊情况
int left = 1, right = x;
while (left <= right) // 使用闭区间 [left, right]
{
int mid = left + (right - left) / 2;
if (mid <= x / mid) // 避免溢出
left = mid + 1; // 更新左边界
else
right = mid - 1; // 更新右边界
}
return right; // 返回右边界,即平方根整数部分
}
};
[1, x]
,因为平方根不会小于 1 且不会大于 x
(对于 x≥1)。mid = left + (right - left + 1) / 2
来计算中点,避免直接加法可能导致的溢出。right = mid - 1
,排除当前的 mid
和右侧部分。left = mid
,保留当前的 mid
。left == right
,此时 left
即为平方根的整数部分。暴力解法
class Solution
{
public:
int mySqrt(int x)
{
if (x == 0) return 0; // 特殊情况处理
int y = 0;
while ((long long)y * y <= x) // 防止溢出
{
y++;
}
return y - 1; // 返回满足条件的最大整数
}
};
时间复杂度
0
遍历到 x\sqrt{x},因此时间复杂度为 O(x\sqrt{x})。
空间复杂度
y
和计算,不需要额外空间。优缺点分析
优点
缺点
long long
类型,会因为 y2 的溢出问题导致错误结果。[1, x]
中寻找满足 y2≤x 的最大整数 y
。x = 0
和 x = 1
等特殊情况。mid <= x / mid
避免溢出问题。通过上述例题分析可以看出,二分查找的核心用途是 快速缩小搜索范围,解决有序数据、边界条件和单调问题等场景。在实际问题中,熟练掌握二分查找模板和变体是解决高效算法问题的关键。
路虽远,行则将至;事虽难,做则必成
亲爱的读者们,下一篇文章再会!!!