一直以来,二分查找法都是所有算法中最让我头疼的一个tag,并因此错过不止一个很好的offer。二分查找法看似简单,实则变种多,边界条件不好把握。经过一段时间的摸索,终于有了一些自己的感悟。如果写的不对,还请评论区的大佬们赐教。
言归正传,我把二分查找法总结为以下三点:
-
首先二分查找的先诀条件是有序数组。只有一个数组是有序的时候才可以使用二分查找法,否则不可用。当我们试图使用二分查找法去解题时,也首先要明确数组是否是有序数组或者能否构建有序数组或者是否是有序数组的变种。
-
依据第一条,任何二分查找的题目都是在利用两个规律。即有序数组的规律和二分查找的规律。有序数组的规律是对于一个有序数组,数组后面的数永远大于等于前面的数。二分查找的规律是永远让我们要查找的数在缩小的范围内,范围每次缩小一半。
-
令人头痛的边界。我暂时把二分查找分为两类。找数的和找最大/最小值的。对于找一个具体target的二分法,边界条件设成=。因为我们要遍历整个数组去找到答案。对于查找最大/最小数的二分法,边界条件要设成>或者<。因为target是未知的,它是我们不断压缩范围得到的结果。我们不能也没有必要去追究左指针和右指针相同时做什么。
来看题:
4. leetcode 704
最经典二分法,相信很多小伙伴都会。需要注意的是,这里的边界条件是==,而右指针要从最后一个数的位置开始。
public int search(int[] nums, int target) {
int i =0,j=nums.length-1;
while(i<=j){
int m=(i+j)/2;
if(nums[m]<target){
i=m+1;
}if(nums[m]>target){
j=m-1;
}if(nums[m]==target){
return m;
}
}
return -1;
}
- leetcode 154
这道题是在找旋转排序数组的最小值。请立即回顾本文之前提到的三个点。旋转数组可以看成两个有序数组的拼接。这里先拿mid和right做对比。如果mid>right,那么就意味[mid,right]这个区间数组不是有序的,最小的数就在这里。如果mid<right,就意味着[mid,right]是有序的,最小的数只能在[left,mid]区间。当左右指针相同时,就是我们要的答案。
public int findMin(int[] nums) {
int i=0;int j=nums.length-1;
while(i<j){
int mid=(j+i)/2;
if(nums[mid]>nums[j]){
i=mid+1;
}else if(nums[i]<nums[j]){
j=mid;
}else{
j--;
}
}
return nums[i];
}
- leetcode 33
这道题同样是旋转数组的变种题,是求具体target的。请再次回顾本文之前提到的三个点。
注意求值题的边界是==。其他的思维和上一题相同,但是多了一个隐藏条件:这个数组的左端点值大于右端点值。当值在当前范围内时,我们要缩小范围,当值不在当前范围内时,我们要离开当前范围。这就是左指针和右指针的变化规则。
public int search(int[] nums, int target) {
int left=0,right=nums.length-1;
while(left<=right){
int mid=(left+right)/2;
if(nums[mid]==target){
return mid;
}else if(nums[mid]<nums[right]){
if(nums[mid]<target&&target<=nums[right]){
left=mid+1;
}else{
right=mid-1;
}
}else{
if(nums[left]<=target&&target<nums[mid]){
right=mid-1;
}else{
left=mid+1;
}
}
}
return -1;
}
- leetcode 35
这是比较有意思的一道题。因为target不一定存在。但其实按照存在来做即可。因为不存在的情况答案已经有了,就是指针的值。
int left=0,right=nums.length-1;
while(left<=right){
int mid = (right+left)/2;
if(target>nums[mid]){
left=mid+1;
}else if(target<nums[mid]){
right=mid-1;
}else if(nums[mid]==target){
return mid;
}
}
return left;