使用二分法可将查找的时间复杂度从o(n)降低至o(logn)。使用二分法要特别注意搜索的区间是否有遗漏,大部分的出错点都在于此。二分查找常用于查找某个数或是查找某个边界
题眼:有序
查找某个数的框架模板:
给定一个
n
个元素有序的(升序)整型数组nums
和一个目标值target
,写一个函数搜索nums
中的target
,如果目标值存在返回下标,否则返回-1
。
示例 1:输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2 输出: -1 解释: 2 不存在 nums 中因此返回-1
int binarySearch(int[] nums, int target){
if(nums == null || nums.length == 0){
return -1;
}
int left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;//这样写可以避免left+right过大而溢出
if(nums[mid] == target){
return mid;
}
else if(nums[mid] < target){
left = mid + 1;
}
else{
right = mid - 1;
}
}
return -1;
}
你总共有 n 枚硬币,你需要将它们摆成一个阶梯形状,第 k 行就必须正好有 k 枚硬币。给定一个数字 n,找出可形成完整阶梯行的总行数。n 是一个非负整数,并且在32位有符号整型的范围内。
示例 1:
n = 5
硬币可排列成以下几行:
¤
¤ ¤
¤ ¤因为第三行不完整,所以返回2.
示例 2:
n = 8
硬币可排列成以下几行:
¤
¤ ¤
¤ ¤ ¤
¤ ¤因为第四行不完整,所以返回3.
class Solution {
public int arrangeCoins(int n) {
int left = 0, right = n;
while(left<=right){
long mid = left + (right - left) / 2;
long sum = mid * (mid+1) / 2; //公式需要推导一下
if(sum == n){
return (int)mid;
}else if(sum < n){
left = (int)mid + 1;
}
else if(sum > n){
right = (int)mid - 1;
}
}
return right;
}
}
查找某个边界的框架模板:
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true所以,4 是第一个错误的版本。
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int left = 1, right = n;
while(left <= right){
int mid = left + (right - left) / 2;
boolean temp = isBadVersion(mid);
if(temp == false){
left = mid + 1;
}else if(temp == true){
right = mid - 1;//逼近左边界
}
}
return left;
}
}
珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
示例 1:
输入: piles = [3,6,7,11], H = 8
输出: 4示例 2:
输入: piles = [30,11,23,4,20], H = 5
输出: 30示例 3:
输入: piles = [30,11,23,4,20], H = 6
输出: 23
class Solution {
public int minEatingSpeed(int[] piles, int H) {
int left = 1, right = getMax(piles);
while(left <= right){
int mid = left+ (right - left) / 2;
boolean temp = canFinish(mid, piles, H);
if(temp == true){
right = mid - 1;
}else if(temp == false){
left = mid + 1;
}
}
return left;
}
private int getMax(int[] piles){
int max = 0;
for(int n: piles){
max = Math.max(max, n);
}
return max;
}
private boolean canFinish(int speed, int[] piles, int H){
int h = H;
for(int i = 0; i < piles.length; i++){
if(piles[i]%speed == 0){
h = h - (piles[i]/speed);
}else{
h = h - (piles[i]/speed) - 1;
}
if(h<0){
return false;
}
}
return true;
}
}
传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。
传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。
返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力。
示例 1:
输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 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], D = 3
输出:6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4示例 3:
输入:weights = [1,2,3,1,1], D = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
class Solution {
public int shipWithinDays(int[] weights, int D) {
int left = getMax(weights), right = sum(weights);
while(left <= right){
int mid = left + (right - left) / 2;
boolean temp = canFinish(mid, weights, D);
if(temp == true){
right = mid - 1;
}else if(temp == false){
left = mid + 1;
}
}
return left;
}
private int getMax(int[] weights){
int max = 0;
for(int n: weights){
max = Math.max(max, n);
}
return max;
}
private int sum(int[] weights){
int sum = 0;
for(int n: weights){
sum = sum + n;
}
return sum;
}
private boolean canFinish(int minWeight, int[] weights, int D){
int d = D;
int tempWeight = 0;
for(int i = 0; i < weights.length; i++){
if(tempWeight + weights[i] <= minWeight){
tempWeight = tempWeight + weights[i];
}else{
d = d - 1;
tempWeight = weights[i];
}
if(d < 1){
return false;
}
}
return true;
}
}
例题6:LeetCode 1482.制作m束花所需要的最少天数
给你一个整数数组 bloomDay,以及两个整数 m 和 k 。现需要制作 m 束花。制作花束时,需要使用花园中相邻的 k 朵花 。花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好可以用于一束花中。请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1 。
示例 1:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 1
输出:3
解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。
现在需要制作 3 束花,每束只需要 1 朵。
1 天后:[x, _, _, _, _] // 只能制作 1 束花
2 天后:[x, _, _, _, x] // 只能制作 2 束花
3 天后:[x, _, x, _, x] // 可以制作 3 束花,答案为 3示例 2:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 2
输出:-1
解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1 。示例 3:
输入:bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3
输出:12
解释:要制作 2 束花,每束需要 3 朵。
花园在 7 天后和 12 天后的情况如下:
7 天后:[x, x, x, x, _, x, x]
可以用前 3 朵盛开的花制作第一束花。但不能使用后 3 朵盛开的花,因为它们不相邻。
12 天后:[x, x, x, x, x, x, x]
显然,我们可以用不同的方式制作两束花。示例 4:
输入:bloomDay = [1000000000,1000000000], m = 1, k = 1
输出:1000000000
解释:需要等 1000000000 天才能采到花来制作花束示例 5:
输入:bloomDay = [1,10,2,9,3,8,4,7,5,6], m = 4, k = 2
输出:9
class Solution {
public int minDays(int[] bloomDay, int m, int k) {
int left = 1, right = getMax(bloomDay);
int result = -1;
while(left <= right){
int mid = left + (right - left) / 2;
boolean temp = canFinish(mid, bloomDay, m, k);
if(temp == true){
right = mid - 1;
}else if(temp == false){
left = mid + 1;
}
}
if(left <= getMax(bloomDay)){
result = left;
}
return result;
}
private boolean canFinish(int mid, int[] bloomDay, int m, int k){
int slow = 0, fast = 0;
while(fast != bloomDay.length){
if(bloomDay[fast] - mid <= 0){//摘够k朵
if(fast - slow + 1 == k){
m = m - 1;
slow = fast+1;
}
}
if(bloomDay[fast] - mid > 0){//遇到没开的花
slow = fast+1;
}
fast = fast + 1;
if(m == 0){
return true;
}
}
return false;
}
private int getMax(int[] nums){
int max = 0;
for(int item: nums){
max = Math.max(item, max);
}
return max;
}
}
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。数组长度 n 满足: 1 ≤ n ≤ 1000, 1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2输出:
18解释:
一共有四种方法将nums分割为2个子数组。其中最好的方式是将其分为[7,2,5] 和 [10,8],因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
class Solution {
public int splitArray(int[] nums, int m) {
int left = getMax(nums), right = sum(nums);
while(left <= right){
int mid = left + (right - left) / 2;
System.out.println(left+"_"+right+"_"+mid);
boolean temp = canFinish(mid, nums, m);
if(temp == true){
right = mid - 1;
}else if(temp == false){
left = mid + 1;
}
}
return left;
}
private boolean canFinish(int mid, int[] nums, int m){
int temp = mid;
for(int i = 0; i < nums.length; i++){
if(temp - nums[i] >= 0){
temp = temp - nums[i];
}else if(temp - nums[i] < 0){
if(m > 1){
temp = mid - nums[i];
m = m - 1;
}else{
return false;
}
}
}
return true;
}
private int getMax(int[] nums){
int max = 0;
for(int item: nums){
max = Math.max(item, max);
}
return max;
}
private int sum(int[] nums){
int result = 0;
for(int item: nums){
result = result + item;
}
return result;
}
}
为了提高自己的代码能力,小张制定了 LeetCode 刷题计划,他选中了 LeetCode 题库中的 n 道题,编号从 0 到 n-1,并计划在 m 天内按照题目编号顺序刷完所有的题目(注意,小张不能用多天完成同一题)。在小张刷题计划中,小张需要用 time[i] 的时间完成编号 i 的题目。此外,小张还可以使用场外求助功能,通过询问他的好朋友小杨题目的解法,可以省去该题的做题时间。为了防止“小张刷题计划”变成“小杨刷题计划”,小张每天最多使用一次求助。我们定义 m 天中做题时间最多的一天耗时为 T(小杨完成的题目不计入做题总时间)。请你帮小张求出最小的 T是多少。
示例 1:
输入:time = [1,2,3,3], m = 2
输出:3
解释:第一天小张完成前三题,其中第三题找小杨帮忙;第二天完成第四题,并且找小杨帮忙。这样做题时间最多的一天花费了 3 的时间,并且这个值是最小的。
示例 2:
输入:time = [999,999,999], m = 4
输出:0
解释:在前三天中,小张每天求助小杨一次,这样他可以在三天内完成所有的题目并不花任何时间。
class Solution {
public int minTime(int[] time, int m) {
if(m >= time.length){
return 0;
}
int left = 0, right = sum(time);
while(left <= right){
int mid = left + (right - left) / 2;
boolean temp = canFinish(mid, time, m);
if(temp == true){
right = mid - 1;
}else if(temp == false){
left = mid + 1;
}
}
return left;
}
private int sum(int[] time){
int s = 0;
for(int i = 0; i < time.length; i++){
s = s + time[i];
}
return s;
}
private boolean canFinish(int mid, int[] time, int m){
int max = 0, temp = mid;
for(int i = 0; i < time.length; i++){
max = Math.max(time[i], max);
if(temp - time[i] + max >=0){
temp = temp - time[i];
}else if(temp - time[i] + max < 0){
if(m > 1){
temp = mid - time[i];
max = time[i];
m = m - 1;
}else{
return false;
}
}
}
return true;
}
}
不难发现,例题3,例题4,例题5,例题6, 例题7,例题8的核心框架其实都是一样的,都是例题3的代码,例题4,例题5,例题6,例题7,例题8只是赋予了不同的所要逼近的边界条件。
例题9:LeetCode 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 left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == target){
if(nums[left] != target){
left = left + 1;//逼近左边界
}
if(nums[right] != target){
right = right - 1;//逼近右边界
}
if(nums[right] == target && nums[left] == target){
break;
}
}else if(nums[mid] > target){
right = mid - 1;
}else if (nums[mid] < target){
left = mid + 1;
}
}
if(left > right){
return new int[]{-1, -1};
}else{
return new int[]{left, right};
}
}
}
前面提到,使用二分法的一个前提是数组是有序的,如果数组不是严格有序,也可以利用局部有序的特点,使用二分法。如LeetCode上的排序旋转数组系列:
例题10:LeetCode 153.寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] 。请找出其中最小的元素。限制时间复杂度为 O(log n) 。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0示例 3:
输入:nums = [1]
输出:1
旋转排序数组可以看作是两个升序数组拼接在一起,且第二个升序数组的起始值小于第一个升序数组的结束值,所以可以利用这一特点寻找分界点:
class Solution {
public int findMin(int[] nums) {
int n = nums.length;
if(n == 1){
return nums[0];
}else if(nums.length>1){
if(nums[0] < nums[n - 1]){
return nums[0];
}
}
int left = 0, right = n - 1;
int result = 0;
while(left <= right){
int mid = left + (right-left)/2;
if(nums[mid] > nums[mid+1]){//下一元素比当前元素小,找到分界点
result = nums[mid+1];
break;
}else if(nums[left] > nums[mid]){//左边不是严格有序,分界点在左边
right = mid; //特别注意这里是right = mid 而不是 right = mid + 1,因为终止条件实际上要搜索两个数(且为中间的和中间偏右的),所以当右边界收缩时,需要少收缩1个
}else if(nums[left] < nums[mid]){//右边不是严格有序,分界点在右边
left = mid + 1;
}
}
return result;
}
}
例题11:LeetCode: 154.寻找旋转排序数组中的最小值II
假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。请找出其中最小的元素。注意数组中可能存在重复的元素。限制时间复杂度为 O(log n) 。
示例 1:
输入: [1,3,5]
输出: 1示例 2:
输入: [2,2,2,0,1]
输出: 0
class Solution {
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while(left <= right){
if(nums[left] < nums[right]){//在当前搜索区间里,左边元素小于右边元素,说明是升序的,左边即为最小
return nums[left];
}
int mid = left + (right-left)/2;
if(nums[left] > nums[mid]){//在当前搜索区间里,左边元素大于中间元素,说明在当前搜索区间里,左边到中间这一段不是严格有序的,应当在这一段里查找
right = mid;
}else if(nums[left] < nums[mid]){//在当前搜索区间里,左边元素小于中间元素,说明在当前搜索区间里,中间到右边这一段不是严格有序的,应当在这一段里查找
left = mid;
}else if(nums[left] == nums[mid]){//在当前搜索区间里,左边元素等于中间元素,说明在当前搜索区间里,中间的和左边的相等,可以成为左边元素的替代,搜索区间左边可以收缩一步
left = left + 1;
}
}
return nums[right];
}
}
例题12:LeetCode 33.搜索旋转排序数组
升序排列的整数数组 nums 在预先未知的某个点上进行了旋转(例如, [0,1,2,4,5,6,7] 经旋转后可能变为 [4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。限制时间复杂度为 O(log n) 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1示例 3:
输入:nums = [1], target = 0
输出:-1
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
if(n == 1){
if(nums[0] == target){
return 0;
}else{
return -1;
}
}
int result = -1, xuanzhuan=0, left, right;
if(nums[0] < nums[n-1]){
xuanzhuan = 0;
}
else{
left = 0;
right = n - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] > nums[mid+1]){
xuanzhuan = mid+1;
break;
}else if(nums[left] > nums[mid]){
right = mid;
}else if(nums[left] < nums[mid]){
left = mid;
}
}
}
if(target <= nums[n-1]){
left = xuanzhuan;
right = n-1;
}else{
left = 0;
right = xuanzhuan - 1;
}
while(left<=right){
int mid = left + (right - left) / 2;
if(nums[mid]<target){
left = mid+1;
}else if(nums[mid]>target){
right = mid-1;
}else if(nums[mid] == target){
result = mid;
break;
}
}
return result;
}
}
我们把符合下列属性的数组 A 称作山脉:
A.length >= 3
存在 0 < i < A.length - 1 使得A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1]给定一个确定为山脉的数组,返回任何满足 A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1] 的 i 的值。
示例 1:
输入:[0,1,0]
输出:1示例 2:
输入:[0,2,1,0]
输出:1
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int left = 0, right = arr.length-1;
while(left <= right){
int mid = left + (right - left) / 2;
System.out.println(left+"_"+mid+"_"+right);
if(arr[mid] > arr[mid+1] && arr[mid] > arr[mid-1]){//先比较mid和mid+1,因为mid+1不会越界
return mid;
}else if(arr[mid] < arr[mid+1]){//先比较mid和mid+1,因为mid+1不会越界
left = mid + 1;
}else if(arr[mid] < arr[mid-1]){
right = mid - 1;
}
}
return -1;
}
}
例题14: LeetCode 1095.山脉数组中查找目标值
(这是一个 交互式问题 )
给你一个 山脉数组 mountainArr,请你返回能够使得 mountainArr.get(index) 等于 target 最小 的下标 index 值。如果不存在这样的下标 index,就请返回 -1。
何为山脉数组?如果数组 A 是一个山脉数组的话,那它满足如下条件:
A.length >= 3
存在 0 < i < A.length - 1 使得A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1]你将 不能直接访问该山脉数组,必须通过 MountainArray 接口来获取数据:
MountainArray.get(k) - 会返回数组中索引为k 的元素(下标从 0 开始)
MountainArray.length() - 会返回该数组的长度注意:
对 MountainArray.get 发起超过 100 次调用的提交将被视为错误答案。此外,任何试图规避判题系统的解决方案都将会导致比赛资格被取消。
示例 1:
输入:array = [1,2,3,4,5,3,1], target = 3
输出:2
解释:3 在数组中出现了两次,下标分别为 2 和 5,我们返回最小的下标 2。示例 2:
输入:array = [0,1,2,4,2,1], target = 3
输出:-1
解释:3 在数组中没有出现,返回 -1。
class Solution {
public int findInMountainArray(int target, MountainArray mountainArr) {
int left = 0, right = mountainArr.length() - 1;
int peekIndex = 0;
while(left <= right){
int mid = left + (right - left) / 2;
int tempMid = mountainArr.get(mid);
int tempMidAfter = mountainArr.get(mid+1);
if(tempMid>tempMidAfter && tempMid>mountainArr.get(mid-1)){
peekIndex = mid;
break;
}else if(tempMid < tempMidAfter){
left = mid + 1;
}else if(tempMid < mountainArr.get(mid-1)){
right = mid - 1;
}
}
if(target == mountainArr.get(peekIndex)){
return peekIndex;
}
left = 0;
right = peekIndex;
while(left <= right){
int mid = left + (right - left)/2;
int tempMid = mountainArr.get(mid);
if(tempMid == target){
return mid;
}else if(tempMid < target){
left = mid + 1;
}else if(tempMid > target){
right = mid - 1;
}
}
left = peekIndex;
right = mountainArr.length() - 1;
while(left <= right){
int mid = left + (right - left)/2;
int tempMid = mountainArr.get(mid);
if(tempMid == target){
return mid;
}else if(tempMid < target){
right = mid - 1;
}else if(tempMid > target){
left = mid + 1;
}
}
return -1;
}
}