二分查找
二分查找在使用时必须满足必须采用顺序存储结构且必须按关键字大小有序排列,其时间复杂度为:O(log2n)
二分查找(binary search)从数组(有序)的中间开始搜索,逐步缩小搜索区间:
- 首先将要查找的元素数组中间元素相比,如果相等,则返回答案、搜索结束;
- 如果要查找的元素大于数组中间元素,则中间数字向左的所有数字都小于目标值,全部排除,搜索中间数字向右的一半数组;
- 如果要查找的元素小于数组中间元素,则中间数字向右的所有数字都大于目标值,全部排除,搜索中间数字向左的一半数组;

在二分查找中,目标元素的查找区间的定义十分重要,不同的区间的定义写法不一样:
- 左闭右闭
[left, right] - 左闭右开
[left, right)
题目1:704.二分查找

解法一(左闭右闭[left, right])
左闭右闭的区间要注意两点:
while (left <= right)要使用 <= ,因为left == right是有意义的,所以使用 <=if (nums[middle] > target) right要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
class Solution {
public:
int search(vector<int>& nums, int target)
{
int left = 0; //下标从0开始
int right = nums.size() - 1; //定义target在左闭右闭的区间里[left,right] nums.size()求数组中元素个数
while (left <= right) //当left == right时,区间[left, right]仍然有效
{
int mid = left + ((right - left) / 2); // 防止溢出 等同于(left + right)/2;int会向下取整
if (nums[mid] > target)
{
right = mid - 1; // target 在左区间,所以[left, middle - 1]
}
else if (nums[mid] < target) // target 在右区间,所以[middle + 1, right]
{
left = mid + 1;
}
else // nums[middle] == target
{
return mid; // 数组中找到目标值,直接返回下标
}
}
return -1; //未找到目标值
}
};

解法二(左闭右开[left, right))
左闭右开的区间要注意两点:
- 循环条件使用
while (left < right) if (nums[middle] > target), right = middle,因为当前的 nums[middle] 是大于 target 的,不符合条件,不能取到 middle,并且区间的定义是 [left, right),刚好区间上的定义就取不到 right, 所以 right 赋值为 middle。
class Solution {
public:
int search(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
{
int mid = left + ((right - left) >> 1); // >>右移运算符 向右移动一位相当于除2
if (nums[mid] > target)
{
right = mid; // target 在左区间,在[left, middle)中
}
else if (nums[mid] < target)
{
left = mid + 1; // target 在右区间,在[middle + 1, right)中
}
else // nums[middle] == target
{
return mid; // 数组中找到目标值,直接返回下标
}
}
return -1; // 未找到目标值
}
};

这里可以用一个三个数的数组去理解为什么要写right = middle
参考资料:
【二分查找】详细图解
题目2:35.搜索插入位置

首先这一题和前面704前半部分是一样的,重点是如果目标值不存在,要返回按顺序插入的位置

在数组中插入目标值,总共有四种情况:
- 目标值在数组所有元素之前
- 目标值等于数组中某一个元素
- 目标值在数组某两个数之间
- 目标值在数组所有元素之后
解法一(左闭右闭[left, right])
class Solution {
public:
int search(vector<int>& nums, int target)
{
int left = 0; //下标从0开始
int right = nums.size() - 1; //定义target在左闭右闭的区间里[left,right]
while (left <= right) //当left == right时,区间[left, right]仍然有效
{
int mid = left + ((right - left) / 2); // 防止溢出 等同于(left + right)/2;int会向下取整
if (nums[mid] > target)
{
right = mid - 1; // target 在左区间,所以[left, middle - 1]
}
else if (nums[mid] < target) // target 在右区间,所以[middle + 1, right]
{
left = mid + 1;
}
else // nums[middle] == target
{
return mid; // 数组中找到目标值,直接返回下标
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0, -1]
// 目标值等于数组中某一个元素 return middle;
// 目标值在数组某两个数之间 [left, right],return right + 1
// 目标值在数组所有元素之后的情况 [left, right], return right + 1
return right + 1; //return left;
}
};
逐一对四种四种情况进行分析:

1.目标值在数组所有元素之前:
假设target为0,按照左闭右闭left为0、rigth为4;
按照代码if (nums[mid] > target) {right = mid - 1; }运行后mid=2、right=1;
此时仍然满足left <= right,继续运行代码,此时mid=0(向下取整)、right=-1;
不再满足left <= right,没有找到target,此时left=0、right=-1,目标值在数组所有元素之前,返回值应该是0,即return right + 1。
2.目标值等于数组中某一个元素:
这种情况其实就是再数组中找到了目标元素,代码运行后会返回mid,而mid=right+1,即return right + 1。
3.目标值在数组某两个数之间:
假设target为9,按照左闭右闭left为0、rigth为4;
按照代码if (nums[mid] > target) {right = mid - 1; }运行后mid=2、left=3;
此时仍然满足left <= right,继续运行代码,此时mid=3(向下取整)、right=2;
不再满足left <= right,没有找到target,此时left=3、right=2,目标值在索引2和3之间,按顺序插入应该在索引2之后,即return right + 1。
4.目标值在数组所有元素之后:
与目标值在数组所有元素之前同理。
解法二(左闭右开[left, right))
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n; // 定义target在左闭右开的区间里,[left, right) target
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
return right;
}
};

