LeetCode-数组篇

二分查找

我觉得可以跳过这下面三个 方式 ,直接到第二点 4种问法 ,解决一切二分法

  你真的懂二分查找吗?
  曾经我以为我也是懂二分查找的,就是浅浅的通过几道力扣题目 我就觉得,诶 ,二分查找不过如此。

  二分查找并不简单,Knuth 大佬(发明 KMP 算法的那位)都说二分查找:思路很简单,细节是魔鬼。很多人喜欢拿整型溢出的 bug 说事儿,但是二分查找真正的坑根本就不是那个细节问题,而是在于到底要给 mid 加一还是减一,while 里到底用 <= 还是 <。

  计算 mid 时需要防止溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 left 和 right 太大直接相加导致溢出。也就是 left+right>Integer.MAX_VALUE溢出

通用 模板
int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

每个···都是代表一个容易出bug的地方

其实就是边界问题 :
  只要你明白了「搜索区间」这个概念,就能有效避免漏掉元素
  假如 right =nums.length 的时候 [0,nums.length) 就是左闭右开区间 ,下面每个区间也得左闭右开

		    while 判断也就是<// 关注点    当你左闭右开的时候 最后一个区间 [left,left) 
		    而这个left在上个区间就已经索引过,所以当left==left可以退出
			right = mid ;// 关注点
			left = mid +1;// 无论改哪这个都是不变化的
			while(left < right) 的终止条件是 left == right ,
			while(left < right) 的终止条件是 left == right,写成区间的形式就是 [left, right),
			或者带个具体的数字进去 [2, 2),但是此时我们的区间是[2,2),
			就是代表我们的区间是左闭右开的所以这时候 while 循环终止是正确的,直接返回 -1 即可。

  假如 right =nums.length-1 的时候 [0,nums.length-1] 就是左闭右闭区间 ,下面每个区间也得左闭右闭

		    while 判断也就是<=// 关注点    当你左闭右闭的时候 最后一个 肯定是 left 和left 比较 
			right = mid -1;// 关注点            
			left = mid +1;// 无论改哪这个都是不变化的  
				while(left <= right) 的终止条件是 left == right + 1,
			写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],
			可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。
			所以这时候 while 循环终止是正确的,直接返回 -1 即可。

  上面两种代表 着后面的代码中··· 不同

  下面分为三种情况来讲解

一、寻找一个数(基本的二分搜索)

以下是最常见的代码形式,其中的标记是需要注意的细节:

int binarySearch(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)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
    }
    return -1;
}

写法二,左闭右开区间

int binarySearch(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)
            left = mid + 1; 
        else if (nums[mid] > target)
            right = mid; // 注意
    }
    return -1;
}

二、寻找左侧边界的二分搜索

以下是最常见的代码形式,其中的标记是需要注意的细节:
写法一,左闭右开区间

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length; // 注意

    while(left < right) {// 注意
        int mid = left + (right - left) / 2;
        if(nums[mid] == target)
            right = mid; 
        else if (nums[mid] < target)
            left = mid + 1; 
        else if (nums[mid] > target)
            right = mid-1; // 注意
    }
  // target 比所有数都大
	if (left == nums.length) return -1;
 // 类似之前算法的处理方式   这里已经比较了target比所有数小的
	return nums[left] == target ? left : -1;

}

写法二,左闭右闭区间

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
    // 检查出界情况
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;
}

  可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。

三、寻找右侧边界的二分查找

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;
    
    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    if (right < 0 || nums[right] != target)
        return -1;
    return right -1; // 注意 最后会在右边界 第一个元素 也就是right的指针上 
}

写法二,左闭右闭区间 left>right

	int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length -1;
    
    while (left <= right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid-1;
        }
    }
     if (right < 0 || nums[right] != target)
        return -1;
     return right; // 注意
}

  当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。

②. 二分查找 的4 种问法

  数组 nums = [5,7,7,8,8,10], target = 8
  问法一 :找到这个数组中 >=8的开始坐标
  问法二 :找到这个数组中> 8的开始坐标
  问法三: 找到这个数组中 <= 8的 最大坐标
  问法四 : 找到这个数组中<8的最大坐标
其实这四种是一种问法 我们用问法一来当中核心问法
   问法二和问法一转换 可以查找>=9 的开始坐标
   问法三和问法一转换 可以查找>=9 的开始坐标 -1
   问法四和问法一转换 可以查找>=8 的开始坐标 -1
下面我用三种写法表示出 问法一的

1.左闭右闭区间

//nums = [5,7,7,8,8,10], target = 8 
public int[] searchbound(int[] nums, int target) {
		int left  = 0;
		int right =nums.length-1; //[left,right]闭区间
		while(left <= right ){ // 区间不为空
			int mid = (right - left)/2 + left ; 
			if (nums[mid]<target){
				left =mid +1;// [mid+1,right]闭区间
			}else if (nums[mid]>=target){//两种合在一起
					right =mid - 1;// [left,mid-1]闭区间
			}
		}
		return left ; 
		//循环不变量
		//left -1 始终都是小于target的数
		//right + 1 始终都是大于target的数 
		//最后 他right 是在 7 left是在 8 [left,right) =>[3,2]---索引
}

2.左闭右开区间

//nums = [5,7,7,8,8,10], target = 8 
public int[] searchbound(int[] nums, int target) {
		int left  = 0;
		int right =nums.length;//[left,right)闭区间
		while(left < right ){ 	// 区间不为空
			int mid = (right - left)/2 + left ; 
			if (nums[mid]<target){
				left =mid +1;		// (mid,right]闭区间
			}else if (nums[mid]>=target){//两种合在一起
					right =mid;  // [left,mid)闭区间
			}
		}
		return left ; //right也行
		//循环不变量 
		//left -1 始终都是小于target的数
		//right + 1 始终都是大于target的数 
		//最后 他right 是在 8 left是在 8	[left,right) =>[3,3)---索引
}

3.左开右开区间

//nums = [5,7,7,8,8,10], target = 8 
public int[] searchbound(int[] nums, int target) {
		int left  = -1;
		int right =nums.length;//(left,right)闭区间
		while(left +1 < right ){ 	// 区间不为空
			int mid = (right - left)/2 + left ; 
			if (nums[mid]<target){
				left =mid ;		// (mid,right)闭区间
			}else if (nums[mid]>=target){//两种合在一起
					right =mid;  // (left,mid)闭区间
			}
		}
		return right ; 
		//循环不变量 
		//left -1 始终都是小于target的数
		//right + 1 始终都是大于target的数 
		//最后 他right 是在 7 left是在 8	(left,right) =>(2,3)---索引
		// left+1==right退出
}

三种方法是一样的本质 就是对搜索区间 ,边界 的具体分析

  看完可以写写下面的题练练手
LeetCode 704二分查找
LeetCode 34 在排序数组中查找元素的第一个和最后一个位置
LeetCode 35. 搜索插入位置

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Red-P

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值