二分
前言
二分查找的细节:
1.循环退出条件
2.计算中间索引溢出的bug
3.计算中间元素位置的时候是下取整
4.left和right的更新
二分查找的变形问题
1.查找第一个等于目标值的索引
2.查找第一个大于等于目标值的索引
3.查找最后一个等于目标值的索引
4.查找最后一个小于等于目标值的索引
5.应用:如何定位ip对应的城市
6.排除法的二分查找
实现的小结:
思路1.不断在循环体中查找目标元素
思路2.在循环体中不断的排除一定不存在目标元素的区间
二分查找数组未必有序的题型:
1.旋转数组问题
2.山脉数组问题
一、二分查找代码
1.在一个有序数组中查找目标元素target
/**
* @author zzyuan
* @create 2021-07-05 20:21
*/
public class BasicBinarySearch {
//最基本的二分搜索代码
//要求:数组是有序的
//时间复杂度: o(logn)
//空间复杂度: o(1)
public boolean contains(int[] nums , int target){
if(nums == null || nums.length == 0)
return false;
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(target == nums[mid]){
return true;
}else if(target < nums[mid]){
right = mid - 1;//下一次搜索区间[left ... mid-1]
}else {//target > nums[mid]
left = mid + 1;//下一次搜索区间[mid+1 ... right]
}
}
return false;
}
//时间复杂度:o(logn)
//空间复杂度: o(logn)
//最基本的二分查找-递归写法
private boolean contains(int[] nums , int left , int right , int target){
if(left > right) return false;
int mid = left + (right - left) / 2;
if(nums[mid] == target) return true;
if(nums[mid] > target) return contains(nums,left,mid-1,target);
else return contains(nums,mid+1,right,target);
}
//对降序的数组进行二分查找
//改一下符号即可
}
2.查找第一个等于目标值target的下标
在包含重复元素数组中找到第一个等于target的下标
public int firstTargetElement(int[] nums , int target){
if(nums == null || nums.length == 0) return -1;
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(target == nums[mid]){
if(mid == 0 || nums[mid - 1] != target) return mid;
else right = mid - 1;
}else if(target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return -1;
}
3.查找第一个大于等于目标值target的下标
public int firstGETargetElement(int[] nums , int target){
if(nums == null || nums.length == 0) return -1;
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(target <= nums[mid]){
if(mid == 0 || nums[mid - 1] < target) return mid;
else right = mid - 1;
}else{//target > nums[mid]
left = mid + 1;
}
}
return -1;
}
4.查找最后一个等于目标值target的下标
public int lastTargetElement(int[] nums , int target){
if(nums == null || nums.length == 0) return -1;
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(target == nums[mid]){
//1.mid是数组的最后一个元素或者mid后面那个元素不等于mid,我们就返回target
if(mid == nums.length - 1 || nums[mid + 1] != target) return mid;
else left = mid + 1;
}else if(target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return -1;
}
5.查找最后一个小于等于目标值target的下标
public int lastLETargetElement(int[] nums , int target){
if(nums == null || nums.length == 0) return -1;
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(target >= nums[mid]){
if(mid == nums.length - 1 || nums[mid+1] > target) return mid;
else left = mid + 1;
}else{//target > nums[mid]
right = mid - 1;
}
}
return -1;
}
6.二分搜索应用案例:如何快速定位IP对应的城市
使用二分查找:
public class IpLocationParser {
private static class IpLocation {
public long startIp;
public long endIp;
public String locationCity;
}
private static ArrayList<IpLocation> sortedIpLocations = new ArrayList<>();
static {
try {
// 1. 读取文件,解析 ip 地址段
BufferedReader reader =
new BufferedReader(new FileReader("data\\ip_location.txt"));
String line = null;
while ((line = reader.readLine()) != null) {
String[] temps = line.split(" ");
IpLocation ipLocation = new IpLocation();
ipLocation.startIp = ip2Score(temps[0]);
ipLocation.endIp = ip2Score(temps[1]);
ipLocation.locationCity = temps[2];
sortedIpLocations.add(ipLocation);
}
} catch (IOException e) {
throw new RuntimeException("解析 ip 地址库出错" + e);
}
// 2. 按照起始 ip 进行升序排列
// 时间复杂度:O(nlogn)
Collections.sort(sortedIpLocations, new Comparator<IpLocation>() {
@Override
public int compare(IpLocation o1, IpLocation o2) {
if (o1.startIp < o2.startIp) return -1;
else if (o1.startIp > o2.startIp) return 1;
else return 0;
}
});
}
// 将ip转成长整型
public static Long ip2Score(String ip) {
String[] temps = ip.split("\\.");
Long score = 256 * 256 * 256 * Long.parseLong(temps[0])
+ 256 * 256 * Long.parseLong(temps[1])
+ 256 * Long.parseLong(temps[2])
+ Long.parseLong(temps[3]);
return score;
}
// 二分查找指定 ip 对应的城市
// 时间复杂度:O(logn)
public static String getIpLocation(String ip) {
long score = ip2Score(ip);
// 3. 在 sortedIpLocations 中找到最后一个 startIp 小于等于 score 的这个 ip 段
int left = 0;
int right = sortedIpLocations.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (score >= sortedIpLocations.get(mid).startIp) {
if (mid == sortedIpLocations.size() - 1
|| sortedIpLocations.get(mid + 1).startIp > score) {
if (score <= sortedIpLocations.get(mid).endIp) {
return sortedIpLocations.get(mid).locationCity;
}
} else {
left = mid + 1;
}
} else { // target < nums[mid]
right = mid - 1;
}
}
return null;
}
public static void main(String[] args) {
System.out.println(getIpLocation("202.101.48.198"));
}
}
二、LeetCode
1.704. 二分查找
二分的两种思路
1.不断在循环体中查找目标元素
2.在循环体中排除一定不存在目标元素的区间
思路1代码实现:
class Solution {
//不断在循环体中查找目标元素
public int search(int[] nums, int target) {
int l = 0 ;
int r = nums.length - 1;
while(l <= r){
int m = l + (r - l) / 2;
if(nums[m] < target){
l = m + 1;
}else if(nums[m] > target){
r = m - 1;
}else{
return m;
}
}
return -1;
}
}
分类法1:
1.target <= nums[mid]
2.target > nums[mid]
分类法1代码实现:
class Solution {
//在循环体中排除一定不存在目标元素的区间
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int left = 0;
int right = nums.length - 1;
// 搜索区间是 [left...right] 中的每个元素
while (left < right) {
int mid = left + (right - left) / 2;
if (target > nums[mid])
left = mid + 1;
else
right = mid;
}
// 循环结束后:left == right
// 需要后处理,因为在循环中,还有一个元素没有处理
return (nums[left] == target) ? left : -1;
}
}
分类法2:
1.target < nums[mid]
2.target >= nums[mid]
分类法2代码实现:
class Solution {
//在循环体中排除一定不存在目标元素的区间
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int left = 0;
int right = nums.length - 1;
// 搜索区间是 [left...right] 中的每个元素
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (target < nums[mid])
right = mid - 1;
else
left = mid;
}
// 循环结束后:left == right
// 需要后处理,因为在循环中,还有一个元素没有处理
return (nums[left] == target) ? left : -1;
}
}
2.34. 在排序数组中查找元素的第一个和最后一个位置
代码如下:
class Solution {
//在循环体中排除一定不存在目标元素的区间
public int[] searchRange(int[] nums, int target) {
if(nums == null || nums.length == 0)
return new int[]{-1,-1};
int l = 0;
int r = nums.length - 1;
int start = 0;
int end = 0;
while(l < r){
int mid = l + r >> 1;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
if(nums[l] != target) return new int[]{-1,-1};
start = l;
l = 0 ;
r = nums.length - 1;
while(l < r){
int mid = l + r + 1 >> 1;
if(nums[mid] <= target) l = mid;
else r = mid - 1;
}
end = l;
return new int[]{start,end};
}
}