前言
二分查找也叫做折半查找,时间复杂度为 l o g 2 n log_{2}n log2n,适用的条件是数组需要有序,其中主要涉及到3个变量——left,right,mid。
一、二分查找方法介绍
二分查找的难点有3个:总体要遵循的原则是循环不变量,即区间的定义在while每次循环的收获都要保持一致,最常见的有两种方式:左闭右闭和左闭右开。区间的定义也影响了right的更新。
- left、right、 mid的定义,以及更新:right=mid还是right=mid-1
- while的条件判断:left<right还是left<=right
- 答案的返回:是返回mid、right、left还是经过操作的其它变量
案例:
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
题目链接:704 二分查找
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
方式1:左闭右闭
class Solution {
public int search(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;
}
}
方式二:左闭右开
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length;
while(left < right){
int mid = left+(right-left)/2;
if(nums[mid] == target){
return mid;
}else if(nums[mid] > target){
right = mid;
}else if(nums[mid] < target){
left = mid+1;
}
}
return -1;
}
}
图解:
二、二分算法小技巧
2.1 难点和解决方案
上面关于二分查找算法的介绍中使用了两种区间定义,不同的区间定义,对于left和right的更新规则会不一样。二分算法迷惑性很强,总感觉一看就会但是一些就废,菜鸡飞(本人外号)在平时做题的时候经常会出现陷入死循环或者是不知道最后应该返回那个变量的值。本节将会介绍难点以及具体的解决方案:
- 目前大多数的博客关于区间的定义都有两种写法,左闭右闭或者左闭右开,因此初学者(比如像我这种菜鸡往往就会很纠结,到底选哪一个)。解决方案:难的原因是选择多了才会纠结选哪一个,在做算法题的时候直接选择左闭右闭的区间定义。
- 对于right和left指针更新比较模糊,模糊的主要原因在于有两种区间定义,但是需要始终保持区间不变性。解决方案:当只选择左闭右闭的时候,在你写代码的时候脑子的逻辑不会想着左闭右开的更新规则。
- 最后一个难点就是,不知道最后返回的结果是什么,是left还是right或者是其他变量。解决方案:一个很好的方法就是在刚开始的时候用一个变量ans来记录答案,在区间进行更新的时候同时更新ans.
2.2 左闭右闭代码改写
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len-1;
int ans = -1; // 用来记录最终的答案
while(left <= right){
int mid = (left+right)/2;
if(nums[mid] == target){
ans = mid; // 同时更新ans
break;
}else if(nums[mid] > target){
right = mid-1;
}else if(nums[mid] < target){
left = mid+1;
}
}
return ans;
}
}
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] res = new int[2];
res[0] = findLeft(nums, target);
res[1] = findRight(nums, target);
return res;
}
public int findLeft(int[]nums, int target){
int len = nums.length;
int left = 0;
int right = len-1;
int ans = -1;
while(left <= right){
int mid = (left+right)/2;
if(nums[mid] == target){
ans = mid;
right = mid-1;
}else if(nums[mid] > target){
right = mid-1;
}else if(nums[mid] < target){
left = mid+1;
}
}
return ans;
}
public int findRight(int[]nums, int target){
int len = nums.length;
int left = 0;
int right = len-1;
int ans = -1;
while(left <= right){
int mid = (left+right)/2;
if(nums[mid] == target){
ans = mid;
left = mid+1;
}else if(nums[mid] > target){
right = mid-1;
}else if(nums[mid] < target){
left = mid+1;
}
}
return ans;
}
}
class Solution {
public int mySqrt(int x) {
if(x==0 || x==1){
return x;
}
int left = 1;
int right = x/2;
int ans = 1;
while(left <= right){
int mid = left+(right-left)/2;
// 防止数据溢出 long tmp = (long)(mid*mid);这句代码和下面的代码有区别
long tmp = (long)mid*mid;
if(tmp <= x){
ans = mid;
left = mid+1;
}else if(tmp > x){
right = mid-1;
}
}
return ans;
}
}
162.寻找峰值
暴力代码:
class Solution {
public int findPeakElement(int[] nums) {
int len = nums.length;
len = len+2;
int[] arr = new int[len];
arr[0] = Integer.MIN_VALUE;
arr[len-1] = arr[0];
for(int i=1; i<len-1; i++){
arr[i] = nums[i-1];
}
// 暴力
for(int i=1; i<len-1; i++){
if(arr[i-1]>arr[i] && arr[i]<arr[i+1]){
return i-1;
}else if(arr[i-1]<arr[i] && arr[i]>arr[i+1]){
return i-1;
}
}
return 0; // 处理特例
}
}
二分代码:
public int findPeakElement(int[] nums) {
int len = nums.length+2;
int[] arr = new int[len];
arr[0] = Integer.MIN_VALUE;
arr[len-1] = arr[0];
for(int i=1; i<=len-2; i++){
arr[i] = nums[i-1];
}
int left = 1;
int right = len-2;
int ans = 1;
while(left <= right){
int mid = (left+right)/2;
// 搜索峰顶的情况
if(arr[mid] < arr[mid-1]){
right = mid-1;
ans = right;
}else if(arr[mid] <arr[mid+1]){
left = mid+1;
ans = left;
}else{
ans = mid;
break;
}
}
return ans-1;
}
}
三、推荐练习题目
3.1 常规题目:数组是有序的
3.2 变形:不能立马看出数组有序(难度较大)
总结
二分查找就是不断缩小查找的范围,最简单的二分查找,是那种有序的数组并且数组里面还没有重复元素的,但是对于那种变形的题目,不能想到可以使用二分进行操作的题目是难度比较大的,比如寻找峰值。最核心的一点就是确认只使用左闭右闭的情况,然后用一个变量来记录答案,在每次更新left和right的同时根据逻辑更新ans。