【算法篇】二分查找算法:基础篇
题目链接:
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
当left=right的时候,就是最终结果,无需判断
当我们在题目中发现这个数组有二段性的时候,就可以使用二分查找算法了
我们不能同时查找开始位置和结束位置,所以我们就先开始去查找第一个位置;
也就是开始位置:
第一步:查找区间的左端点:
根据目标值target把整个数组划分为两个部分:
把数组分为两个部分,
- 左边全部都是小于t
- 右边全部都是大于等于t
定义一个left下标指向数组的左端点,定义一个right下标指向数字的右端点,也就是指向数组的最后一个位置
接着定义一个mid中间值,把这个中间值设置为x,
分情况讨论:
1: x < t:
当这个中间值小于t的时候,落于左边的区间中,也就是没有t的区间中,此时就需要让left向右移动了,让left = mid + 1,去新的区间【left,right】,中寻找新的结果:
2: x >= t:
此时把大于和等于合在一起,不要分情况讨论了,注意,我们目前要寻找的是左边的端点,所以当中间值mid >= t的时候,就让right向左移动去寻找左端点,但是不是让rihgt = mid - 1了,万一这个mid 刚好是左端点,此时就应该让right = mid即可,更新完毕之后,就会去新的区间【left,right】中寻找了
难点是接下来的细节处理:
第一个细节:
循环的条件:我们什么时候去执行这个循环
这个循环条件有两个可以选择:
- left <= right
- left < right
此时我们只能选择第二种循环条件,因为第一种会陷入死循环:
这个循环条件中,我们不能让left = right
我们分情况来讨论:
第一种情况:
在这个【left,right】区间中含有这个taget:
![[Pasted image 20250514151027.png]]
第二种情况:
在这个【left,right】区间中,全部都是大于target的:
![[Pasted image 20250514151844.png]]
第三种情况:
这个【left,right】区间中所有的值全部都小于target:
![[Pasted image 20250514152325.png]]
上面我们分了三种情况去讲解为什么不需要在left = right的时候去再次执行这个循环,
下面去讲解一下为什么不可以在循环条件中加上left = right
因为一旦加上了这个left =right,就会导致死循环
在刚刚的第一种情况中会陷入死循环:
![[Pasted image 20250514153354.png]]
所以这就是为什么在循环条件中不可以加入left = right的原因
第二个细节:
求中点的操作:
这个中点mid应该如何去求出来:
之前我们去计算中点mid有两种方式:
- mid = left + (right - left) / 2
- mid = left + (right - left +1) / 2
第一种求中点的位置是求的靠左的位置
第二种求中点的位置是求的是靠右的位置
当使用第二种情况去求出右端点的时候会导致死循环:
当最终的时候,只剩下两个元素的时候:
![[Pasted image 20250514154704.png]]
所以我们求中点不可以使用第二种方法,只能去使用第一种方法了
所以以上就是求出左端点的全部过程了
第二步:查找区间的右端点
继续根据目标值把数据划分为两个部分:
- 左边的部分:小于等于target : 移动左指针,left = mid
- 右边的部分:大于target:移动右指针:right = mid - 1
之前讲述过,求中点的计算方式一共有两种:
- mid = left + (right - left) / 2
- mid = left + (right - left +1) / 2
但是中点mid的计算方式不同:
![[Pasted image 20250514160645.png]]
所以在查找区间的右端点的时候,计算中点的方式就是:
mid = left + (right - left +1) / 2
下面就是这道题目的具体代码了:
class Solution {public int[] searchRange(int[] nums, int target) {int[] ret = new int[2];ret[0] = ret[1] = -1;if(nums.length == 0){return ret;}int left = 0,right = nums.length-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 ret;}else{ret[0] = left;}left = 0;right = nums.length-1;while(left <right){int mid = left+(right - left+1) /2;if(nums[mid] <= target){left = mid;}else{right = mid-1;}}if(nums[left] != target){return ret;}else{ret[1] = right;}return ret;}
}
下面去按照8步归纳法:
第一步:用自己的话,描述一下眼前的这个问题,它是一个怎么样的问题?
给定一个数组,和目标值,在这个数组中找出目标值的开始下标和结束下标。
第二步:这个问题有哪些特征,能让我们去判断,它属于这一类的问题?
这个数组是可以划分为两段的,分开来看,先去找左端点,就会将这个数组划分为小于target的区间和大于 等于target的区间,就可以根据这个二段性去使用二分查找方法解决问题了
第三步:想要解决这类问题,切入点啥,即第一步我们要从哪里开始?
切入点,先去查找左端点,方法:根据这个target将数组划分为两段,第一段是小于target的区间,第二段是大于等于目标值的区间,然后使用二分找出这个左端点
第四步:解决这个问题的流程是怎么样的?这里说的流程是指,这道题可以分成12345…步,只要按照12345这个顺序做下去,我们就能解决这个问题。
- 先去找左端点,根据target将数组分为小于target的区间和大于等于target的区间,使用二分查找方法找出左端点,定义出左右下标,如果这个mid的值是小于target的,那就是位于小于target的区间,此时就让left = mid+1 、, 如果这个mid的值是大于等于target的,那么这个mid的值是位于大于target的区间的,那就让right = mid 即可
- 当left == right的时候,就说明此时这个left == right值就是目标值,这个left/right就是左端点了
- 根据第一步,即可求出左端点,如果没有左端点,那就返回-1
- 下面来求出右端点:方法一样,定义出左右下标,使用二分,根据这个target将这个数组划分为两个区间,一个是小于等于target的区间,一个是大于target的区间,使用二分查找方法,如果mid的值 <= target,那就是位于<=target的区间,那就让left = mid ,如果mid的值>target,那就让right =mid -1
- 最后遍历下来,当left= right的时候,循环结束,此时left = right的位置就是右端点了
- 最后使用一个两个长度的数组将这个左端点和右端点的下标放进去返回即可
第五步:在流程的12345步中,每一步的目标是什么(就是要求到些什么)?每一步需要用到哪些知识
- 当数组可以被划分为二段的时候,就可以使用二分查找方法
- 如果左区间是 <target,右区间是>= target的话,那么此时如果mid的值是位于左区间的,就left = mid+1,如果mid的值是位于右区间的,那就right = mid
- 此时这个中点的求法:mid = left + (right- left) / 2
- 当去求出右端点的时候,区间划分是< target的区间和 >= target的区间,此时这个中点的求法:mid = left + (right - left +1) /2 ;
- 如果最后没有找到,那就直接返回一个只有两个元素的数组即可
- 这个结果数组初始化为两个元素,里面是两个-1,如果一开始这个参数中的目标数组中就是一个元素都没有,那就直接返回这个只有两个-1的结果数组
第六步:要思考在运用这些知识和技巧的时候,有些需要注意的地方。
错误的区间划分方式
第一步:求出左端点的过程中,一定是要先划分出小于target区间和大于等于target区间的,不可以反回来,也就是在求左端点的时候一定不可以划分出小于等于target的区间和大于target的区间,这样是错误的方法,因为如果在求左端点的时候,按照这个错误的方法划分出了区间,会使得:
原因如下:
这种划分区间方式会导致左端点查找逻辑失效。主要问题在于当nums[mid] == target
时,你将 mid 赋值给 left,这可能会跳过真正的左端点。
在找左端点的过程中,按照这种错误的思路:
假设我们有数组[5,7,7,8,8,10]
,目标值target=8
。正确的左端点索引是 3。
- 当
nums[mid] <= target
时,令left = mid
- 当
nums[mid] > target
时,令right = mid-1
在第一次二分查找时:
- 初始区间
[0,5]
,mid=2,nums[2]=7
,满足nums[mid] <= target
,于是left=2
- 新区间
[2,5]
,mid=3,nums[3]=8
,满足nums[mid] <= target
,于是left=3
- 新区间
[3,5]
,mid=4,nums[4]=8
,满足nums[mid] <= target
,于是left=4
- 新区间
[4,5]
,mid=4,nums[4]=8
,满足nums[mid] <= target
,于是left=4
- 此后陷入死循环,无法找到左端点 3
区间划分方式无法保证左端点被包含在搜索区间内。当nums[mid] == target
时,真正的左端点可能在 mid 的左侧,但你的代码却将搜索范围向右移动,导致左端点被跳过。
第七步:要解决这个问题,最终的目标是啥?也就是说,我最终要求出的是啥?
最终的目标是求出目标数组中的含有目标值的数组下标
第八步:重新对上面的第一步至第七步,进行回顾和揣摩(包括问题类型,特征,切入点,解决过程,1234567…步,每步需要用的哪些知识方法)
(2)AI+8步归纳结合
在模仿完题目后,先自己用8步归纳法对错题进行归纳,然后让deepseek,按照8步归纳法的原则,对题目进行总结归纳,你再把自己总结归纳出的东西,跟AI对比,看看有哪些地方可以改进。