引言
二分查找算法是非常非常重要的一个算法,思路简单,但是边界条件不好把控,很容易考虑不周出错,本文将提供一种二分查找算法万能模板,该算法模板来自左神-yyds,简单清晰,针对任何二分边界问题都能手到擒来。
标准二分法
题目:给定一个已排序的数组 及目标值 ,返回 在数组中的位置,不存在则返回 。
相信所有人闭着眼睛都能写出来,它是最基础的二分思想,边界条件考虑简单,于是写出如下的代码:
public static int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
//这个mid的位置是偏左的,当数组长度为奇数时候,mid为正中间,当数组长度为偶数时候,mid是靠近左边
int mid = left + ((right - left) / 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
难度太 Easy,现在让你在修改标准二分法,有如下要求:
- 给定一个包含重复元素的排序数组 ,请查找 在数组中的左边界: , ,结果应该返回 ,如果不存在,请返回 。
- 给定一个包含重复元素的排序数组 ,请查找 在数组中的右边界: , ,结果应该返回 。如果不存在,请返回 。
如果你能快速写出来,说明你的二分法是过关的,边界条件把控的很好👍🏻。
如果你跟我一样,每次写二分都要卡一卡,没有一套标准的模板,此二分算法框架来自左神-yyds。
查找左边界
题目:给定一个包含重复元素的排序数组 ,请查找 在数组中的左边界: , ,结果应该返回 ,如果不存在,请返回 。
public static int zuoShenSearchLeft(int[] arr, int target) {
int l = 0, r = arr.length - 1;
//记录最左边的位置,每次更新,注意此时的 while 循环是可以走到 l = r 的
int index = -1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (target <= arr[mid]) {
//左边界,继续往左找
index = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
//没找到,此时返回 -1
if (index == -1) return -1;
System.out.println(index);
return arr[index] == target ? index : -1;
}
引申:给定一个排序数组 , ,在 中查找第一个比它小的数的位置。返回位置 。
这个边界查找在很多算法题中都有使用到,特别重要。
public static int zuoShenSearchLower(int[] arr, int target) {
int l = 0, r = arr.length - 1;
if (arr[r] < target) return r;
//记录最左边的位置,每次更新,注意此时的 while 循环是可以走到 l = r 的
int index = -1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
//左边界,继续往左找
if (target <= arr[mid]) {
index = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
//没找到,此时返回 -1
if (index == -1) return -1;
return index - 1;
}
查找右边界
题目:给定一个包含重复元素的排序数组 ,请查找 在数组中的右边界: , ,结果应该返回 ,如果不存在,请返回 。
public static int zuoShenSearchRight(int[] arr, int target) {
int l = 0, r = arr.length - 1;
//记录最右边的位置,及时更新
int index = -1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
//右边界,则继续往右找
if (target >= arr[mid]) {
index = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
if (index == -1) return -1;
return arr[index] == target ? index : -1;
}
引申:给定一个排序数组 , ,在 中查找第一个比它大的数的位置。返回位置 。
这个边界查找在很多算法题中都有使用到,特别重要。
public static int zuoShenSearchUpper(int[] arr, int target) {
int l = 0, r = arr.length - 1;
if (arr[r] < target) return -1;
if (arr[0] > target) return 0;
//记录最右边的位置,及时更新
int index = -1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
//右边界,则继续往右找
if (target >= arr[mid]) {
index = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
if (index == -1) return -1;
return index + 1;
}
算法实战
1.局部最小值问题
题目:无序数组,任意两个相邻的数不相等,返回一个局部最小值。注意:这个局部最小值可能有多个,只要找到一个就可以。
分析:
- 如果一个数组是升序排列,则局部最小值是 0 位置
- 如果一个数组是降序排列,则局部最小值是 位置
- 如果上述都不满足,那这个局部最小位置一定在中间。
public static int getLessIndex(int[] arr) {
if (arr == null || arr.length < 1) return -1;
//1.(0~1)是升序
if (arr.length == 1 || arr[0] < arr[1]) return arr[0];
//2.(n-2,n-1)是降序
int n = arr.length;
if (arr[n - 2] > arr[n - 1]) return arr[n - 1];
//3.局部最小值在中间位置,此时用二分查找
int l = 1, r = n - 2;
while (l < r) {
int mid = (l + r) / 2;
if (arr[mid] > arr[mid - 1]) {
//左边升序
r = mid - 1;
} else if (arr[mid] > arr[mid + 1]) {
//右边降序
l = mid + 1;
} else {
return mid;
}
}
return l;
}
2.在排序数组中查找元素的第一个和最后一个位置
class Solution {
public int[] searchRange(int[] nums, int target) {
if(nums == null || nums.length == 0) return new int[]{-1,-1};
int n = nums.length;
if(nums[0] > target || nums[n - 1] < target) return new int[]{-1,-1};
int left = getLeftBounds(nums,target);
int right = getRightBounds(nums,target);
return new int[]{left,right};
}
public int getLeftBounds(int[] nums,int target){
int n = nums.length;
int l = 0, r = n - 1;
int index = -1;
while(l <= r){
int m = l + (r - l) / 2;
if(target <= nums[m]){
index = m;
r = m - 1;
}else{
l = m + 1;
}
}
if(index == -1) return -1;
return nums[index] == target ? index : -1;
}
public int getRightBounds(int[] nums,int target){
int n = nums.length;
int l = 0, r = n - 1;
int index = -1;
while(l <= r){
int m = l + (r - l) / 2;
if(target >= nums[m]){
index = m;
l = m + 1;
}else{
r = m - 1;
}
}
if(index == -1) return -1;
return nums[index] == target ? index : -1;
}
}
3. 搜索插入位置
思路:这不就是找 在排序数组中的位置。
class Solution {
public int searchInsert(int[] nums, int target) {
if(nums == null || nums.length == 0) return 0;
int n = nums.length;
if(nums[0] > target) return 0;
if(nums[n - 1] < target) return n;
int l = 0, r = n - 1;
int index = -1;
while(l <= r){
int m = l + (r - l) / 2;
if(target <= nums[m]){
index = m;
r = m - 1;
}else{
l = m + 1;
}
}
return index;
}
}
4. 寻找峰值
class Solution {
/**
* 寻找峰值,即凸位置
* label:二分查找,判断 m 的左边和右边
* 1.如果 num[0] > num[1] 则直接返回,num[n-2] < num[n-1] 直接返回
* 2.否则二分查找
*/
public int findPeakElement(int[] nums) {
if (nums == null || nums.length == 0) return -1;
int n = nums.length;
if (n == 1) return 0;
if (nums[0] > nums[1]) return 0;
if (nums[n - 2] < nums[n - 1]) return n - 1;
int l = 1, r = n - 2;
//注意 while 条件
while (l < r) {
int m = (l + r) / 2;
if (nums[m] > nums[m - 1] && nums[m] > nums[m + 1]){
return m;
}else if (nums[m] < nums[m - 1]){
r = m - 1;
}else {
l = m + 1;
}
}
return l;
}
}
结语
学习是枯燥而孤独的
无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心烦躁、焦虑,毁掉你本就不多的热情和定力。