二分查找:思路很简单,细节是魔鬼!
“传统的”二分查找法模板的问题
(1)取中位数索引的代码有问题
int mid = (left + right) / 2
在 left
和 right
都比较大的时候,left + right
很有可能超过 int 类型能表示的最大值,即整型溢出,为了避免这个问题,应该写成:
int mid = left + (right - left) / 2 ;
or
int mid = (left + right) >>> 1 ;//推荐写法
(2)循环可以进行的条件写成 while (left <= right)
时,在退出循环的时候,需要考虑返回 left
还是 right
“神奇的”二分查找法模板
不需要考虑返回 left
还是 right
/**
* 二分法模板
* @param nums 二分法查找的数组
* @param target 目标数
* @return 目标数的下标
*/
public int search(int[] nums, int target) {
int len = nums.length;
//左右边界
int left = 0;
int right = len - 1;
// 退出循环条件,不必写出是返回left还是right
while (left < right) {
//取右中位数
int mid = (left + right + 1) >>> 1;
//分支逻辑,注意:写完分支逻辑后再看看是否有必要调整中位数
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid;
}
}
if (nums[left] == target) {
return left;
}
return -1;
}
/*
* 此题是leetcode.704
* 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,
* 写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
*
* 示例 1:
* 输入: nums = [-1,0,3,5,9,12], target = 9
* 输出: 4
* 解释: 9 出现在 nums 中并且下标为 4
*/
根据实际环境选择是左中位数还是右中位数,见下面例子(leetcode.34题)
左中位数:int mid = (left + right) >> 1
右中位数:int mid = (left + right + 1) >> 1
/**
* @author mys
* @version 2019.8.10
*/
package com.mys;
import org.junit.Test;
public class SearchRange34 {
/**
* 方法一:线性扫描
* @param nums 数组
* @param target 目标数
* @return 目标数的起始位置和终止位置
*/
public int[] searchRange1(int[] nums, int target) {
//如果没有目标数,则返回{-1, -1}
int[] targetRange = {-1, -1};
//从左边开始扫描,如果找到目标数,记录位置作为起始位置,并中止循环
for (int i = 0; i < nums.length; i ++) {
if (nums[i] == target) {
targetRange[0] = i;
break;
}
}
//如果扫描一遍过后,没有找到目标数,即targetRange[0] == -1
//说明没有目标数,返回{-1, -1}
if (targetRange[0] == -1) {
return targetRange;
}
//现在从右边开始扫描,查找目标数的中止位置
for (int j = nums.length - 1; j >= 0; j --) {
if (nums[j] == target) {
targetRange[1] = j;
break;
}
}
return targetRange;
}
public int[] searchRange2(int[] nums, int target) {
//如果没有目标数,则返回{-1, -1}
int[] targetRange = {-1, -1};
int len = nums.length;
//预判
if (len == 0) {
return targetRange;
}
targetRange[0] = findLowerBound(nums, target);
//细节:如果左边界搜索不到,右边界就没有必要搜索了
if (targetRange[0] == -1) {
return targetRange;
}
targetRange[1] = findUpBoud(nums, target);
return targetRange;
}
/**
* 找到target第一个索引
* 逻辑:当 target == mid 时,并不能确定mid是起始位置,但一旦 mid < target 时,mid及mid左边肯定不是起始位置
* mid右边的数 >= target时,当 mid=target 的第一个索引位置就是 起始位置
* @param nums
* @param target
* @return
*/
private int findLowerBound(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len - 1;
while (left < right) {
//左中位数
int mid = (left + right) >>> 1;
//分支逻辑
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
//如果不存在目标数
if (nums[left] != target) {
return -1;
}
return left;
}
/**
* 找到target最后一个索引
* 逻辑:当 target==mid 时,同样不能确定是否为中止位置,但一旦 mid>target 时, mid及mid右边的肯定不是中止位置
* 因此, mid左边的数 <=target 时,当 mid=target 的最后一个索引就是 中止位置
* @param nums
* @param target
* @return
*/
private int findUpBoud(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len - 1;
//分支逻辑
while (left < right) {
//右中位数
int mid = (left + right + 1) >>> 1;
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid;
}
}
//如果不存在目标数
if (nums[left] != target) {
return -1;
}
return left;
}
@Test
public void fun() {
int[] nums = {5,7,7,7,8,10};
int target = 7;
int[] result = searchRange2(nums, target);
System.out.println("{" + result[0] + ", " + result[1] + "}");
}
}