最基本的二分查找题目:
给定一个 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 中的所有元素是不重复的。
-
1 <= nums <= 2^31 - 1
思路
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不
是唯一的( 有些题目 元素可以重复,片尾有力扣练习题),这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如
- 到底是
while(left < right)
还是while(left <= right)
, - 到底是
right = middle
呢,还是要right = middle - 1
呢?
写二分法经常写乱,主要是因为**对区间的定义没有想清楚。非递归写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。就是while( 条件中是否满足等号 )
二分法第一种写法,
第一种写法,我们定义 target 是在一个在左闭右闭的区间里 ,也就是[left, right] , 会检查上边界,当 letf = right 的时候也进行判断,
最后如果未查到target 退出while的时候 则 right +1 = letf 就是 下标 letf在right后面 ,这时候 返回的 right +1 = letf 是与target最近的 大的元素下标。
比较好好理解的 即 吧区间分为 [left, mid ] mid [mid + 1 , right]
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
//当left==right,区间[left, right]依然有效,所以用 <=
while (left <= right) {
// 防止溢出 等同于(left + right)/2 因为 在力扣上大部分题目 都会取到int 的整数上线部分 2^32 -1
int mid = left + ((right - left) >> 1);
if (nums[mid] > target){
right = mid - 1; //这里是与 while 判断是 重点
}else if (nums[mid] < target){
left = mid + 1;
}else {
return mid;
}
}
return -1;
}
二分法第二种写法
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。吧区间分成[letf ,mid)
[mid ,right] 本次检查的时候不会检查上边界!!!即当只有两个元素的时候 如果 mid = 下元素角标,如果下元素不等于 那么 默认 返回
mid+1那个元素的角标 即 right不会检查。
最后如果未查到target 退出while的时候 则 right letf ,这时候 返回的是与target最近的 大的元素下标。
这个写法区间分为[letf , mid] ,[mid + 1 , right]
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
//不检测left==right,即区间[left, right)依然有效,所以用 <
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (arr[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else {
left = middle + 1; // target 在右区间,在[middle, right)中
}
}
return letf; //如果查找到 left 和 right 是一样的
}
}
相关题目推荐 力扣
- 35.搜索插入位置(opens new window)
- 考前面几个方法最后放回的letf 与 right 之间的关系
- 34.在排序数组中查找元素的第一个和最后一个位置
- 与经典二分查找类似,只是需要找到nums[mid] = target 后需要找到前后边界
- 69.x 的平方根
- 如果浮点数的 几次方根,注意一下mid 要用 float 定义
- 367.有效的完全平方数
- 剑指 Offer II 076. 数组中的第 k 大的数字
- 这个 也是 二分的实现 不过 mid 不等于 (left + right ) / 2 而是 mid = 快速排序一轮后 返回的下标
- 只要是吧问题分成两个区间,不断缩小查找范围都可以考虑用到二分实现
class Solution {
public int findKthLargest(int[] nums, int k) {
//倒数K大 也就等于第K小
k = nums.length - k;
//随便取一个数 初始化
int key= nums[k];
int postion = -1;
int left = 0 , right = nums.length-1;
while(postion != k){
postion = partition(nums, left, right);
//只有 postion 比 k大的时候 对于左边排序才有意义 右边不需要排序 我们只是找
if(postion > k ){
right = postion -1;
partition(nums, left, right);
} else if (postion < k) {
left = postion + 1;
partition(nums , left ,right );
} else {
//下标为K的时候 返回key
key = nums[k];
}
}
return key;
}
// 快排 因为快排每次排序完成后都会 有一个数必定排序完成 利用这个特性进行选择
private int partition(int[] nums, int lo, int hi) {
int v = nums[lo];
int i = lo, j = hi + 1;
while (true) {
while (++i <= hi && nums[i] < v);
while (--j >= lo && nums[j] > v);
if (i >= j) {
break;
}
int t = nums[j];
nums[j] = nums[i];
nums[i] = t;
}
nums[lo] = nums[j];
nums[j] = v;
return j;
}
}
参考链接
https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html#%E6%80%BB%E7%BB%93
侵删