1. 定义
wiki:是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半
2. 常见问题
- 集合中搜索值为target的位置
- 集合中搜索值为target的左边界
- 集合中搜索值为target的右边界
- 集合中搜索值大于target的最后一个值的下标
-
3. 二分搜索法要点
3.1 搜索范围定义
以常见问题1举例
public static int findTargetValue(int[] arr,int target) {int left = 0;int right = arr.length-1;while(left <= right) {int mid = left + (right - left)/2;if(arr[mid] == target) {return mid;} else if(arr[mid] > target) {right = mid - 1;} else if(arr[mid] < target) {left = mid + 1;}}return -1;}
可以看到left初始化为0,right初始化为
arr.length-1,它代表着我们首次的搜索范围,即一个闭区间[left,right],循环何时结束呢,当left>right时,即当搜索区间为[right+1,right]时,区间为无效区间,那么循环也就停止了,不论多少次循环我们只要记得如果是这种写法,那么我们每次的搜索区间都是一个闭区间
接下来我们看下搜索区间为左闭右开的写法public static int findTargetValue2(int[] arr,int target) {int left = 0;int right = arr.length;while(left < right) {int mid = left + (right - left) / 2;if(arr[mid] == target) {return mid;} else if(arr[mid] > target) {right = mid;} else if(arr[mid] < target) {left = mid + 1;}}return -1;}
这里与上面左闭右闭的写法不同的在于初始化
right=arr.length,这个下标值是有效下标值加1,因此初始化的搜索区间为[left,right),之后每轮的搜索区间虽然搜索的范围在减小,但是依然保持左闭右开区间,这里当arr[mid] > target时更新右边界为mid,因为我们的搜索区间为左闭右开,那么下次搜索区间就是[left,mid),mid的值不会被再次取到。搜索结束时left=right,此时搜索区间为[left,left),区间有效长度为0,搜索结束
个人更加习惯左闭右闭的写法,之后的代码也会以左闭右闭的搜索区间为基础展示3.2 目标值与集合中值的范围关系
假设集合中值范围为
(x,n),那么目标值与集合中值的范围关系可能有如下三种 在集合范围之外,即小于集合中的最小值或大于集合中值的最大值
- 在集合范围内但集合中不存在目标值
- 在集合范围内且集合中存在目标值
4. 常见问题解答
已经展示了如何在有序集合中搜索固定值的写法,接下来我们将展示其他几个常见问题的解法
寻找目标值的左边界
public static int findTargetLeftBorder(int[] arr,int target) {int left = 0;int right = arr.length-1;int pre = -2;while(left <= right) {int mid = left + (right - left) / 2;if(arr[mid] >= target) {pre = mid;right = mid -1;} else {left = mid + 1;}}return pre;}
寻找目标值的左边界,即在寻找到目标值后不会跳出循环,而是继续向左搜索,可以想象是右边界不断向左缩进的过程,当
arr[mid] >= target时,我们需要做的动作都是右边界向左边缩进。这里为什么是需要一个pre呢,因为当循环结束时right值肯定不等于mid,因为right = mid -1,所以我们需要一个pre记录边界下标,如果集合内存在该值,pre的值就是左边界的下标寻找目标值的右边界
public static int findTargetRightBorder(int[] arr,int target) {int left = 0;int right = arr.length-1;int pre = -2;while(left <= right) {int mid = left + (right - left) / 2;if(arr[mid] <= target) {pre = mid;left = mid + 1;} else {right = mid - 1;}}return pre;}
寻找右边界即在找到目标值时继续向右搜索,这里是左搜索边界不断向右移动的过程,当
arr[mid] <= target时,移动左边界继续向右寻找。同寻找左边界一样,如果集合存在目标值,那么pre的值就是目标值的右边界寻找集合中小于目标值的最后一个值的下标
// 有序集合:int[] arr = {1,2,2,4,6,9,10,12},寻找小于3的最后一个值的下标,// 这里应该返回2,因为下标为2的值为2,它是最后一个小于3的值public static int findLastLessThanTarget(int[] arr,int target) {int left = 0;int right = arr.length-1;int pre = -2;while(left <= right) {int mid = left + (right - left) / 2;if(arr[mid] < target) {pre = mid;left = mid + 1;} else {right = mid-1;}}return pre;}
寻找集合中大于目标值的最后一个值的下标
// 有序集合:int[] arr = {1,2,2,4,6,9,10,12},寻找大于8的最后一个值的下标,// 这里应该返回5,因为下标为5的值为9,它是最后一个大于8的值public static int findLastBigThanTarget(int[] arr,int target) {int left = 0;int right = arr.length-1;int pre = -2;while(left <= right) {int mid = left + (right - left) / 2;if(arr[mid] > target) {pre = mid;right = mid -1;} else {left = mid + 1;}}return pre;}
5. 题目
- 分隔绳子
给定一个整数数组 ribbons 和一个整数 k,数组每项 ribbons[i] 表示第 i 条绳子的长度。对于每条绳子,你可以将任意切割成一系列长度为正整数的部分,或者选择不进行切割。
例如,如果给你一条长度为 4 的绳子,你可以:
保持绳子的长度为 4 不变;
切割成一条长度为 3 和一条长度为 1 的绳子;
切割成两条长度为 2 的绳子;
切割成一条长度为 2 和两条长度为 1 的绳子;
切割成四条长度为 1 的绳子。
你的任务是最终得到 k 条完全一样的绳子,他们的长度均为相同的正整数。如果绳子切割后有剩余,你可以直接舍弃掉多余的部分。
对于这 k 根绳子,返回你能得到的绳子最大长度;如果你无法得到 k 根相同长度的绳子,返回 0。
示例 1:
输入: ribbons = [9,7,5], k = 3
输出: 5
解释:
- 把第一条绳子切成两部分,一条长度为 5,一条长度为 4;
- 把第二条绳子切成两部分,一条长度为 5,一条长度为 2;
- 第三条绳子不进行切割;
现在,你得到了 3 条长度为 5 的绳子。
示例 2:
输入: ribbons = [7,5,9], k = 4
输出: 4
解释:
- 把第一条绳子切成两部分,一条长度为 4,一条长度为 3;
- 把第二条绳子切成两部分,一条长度为 4,一条长度为 1;
- 把第二条绳子切成三部分,一条长度为 4,一条长度为 4,还有一条长度为 1;
现在,你得到了 4 条长度为 4 的绳子。
示例 3:
输入: ribbons = [5,7,9], k = 22
输出: 0
解释: 由于绳子长度需要为正整数,你无法得到 22 条长度相同的绳子。
提示:
1 <= ribbons.length <= 105
1 <= ribbons[i] <= 105
1 <= k <= 109
public static int maxLength(int[] ribbons, int k) {int max = Integer.MIN_VALUE;for(int i = 0;i < ribbons.length;i++) {max = Math.max(max,ribbons[i]);}int left = 1;int right = max;int pre = -1;while(left <= right) {int mid = left + (right - left) / 2;int count = getSplitNum(ribbons,mid);if(count >= k) {pre = mid;left = mid + 1;} else {right = mid -1;}}return pre == -1?0:pre;}private static int getSplitNum(int[] ribbons,int mid) {int count = 0;for(int len:ribbons) {count += len / mid;}return count;}public static void main(String[] args) {int[] ribbons = new int[]{1,1,1};System.out.println(maxLength(ribbons,3));}
