算法和刷题——二分法

二分法的思想、二分法模板、不同条件下二分法的写法、二分法练习
摘要由CSDN通过智能技术生成

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】初始leftright的赋值、【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).分界线没办法在leftright之间
      • 如果是第一种划分也就是nums[mid] = target时更新右边界的不断向左收缩的情况,left = right在分界线右边。取分界线左侧的则返回left - 1,取分界线右侧的则返回left
      • 如果是第二种划分也就是nums[mid] = target时更新左边界不断向右收缩的情况,此时left = right在分界线右边。取分界线左侧时返回left-1,取分界线右侧时返回left

主要是看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
所以查找的起始点在右上角或左下角,右上角时往左走变小,往下走变大

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值