二分查找,通过不断二分缩小查找范围。
二分查找并不简单,KMP发明人之一Knuth都说二分查找:思路很简单,细节是魔鬼。二分查找里真正的坑不是整型溢出的bug,而是在于到底要给 mid 加一还是 减一,while 里到底用 <= 还是 <。
if的情况能详细尽量详细.
划分 [left, mid] 与 [mid + 1, right] ,mid 被分到左边,对应 int mid = left + (right - left) / 2;;
划分 [left, mid - 1] 与 [mid, right] ,mid 被分到右边,对应 int mid = left + (right - left + 1) / 2;。
至于为什么划分是这种对应关系,原因在于区间只有 22 个数的时候,如果中间数的取法不对,一旦进入的分支不能使得区间缩小,会出现 死循环。暂时不理解问题不大,需要在练习中进行调试;
注意:向一个方向搜索的时候,mid注意向上取整还是向下取整.
153、154 题:搜索最小值
目录
(如果中值 < 右值,则最小值在左半边,可以收缩右边界 right=mid。
如果中值 > 右值,则最小值在右半边,可以收缩左边界 left=mid+1。
0.基本模板
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = (right + left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
1、寻找旋转排序数组中的最小值 (题目)
单调递增的序列:
*
*
*
*
*
做了旋转:
*
*
*
*
*
用二分法查找,需要始终将目标值(这里是最小值)套住,并不断收缩左边界或右边界。
左、中、右三个位置的值相比较,有以下几种情况:
左值 < 中值, 中值 < 右值 :没有旋转,最小值在最左边,可以收缩右边界 (right =mid)
右
中
左
左值 > 中值, 中值 < 右值 :有旋转,最小值在左半边,可以收缩右边界 (right =mid)
左
右
中
左值 < 中值, 中值 > 右值 :有旋转,最小值在右半边,可以收缩左边界 (left肯定不是mid,left = mid+1)
中
左
右
左值 > 中值, 中值 > 右值 :单调递减,不可能出现
左
中
右
分析前面三种可能的情况,会发现情况1、2是一类,情况3是另一类。
如果中值 < 右值,则最小值在左半边,可以收缩右边界。
如果中值 > 右值,则最小值在右半边,可以收缩左边界。
通过比较中值与右值,可以确定最小值的位置范围,从而决定边界收缩的方向。
而情况1与情况3都是左值 < 中值,但是最小值位置范围却不同,这说明,如果只比较左值与中值,不能确定最小值的位置范围。
所以我们需要通过比较中值与右值来确定最小值的位置范围,进而确定边界收缩的方向。主要通过向右收缩边界。
想一下模板:
class Solution {
public int findMin(int[] nums) {
int left =0,right = nums.length-1;
while (left<= right){
int mid = left +(right-left)/2;
if (nums[mid] >= nums[right])
left = mid+1;
else
right =mid;
}
return nums[right];
}
}
或者这个更容易理解:
class Solution {
public int findMin(int[] nums) {
int left =0,right = nums.length-1;
while (left<right){ //查找到的时候,刚好左右相等在对应位置上,
int mid = left +(right-left)/2;
if (nums[mid] <= nums[right])
right =mid;
else
left = mid+1;
}
return nums[left];
}
}
再讨论一个问题:
为什么左右不对称?为什么比较mid与right而不比较mid与left?能不能通过比较mid与left来解决问题?
左右不对称的原因是:
这是循环前升序排列的数,左边的数小,右边的数大,而且我们要找的是最小值,肯定是偏向左找,所以左右不对称了。
为什么比较mid与right而不比较mid与left?
具体原因前面已经分析过了,简单讲就是因为我们找最小值,要偏向左找,目标值右边的情况会比较简单,容易区分,所以比较mid与right而不比较mid与left。
那么能不能通过比较mid与left来解决问题?
能,转换思路,不直接找最小值,而是先找最大值,最大值偏右,可以通过比较mid与left来找到最大值,最大值向右移动一位就是最小值了(需要考虑最大值在最右边的情况,右移一位后对数组长度取余)。
先求旋转数组的最大值,再加一(又学会一道题目【求旋转数组的最大值】)
道理一样,如果 左<中<右,则最大值肯定在右边,不包含mid,left=mid;(其实是mid+1,但mid肯定行,为了保持和后边一致) 如果 左<中>右,则最大值肯定在右边,包含mid .left =mid;
如果 左>中<右,则最大值肯定在左边,right =mid -1; (注意这里向左收缩边界,注意二分的时候,向上取整而不是向下取整。假设数组1,3,5,7 left=mid 的时候,mid-1会导致右边的7被淘汰)
class Solution {
public int findMin(int[] nums) {
int left =0,right = nums.length-1;
while (left<= right){
int mid = left +(right-left+1)/2; // 注意区分
if (nums[mid] <= nums[left])
right = mid -1;
else
left =mid ;
}
return nums[(left + 1) % nums.length];
}
}
2、154. 寻找旋转排序数组中的最小值 II
class Solution {
public int findMin(int[] nums) {
int left =0,right = nums.length-1;
while (left<=right){
int mid = left +(right-left)/2;
if (nums[mid] == nums[right])
right -=1 ; // 想想为什么这里不是left+=1?看看比较的对象
else if (nums[mid] > nums[right])
left = mid+1;
else
right =mid;
}
return nums[left];
}
}