【Java】二分搜索系列学习+例题

本文介绍了二分搜索的基本原理,包括原始二分搜索、搜索左侧和右侧边界,并通过实例演示了在查找元素位置、峰顶索引和数据结构优化中的应用。后续章节涉及了力扣平台的经典题目,如查找目标值范围、插入位置和矩阵搜索,展示了算法在实际场景中的灵活运用。
摘要由CSDN通过智能技术生成


前言

博主最近学习了二分搜索的相关知识,于是打算写一篇文章进行总结。本文将由浅入深地,依据刷过的例题来分享二分搜索的基础及应用。部分内容借鉴了labuladong算法笔记。


一、原始二分搜索

最简单的二分搜索就是,给定一个有序的数组,查找该数组中的指定元素,返回该元素对应的索引。
一般性的二分查找框架如下所示:

int binarySearch(int[] nums, int target) {
//设置两个指针left/right,规定查找区间
 int left = 0, right = ...;
 while(...) {
 	int mid = left + (right - left) / 2;
 	if (nums[mid] == target) {
 		...
 	} else if (nums[mid] < target) {
 		left = ...
 	} else if (nums[mid] > target) {
 		right = ...
 	}
 }
 return ...;
}

下面是一个经典例题:给定一个数组arr,需要查找的目标值target,返回该目标值target在arr中的索引。如果没有找到,则返回-1。

package _二分搜索;

public class _01原始二分搜索 {
	private static int[] arr = {1,2,4,6,9,14,18,20,25,29};
	private static int target;
	public static void main(String[] args) {
		target = 11;
		int ans = BinarySearch();
		System.out.println("值"+target+"对应的索引是:"+ans);
	}
	
	private static int BinarySearch() {
		int low = 0;
		int high = arr.length;	//搜索区间是【low,high】闭区间
		int mid;
		//注意是<=,而不是<。如果不加=,那么在搜索的时候会漏掉一个元素
		while(low <= high) {	
			mid = low + (high - low)/2;		//等价于mid=(low+high)/2,不过那样写溢出的概率更小
			if(arr[mid] == target) {
				return mid;
			}
			else if(arr[mid] > target) {
				high = mid - 1;
			}
			else if(arr[mid] < target) {
				low = mid + 1;
			}
		}
		return -1;	//如果没有找到target,那么返回-1
	}
}

二、搜索左侧边界

问题背景:
如果使用原始的二分搜索方法,那么对于target为6的数组{1,6,6,6,9},返回的索引值就是2。
如果想要得到target的左侧边界1,右侧边界3,原始的二分搜索算法对此是没有办法的,所以要采用改进后的二分搜索算法。

package _二分搜索;
/**
 * 寻找左侧边界的二分搜索
 * 问题背景:
 * 如果使用原始的二分搜索方法,那么对于target为6的数组{1,6,6,6,9},返回的索引值就是2。
 * 如果想要得到target的左侧边界1,右侧边界3,原始的二分搜索算法对此是没有办法的,所以要采用改进后的二分搜索算法。
 * @author ZZJ
 *
 */
public class _02搜索左侧边界 {
	private static int[] arr = {1,6,6,6,9};
	private static int target;
	public static void main(String[] args) {
		target = 0;
		int ans = LeftBoundBinarySearch();
		System.out.println("值"+target+"对应的索引是:"+ans);
	}
	
