前言
二分查找的思想是简单易懂的,但是在具体实现的时候能被一些细节给逼疯。今天学习了一下二分查找相关的知识与小细节,听取同学的推荐,参考了大神“灵茶山艾府”的教学视频。
下面就以一道算法题为例子,来写一下二分查找的方法。但这篇博客我会不局限于这道题,尽量去着笔于二分查找
的算法本身。
原题描述
34. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入: nums = [], target = 0
输出:[-1,-1]
提示: 0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109<= target <= 109
解答
方法一:固定套路
思想
其实这道题从有序数组
和O(log n)时间复杂度
来看,很显然需要用到二分查找。
也就是需要用二分查找找到目标元素target
出现的第一个位置和最后一个位置。
二分查找在实现的时候,根据左右边界开闭区间
的不同,有三种实现方式:闭区间[l, r]
、左开右闭(l, r]
、开区间(l, r)
。
我这里就用闭区间的方式来做,但是任何方式其实都是可以的的。
对于查找target
的第一个位置,其实查找的就是>= target
的第一个元素,在这里将查找>= target
的函数记作findNotLess()
。
但是在找到之后需要判断一下找到的位置是不是合法(因为如果这个数组元素都比target
小,那么找到的位置是超出了数组边界的);
还需要判断找到的位置是不是target
(如果数组中不存在target
,那么可能找到的是例如target+1
这样的比target
还要大的元素)。
像下图所示的例子。
如果经过判断发现,target
存在数组中,并且找到了第一个出现的位置,那么如何找target
的最后一个的位置?
答:找最后一个target
其实就需要找到> target
的第一个元素的位置,然后把这个位置-1即可。
也就相当于去找>= (target+1)
的位置,然后将找到的下标-1,就得到了最后一个target
的位置。(因为经过刚刚的判断,target
一定存在,那么找到>= (targe+1)
的位置,它左边一定是最后一个target
)
可以发现,在找最后一个target
时,用的是一种偷懒的方法来实现的,我把这种方式称作一种固定套路。
因此,在非递减顺序排列
的int数组
中,可以在这里进行一下拓展和总结:
- 查找
>= target
的第一个位置,直接刚刚所说的findNotLess(target)
方法; - 查找
> target
的第一个位置,可以转换成查找>= target+1
,也就是findNotLess(target+1)
; - 查找
< target
的最后一个位置,可以转换成查找>= target
的第一个位置,然后将找到的下标减一。也就是findNotLess(target)-1
- 查找
<= target
的最后一个位置,可以转换成查找> target
的第一个位置再减一。二次转换成查找>= target+1
的第一个位置再减一,也就是findNotLess(target+1)-1
注意:
上述查找时,在查找大于或大于等于一个数的时候,都是查找的第一个位置。
在查找小于或小于等于一个数的时候,都是查找的最后一个位置。
这是因为在非递减排序数组中,查找大于某个数的最后一个数是没意义的,一定在数组的最后面。
同样的,查找小于某个数的第一个数也是没意义的,一定是数组的开头。
代码实现
C++代码如下
class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {int n = nums.size();int start = findNotLess(nums, target);if(start == n || nums[start] != target)return {-1, -1};int end = findNotLess(nums, target+1)-1;return {start, end};}int findNotLess(vector<int>&nums, int target){int n = nums.size();int l = 0, r = n-1;while(l <= r){int mid = l + (r-l)/2; // 防止溢出的计算方式if(nums[mid] >= target)r = mid-1; // r+1 都是大于等于targetelsel = mid+1; // l-1都是小于target}return r+1; // 返回的是大于等于target的第一个数的下标}
};
复杂度分析
时间复杂度
每次都将任务拆分成了之前的一半,O(logn)
空间复杂度
没有额外的空间开销,O(1)
方法二:灵活应用
思想
灵活应用的时候就是不仅仅只用方法一中的FindNotLess()
函数,在查找出现的最后一个target
的时候,通过更改函数中的比较方式来进行实现。
也就是下面这一部分:
if(nums[mid] >= target)r = mid-1; // r+1 都是大于等于target
elsel = mid+1; // l-1都是小于target
在上述代码中,这个部分是使得r+1
都是>=target
,l-1
都是<target
。用这种方式,在结束的时候r+1
的位置就是第一个target
(假设target
存在数组中)
我们需要改成l-1
都是<=target
,r+1
都>target
,这样结束的时候l-1
的位置就是最后一个target
。修改过后的代码见下。
代码实现
C++代码如下
其中find_left()
函数就是上面的findNotLess()
函数。
class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {int n = nums.size();int start = find_left(nums, target);if(start == n || nums[start] != target)return {-1, -1};int end = find_right(nums, target);return {start, end};}int find_right(vector<int> &nums, int target){int n = nums.size();int l = 0, r = n-1;while(l <= r){int mid = l + (r-l)/2;if(nums[mid] <= target) // 注意这个地方的不同!!!l = mid+1;elser = mid-1;}return l-1; // 返回的是<=target的最后一个数}int find_left(vector<int>&nums, int target){int n = nums.size();int l = 0, r = n-1;while(l <= r){int mid = l + (r-l)/2;if(nums[mid] >= target)r = mid-1; // r+1 都是大于等于targetelsel = mid+1; // l-1都是小于target}return r+1; // 返回的是大于等于target的第一个数的下标}
};
复杂度分析
时间复杂度
O(logn)
空间复杂度
O(1)