学习内容来自《代码随想录》
本次任务描述:
704. 二分查找
题目建议: 大家能把 704 掌握就可以,35.搜索插入位置 和 34. 在排序数组中查找元素的第一个和最后一个位置 ,如果有时间就去看一下,没时间可以先不看,二刷的时候在看。
先把 704写熟练,要熟悉 根据 左闭右开,左闭右闭 两种区间规则 写出来的二分法。
题目链接:力扣
文章讲解:代码随想录
视频讲解:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili
704 二分查找(一看就会一做就废)
1 题目描述
给定一个 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]之间。
2 我的思路(小白勿喷)
//1、每次找到数组的中间值,将target和中间值n=nums[nums.length/2-1]进行比较
//2、如果target=n,则返回中间值的下标
//如果target<n,则将target和中间值左边的数组进行比较-递归,
//如果target>n,则将target和中间值右边的数组进行比较
//3、找到target则返回下标,没有则返回-1
问题:
1、理解了二分查找的思想,但是在实现的时候,想将原数组拆分成左右数组,这就涉及到如何拆分存储,没有想到用两个指针。
2、第二是在实现重复比较这一过程时,想到了递归来实现。
3 题解
3.1 思路
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
下面我用这两种区间的定义分别讲解两种不同的二分写法。
3.2 代码
3.2.1 方式一:左闭右闭的区间,也就是[left, right]
第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target)时, right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
- 同理,if (nums[middle] < target)时, left要赋值为 middle + 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的右区间开始下标位置就是 middle + 1
java版本
class Solution {
public int search(int[] nums, int target) {
//1、每次找到数组的中间值,将target和中间值进行比较
//2、如果target=n,则返回中间值的下标
//如果target<n,则将target和中间值左边的区间进行比较,
//如果target>n,则将target和中间值右边的区间进行比较
//3、找到target则返回下标,没有则返回-1
//由于nums是有序的,如果targe大于最大值或者小于最小值,直接返回,避免都次循环
if(target <nums[0] || target > nums[nums.length - 1]){
return -1;
}
int left = 0, right = nums.length - 1;
//定义target在左闭右闭的区间里,[left, right]
while (left <= right){//当left==right,区间[left, right]依然有效,所以用 <=
int mid = left + ((right - left)>>1);// 防止溢出 等同于(left + right)/2
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;//target 在右区间,所以[middle + 1, right]
else if (nums[mid] > target)
right= mid -1;// target 在左区间,所以[left, middle - 1]
}
return -1;
}
}
python 版本
class Solution:
def search(self, nums: List[int], target: int) -> int:
#左闭右闭思路,即[left,right]为有效区间
left = 0
right = len(nums) - 1 #[left,right]能涵盖整个数组。所以right下标为长度-1
if target < nums[left] or target > nums[right]:
return -1
while left <= right:
mid = left + (right - left)//2
if target == nums[mid]:
return mid
elif target < nums[mid]: #目标元素在数组左区间
right = mid - 1
elif target > nums[mid]: #目标元素在数组右区间
left = mid + 1
return -1
复杂度分析
时间复杂度为O(logN),
例如这个数组中一共有n=40个数据,每比较一次都会缩小一半范围,2^7=64 要比较7次,f(40)=log40=7
空间复杂度:O(1)
3.2.2 方式二: 左闭右开的区间,也就是[left, right)
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。
右指向的元素无效
有如下两点:
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)
java版本
class Solution {
public int search(int[] nums, int target) {
//1、每次找到数组的中间值,将target和中间值进行比较
//2、如果target=n,则返回中间值的下标
//如果target<n,则将target和中间值左边的区间进行比较,
//如果target>n,则将target和中间值右边的区间进行比较
//3、找到target则返回下标,没有则返回-1
//由于nums是有序的,如果targe大于最大值或者小于最小值,直接返回,避免都次循环
if(target <nums[0] || target > nums[nums.length - 1]){
return -1;
}
int left = 0, right = nums.length;//因为右开,右指向的元素无效,故right不能指向最后一个有效元素,否则可能会比对不到最后一个元素,应该指向最后一个元素的后一位 right= nums.length
//定义target在左闭右闭的区间里,[left, right)
while (left < right){//当left==right,区间[left, right)无效,所以用 <
int mid = left + (right - left)/2;// 防止溢出 等同于(left + right)/2
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;//target 在右区间,在[middle + 1, right)中
else if (nums[mid] > target)
right= mid ;//target 在左区间,在[left, middle)中
}
return -1;
}
}
python 版本:略
复杂度分析
时间复杂度为O(logN)
空间复杂度:O(1)O(1)
4 总结
4.1 两种区间影响的区别点
1) 右指针所指的元素不同,原则是右指针表达的位置要包含所有有效数据,否则导致遗漏比较
如果是[left, right],则右指针指向要搜寻范围的最后一个元素
如果是[left, right),则右指针指向要搜寻范围的最后一个元素的后一个位置
2) 循环范围
如果是[left, right],循环范围是while (left <= right),因为当left==right,区间[left, right]依然有效,不至于遗漏比较
如果是[left, right),循环范围是while (left <right),因为当left==right,区间[left, right)是无效区间,不用循环了。
影响代码
[left, right]:
while (left <= right){
int left = 0, right = nums.length - 1;
right = mid - 1;
left = mid + 1;//左指针一样,指向新区间的第一个元素
}
[left, right):
while (left < right){
int left = 0, right = nums.length;
right = mid;
left = mid + 1
}
4.2 (容易忘记的前置处理)因为是有序数组,进入循环前,先判断一下要搜寻的target值有没有超过要查询的范围,可以减少循环
if(target <nums[0] || target > nums[nums.length - 1]){
return -1;
}
4.3 如何写两个指针下标的中间位置的下标
int mid = left + (right - left)/2;// 防止溢出 等同于(left + right)/2
或 int mid = left + (right - left)>>1