	private static int LeftBoundBinarySearch() {
		int low = 0;
		int high = arr.length;	//此时的搜索区间是左开右闭区间[low,high)
		int mid;
		while(low < high) {
			/*
			 * 注意这里不是low<=high了。因为若while中的判断条件为low<=high,那么当low==high的时候,
			 * 还是会进入循环。但此时搜索区间为[low,high),即[low,low)或者[high,high),区间里面已经为空,已经没有数字了,
			 * 也就说明此时程序已经可以正确终止了。
			 */
			mid = low + (high - low )/2;
			if(arr[mid] == target) {
				/*
				 * 注意这里也不是return mid;了,而是high = mid。因为:
				 * 此时是搜索左侧边界,需要压缩右边的区间。
				 */
				high = mid;
			}
			else if(arr[mid] > target) {
				high = mid;	//不是high=mid+1; 因为这是左闭右开区间
			}
			else if(arr[mid] < target) {
				low = mid + 1;	
			}
		}
		/*
		 * 考察⼀下 left 的取值范围,免得索引越界。假如输入的 target 非常大,那么就会⼀直触发
		 * nums[mid] < target 的 if 条件,low会⼀直向右侧移动,直到等于high,while 循环结束。
		 */
		if (low == arr.length) return -1;	// 此时 target ⽐所有数都⼤,返回 -1
		
		/*
		 * 在返回的时候额外判断⼀下nums[left]是否等于target,如果不等于,就说明target不存在。
		 */
		if(arr[low] == target) {
			//无论return low还是high,都是一样的,因为此时low==high
			return low;
		}
		else {
			return -1;
		}
		
	}
}

三、搜索右侧边界

和搜索左侧边界相似。

package _二分搜索;

public class _03搜索右侧边界 {
	private static int[] arr = {1,6,6,6,9};
	private static int target;
	public static void main(String[] args) {
		target = 6;
		int ans = RightBoundBinarySearch();
		System.out.println("值"+target+"对应的索引是:"+ans);
	}
	
	private static int RightBoundBinarySearch() {
		int low = 0;
		int high = arr.length;
		int mid;
		while(low < high) {
			mid = low + (high - low) / 2;
			if(arr[mid] == target) {
				low = mid + 1;
			}
			else if(arr[mid] > target) {
				high = mid;
			}
			else if(arr[mid] < target) {
				low = mid + 1;
			}
		}
		if(high == arr.length)
			return -1;
		if(arr[low - 1] == target) {
			return low - 1;	//注意,这里是low-1
			/*
			 * 为什么是low-1,因为我们对 left 的更新必须是 low = mid + 1,就是说 while 循环结束时,nums[low] ⼀定不等于
			   target 了,而nums[low-1]才可能是target。
			 */
		}
		else {
			return -1;
		}
	}
}

力扣34 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1: 输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]

示例 2: 输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]

示例 3: 输入:nums = [], target = 0 输出:[-1,-1]

思路:如果已经掌握了上述查找左侧、右侧边界的方法,那么这题将非常简单。创建两个函数,分别查找左侧、右侧边界,然后将两个答案合并即可。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] ans = new int[2];
		int temp1 = leftBoundBinarySearch(nums,target);
		int temp2 = rightBoundBinarySearch(nums,target);
		ans[0] = temp1;
		ans[1] = temp2;
		System.out.println(temp1+" "+temp2);
		return ans;
    }
    private static int rightBoundBinarySearch(int[] nums, int target) {
		int low = 0;
		int high = nums.length;
		int mid;
		while(low < high) {
			mid = low + (high - low )/2;
			if(nums[mid] == target) {
				low = mid + 1;
			}
			else if(nums[mid] > target) {
				high = mid;
			}
			else if(nums[mid] < target) {
				low = mid + 1;
			}
		}
		//如果数组为空直接返回-1
		if(nums.length == 0)
			return -1;
		//如果最后low为0,查看nums[low]是否等于target。(主要为了防止下面low-1产生的数组越界)
		if(low == 0 && nums[low] == target) {
			return 0;
		}
		if(low == 0 && nums[low] != target) {
			return -1;
		}
		
		if(nums[low - 1] == target) {
			return low-1;
		}
		else {
			return -1;
		}	
	}

	private static int leftBoundBinarySearch(int[] nums, int target) {
		int low = 0;
		int high = nums.length;
		int mid;
		while(low < high) {
			mid = low + (high - low )/2;
			if(nums[mid] == target) {
				high = mid;
			}
			else if(nums[mid] > target) {
				high = mid;
			}
			else if(nums[mid] < target) {
				low = mid + 1;
			}
		}
		if(low == nums.length)
			return -1;
		if(nums[low] == target) {
			return low;
		}
		else {
			return -1;
		}
	}
}

