1. 二分法
基本问题:根据给出的target,找有序数列中和target相关的某个位置,可以是找target在数组中的index,target开始出现、结束出现的位置
基本思想:先确定待查数据的范围(排除了一些不可能的范围),将大区间划分为了可能出现和不可能出现两个小区间,直到区间位空或区间足够小
1. 整数二分
有序连续数列查找位置,不一定是数组表示的
1. 最基本的二分法
【问题描述】:给出有序整型数组nums和整型target,寻找target在数组中出现的位置,若找不到则返回-1
public int binarySearch(int[] nums,int target){
int left = 0;
int right = nums.length-1; //【1】
while(left <= right){ //【2】
int mid = left + (right-left) / 2;
if(target == nums[mid]){
return mid; //【3】
}else if(target < nums[mid]){
right = mid - 1; //【4.1】
}else{
left = mid + 1; //【4.2】
}
}
return -1; //【5】
}
代码中可变得地方有四个
【1】初始left
和righ
t的赋值、【2】while的终止条件、【3】找到nums[mid] = target
时的赋值、【4】循环中取左右区间时边界赋值、【5】返回值
2. 初始和循环中涉及到的区间相关问题【1】【2】【4】
每次探测的区间可以是[left , right]
的闭区间或是[left , right)
的左闭右开区间及其他
【1】left、right
的写法:应该保证数组不会越界(以right为例)。
【2】while
的写法:应该保证区间为空时终止循环。
【4】循环中边界更新: 应该保证mid不出现再下一次循环中也就是不在下一个半区间里。
2.1 闭区间
- 【1】:
right = nums.length
, 则index有可能取到nums.length
然而不存在nums[nums.length]
这一项,所以是right = nums.length - 1;
- 【2】:
while(left <= right)
,因为left > right
时[left , right]
区间为空,结束while循环 - 【4】:
left = mid + 1;right = mid - 1;
2.2 左闭右开
- 【1】:可以写
right = nums.length
,因为右边界是开的所以不会取到nums[nums.length]
这一项,不会有越界问题 - 【2】:
while(left > right)
,因为left = right
时,[left , right)]
区间为空,结束while循环 - 【4】:
left = mid + 1;right = mid;
,下一轮right = mid
时,取左区间实际上是[left , oldmid)
不包含上一轮的mid
3. 返回值的问题【3】【5】
1. 找到target时返回其index,否则返回-1
【3】return mid;
【5】return -1;
2. 找到target时返回其index,否则找target可以插入的位置
- 若数列无重复元素,找到target直接返回index。未找到target时就有两种选择:返回小于target的最大值的index,则target可以插入到index的下一个;返回大于target的最小值的index,则target可以插入到index的上一个。对应于之后所说的边界左侧或右侧。
- 若数列有重复元素,则找到target时返回index就可以有很多选择,比如返回小于target的最大值的index,则target可以插入到index的下一个,返回第一次出现target的index,则target插入index的上一个;返回大于target的最小值的index,则target可以插入到index的上一个,返回最后一次出现target的index,则target可以插入index的下一个。未找到target时和无重复元素数列一样有两个返回选择即边界左侧或右侧。
1. 序列中有重复元素
按target值大小可以对序列有两种划分,此处target=4
- 1 2 3 | 4 4 4 4 5 6 7
可以用来找小于target的最大值即分界线左边或第一次出现target的位置即分界线右侧 - 1 2 3 4 4 4 4 | 5 6 7
可以用来找大于target的最小值即分界线右边或最后一次出现target的位置即分界线左侧
即有两种划分方式每种划分方式,每种方式都可以返回边界线左侧或右侧的位置,从而得到和target相关的各个位置
划分方式和边界线在代码中的体现:
- 划分方式:由【3】
nums[mid] = target
时更新左边界还是更新右边界决定。若更新右边界则相当于取了左半边,分界线逐渐左移最终得到靠左划分的方式也就是第一种划分;若更新左边界则相当于取了右半边,分界线逐渐右移得到靠右划分的也就是第二种划分。 - 取边界线的左侧or右侧:看【2】while终止时left和right值决定【5】返回值.
- 若是闭区间,while终止时
left > right
也就是left= right+1
;left在right右边一个位置,分界线在left和right之间。所以取分界线左侧的则返回right
或者left-1
.取分界线右侧的则返回left
或者right+1
;为了记忆方便可以只选返回left
某个形式。 - 若是开区间while终止时
left = right
(见2.2).分界线没办法在left
和right
之间- 如果是第一种划分也就是
nums[mid] = target
时更新右边界的不断向左收缩的情况,left = right
在分界线右边。取分界线左侧的则返回left - 1
,取分界线右侧的则返回left
; - 如果是第二种划分也就是
nums[mid] = target
时更新左边界不断向右收缩的情况,此时left = right
在分界线右边。取分界线左侧时返回left-1
,取分界线右侧时返回left
;
- 如果是第一种划分也就是
- 若是闭区间,while终止时
主要是看while终止时left和right的值的关系,while终止时分界线在哪里
闭区间:left = right + 1
,分界线在两者中间
开区间:left = right
,向左收缩分界线更左,向右收缩分界线更右
可以写一个方法同时具有搜索左边界和右边界的功能,即设置一个boolean lower
参数,true时搜索左边界,false时搜索右边界
搜索左边界即不断向左收缩:不仅nums[mid] > target
需要更新右边界,nums[mid] = target
时也要更新右边界,所以lower = true
时可以把这两种情况合并,同时可以和搜索右边界时nums[mid] > target
的情况继续合并。其他情况都会更新右边界
lico_34 在排序数组中查找元素的第一个和最后一个位置
//此处是闭区间,返回向左收缩或向右收缩的边界线的右侧,所以向右收缩找最后一个位置时注意最终的结果应该-1
public int binarySearch(int[] nums, int target, boolean lower) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
2. 序列中没有重复元素
按target的值划分序列可以将序列划分为两部分,分界线左边严格<target,分界线右边严格>target,此处target=4
- 1 2 3 | 5 6 7 8.即只有唯一的划分方式,则向左收缩和向右收缩都正确,在代码中对应【3】
nums[mid] = target
时更新左边界或更新右边界都对。 - 取边界线左侧or边界线右侧,和有重复元素时的讨论是一样的,代码改动相同。
4. 一些注意
- 按照上面讨论对于分界线在数组首位的情况,返回的index可能是
0
也可能返回的是-1
,可能是nums.length - 1
也可能是nums.length
,如果后面还用这个结果就需要根据自己写的代码特别注意 - 在计算
mid
的值时,如果使用位移运算代替除法,要注意运算顺序的为题/2
是向零取整(有截断所以负数截断后变大了,右移一位是向下取整 所以负数且是奇数时右移和/2
不等价)- 加法和右移运算同级,但是由于左结合 所以要给右移运算加括号
int mid = left + ((right - left) >> 1);
2. 小数二分
- 【2】 while终止条件:区间长度低于精度时结束
- 【4】更换区间:取左区间:right = mid;取右区间:left=mid;
- 【5】返回值 left和right都可以
final double eps = 1e-8;//保留6位小数
double binarySearch(double left, double right){
while(right - left > eps){
double mid = (left + hight) / 2;
if(f(mid) > target){
right = mid;
}else{
left = mid;
}
}
return left;
}
3. 一些二分的练习题
见leetcode官网的二分查找题单hhh……
1. 使用二分思想的
1. lico_4 寻找两个正序数组的中位数
lico_4 寻找两个正序数组的中位数
利用的不断排除不可能区域,继续在可能区域中寻找的思想
相当于找两个数组{A,B}中第k(中位数在排序中的位置,根据两数组的总数分奇偶情况可以计算出来k值)个小的数,比较两个数组各自第[k/2-1]
个数(这两个数之前的数各自有k/2-1
个数),对A[k/2-1]
和B[k/2−1]
中的较小值,最多只会有 A中的(k/2-1)
+B中的(k/2−1)
≤k−2 个元素比它小,所以两个之中较小的数不可能是{A,B}中第k个小的数。这样每次能排除k/2
个数
2. 剑指_4 二维数组查找
剑指_4 二维数组查找
二分法用于有序数列,是每次比较mid和target,看是往mid左边小区间走还是mid右边小区间走
对于二维数组每个维度上都是有序数列,也就是行、列方向上都可以往小走或往大走,就有4个方向,但是二维数组也是有边界的,对于边界上的点并不是4个方向都可以,要保证起始时查看的位置既有小于他的又有大于他的,起始位置就相当于二分查找中的最初的mid
所以查找的起始点在右上角或左下角,右上角时往左走变小,往下走变大