这种写法的查找目标值与704第二种写法同理,返回插入顺序与35的第一种写法同理。
题目3:34.在排序数组中查找元素的第一个和最后一个位置

解法:二分查找
解题思路:
看到有序数组就要想想能不能用二分查找
寻找target在数组里的左右边界,有如下三种情况:
- 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2,此时应该返回{-1, -1}
- 情况二:target 在数组范围中,但数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
- 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
目标值左边界就是数组中第一个小于目标值的位置,右边界就是第一个大于目标值的位置
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int leftborder = getleftborder(nums, target);
int rightborder = getrightborder(nums, target);
if (leftborder == -2 || rightborder == -2) return { -1, -1 }; //target在数组范围的右边或者左边
if (rightborder - leftborder > 1) return { leftborder + 1, rightborder - 1 }; //target在数组范围中,且数组中存在target
return { -1, -1 }; //target在数组范围中,但数组中不存在target
}
int getrightborder(vector<int>& nums, int target) { //寻找target的右边界(不包括target)
int left = 0, right = nums.size() - 1;
int rightborder = -2; //记录rightborder没有被赋值的情况(即target在数组范围的左边,例如数组[3,4],target为2)
while (left <= right) {
int mid = left + ((right - left) / 2);
if (nums[mid] > target) {
right = mid - 1; //target在左区间,搜索区间为[left, middle - 1]
}
else if (nums[mid] <= target) { //nums[mid] < target,target在右区间,搜索区间为[mid + 1, right]
left = mid + 1; //直到找到nums[mid]==target,此时右边界为mid + 1(第一个比target大的数的索引)
rightborder = mid + 1; //这里是将大于和等于两种情况合并了,不合并也没有关系,其实就是找nums[mid]==target,然后将mid+1,就是第一个比target大的数的索引
}
}
return rightborder;
}
int getleftborder(vector<int>& nums, int target) { //寻找target的左边界leftborder
int left = 0, right = nums.size() - 1;
int leftborder = -2; //记录leftBorder没有被赋值的情况(即target在数组范围的右边,例如数组[3,4],target为5),为了处理情况一
while (left <= right) {
int mid = left + ((right - left) / 2);
if (nums[mid] >= target) { //nums[mid] > target,target在左区间,搜索区间为[left, middle - 1]
right = mid - 1; //直到找到nums[mid]==target,此时左边界为mid - 1(第一个比target小的数的索引)
leftborder = mid - 1;
}
else { //target在右区间,搜索区间为[mid + 1, right]
left = mid + 1;
}
}
return leftborder;
}
};
其实代码的思路就是找左右边界,怎么找?比如找右边界,如果找到目标值,并不返回,而是缩小数组的左边界,确保找到的最右边的目标值边界。
这里不明白的可以打上断点看看。
在代码中,getrightborder和getleftborder返回的是目标值的左右边界,而不是目标值的开始位置和结束位置,因此return { leftborder + 1, rightborder - 1 };,需要左边界加一、右边界减一
题目4:69.x的平方根

解题思路:
x的平方根的整数部分一定是满足k2<=x的最大k值,因此用二分查找
class Solution {
public:
int mySqrt(int x) {
int left = 0; //左边界
int right = x - 1;
int ans = 0; //平方根的整数部分
if(x == 0 || x == 1) {
return x;
}
while (left <= right) {
int mid = left + (right - left) / 2;
if ((long long) mid * mid <= x) { //long long防止有符号整数溢出
ans = mid;
left = mid + 1;
}
else {
right = mid - 1;
}
}
return ans;
}
};
题目5:367.有效的完全平方数

暴力解法
如果num是一个完全平方数,那么一定存在一个数满足x2=num,那么就遍历数组寻找这个数
class Solution {
public:
bool isPerfectSquare(int num) {
long x = 1;
long square = 1;
while (square <= num) {
if (square == num) {
return true;
}
x++;
square = x * x;
}
return false;
}
};
二分查找
如果正整数x满足x2=num,那么也一定满足1<=x<=num
class Solution {
public:
bool isPerfectSquare(int num) {
int left = 0;
int right = num;
while (left <= right) {
int mid = (right - left) / 2 + left;
long square = (long)mid * mid;
if (square < num) {
left = mid + 1;
}
else if (square > num) {
right = mid - 1;
}
else {
return true;
}
}
return false;
}
};
算法的时间复杂度:关于时间复杂度,你不知道的都在这里!
本系列刷题步骤:代码随想录
本文详细介绍了二分查找算法在解决数组查找问题中的应用,包括704.二分查找、35.搜索插入位置、34.在排序数组中查找元素的第一个和最后一个位置等题目。通过不同区间定义的解法对比,阐述了二分查找的实现细节和注意事项,并提供了针对不同场景的解题策略。此外,还涉及了69.x的平方根和367.有效的完全平方数的解题思路,展示二分查找在求解数学问题中的有效性。

4859

被折叠的 条评论
为什么被折叠?



