[Leetcode] 二分查找算法指南(基础篇)

这篇博客详细介绍了二分查找算法在LeetCode中的应用,包括标准二分查找、寻找边界值,以及解决具体问题如搜索二维矩阵、旋转排序数组、寻找最接近的元素和中位数等。通过实例和解题思路,解析了如何灵活运用二分查找算法解决不同类型的编程问题。
摘要由CSDN通过智能技术生成
前言

二分查找算法作为一种常见的查找方法,将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间,具有很大的应用场景,而在 LeetCode 中,要运用二分搜索法来解的题目也有很多,但是实际上二分查找法的查找目标有很多种,而且在细节写法也有一些变化。

问题形式

二分查找算法充分利用了元素间的次序关系,采用分治策略,可在最坏的情况下用 O ( l o g N ) O(logN) O(logN) 完成搜索任务。
题目问法大致有这几种

  • 查找和目标值完全相等的数
  • 查找区间的某个边界值
  • 利用子函数对中间值进行判断,确定查找的方向
  • 利用二分思想查找函数的极大值
解题思路与模板

根据前面的描述可以看出,这类问题的核心就是在于确定三要素:搜索区间终止条件折半方向

标准二分查找
/** 元素按升序排列,下同 */
int binarySearch(int[] arr, int target) {
   
	int left = 0;
	int right = arr.length - 1;
	while (left <= right) {
   
		int mid = (left + right) >>> 1; // 无符号右移,即使前面溢出也能得到正确结果
		if (arr[mid] == target) {
   
			return mid;
		} else if (arr[mid] < target) {
   
			left = mid + 1; // 当前元素比目标值小,则收缩左边界,查找右半边
		} else if (arr[mid] > target) {
   
			right = mid - 1; // 当前元素比目标值小,则收缩右边界,查找左半边
		}			
	}
	return -1; // 未找到
}

快问快答
Q:为什么 while 循环的终止条件是 left <= right ?而我看到有的代码里是 left < right ?
A:两者都可以使用,但它们代表不同的搜索区间,需要配合不同的右侧边界。带个例子来看,假如我们在 [ 1 , 3 , 5 , 7 , 9 ] [1,3,5,7,9] [1,3,5,7,9] 中搜索元素 3 3 3 的索引位置。我们在代码里加一些输出语句来看看吧。

循环条件是 left <= right 时
第 1 次循环开始:left = 0, right = 4, mid = 2
第 1 次循环结束:left = 0, right = 1, mid = 2
第 2 次循环开始:left = 0, right = 1, mid = 0
第 2 次循环结束:left = 1, right = 1, mid = 0
第 3 次循环开始:left = 1, right = 1, mid = 1
找到了 target = 3 ,索引是 1

结果正确。下面我们把 <= 改为 <,其他条件不变

循环条件是 left < right 时
第 1 次循环开始:left = 0, right = 4, mid = 2
第 1 次循环结束:left = 0, right = 1, mid = 2
第 2 次循环开始:left = 0, right = 1, mid = 0
第 2 次循环结束:left = 1, right = 1, mid = 0
未找到 target = 3

出现了错误!我们注意到在第二次循环结束时已经有 l e f t = r i g h t left=right left=right,不满足第三次循环开始条件了。导致了 m i d mid mid 遗漏了索引 1 的位置。如果我们一定要用 l e f t &lt; r i g h t left&lt;right left<right 作为循环条件呢?可以通过修改右侧边界的方式来实现,看下面的代码。

/** 使用 while (left < right) 时的正确代码,注意 right 的取值 */
int binarySearch(int[] arr, int target) {
   
	int left = 0;
	int right = arr.length; // 注意这里
	while (left < right) {
    // 改用 "<"
		int mid = (left + right) >>> 1;
		if (arr[mid] == target) {
   
			return mid;
		} else if (arr[mid] < target) {
   
			left = mid + 1;
		} else if (arr[mid] > target) {
   
			right = mid; // 注意这里
		}			
	}
	return -1;
}

这次我们看到了正确的结果。

循环条件是 left < right 时
第 1 次循环开始:left = 0, right = 5, mid = 2
第 1 次循环结束:left = 0, right = 2, mid = 2
第 2 次循环开始:left = 0, right = 2, mid = 1
找到了 target = 3 ,索引是 1

