文章目录
前言
博主最近学习了二分搜索的相关知识,于是打算写一篇文章进行总结。本文将由浅入深地,依据刷过的例题来分享二分搜索的基础及应用。部分内容借鉴了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);
}
}