二分搜索算法
基于labuladong的算法小抄,网站地址点我,视频版地址点我
建议直接看网站的文字版,视频作为补充查阅资料
1、二分查找框架
二分查找:细节决定成败,关键是在于到底要给mid加一还是减一,while里面到底用<=还是<
二分查找框架:
// 二分查找框架(返回target在arr数组中的下标)
int binarySearch(int[] arr, int target) {
int left = 0, right = ...;
// 循环条件
while (...){
int mid = left + (right - left) / 2;
if (target == arr[mid]) {
...
} else if (target < arr[mid]) {
right = ...;
} else if (target > arr[mid]) {
left = ...;
}
}
return ...;
}
技巧总结:
- 不要出现else,而是把所有可能性用else if写清楚
- … 就是可能出现细节问题的地方
- 计算mid时要注意溢出的问题
- 数组必须使单调数组才可以使用该算法
2、寻找一个数
这是一个最基本的二分搜索,即搜索一个数,如果存在,返回其索引,否则返回-1
力扣704题,二分查找
采用左闭右闭区间做:
[704]二分查找
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 先行条件判断
if (target < nums[left] || target > nums[right]) {
return -1;
}
// 在左闭右闭区间进行循环
while (left <= right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
return mid;
} else if (target < nums[mid]) {
right = mid - 1;
} else if (target > nums[mid]) {
left = mid + 1;
}
}
// 遍历完所有元素都没找到
return -1;
}
}
- 为什么while循环中是<=,而不是<呢?
- 这个取决于初始化时候的right值,在进行搜索时候每次的区间是左闭右闭还是左闭右开
- 为什么left=mid+1,right=mid-1呢?
- 这个取决于每次搜索区间结束后,下一个搜索区间应是什么样子的,如何剔除已经搜索过的mid元素
- 算法的缺陷
- 如果数组元素重复,该算法只能找到重复元素中的一个元素,不能向左或者向右线性搜索
- 需要向左向右线性搜索,就需要左闭右开或者左开右闭区间
3、寻找左侧边界的二分搜索
代码框架如下:
// 左闭右开区间
int leftIndex(int[] nums, int target) {
int left = 0, right = nums.length;
// 遍历
while (left < right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
right = mid;
} else if (target < nums[mid]) {
right = mid;
} else if (target > nums[mid]) {
left = mid + 1;
}
}
return left;
}
- 为什么while中是<?
- 因为是左闭右开区间
- 为什么没有返回-1的操作?如果nums中不存在target这个值,怎么处理?
- 在返回的之后判断一下nums[left]是否等于target
- 还要考虑left是否越界的情况
- 为什么left=mid+1,right=mid?
- 因为搜索区间是左闭右开,已经判断过mid
- 下一步应该去mid的左侧或者右侧区间搜索,即[left,mid)或者是[mid+1,right)
- 为什么能够搜索到左侧边界?
- 关键是在于nums[mid]==target这种情况的处理
- 找到target后不是立即返回,而是更新搜索区间的上界right,不断向左收缩
- 为什么返回left而不是right?
- 其实返回left和返回right都是一样的
- 如果将搜索区间变成左闭右闭,代码如下:
// 左闭右闭区间
int leftIndex(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 遍历
while (left <= right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
right = mid - 1;
} else if (target < nums[mid]) {
right = mid - 1;
} else if (target > nums[mid]) {
left = mid + 1;
}
}
// 判断是否越界
if (left == nums.length) {
return -1;
}
// 判断是否找到目标值
return nums[left] == target ? left : -1;
}
4、寻找右侧边界的二分查找
左闭右开:
// 左闭右开(寻找右侧边界)
int rightIndex(int[] nums, int target) {
int left = 0, right = nums.length;
// 遍历
while (left < right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
left = mid + 1;// 收缩左侧边界
} else if (target > nums[mid]) {
left = mid + 1;// 去mid的右侧寻找
} else if (target < nums[mid]) {
right = mid;// 去mid的左侧寻找
}
}
// 判断是否越界
if (left - 1 < 0) {
return -1;
}
// 判断是否找到目标值
return nums[left - 1] == target ? left - 1 : -1;
}
- 为什么这个算法可以找到右侧边界?
- 当nums[mid]==target时,不会立即返回,而是继续向右搜索
- 为什么最后返回left-1?
- 关键是在于锁定右侧边界时的这个条件判断
5、逻辑统一
力扣第34题,排序数据中查找元素的第一个和最后一个位置
[34]在排序数组中查找元素的第一个和最后一个位置
class Solution {
public int[] searchRange(int[] nums, int target) {
// 初步筛选条件
if (nums.length == 0 || target < nums[0] || target > nums[nums.length - 1]) {
return new int[]{-1, -1};
}
int leftIndex = leftIndex(nums, target);
int rightIndex = rightIndex(nums, target);
return new int[]{leftIndex, rightIndex};
}
// 找到开始位置
int leftIndex(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
right = mid;
} else if (target < nums[mid]) {
right = mid;
} else if (target > nums[mid]) {
left = mid + 1;
}
}
return target == nums[left] ? left : -1;
}
// 找到结束位置
int rightIndex(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
left = mid + 1;
} else if (target < nums[mid]) {
right = mid;
} else if (target > nums[mid]) {
left = mid + 1;
}
}
return target == nums[left - 1] ? left - 1 : -1;
}
}