链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/solution/by-xun-ge-v-hzfd/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


想详细了解学习二分查找可看链接[二分查找算法详解]
题目要求在已经排序数组中寻找特定元素,完全返回二分查找的统计,直接使用二分查找对于元素位置,再对位置进行检索,寻找开始的地方和结束的地方即可。
应该注意边界情况的处理
代码注释超级详细
相信对于二分查找边界的处理都觉得有点难,下面整理了二分查找边界的所有情况
说到二分查找相信我们都能说出个一二三,但是每次到写代码时,却总是差一点,最多情况往往是边界不知道怎么取,现在就说一下,二分查找存在的边界问题。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。相信很多同学都和我一样,在条件判断时总是不知道是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义有以下四种,左闭右闭即[left, right],或者左闭右开即[left, right),或者左开右闭即(left, right],或者左开右开即(left, right),其中左闭右闭即[left, right]比较常用,基本思路不变只是控制了一些变量选择
下面我用这四种区间的定义分别讲解四种不同的二分写法。
以下分析基于理论情况,实际题目中我们比较常用第一种情况和第二种情况
第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间:
while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
if (nums[mid] > target) right 要赋值为 mid - 1,因为当前这个nums[mid]一定不是target,那么接下来要查找的左区间结束下标位置就是 mid - 1,因为区间 -1了,所以取不到mid了
if (nums[mid] < target) left 要赋值为 mid +1,因为当前这个nums[mid]一定不是target,那么接下来要查找的右区间结束下标位置就是 mid + 1,因为区间 +1了,所以取不到mid了
if (nums[mid] == target) 找到了我们需要的值,返回下标mid
例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
代码如下:(详细注释)
- // 版本一
- while(left <= right)// 因为left == right的时候,在[left, right]是有效的空间,即相等时可以取到该元素,所以使用 <=
- {
- int mid = (left + right)/2;
- //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
- if(nums[mid] > target)
- {
- right = mid - 1;// target 在左区间,所以[left, mid - 1]
- }
- else if(nums[mid] < target)
- {
- left = mid + 1;// target 在右区间,所以[mid + 1, right]
- }
- else if(nums[mid] == target)
- {
- return mid;// 数组中找到目标值,直接返回下标
- }
- }
-
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同:
while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
if (nums[mid] > target) right 更新为 mid,因为当前nums[mid]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为mid,即:下一个查询区间不会去比较nums[mid],因为是开区间,所以取不到
if (nums[mid] < target) left 更新为 mid + 1,因为当前nums[mid]不等于target,去右区间继续寻找,而寻找区间是左闭右开区间,所以left更新为mid + 1,即:下一个查询区间不会去比较nums[mid],因为区间+1了,所以取不到mid了
if (nums[mid] == target) 找到了我们需要的值,返回下标mid
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)
代码如下:(详细注释)
- // 版本二
- while(left < right)// 因为left == right的时候,在[left, right)是无效的空间,即相等时取不到该元素,所以使用 <
- {
- int mid = (left + right)/2;
- //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
- if(nums[mid] > target)
- {
- right = mid;// target 在左区间,所以[left, mid)
- }
- else if(nums[mid] < target)
- {
- left = mid + 1;// target 在右区间,所以[mid + 1, right)
- }
- else if(nums[mid] == target)
- {
- return mid;// 数组中找到目标值,直接返回下标
- }
- }
如果说定义 target 是在一个在左开右闭的区间里,也就是(left, right] ,那么二分法的边界处理方式则截然不同:
while (left < right),这里使用 < ,因为left == right在区间(left, right]是没有意义的
if (nums[mid] > target) right 更新为 mid - 1,因为当前nums[mid]不等于target,去左区间继续寻找,而寻找区间是左开右闭区间,所以right更新为mid - 1,即:下一个查询区间不会去比较nums[mid],因为区间-1了,所以取不到mid了
if (nums[mid] < target) left 更新为 mid,因为当前nums[mid]不等于target,去右区间继续寻找,而寻找区间是左开右闭区间,所以left更新为mid,即:下一个查询区间不会去比较nums[mid],因为是开区间,所以取不到
if (nums[mid] == target) 找到了我们需要的值,返回下标mid

在数组:1,2,3,4,7,9,10中查找元素7,如图所示:(注意和方法二的区别)
- // 版本三
- while(left < right)// 因为left == right的时候,在(left, right]是无效的空间,即相等时取不到该元素,所以使用 <
- {
- int mid = (left + right)/2;
- //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
- if(nums[mid] > target)
- {
- right = mid - 1;// target 在左区间,所以(left, mid]
- }
- else if(nums[mid] < target)
- {
- left = mid;// target 在右区间,所以(mid + 1, right]
- }
- else if(nums[mid] == target)
- {
- return mid;// 数组中找到目标值,直接返回下标
- }
- }
如果说定义 target 是在一个在左开右开的区间里,也就是(left, right) ,那么二分法的边界处理方式则截然不同:
while (left < right),这里使用 < ,因为left == right在区间(left, right)是没有意义的
if (nums[mid] > target) right 更新为 mid ,因为当前nums[mid]不等于target,去左区间继续寻找,而寻找区间是左开右闭区间,所以right更新为mid ,即:下一个查询区间不会去比较nums[mid],因为是开区间,所以取不到
if (nums[mid] < target) left 更新为 mid,因为当前nums[mid]不等于target,去右区间继续寻找,而寻找区间是左开右闭区间,所以left更新为mid,即:下一个查询区间不会去比较nums[mid],因为是开区间,所以取不到
if (nums[mid] == target) 找到了我们需要的值,返回下标mid

在数组:1,2,3,4,7,9,10中查找元素7,如图所示:(注意和方法三的区别)
- // 版本四
- while(left < right)// 因为left == right的时候,在(left, right)是无效的空间,即相等时取不到该元素,所以使用 <
- {
- int mid = (left + right)/2;
- //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
- if(nums[mid] > target)
- {
- right = mid;// target 在左区间,所以(left, mid)
- }
- else if(nums[mid] < target)
- {
- left = mid;// target 在右区间,所以(mid, right)
- }
- else if(nums[mid] == target)
- {
- return mid;// 数组中找到目标值,直接返回下标
- }
- }
二分法是非常重要的基础算法,为什么会对于二分法都是一看就会,一写就废?
其实主要就是对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。
区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。
- /**
- * Note: The returned array must be malloced, assume caller calls free().
- */
- //左闭右闭
- int* searchRange(int* nums, int numsSize, int target, int* returnSize){
-
- int * ans = malloc(sizeof(int) * (2));//初始化变量
- int left = 0, right = numsSize-1;
- *returnSize = 0;
- while(left <= right)//二分查找
- {
- int mid = left + (right - left) / 2;//防止溢出
- if(nums[mid] == target)//寻找到了目标元素
- {
- int m = mid;
- while(mid > 0 && nums[mid-1] == target)//检索开始位置
- {
- mid--;
- }
- ans[(*returnSize)++] = mid;
- while(m < numsSize-1 && nums[m+1] == target)//检索结束位置
- {
- m++;
- }
-
- ans[(*returnSize)++] = m;
- return ans;
- }
- else if(nums[mid] < target)//二分模板
- {
- left = mid+1;
- }
- else
- {
- right = mid-1;
- }
- }
- if(*returnSize == 0)
- {
- ans[(*returnSize)++] = -1;
- ans[(*returnSize)++] = -1;
- }
- return ans;
- }
-
- 作者:xun-ge-v
- 链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/solution/by-xun-ge-v-hzfd/
- 来源:力扣(LeetCode)
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。