通过上面的比较不难发现,当使用 l e f t ≤ r i g h t left \leq right leftright 时,相当于是在闭区间 [ l e f t , r i g h t ] [left,right] [left,right] 进行搜索;而当使用 l e f t &lt; r i g h t left&lt;right left<right 时,相当于是在左闭右开区间 [ l e f t , r i g h t ) [left,right) [left,right) 进行搜索。对于以上两种形式,如果你已经理解了边界的概念,记住它们自然不在话下。如果害怕混淆,博主的建议是按习惯只记一种就可以了。

Q:标准算法有什么局限性?
A:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。比如说给你数组 [ 1 , 3 , 3 , 3 , 4 ] [1,3,3,3,4] [1,3,3,3,4],需要搜索元素 3 3 3 的索引,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。然而这样的需求很常见。你也许会说,找到一个 target 索引,然后向左或向右线性搜索不行吗?可以但是不好,假如有大量的重复元素,那样就难以保证二分查找对数级的复杂度了。下面我们就来讨论边界的二分查找算法。

寻找第一个不小于目标值的位置

我们注意到在上面的例子中,要找到元素 3 3 3 的左侧边界,相当于是要找到第一个不小于元素 3 3 3 的位置,我们只需要修改两处代码就能实现。

int lowerBound(int[] arr, int target) {
   
	int left = 0;
	int right = arr.length;
	while (left < right) {
   
		int mid = (left + right) >>> 1;
		if (arr[mid] == target) {
   
			right = mid; // 注意这里
		} else if (arr[mid] < target) {
   
			left = mid + 1;
		} else if (arr[mid] > target) {
   
			right = mid;
		}			
	}
	return right; // 注意这里,也可以写为left, 循环终止时它们是相等的
}

快问快答
Q:为什么这样能找到左边界?
A:关键在于对找到 t a r g e t target target 后这种情况的处理,我们并没有直接返回索引值,而是继续收缩右边界令 r i g h t = m i d right = mid right=mid,最终达到锁定左边界的目的。

Q:为什么没有返回 -1 的操作?如果数组不存在这样的 target 该怎么办?
A:我们继续通过打印输出结果来看,假如我们在 [ 0 , 1 , 3 , 3 , 3 , 7 , 8 ] [0,1,3,3,3,7,8] [0,1,3,3,3,7,8] 中搜索元素 3 3 3

开始查找,target = 3
第 1 次循环开始:left = 0, right = 7, mid = 3
第 1 次循环结束:left = 0, right = 3, mid = 3
第 2 次循环开始:left = 0, right = 3, mid = 1
第 2 次循环结束:left = 2, right = 3, mid = 1
第 3 次循环开始:left = 2, right = 3, mid = 2
第 3 次循环结束:left = 2, right = 2, mid = 2
结束查找,索引是 2

结果是索引 2,也就是第一个 3 3 3 的位置,正确。然后我们来看下搜索元素 4 4 4

开始查找,target = 4
第 1 次循环开始:left = 0, right = 7, mid = 3
第 1 次循环结束:left = 4, right = 7, mid = 3
第 2 次循环开始:left = 4, right = 7, mid = 5
第 2 次循环结束:left = 4, right = 5, mid = 5
第 3 次循环开始:left = 4, right = 5, mid = 4
第 3 次循环结束:left = 5, right = 5, mid = 4
结束查找,索引是 5

结果是索引 5,也就是元素 7 7 7 的位置。不难看出我们找到了不小于目标值的第一个位置。实际上在 C++ 标准库中有专门的函数 lower_bound,替我们实现了同样的功能。此外,该类问题还可以变形为寻找最后一个小于目标值的位置。我们只需要在返回结果那里继续左移一步,改为 return right - 1 就可以了。

寻找第一个大于目标值的位置

有了上面的分析,那么这里就很容易理解了。对于这种情况,我们需要不断收缩左侧边界,来看代码。

int upperBound(int[] arr, int target) {
   
	int left = 0;
	int right = arr.length;
	while (left < right) {
   
		int mid = (left + right) >>> 1;
		if (arr[mid] == target) {
   
			left = mid + 1; // 注意这里
		} else if 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值