mid的计算方式
1 2 3 4 int mid1 = right - ((right - left ) >> 1 );int mid2 = left + (right - left ) / 2 ; 当right = 5 , left = 4 时 mid1 = 5 , mid2 = 4
总结经验
求大于(等于)target的最小值返回left
思路:尽量执行right = mid - 1,当最后一次满足check(mid) <= target并向左滑动右区间,之后只会向右滑动左区间,最后left = right + 1退出while循环,left就是最小的最大值
1 2 3 4 5 6 7 8 while (left <= right) { int mid = (left + right) >> 1 ; if (check(mid) <= target) right = mid - 1 ; else left = mid + 1 ; }return left;
while的执行条件为left <= right
if (check(mid) <= m) // mid大了 right = mid - 1;
return left ;
只要nums[mid] > target,令right = mid - 1,当遇到大于target的最小值时,也会让right = mid - 1,之后在while循环中都不满足nums[mid] > target,只会执行left = mid + 1,直到left = right + 1退出while循环,此时的left便是大于target的最小值
求小于target的最大值返回right
求大于等于target的最小值
如果target在数组中,则会在while循环内被return
如果target不在数组中,while会遍历到left>right结束循环
在left>right的上一步一定是left == right,此时nums[mid]<target,由于右边界是right,可以确定target一定小于nums[right + 1] (或者nums[right]已经是右边界了)
所以while退出循环时,left指向的位置是大于target的,插入在这个位置就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int searchInsert (int [] nums, int target) { int left = 0 , right = nums.length - 1 ; while (left <= right) { int mid = (left + right) >> 1 ; if (nums[mid] == target) return mid; else if (nums[mid] > target) right = mid - 1 ; else left = mid + 1 ; } return left; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int peakIndexInMountainArray (int [] arr) { int left = 1 , right = arr.length - 2 ; while (left <= right) { int mid = (left + right) >> 1 ; if (arr[mid] > arr[mid - 1 ] && arr[mid] > arr[mid + 1 ]) return mid; else if (arr[mid] > arr[mid - 1 ]) left = mid + 1 ; else right = mid - 1 ; } return -1 ; } }
优质题解 ,这道题需要在纸上模拟
在单一元素x之前,成对出现的第一个元素下标一定是偶数
在单一元素x之后,成对出现的第一个元素下标一定是奇数
举例
1 2 数组:1 1 2 2 3 4 4 5 5 下标:0 1 2 3 4 5 6 7 8
单一元素x的值为3,下标为4
在x之前,成对出现的元素有1 1, 2 2,第一个1的下标为0,第一个2的下标为2,均为偶数
在x之后,成对出现的元素有4 4,5 5,第一个4的下标为5,第一个5的下标为7,均为奇数
因此可以根据mid下标的奇偶性来进行二分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public int singleNonDuplicate (int [] nums) { int n = nums.length; int left = 0 , right = n - 1 ; while (left < right) { int mid = (left + right) >> 1 ; if (mid % 2 == 0 ) { if (mid + 1 < n && nums[mid] == nums[mid + 1 ]) left = mid + 2 ; else right = mid; } else { if (mid - 1 >= 0 && nums[mid] == nums[mid - 1 ]) left = mid + 1 ; else right = mid - 1 ; } } return nums[right]; } }
如果nums时未排序的,那么这就是另一类经典面试题
将数组中所有数字异或,最终的结果就是那个唯一只出现一次的数字
1 2 3 4 5 6 7 8 class Solution { public int singleNonDuplicate (int [] nums) { int res = 0 ; for (int i = 0 ; i < nums.length; ++i) res = res ^ nums[i]; return res; } }
方法一: 前缀和 + 二分查找
剑指offer题解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution { private int total; private int [] preSum; private int n; public Solution (int [] w) { n = w.length; preSum = new int [n]; for (int i = 0 ; i < n; ++i) { total += w[i]; preSum[i] += total; } } public int pickIndex () { Random random = new Random (); int p = random.nextInt(total); int left = 0 , right = n - 1 ; while (left <= right) { int mid = (left + right) >> 1 ; if (preSum[mid] == p) return mid + 1 ; else if (preSum[mid] > p) right = mid - 1 ; else left = mid + 1 ; } return left; } }
方法一:二分查找
求平方小于等于x的最大值
如果x取根号为整数,会在while循环被返回
如果x取根号不为整数,while循环不会return,结束时right = left - 1,\(right^2 < x\) 并且\(left^2 > x\) , right就是所求答案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int mySqrt (int x) { int left = 1 , right = x; while (left <= right) { int mid = right - (right - left >> 1 ); if (mid == x / mid) return mid; else if (mid < x / mid) left = mid + 1 ; else right = mid - 1 ; } return right; } }
方法二:二分查找另一种写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public int mySqrt (int x) { int left = 1 , right = x; while (left <= right) { int mid = right - (right - left >> 1 ); if (mid <= x / mid) { if ((mid + 1 ) > x / (mid + 1 )) return mid; left = mid + 1 ; } else right = mid - 1 ; } return 0 ; } }
方法一:二分查找
1 2 3 4 if (hours <= h) { res = mid; right = mid - 1 ; }
只要hours <= h,说明速度太快,降低速度,并且记录这次能满足hours = h的速度
之后总会进入else语句,最终left > right退出循环
最后一次记录的res就是答案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution { public int minEatingSpeed (int [] piles, int h) { int n = 0 ; for (int pile : piles) n = Math.max(n, pile); int left = 1 , right = n,res = -1 ; while (left <= right) { int mid = (left + right) >> 1 ; int hours = getHours(mid, piles); if (hours <= h) { res = mid; right = mid - 1 ; } else { left = mid + 1 ; } } return res; } private int getHours (int speed, int [] piles) { int hours = 0 ; for (int pile : piles) hours += (pile + speed - 1 ) / speed; return hours; } }
方法二:二分查找另一种写法
与方法一类似,不过不需要用一个变量记录答案
最后一次满足在h小时内吃完香蕉后,right = mid - 1,这个mid就是答案
之后一直循环else,最后left = right + 1退出循环,这个left就是最后一次满足在h小时内吃完香蕉的su'du
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution { public int minEatingSpeed (int [] piles, int h) { int n = 0 ; for (int pile : piles) n = Math.max(n, pile); int left = 1 , right = n; while (left <= right) { int mid = (left + right) >> 1 ; int hours = getHours(mid, piles); if (hours <= h) { right = mid - 1 ; } else left = mid + 1 ; } return left; } private int getHours (int speed, int [] piles) { int hours = 0 ; for (int pile : piles) hours += (pile + speed - 1 ) / speed; return hours; } }
和排序数组中只出现一次的数字类似
方法一:二分查找
如果isBadVersion(mid)为true,说明当前版本或者之前的版本出错,那么令right = mid - 1(即使是mid出错了)往前面搜。当遇到第一个出错的版本(我们不知道是不是第一个),并且执行right = mid - 1,之后的while循环中只会执行else部分,
即left = mid + 1;最终left会等于right + 1并且退出while循环,这个left就是第一个出错的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Solution extends VersionControl { public int firstBadVersion (int n) { int left = 1 , right = n; while (left <= right) { int mid = right - ((right - left) >> 1 ); if (isBadVersion(mid)) right = mid - 1 ; else left = mid + 1 ; } return left; } }
方法一:两端添加负无穷
一定要return 0,解决只有一个数的用例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int findPeakElement (int [] arr) { int n = arr.length, left = 1 , right = n; int [] nums = new int [n + 2 ]; for (int i = 0 ; i < n; ++i) nums[i + 1 ] = arr[i]; nums[0 ] = nums[n + 1 ] = Integer.MIN_VALUE; while (left <= right) { int mid = right - ((right - left) >> 1 ); if (nums[mid] > nums[mid - 1 ] && nums[mid] > nums[mid + 1 ]) return mid - 1 ; if (nums[mid] < nums[mid + 1 ]) left = mid + 1 ; else right = mid - 1 ; } return 0 ; } }
方法二:
int mid = left + (right - left) / 2; // mid要选较小的
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int findPeakElement (int [] nums) { int n = nums.length, left = 0 , right = n - 1 ; while (left < right) { int mid = left + (right - left) / 2 ; if (mid + 1 < n && nums[mid] > nums[mid + 1 ]) right = mid; else left = mid + 1 ; } return left; } }
方法一:二分查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public boolean isPerfectSquare (int num) { int left = 1 , right = num; while (left <= right) { int mid = right - ((right - left) >> 1 ); if (mid == num / mid && num % mid == 0 ) return true ; if (mid > num / mid) right = mid - 1 ; else left = mid + 1 ; } return false ; } }
求大于target的最小值
首先判断target是否大于letters中最后一个字符,如果大于,直接返回letters[0]
进入while循环,如果letters[mid] - 'a' <= target - 'a',令left = mid + 1,最后一次执行这个else循环的letters[mid]就是答案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public char nextGreatestLetter (char [] letters, char target) { int left = 0 , right = letters.length - 1 ; if (target - 'a' >= letters[right] - 'a' ) return letters[0 ]; while (left <= right) { int mid = (left + right) >> 1 ; if (letters[mid] - 'a' > target - 'a' ) right = mid - 1 ; else left = mid + 1 ; } return letters[left]; } }
方法一:二分查找
二分两次,第一次找大于等于target的最小值,第二次找小于等于target的最大值,从而找到target第一个以及最后一个位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public int [] searchRange(int [] nums, int target) { int [] res = new int []{-1 , -1 }; if (nums.length == 0 ) return res; int resL = -1 , resR = -1 ; int left = 0 , right = nums.length - 1 ; while (left <= right) { int mid = (left + right) >> 1 ; if (nums[mid] >= target) right = mid - 1 ; else left = mid + 1 ; } if (left >= 0 && left < nums.length) resL = nums[left] == target ? left : -1 ; left = 0 ; right = nums.length - 1 ; while (left <= right) { int mid = (left + right) >> 1 ; if (nums[mid] > target) right = mid - 1 ; else left = mid + 1 ; } if (right >= 0 && right < nums.length) resR = nums[right] == target ? right : -1 ; res = new int []{resL, resR}; return res; } }
优质题解
方法一:二分查找
不能写成getSplitNum(nums, mid) >= k,以下这个例子
输入:nums = [7,2,5,10,8], m = 2 输出:18 解释: 一共有四种方法将 nums 分割为 2 个子数组。 其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。 因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
如果写成getSplitNum(nums, mid) >= k,当mid=21(答案是18),得到的getSplitNum(nums, mid) = 2 >= k,会继续让mid往更大的方向搜
1 2 3 4 5 if (getSplitNum(nums, mid) > k) left = mid + 1 ;else right = mid - 1 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution { public int splitArray (int [] nums, int k) { int left = 0 , right = 0 ; for (int num: nums) { if (left < num) left = num; right += num; } while (left <= right) { int mid = (left + right) >> 1 ; if (getSplitNum(nums, mid) > k) left = mid + 1 ; else right = mid - 1 ; } return left; } private int getSplitNum (int [] nums, int maxSum) { int splitNum = 1 , curSum = 0 ; for (int num : nums) { if (curSum + num > maxSum) { ++splitNum; curSum = 0 ; } curSum += num; } return splitNum; } }
二刷
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { public int splitArray (int [] nums, int k) { int l = 0 , r = 0 ; for (int x : nums) { r += x; l = Math.max(l, x); } while (l <= r) { int mid = (l + r) >> 1 ; if (check(nums, k, mid)) r = mid - 1 ; else l = mid + 1 ; } return l; } private boolean check (int [] nums, int k, int mid) { int bucket = 1 , sum = 0 ; for (int x : nums) { if (sum + x > mid) { ++bucket; sum = 0 ; } sum += x; } return bucket <= k; } }
和分割数组的最大值相比,加了一个条件,每个分割后的数组减去其中的最大值
getSpendDay函数,在每一个新的一天第一次要刷的题先不加入totalTime,先记录maxTime,最后少计入时长的那道题就是需要花费时间最长的题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public int minTime (int [] time, int m) { int left = 0 , right = 0 ; for (int t : time) right += t; while (left <= right) { int mid = (left + right) >> 1 ; if (getSpendDay(time, mid) <= m) right = mid - 1 ; else left = mid + 1 ; } return left; } public int getSpendDay (int [] time, int limit) { int totalTime = 0 , maxTime = 0 , day = 1 ; for (int t : time) { int nextTime = Math.min(maxTime, t); if (totalTime + nextTime <= limit) { totalTime += nextTime; maxTime = Math.max(maxTime, t); } else { ++day; totalTime = 0 ; maxTime = t; } } return day; } }
方法一:二分查找
求能满足条件的最小值
如果用以下代码
1 2 3 4 5 if (check(bloomDay, k, mid) <= m) left = mid + 1 ;else right = mid - 1 ;return right;
举例
输入: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
mid == 5,check(bloomDay, k, mid) == 3(m),此时还左区间还要向右边滑动当然错了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Solution { public int minDays (int [] bloomDay, int m, int k) { int n = bloomDay.length; if (n / m < k) return -1 ; int left = 0 , right = 0 ; for (int flower : bloomDay) right = Math.max(right, flower); while (left <= right) { int mid = (left + right) >> 1 ; if (check(bloomDay, k, mid) < m) left = mid + 1 ; else right = mid - 1 ; } return left; } private int check (int [] bloomDay, int k, int days) { int bouquets = 0 , curFlower = 0 ; for (int flower : bloomDay) { if (flower <= days) { ++curFlower; if (curFlower == k) { ++bouquets; curFlower = 0 ; } } else { curFlower = 0 ; } } return bouquets; } }
和分割数组的最大值一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution { public int shipWithinDays (int [] weights, int days) { int n = weights.length, left = 0 , right = 0 ; for (int w : weights) { left = Math.max(left, w); right += w; } while (left <= right) { int mid = (left + right) >> 1 ; if (check(weights, mid) <= days) right = mid - 1 ; else left = mid + 1 ; } return left; } private int check (int [] weights, int capacity) { int total = 0 , spendDay = 1 ; for (int w : weights) { if (total + w <= capacity) total += w; else { ++spendDay; total = w; } } return spendDay; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution { public int maxDistance (int [] position, int m) { Arrays.sort(position); int n = position.length, left = 1 , right = position[n - 1 ] - position[0 ]; while (left <= right) { int mid = (left + right) >> 1 ; if (check(position, mid) >= m) left = mid + 1 ; else right = mid - 1 ; } return right; } private int check (int [] position, int limit) { int pre = position[0 ], count = 1 ; for (int i = 1 ; i < position.length; ++i) { if (position[i] - pre >= limit) { pre = position[i]; ++count; } } return count; } }