• LeetCode·34.在排序数组中查询元素的第一个和最后一个位置·二分查找


    链接: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]比较常用,基本思路不变只是控制了一些变量选择

    下面我用这四种区间的定义分别讲解四种不同的二分写法。

    以下分析基于理论情况,实际题目中我们比较常用第一种情况和第二种情况

    二分法第一种写法,左闭右闭即[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,如图所示:

    代码如下:(详细注释)

    1. // 版本一
    2. while(left <= right)// 因为left == right的时候,在[left, right]是有效的空间,即相等时可以取到该元素,所以使用 <=
    3. {
    4. int mid = (left + right)/2;
    5. //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
    6. if(nums[mid] > target)
    7. {
    8. right = mid - 1;// target 在左区间,所以[left, mid - 1]
    9. }
    10. else if(nums[mid] < target)
    11. {
    12. left = mid + 1;// target 在右区间,所以[mid + 1, right]
    13. }
    14. else if(nums[mid] == target)
    15. {
    16. return mid;// 数组中找到目标值,直接返回下标
    17. }
    18. }

    二分法第二种写法,左闭右开即[left, right)

    如果说定义 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,如图所示:(注意和方法一的区别

    代码如下:(详细注释)

    1. // 版本二
    2. while(left < right)// 因为left == right的时候,在[left, right)是无效的空间,即相等时取不到该元素,所以使用 <
    3. {
    4. int mid = (left + right)/2;
    5. //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
    6. if(nums[mid] > target)
    7. {
    8. right = mid;// target 在左区间,所以[left, mid)
    9. }
    10. else if(nums[mid] < target)
    11. {
    12. left = mid + 1;// target 在右区间,所以[mid + 1, right)
    13. }
    14. else if(nums[mid] == target)
    15. {
    16. return mid;// 数组中找到目标值,直接返回下标
    17. }
    18. }

     二分法第三种写法,左开右闭即(left, right]

    如果说定义 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,如图所示:(注意和方法二的区别

    1. // 版本三
    2. while(left < right)// 因为left == right的时候,在(left, right]是无效的空间,即相等时取不到该元素,所以使用 <
    3. {
    4. int mid = (left + right)/2;
    5. //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
    6. if(nums[mid] > target)
    7. {
    8. right = mid - 1;// target 在左区间,所以(left, mid]
    9. }
    10. else if(nums[mid] < target)
    11. {
    12. left = mid;// target 在右区间,所以(mid + 1, right]
    13. }
    14. else if(nums[mid] == target)
    15. {
    16. return mid;// 数组中找到目标值,直接返回下标
    17. }
    18. }

    二分法第四种写法,左开右开即(left, right) 

    如果说定义 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,如图所示:(注意和方法三的区别) 

    1. // 版本四
    2. while(left < right)// 因为left == right的时候,在(left, right)是无效的空间,即相等时取不到该元素,所以使用 <
    3. {
    4. int mid = (left + right)/2;
    5. //如果left+right过大,导致和溢出,可以用mid = left + (right - left) / 2,防止溢出left+right
    6. if(nums[mid] > target)
    7. {
    8. right = mid;// target 在左区间,所以(left, mid)
    9. }
    10. else if(nums[mid] < target)
    11. {
    12. left = mid;// target 在右区间,所以(mid, right)
    13. }
    14. else if(nums[mid] == target)
    15. {
    16. return mid;// 数组中找到目标值,直接返回下标
    17. }
    18. }

    总结

    二分法是非常重要的基础算法,为什么会对于二分法都是一看就会,一写就废

    其实主要就是对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。

    区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。

    代码

    1. /**
    2. * Note: The returned array must be malloced, assume caller calls free().
    3. */
    4. //左闭右闭
    5. int* searchRange(int* nums, int numsSize, int target, int* returnSize){
    6. int * ans = malloc(sizeof(int) * (2));//初始化变量
    7. int left = 0, right = numsSize-1;
    8. *returnSize = 0;
    9. while(left <= right)//二分查找
    10. {
    11. int mid = left + (right - left) / 2;//防止溢出
    12. if(nums[mid] == target)//寻找到了目标元素
    13. {
    14. int m = mid;
    15. while(mid > 0 && nums[mid-1] == target)//检索开始位置
    16. {
    17. mid--;
    18. }
    19. ans[(*returnSize)++] = mid;
    20. while(m < numsSize-1 && nums[m+1] == target)//检索结束位置
    21. {
    22. m++;
    23. }
    24. ans[(*returnSize)++] = m;
    25. return ans;
    26. }
    27. else if(nums[mid] < target)//二分模板
    28. {
    29. left = mid+1;
    30. }
    31. else
    32. {
    33. right = mid-1;
    34. }
    35. }
    36. if(*returnSize == 0)
    37. {
    38. ans[(*returnSize)++] = -1;
    39. ans[(*returnSize)++] = -1;
    40. }
    41. return ans;
    42. }
    43. 作者:xun-ge-v
    44. 链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/solution/by-xun-ge-v-hzfd/
    45. 来源:力扣(LeetCode)
    46. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    树的直径 树形dp+2次dfs
    什么是机器学习中的集成学习,列举几种常见的集成学习算法
    echart在折线显示横纵(横纵线沿着折线展示)
    Web安全——Web安全漏洞与利用上篇(仅供学习)
    C#同步调用sync与异步调用Async
    01【SpringBoot快速入门、yml语法、自动配置、整合框架】
    linux应用hook实例(含源码分析)
    8、【办公自动化】Python实现PDF文件的批量操作
    计算点在线上的投影坐标
    Linux命令老是记不住?一篇文章帮你解决。Linux常用命令汇总
  • 原文地址:https://blog.csdn.net/m0_64560763/article/details/126283480