力扣35 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1: 输入: nums = [1,3,5,6], target = 5 输出: 2

示例 2: 输入: nums = [1,3,5,6], target = 2 输出: 1

示例 3: 输入: nums = [1,3,5,6], target = 7 输出: 4

思路:这道题采用基本的二分搜索框架即可。注意,和原始二分搜索不同的是,如果目标值不存在于数组中,则需要返回它将会被按顺序插入的位置,而不是返回-1。所以在此,如果找不到目标值,我们的返回值需要设置成return left。

package _二分搜索;

public class _leetcode35 {
	public static int searchInsert(int[] nums, int target) {
		int left = 0;
		int right = nums.length - 1;
		int mid;
		while(left <= right) {
			mid = left + (right - left)/2;
			if(nums[mid] == target) {
				return mid;
			}
			else if(nums[mid] < target) {
				left = mid + 1;
			}
			else if(nums[mid] > target) {
				right = mid - 1;
			}
		}
		return left;
    }
	public static void main(String[] args) {
		int[] nums = {1,3,5,6};
		int target = 4;
		int m = searchInsert(nums, target);
		System.out.println(m);
	}
}

力扣74 搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。

示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true

示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false

思路:可以选择用二分查找的思路来解决问题。设置二分查找的low为0,high为数组元素个数,mid=(low+high)/2。将mid转化成矩阵对应行列的的下标,进行判断。

package _二分搜索;
//搜索二维矩阵
public class _leetcode74 {
	public static boolean searchMatrix(int[][] matrix, int target) {
		int low = 0;
		int high = matrix.length*matrix[0].length -1;
		int mid;
		int i,j;
		while(low <= high) {
			mid = low + (high - low)/2;
			i = mid / matrix[0].length;
			j = mid % matrix[0].length;
			if(matrix[i][j] == target) {
				return true;
			}
			else if(matrix[i][j] < target) {
				low = mid + 1;
			}
			else if(matrix[i][j] > target) {
				high = mid - 1;
			}
		}
		return false;
		
    }
	
	public static void main(String[] args) {
		int[][] matrix = {{1,3,5,7},{10,11,16,20},{23,30,34,60}};
		int target = 13;
		boolean temp = searchMatrix(matrix, target);
		System.out.println(temp);
	}
}

力扣852 山脉数组的峰顶索引

符合下列属性的数组 arr 称为 山脉数组 :
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < … arr[i-1] < arr[i]
arr[i] > arr[i+1] > … > arr[arr.length - 1]
给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < … arr[i - 1] < arr[i] > arr[i + 1] > … > arr[arr.length - 1] 的下标 i 。

示例 1:
输入:arr = [0,1,0] 输出:1
示例 2:
输入:arr = [0,2,1,0] 输出:1
示例 3:
输入:arr = [0,10,5,2] 输出:1
示例 4:
输入:arr = [3,4,5,1] 输出:2
示例 5:
输入:arr = [24,69,100,99,79,78,67,36,26,19] 输出:2

说到底,这题就是在找数组中的最大数字。如果采用二分法来解决这个问题。在判断条件中, 需要判断arr[mid]、arr[mid-1]和arr[mid+1]的大小,如果arr[mid]大于arr[mid-1]和arr[mid+1],那么说明我们已经找到了最大元素,否则需要继续进行寻找。
因为判断条件中出现了arr[mid-1]和arr[mid+1],mid+1和mid-1可能会出现数组越界,所以需要将可能越界的情况单独拎出来讨论。

package _二分搜索;

