笔者最近用LeeCode刷算法题时遇到查找目标值在升序排列数组中的左右边界的问题,直觉上肯定这就是个二分法问题,但在如何查找左右边界的问题上钻进了牛角尖,最终经过不懈努力解决了这个问题,故在此分享理清之后的思路,也希望更同在此问题上纠结的小伙伴提供一点帮助。具体的问题描述如下所示:
LeeCode:34. 在排序数组中查找元素的第一个和最后一个位置
题意
给定一个按照升序排列的整数数组 nums 和一个目标值 target,找出给定目标值在数组中的开始位置和结束位置。
如果 target 不在数组中,返回 [-1,-1]。
示例
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
提示
0 <= len(nums) <= 10^5
-10^9 <= nums[i] <= 10^9
nums 是一个非递减数组
-10^9 <= target <= 10^9
解题思路
由于nums 数组存在重复的元素,这就意味着在 nums 数组中利用二分查找 target 返回的下标不具有唯一性,而题目要求查找元素 target 的第一个位置和最后一个位置,即目标值在排序数组nums 中的左右边界。因此这个问题最直观的思路就变成了,利用两次二分法分别查找数组nums中最左侧元素和最右侧元素,这就意味着需要对标准的二分法做以调整。
为了便于理解,我们首先对标准的二分法作以回顾,然后再与调整过的二分法做个比对,梳理一下调整的思路,值得注意的是,不管利用哪种形式的二分法进行查找,要求数组必须按照顺序排列,这是使用二分查找的前提。
1、标准的二分查找
以nums = [1,3,5,7,8],target=3为例,首先初始化left和right指向的位置,对于本示例而言:
left = 0; right = nums.zise() - 1;
由此可计算得到 mid = left + (right - left) / 2;
此时判断nums[mid]与target的关系:
(1) 如果nums[mid] == target,则返回mid,即为target在数组nums中的位置;
(2) 如果nums[mid] > target,则说明mid处存储的数字过大,调整右边界right=mid-1继续查找;
(3) 如果nums[mid] < target,则说明mid处存储的数字过小,调整左边界left=mid-1继续查找;
因此,本实例属于第二种情况,需要调整右界right继续查找。
重复以上步骤,当nums[mid] == target,即找到target的位置,跳出循环;或者当left>right时结束循环,即未查找到数组中存在target元素。
基于以上分析,可通过C++将上述标准的二分查找算法表述如下:
int searchTarget(vector<int>& nums, int target) {
// 定义二分法的区间
left = 0, right = nums.size() - 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 // 查找的值大于目标值,改变右界向左继续查找
right = mid - 1;
}
return -1; // 未在数组中查找到目标值
}
2、二分查找元素边界
标准的二分法与查找元素边界最大的区别是:前者只要找到目标值就结束循环,不再继续查找了;而后者找到目标值还需要继续查找,判断是否存在其他的目标值。
基于这个区别,通过二分查找元素边界的思想为:通过不断的二分循环查找目标值,找到目标值之后,先记住目标值的位置,然后继续向左(查找元素左边界)或向右(查找元素右边界)查找目标值,直到不满足循环条件left<=right时退出循环,则最后一次记录的目标值位置即为查找的元素边界。
以nums = [1,3,3,3,8],target=3,查找元素左边界为例对上述思想加以说明:
首先初始化left和right指向的位置:left = 0; right = nums.zise() - 1;
由此可计算得到 mid = left + (right - left) / 2;
此时判断nums[mid]与target的关系:
(1) 如果nums[mid] == target,则记录mid,这是目标值在nums数组中的一个位置,然后改变二分区间,调整右边界right=mid-1继续查找该元素左侧的目标值;
(2) 如果nums[mid] > target,则说明mid处存储的数字过大,调整右边界right=mid-1继续查找;
(3) 如果nums[mid] < target,则说明mid处存储的数字过小,调整左边界left=mid-1继续查找;
重复以上步骤可以发现,对于查找左边界而言,查找到目标元素记录此位置之后,仍继续向左查找是否存在目标元素,直到不满足循环条件left<=right的时候退出循环,那么最后一次记录的mid即为目标元素的左边界。
同理,查找元素右边界仅需要在nums[mid] == target时,调整二分区间往右便可,即调整左边界left=mid-1继续查找,在此不再赘述。
基于以上分析,可通过C++将上述查找元素左边界的算法表述如下:
int searchRange(vector<int>& nums, int target) {
int leftRange = -1;
// 定义二分法的区间
left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 计算要查找的位置
if (nums[mid] == target) { // 查找到目标值,记录mid,然后向左继续查找
leftRange = mid;
right = mid - 1;
}
else if (nums[mid] < target) // 查找的值小于目标值,改变左界向右继续查找
left = mid + 1;
else // 查找的值大于目标值,改变右界向左继续查找
right = mid - 1;
}
return leftRange; // 返回元素左界的位置,若元素不存在则返回-1
}
3、LeeCode题解
通过上述分析,该题目利用两个二分法获取元素的左右边界,若元素不存在返回{-1,-1}即可,C++程序如下:
vector<int> searchRange(vector<int>& nums, int target) {
// 定义target的存在的位置为-1,-1
int leftRange = -1, rightRange = -1;
// 定义二分法的区间
int left = 0, right = nums.size() - 1;
// 求target的左边界
while (left <= right) {
int mid = left + (right - left) / 2; // 计算要查找的位置
if (nums[mid] == target) { // 搜索到目标值之后暂存该结果,并且继续向左搜索
leftRange = mid;
right = mid - 1;
}
else if (nums[mid] > target) // 查找的值小于目标值,改变左界向右继续查找
right = mid - 1;
else // 查找的值大于目标值,改变右界向左继续查找
left = mid + 1;
}
// 定义二分法的区间
left = 0, right = nums.size() - 1;
// 求target的右边界
while (left <= right) {
int mid = left + (right - left) / 2; // 计算要查找的位置
if (nums[mid] == target) { // 搜索到目标值之后暂存该结果,并且继续向右搜索
rightRange = mid;
left = mid + 1;
}
else if (nums[mid] < target) // 查找的值小于目标值,改变左界向右继续查找
left = mid + 1;
else // 查找的值大于目标值,改变右界向左继续查找
right = mid - 1;
}
return vector<int> {leftRange, rightRange};
}