二分查找总结
二分查找
二分查找是一个非常基础的算法,它的原理很简单,但是写起代码来却很容易出错,尤其是在区间的范围定义时。最近刷《代码随想录》,对二分查找有了更加全面细致的理解,写一个自己的总结。在这里非常感谢《代码随想录》对我这个菜鸡的帮助。
前提条件
- 使用二分查找时数组必须是有序的
- 数组中没有重复元素,否则返回下标可能不一致
边界条件(区间定义)
二分查找的边界定义有两种,这里如果没有理解透彻的话,很容易写出错误的代码
要根据查找区间的定义来进行边界处理
边界定义的不同直接影响循环条件和 right 的更新
-
左闭右闭,即 [left, right] ,也就是 right 下标包含在我们要查找的数组中
-
此时循环条件为 while (left <= right) ,因为 left == right ,即 [left, left] 是有意义的
-
更新 right 时,right = mid - 1,因为 mid 所在下标已经查找过, 要查找 [left, mid - 1]的范围
-
-
左闭右开,即 [left, right), right 下标不包含在我们要查找的数组中
-
此时循环条件为 while (left < right) ,因为left == right ,即 [left, left)是没有意义的
-
更新 right 时,right = mid,因为 要查找 [left, mid) 的范围, 这个范围不包括 mid 下标
-
更新 left 时 ,都是 left = mid + 1
防止溢出
我们看网上很多的代码计算 mid 时
不是 mid = (left + right)/ 2 ,
而是 mid = left + (right - left)/2
两者是等价的,但是前者当 left 和 right 数值都很大时,容易溢出,后者则不会
leetcode相关题目
704. 二分查找
问题描述:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。
这是个二分查找的基本问题,给出两种写法,后面的题目默认只给出左闭右闭的写法
-
左闭右闭写法
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; //没找到返回-1 } };
-
左闭右开写法
class Solution { public: // 左闭右开写法 int search(vector<int>& nums, int target) { int left = 0, right = nums.size(); //区别1:左闭右开区间 while (left < right) { //区别2:判断条件 int mid = left + (right-left)/2; if (nums[mid] == target) { return mid; } else if (nums[mid] > target) { right = mid; //区别3:right处理 } else { left = mid + 1; } } return -1; } };
69. Sqrt(x)
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
提示:
0 <= x <= 231 - 1
这道题目规定了不能使用指数函数和算符,我们想到通过遍历整数的平方,整数是有序的,可用二分查找
这道题目要写出没有问题的代码要注意这几点
- left 要初始化为 1 而非 0 或者特殊处理输入为0或1的情况
- mid * mid == x会溢出, 要用 mid == x / mid (mid 和 target都是整数)
- 如果找到 mid ==x / mid 时返回mid,没找到就返回最大的平方小于 x 的整数 ,即 right。
class Solution {
public:
int mySqrt(int x) {
// left 一定需要是从 1 开始,原因是当输入是 x=1 时,计算出的mid=0,计算 x/mid 时会报错。
// 另外,当输入是 大于等于 1 时也不影响结果计算,当输入是 0 时,输出是 right(0) ,所以也是合理的。
int left = 1, right = x;
//左闭右闭区间
while (left <= right) {
int mid = left + (right - left) / 2;
if (mid > x / mid) {
right = mid - 1;
}
else if (mid < x/mid) {
left = mid + 1;
}
else if (mid == x/mid) {
return mid;
}
}
// 没找到就返回最大的一个平法小于x的值
return right;
}
};
367. 有效的完全平方数
给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
进阶:不要 使用任何内置的库函数,如 sqrt 。
**
示例 1:
输入:num = 16
输出:true
示例 2:
输入:num = 14
输出:false
提示:
1 <= num <= 2^31 - 1
这题与上一题类似,只不过要判断余数是否为零
class Solution {
public:
bool isPerfectSquare(int x) {
int left = 1,right = x;
while (left <= right) {
int mid = left + (right - left)/2;
if (mid == x/mid) {
if (x % mid == 0) { //是否整除
return true;
}
return false;
} else if (mid > x/mid) {
right = mid - 1;
} else if (mid < x/mid) {
left = mid + 1;
}
}
return false;
}
};
35. 搜索插入位置
问题描述:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
示例 1:
*输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0
示例 5:
输入: nums = [1], target = 0
输出: 0
这道题和上一题区别在于,如果没有找到返回插入位置,而不是返回-1。
没有找到最后要返回什么呢?我们考虑以下几种情况
- 查找的元素比数组中所有元素都小,最后 left = 0 和 right = -1 ,我门返回0
- 查找的元素比数组中所有元素都大, 最后left = n + 1 (n为元素个数), right = n
- 查找的元素大小位于数组区级,最后 left = right + 1
所以返回left
class Solution {
public:
//左闭右闭区间
int searchInsert(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;
}
}
// 结束时 left = right + 1
return left;
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109
-
通过二分查找先找到目标值,然后前后遍历,找到起始位置和结束位置,时间复杂度为 O(nlogn)
class Solution { public: vector<int> searchRange(vector<int>& nums, int target) { int right = nums.size() - 1; vector<int> res(2, -1); //初始化起始和结束位置,-1为没找到 int left = 0; //左闭右闭区间 while (left <= right) { int mid = left + (right - left)/2; if (nums[mid] == target) { int i = mid, j = mid; while (i >= 0 && nums[i] == target) {//找到目标值之后,循环遍历寻找起始位置 i--; } while (j < nums.size() && nums[j] == target) {//循环遍历寻找结束位置 j++; } res[0] = i + 1; res[1] = j - 1; return res; } else if (nums[mid] > target) { right = mid - 1; } else { left = mid + 1; } } return res; } };
-
直接通过二分查找找到起始位置和结束位置,时间复杂度为 O(logn)
class Solution { public: vector<int> searchRange(vector<int>& nums, int target) { int right = nums.size() - 1; vector<int> res(2, -1); int left = 0; int rightborder = -2; //记录一下rightborder没有被赋值的情况 int leftborder = -2; //记录一下leftborder没有被赋值的情况 //寻找右边界 while (left <= right) { int mid = left + (right - left)/2; if (nums[mid] > target) { right = mid - 1; } else { left = mid + 1; rightborder = left; } } //寻找左边界 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; leftborder = right; } } //情况一 :寻找的数字大小在数组之外 if (leftborder == -2 || rightborder == -2) { return res; } //情况二 :寻找的数字大小在数组范围内,且存在 if (rightborder - leftborder > 1) { res[0] = leftborder + 1; res[1] = rightborder - 1; return res; } //情况三 :寻找的数字大小在数组范围内,但不存在 return res; } };
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程