public class _leetcode852 {
	public static int peakIndexInMountainArray(int[] arr) {
		int low = 0;
		int high = arr.length-1;
		int mid;
		while(low <= high) {
			mid = low + (high - low)/2;
			//判断mid为0的情况,防止越界
			if(mid==0 && arr[mid]>arr[mid+1]) {
				//比如:arr={7,5,3,2,0};
				return 0;
			}
			else if(mid==0 && arr[mid]<arr[mid+1]) {
				//比如:arr={3,5,3,2,0};
				mid = 1;
			}
			//判断mid为arr.length-1的情况,防止越界
			if(mid==arr.length-1 && arr[mid]>arr[mid-1]) {
				return 0;
			}
			else if(mid==arr.length-1 && arr[mid]<arr[mid-1]) {
				mid = mid - 1;
			}
			
			//二分法经典框架
			if(arr[mid]>arr[mid-1] && arr[mid]>arr[mid+1]) {
				return mid;
			}
			else if(arr[mid]<arr[mid+1]) {
				low = mid+1;
			}
			else if(arr[mid]>arr[mid+1]) {
				high = mid-1;
			}
		}
		return -1;
    }
	
	public static void main(String[] args) {
		int[] arr = {3,5,3,2,0};
		int temp = peakIndexInMountainArray(arr);
		System.out.println(temp);
	}
}

力扣1011 在D天内送达包裹的能力

传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。

传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。

返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。

示例 1:
输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释: 船舶最低载重15 。就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4天:9
第 5 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8),
(9), (10) 是不允许的。


示例 2:
输入:weights = [3,2,2,4,1,4], days = 3
输出:6
解释: 船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4


示例 3:
输入:weights = [1,2,3,1,1], days = 4
输出:3
解释: 第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1

下面思路部分引用力扣网站:链接
如果使用二分查找解决该题。设置一个查找区间,区间中的每一个数,对应一个运载能力。需要寻找到一个运载能力x,使得运载能力x’>x时,可以在days天内运送完所有包裹;当 x’<x时,不可以在days天内运送完整个包裹。
那么,二分查找的初始左右边界应当如何计算呢?
对于左边界而言,由于我们不能「拆分」一个包裹,因此船的运载能力不能小于所有包裹中最重的那个的重量,即左边界为数组 weights 中元素的最大值。
对于右边界而言,船的运载能力也不会大于所有包裹的重量之和,即右边界为数组weights 中元素的和。

我们从上述左右边界开始进行二分查找,就可以保证找到最终的答案。

package _二分搜索;

import java.util.Arrays;

//在D天内送达包裹的能力
public class _leetcode1101 {
	 public static int shipWithinDays(int[] weights, int days) {
		 int low,high,mid;
		 //运载能力最少不能小于所有货物中最重的那个物品
		 low = Arrays.stream(weights).max().getAsInt();
		 high = 0;
		 //运载能力最多不能多于所有货物的总重量
		 for(int i = 0 ; i < weights.length ; i++) {
			 high = high + weights[i];
		 }
		 //查找区间是[low,high)左闭右开区间
		 while(low < high) {
			 mid = low + (high - low) / 2;
			 if(ifCanShip(weights,days,mid)) {
				 high = mid;
			 }
			 else {
				 low = mid + 1;
			 }
		 }
		 return low;
		 
		 
	 }
	 //判断对于运载能力mid,能否在days天内运送完货物
	 private static boolean ifCanShip(int[] weights, int days, int mid) {
		 //mid是当前的运载能力
		int real_days = 1 , temp = 0;
		for(int i = 0 ; i < weights.length ; i++) {
			temp = temp + weights[i];
			if(temp > mid){
				real_days++;
	            temp = weights[i];
	        }
		}
		if(real_days <= days) {
			return true;
		}
		else {
			return false;
		}
	}

	public static void main(String[] args) {
		 int[] weights = {1,2,3,4,5,6,7,8,9,10};
		 int days = 5;
		 int ans = shipWithinDays(weights,days);
		 System.out.println(ans);
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值