多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

高频面试题《二分查找》

高频面试题《二分查找》 在排序数组中查找元素的第一个和最后一个位置题目描述给定一个按照非递减顺序排列的整数数组nums和目标值target找出目标值在数组中的开始位置和结束位置。如果数组中不存在target返回[-1, -1]要求算法的时间复杂度为O(log n)示例输入nums [5, 7, 7, 8, 8, 10], target 8 输出[3, 4]一、为什么使用二分查找题目给出了两个重要条件数组按照非递减顺序排列要求时间复杂度为O(log n)有序数组具备单调性因此可以使用二分查找。普通遍历最坏需要检查数组中的所有元素时间复杂度为O(n)二分查找每次可以排除一半搜索范围时间复杂度为O(log n)例如当数组长度为1024时1024 → 512 → 256 → 128 → ... → 2 → 1最多只需要大约10次查找因为2¹⁰ 1024二、普通二分查找为什么不够普通二分查找只负责寻找任意一个等于target的元素。例如nums [5, 7, 7, 8, 8, 10] target 8普通二分查找可能找到下标3也可能找到下标4。但题目要求返回完整区间[3, 4]因此需要进行两次二分查找寻找第一个大于等于target的位置寻找第一个严格大于target的位置设这两个位置分别为lower_bound upper_bound那么目标值的范围就是[lower_bound, upper_bound - 1]三、左闭右开区间本文采用左闭右开的搜索区间[left, right)其中left对应的位置包含在搜索范围内right对应的位置不包含在搜索范围内初始化left0rightlen(nums)这样[0, len(nums))正好覆盖整个数组。循环条件为whileleftright:当循环结束时left right此时left和right指向最终的边界位置。四、寻找左边界左边界可以定义为数组中第一个大于等于target的位置。也就是寻找第一个满足以下条件的位置nums[i] target情况一nums[mid] target如果nums[mid]target由于数组已经有序mid及其左边的元素都不可能成为答案。因此将左边界更新为leftmid1情况二nums[mid] target如果nums[mid]target说明mid可能是答案但左边还可能存在更靠前的合法位置。因此保留mid继续向左收缩rightmid代码实现deflower_bound(nums,target):left0rightlen(nums)whileleftright:midleft(right-left)//2ifnums[mid]target:leftmid1else:rightmidreturnleft五、手动执行左边界查找对于nums [5, 7, 7, 8, 8, 10] target 8初始状态left 0 right 6第一轮mid 0 (6 - 0) // 2 3 nums[mid] 8因为nums[mid] target执行rightmid更新后left 0 right 3第二轮mid 0 (3 - 0) // 2 1 nums[mid] 7因为nums[mid] target执行leftmid1更新后left 2 right 3第三轮mid 2 (3 - 2) // 2 2 nums[mid] 7执行leftmid1更新后left 3 right 3此时left right不成立循环结束返回3所以第一个大于等于8的位置是下标3。六、寻找右边界为了确定最后一个target的位置可以先寻找第一个严格大于target的位置。也就是寻找第一个满足以下条件的位置nums[i] target如果nums[mid]target说明mid不是第一个大于target的位置需要继续向右寻找leftmid1否则nums[mid]target说明mid可能是答案需要保留mid并继续向左寻找rightmid代码如下defupper_bound(nums,target):left0rightlen(nums)whileleftright:midleft(right-left)//2ifnums[mid]target:leftmid1else:rightmidreturnleftupper_bound返回第一个大于target的位置因此最后一个等于target的位置为upper_bound(nums,target)-1七、两种边界的关键区别寻找左边界时ifnums[mid]target:leftmid1else:rightmid寻找第一个大于目标值的位置时ifnums[mid]target:leftmid1else:rightmid二者的区别只有一个等号lower_boundnums[mid] target upper_boundnums[mid] target当nums[mid] target时寻找左边界继续向左寻找寻找右边界继续向右寻找可以记忆为找左边界相等时向左 找右边界相等时向右。八、如何判断目标值不存在lower_bound找到的是第一个大于等于target的位置但这个位置上的元素不一定等于target。例如nums [5, 7, 7, 8, 8, 10] target 6第一个大于等于6的元素是7其下标为1。因此还需要检查nums[start]target另外如果所有元素都小于targetlower_bound会返回len(nums)例如nums [5, 7, 8] target 10查找结果为start 3 len(nums) 3因此目标值不存在的完整判断是ifstartlen(nums)ornums[start]!target:return[-1,-1]九、为什么必须先判断数组越界下面的判断顺序是安全的ifstartlen(nums)ornums[start]!target:Python 的or具有短路特性。当startlen(nums)为真时Python 不会继续执行nums[start]因此不会发生数组越界。如果把条件反过来ifnums[start]!targetorstartlen(nums):当start len(nums)时程序会先访问不存在的下标从而抛出IndexError: list index out of range十、完整代码classSolution:defsearchRange(self,nums:list[int],target:int)-list[int]:deflower_bound():寻找第一个大于等于 target 的位置。left0rightlen(nums)whileleftright:midleft(right-left)//2ifnums[mid]target:leftmid1else:rightmidreturnleftdefupper_bound():寻找第一个严格大于 target 的位置。left0rightlen(nums)whileleftright:midleft(right-left)//2ifnums[mid]target:leftmid1else:rightmidreturnleft startlower_bound()ifstartlen(nums)ornums[start]!target:return[-1,-1]endupper_bound()-1return[start,end]十一、使用通用函数简化代码也可以将两个二分查找统一为一个通用函数classSolution:defsearchRange(self,nums:list[int],target:int)-list[int]:deflower_bound(value):left0rightlen(nums)whileleftright:midleft(right-left)//2ifnums[mid]value:leftmid1else:rightmidreturnleft startlower_bound(target)ifstartlen(nums)ornums[start]!target:return[-1,-1]endlower_bound(target1)-1return[start,end]这里lower_bound(target)寻找第一个大于等于target的位置。而lower_bound(target 1)对于整数数组而言相当于寻找第一个大于target的位置。需要注意在具有固定整数范围的语言中target 1可能溢出。分别实现lower_bound和upper_bound会更加通用。十二、复杂度分析进行了两次二分查找每次时间复杂度为O(log n)因此总时间复杂度仍然是O(log n)算法只使用了固定数量的变量空间复杂度为O(1)十三、容易犯的错误1. 找到目标值后立即返回ifnums[mid]target:returnmid这样只能找到任意一个目标值不能保证找到左右边界。2. 相等时更新方向错误寻找左边界时相等应该向左收缩rightmid寻找右边界时相等应该继续向右leftmid13. 混用区间定义如果采用左闭右开区间[left, right)就应该保持rightlen(nums)whileleftright:不要随意和闭区间[left, right]的写法混用。4. 返回mid循环结束后应该返回left或者right不能返回mid。因为mid只是最后一次检查的位置当数组为空时mid甚至没有被定义。5. 忘记判断目标值是否存在lower_bound找到的是第一个大于等于目标值的位置不保证该位置的元素一定等于目标值。必须检查startlen(nums)ornums[start]!target十四、二分查找的本质二分查找不只是“在有序数组中寻找某个数”。它更一般的用途是在一个具有单调性的搜索空间中寻找分界点。本题存在两个分界点小于 target | 大于等于 target以及小于等于 target | 大于 target通过两次二分查找确定这两个分界点就能得到目标值的完整区间。最终关系为开始位置 第一个大于等于 target 的位置 结束位置 第一个大于 target 的位置 - 1
返回列表