二分易错点:边界问题
二分查找的难度并不是很大,但经常一写就废。因此整理一些自己经常遇到的坑,用于加强记忆。
二分查找常见的错误基本就是边界问题,常见的边界问题有如下几点:
- 循环逻辑。用left<right还是left<=right。
- 更新区间。更新区间时,middle要不要加一或者减一。
如果使用二分查找解决问题时,这两个地方经常出错,那就是对查找中区间的概念有些欠缺,因此处理不好边界问题。
练习题目:Leetcode704-二分查找
建议通过这个题目明晰边界问题与区间的概念之后,再进行深入的联系。
什么是区间
二分查找是在一个数组(nums)中找到与目标值(target)相等的元素的算法,效率比遍历比较要高。二分查找的查找过程到处都能找到资源,在此不多赘述,我们的重点放在区间上。
数组的长度是nums.length()
,使用二分法时,我们通常会定义左右指针,这两个指针形成一个区间,我们每次在这个区间中寻找元素,这个区间通常是左闭右闭区间[left,right]
或者左闭右开区间[left,right)
。
通常,在最开始定义左右指针的时候,我们会将左指针定义为0,右指针定义为数组长度或者是数组长度减一。右指针定义方式的不同,代表选择的区间方式不同,那么我们后续逻辑编写缩小范围以及确认循环逻辑时,就需要按照已定的区间逻辑进行编写。逻辑不一致,就会导致代码出现问题。
//左闭右开.
int left = 0;
int right = nums.length();
//左闭右闭.
int left = 0;
int right = nums.length() - 1;
左开也可以写二分法,但左开不是二分常用的逻辑,在此也不多赘述。
区间选择的不同给二分带来的影响
区间的选择为什么会给二分后来的逻辑带来影响呢?因为选择区间的时候我们定义了一条规则:区间的边界是否需要进入逻辑循环。这里就影响代码中的两个点:
- 循环条件的判断。左右节点相等时,是否需要进入循环。
- 新区间的判断。循环条件的判断已经确定,所以新区间应当按照既定的规则进行边界的判定,与最开始指定左右节点区间时,保持一致。
循环条件判断
当数组长度为1(或者缩小到长度为1时),我们需要将数组中的元素与目标元素进行对比,因此需要进入循环。
左闭右开区间中[left,right)
,只有一个元素时,左指针比右指针小1。while的循环条件不论是left<=right
还是left<right
都可以进入循环,但是如果选择了错误的区间left<=right
,如果数组中没有匹配的项,就会陷入死循环。因为右指针不可能小于左指针。
左闭右闭区间中[left,right]
,只有一个元素时,左指针与右指针相等。如果想要进入循环,while的循环条件应该是left<=right
。如果没有等号,则不进入循环,明明可以匹配成功,却显示匹配失败的结果。
新区间的判断
当我们定义完区间,以及符合区间逻辑的循环判断条件后,需要注意的点就是缩小区间时,新区间的判断。当缩小数组长度时,如果数组的边界与定义的判断逻辑不符,轻则多进行一次循环,重则找不到对应的数据。
我们每次进行区间的缩小,都会改变左右指针之一。两种区间之间的共同点是左闭,当目标值target大于nums[middle]的数据时,左指针通常可以改变为middle+1,因为middle的值已经经过判断,没必要再进行判断。
接下来只讨论右边区间的不同引起的代码的变化,以及不遵循这样的逻辑可能会带来的后果。
左闭右闭区间中,目标值target小于nums[middle]
,改变右边界时,右指针最好等于middle-1
,因为middle的值已经经过判断,没必要再进入循环。改为middle无非就是可能多进入一次循环。
但是,在左闭右开区间中,改变右边界时,右指针只能等于middle,因为左闭右开区间中,右指针指向的元素本身就不参与计算,如果此时右指针等于middle-1
,元素nums[middle-1]
就没有跟target进行比较,可能会漏掉我们需要找到的元素。