1. 二分搜索模板
1.1. 基本二分搜索
- leetcode 35
- leetcode 74
- leetcode 374
如:int[ ] nums = {1,3,4,5,6,8,12,14,16},target = 8 ,在nums中找到target的索引,找不到返回-1。
public int binarySearch(int[] nums, int target){
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right-left)/2;
if(nums[mid] == target){
return mid;
} else if(nums[mid] > target){
right = mid - 1;
} else if(nums[mid] < target){
left = mid +1;
}
}
return -1
}
说明
line 4:left大于right为结束条件
line 5:不写成 int mid = (right+left)/2是为了防止int类型溢出
line14:while循环中未找到则返回-1
1.2. 查找最左边界
- leetcode 34
- leetcode 275
- leetcode 278
如:int[ ] nums = {1,3,4,8,8,8,12,14,16},target = 8 ,在nums中找到target第一次出现的索引,找不到返回-1。
public int leftBoundary(int[] nums, int target){
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right-left)/2;
if(nums[mid] == target){
right = mid - 1;
} else if(nums[mid] > target){
right = mid - 1;
} else if(nums[mid] < target){
left = mid +1;
}
}
if(left>=nums.length || nums[left]!=target){
return -1;
}
return left;
}
说明
line 4:left大于right为结束条件
line 5:不写成 int mid = (right+left)/2是为了防止int类型溢出
line7:因为要找最左边界,所以向左移动区间
line14:因为返回left,所以要防止left越界,同时要保证left处的值是target
1.3. 查找最右边界
- leetcode 34
如:int[ ] nums = {1,3,4,8,8,8,12,14,16},target = 8 ,在nums中找到target最后一次出现的索引,找不到返回-1。
public int rightBoundary(int[] nums, int target){
int left = 0;
int right = nums.length - 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){
left = mid +1;
}
}
if(right<0 || nums[right]!=target){
return -1;
}
return right;
}
说明
line 4:left大于right为结束条件
line 5:不写成 int mid = (right+left)/2是为了防止int类型溢出
line7:因为要找最右边界,所以向右移动区间
line14:因为返回right,所以要防止right越界,同时保证right处的值是target
2. 二分搜索变种
2.1. 旋转数组
此类题数组不完全有序,有的甚至要考虑重复值,此类题利用数组的部分有序性
- leetcode 33:旋转数组,找某个数,元素不重复
- leetcode 81:旋转数组,找某个数,元素重复
- leetcode 153:旋转数组,找最小值,元素不重复
- leetcode 154:旋转数组,找最小值,元素重复
2.1.1 leetcode 33
public int search(int[] nums, int target) {
int n = nums.length;
int left = 0;
int right = n-1;
while(left<=right){
int mid = left +(right-left)/2;
if(nums[mid]==target){
return mid;
}
if(nums[left]<=nums[mid]){
if(nums[left]<=target && target<nums[mid]){
right = mid-1;
}else{
left = mid+1;
}
}else{
if(nums[mid]<target && target<=nums[right]){
left = mid+1;
}else{
right = mid-1;
}
}
}
return -1;
}
说明
- 若nums[mid]==target,直接返回
- 若nums[left]<=numsmid,说明左侧区间[left,mid)连续递增,此时:
- 若nums[left]<=target<nums[mid],则在左侧区间查找,令right=mid-1;
- 否则,则在右侧区间查找,令left=mid+1。
- 否则说明右侧区间(mid,right]连续递增,此时:
- 若nums[mid]<target<=nums[right],则在右侧区间查找,令left=mid+1;
- 否则,在左侧区间查找,令right=mid-1。
2.1.2 leetcode 81
public boolean search(int[] nums, int target) {
int n = nums.length;
int left = 0;
int right = n-1;
while(left<=right){
int mid = left +(right-left)/2;
if(nums[mid]==target){
return true;
}
if(nums[left]<nums[mid]){
if(nums[left]<=target && target<nums[mid]){
right = mid-1;
}else{
left = mid+1;
}
}else if(nums[left]==nums[mid]){
left++;
}else{
if(nums[mid]<target && target<=nums[right]){
left = mid+1;
}else{
right = mid-1;
}
}
}
return false;
}
说明
- 若nums[mid]==target,直接返回
- 存在重复元素,所以与leetcode33的区别是,当nums[left]==nums[mid]时,无法判断区间的单调性,这个时候通过left++去除重复元素即可。与leetcode33相比只需要增加line16-line17即可。
- 若nums[left]<nums[mid],说明左侧区间[left,mid)连续递增,此时:
- 若nums[left]<=target<nums[mid],则在左侧区间查找,令right=mid-1;
- 否则,则在右侧区间查找,令left=mid+1。
- 否则说明右侧区间(mid,right]连续递增,此时:
- 若nums[mid]<target<=nums[right],则在右侧区间查找,令left=mid+1;
- 否则,在左侧区间查找,令right=mid-1。
2.1.3 leetcode 153
public int findMin(int[] nums) {
int n = nums.length;
int left = 0;
int right = n-1;
while(left<right){
int mid = left +(right-left)/2;
if(nums[mid]<nums[right]){
right = mid;
}else{
left = mid+1;
}
}
return nums[left];
}
说明
- 假设数组最后一个元素为x,以最小值为分界,最小值右边(包含最小值)一定小于x,左边一定大于x。
- 若nums[mid]<nums[right],说明最小值在[left,mid],此时right=mid
- 否则最小值在[mid+1,right]中,此时left=mid+1。
- 找最小值相当于最左边界,需要返回left,通过测试几个例子while中条件需要为left<right。
2.1.4 leetcode 154
public int findMin(int[] nums) {
int n = nums.length;
int left = 0;
int right = n-1;
while(left<right){
int mid = left +(right-left)/2;
if(nums[mid]<nums[right]){
right = mid;
}else if(nums[mid]==nums[right]){
right--;
}else{
left = mid+1;
}
}
return nums[left];
}
说明
- 假设数组最后一个元素为x,以最小值为分界,最小值右边(包含最小值)一定小于等于x,左边一定大于等于x。
- 若nums[mid]<nums[right],说明最小值在[left,mid],此时right=mid
- 若nums[mid]==nums[right],最小值可能在左边也可能在右边,所以需要消除重复值,right–
- 否则最小值在[mid+1,right]中,此时left=mid+1。
- 找最小值相当于最左边界,需要返回left,通过测试几个例子while中条件需要为left<right。
2.2 找峰值
- leetcode 162
如:峰值元素是指其值严格大于左右相邻值的元素。给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值 所在位置即可。可以假设 nums[-1] = nums[n] = -∞ 。
public int findPeakElement(int[] nums) {
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = left + (right-left)/2;
if(mid == nums.length-1){
return mid;
}
if(nums[mid]> nums[mid+1]){
right = mid-1;
} else if(nums[mid]<= nums[mid+1]){
left = mid+1;
}
}
return left;
}
说明
- 二分的mid处,可能是峰值、下坡、上坡
- 是峰值,则找到了
- 是上坡/下坡,则沿着上坡走(相当于爬山),一定能到峰值;但是若沿着下坡走则可能遇到新的峰值也可能一直下坡
- 所以,问题的关键是“爬山”,只沿着上坡方向走
2.3 找重复数
- leetcode 287
如:给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 额外空间。
class Solution {
private int[] nums;
public int findDuplicate(int[] nums) {
this.nums = nums;
int left= 1;
int right = n;
while(left<=right){
int mid = left +(right-left)/2;
if(countNum(mid)>mid){
right = mid-1;
}else{
left = mid+1;
}
}
if(left>n||countNum(left)<=left){
return -1;
}
return left;
}
public int countNum(int n){
int res = 0;
for(int a:nums){
if(a<=n){
res++;
}
}
return res;
}
}
说明
- 用时间换空间
- 二分搜索从1到n的数组,对于每个数a在nums中查找有count个数<=这个数。若count>a,则重复数在1到a之间,否则在a到n之间
2.4 平方数
- leetcode 69
- leetcode 367
如:给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。不能使用任何内置的库函数,如 sqrt 。
public boolean isPerfectSquare(int num) {
long left=0;
long right = num;
while(left<=right){
long mid = left +(right-left)/2;
if(mid*mid==num){
return true;
} else if(mid*mid<num){
left = mid+1;
} else if(mid*mid>num){
right = mid-1;
}
}
return false;
}
说明
line 2、3、5:防止mid*mid溢出,所以要用long类型