二分查找,是个非常牛但是又让人感觉很普通的算法。牛是因为它的时间复杂度只有O(logn),普通是因为人人都知道它并且烂熟于心,刻在DNA里了。
但是二分算法有几个小细节,却不见得每个人都能通透了解。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right)
还是 while(left <= right)
,到底是right = middle
呢,还是要right = middle - 1
呢?这就会导致有的时候程序运行起来存在一些风险,可别小看这些风险,如果程序跑起来有bug,那就是大问题。
原因:
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
下面我用这两种区间的定义分别讲解两种不同的二分写法。
接下来就让我们来看看关于二分查找的一些你值得关注的小细节吧!
首先,我们要确定要进行二分查找的数据范围。常见的数据范围有左闭右闭[a,b]区间以及左闭右开[a,b)区间,别小看这点,一旦确定下来,接下来的代码就要围绕这个区间去展开
以下我们都以缩小右边界为例子
左闭右闭即[left, right]区间
对于[a,b]区间,我们知道它是左闭右闭区间,也就是说,当a=b的时候,它也是成立的。我们举个例子,当a=b=1的时候,区间为[1,1],显然在数学上这是成立的。所以当在比较left++和right--的过程中,他们是可以相等的,所以这时候就应该选择while(left<=right)的写法。
接下来就是right的选择了。因为mid>val,此时的mid显然不需要在下一个区间进行比较了,我们知道mid是大于val的,而且区间是左闭右闭,此时选择right=mid就是不合理的,多余的操作。对于mid-1的大小我们并不明确,因此我们要选择right=mid-1,以进行下一个区间的判断。
tip:如果target大于第一个元素,小于最后一个元素,但是数组里不存在这个数字,或者target小于第一个元素,right最终就会移动到 left的左边,然后就跳出循环。
如果大于最后一个元素,left到right的右边
{
int left = 0;
int right = numssize - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right)
{
// 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target)
{
right = middle - 1; // target 在左区间,所以[left, middle - 1]
}
else if (nums[middle] < target)
{
left = middle + 1; // target 在右区间,所以[middle + 1, right]
}
else return middle; // 数组中找到目标值,直接返回下标
}
// 未找到目标值
return -1;
}
左闭右开即[left, right)区间
对于[a,b)区间,还是以缩小右边界我们知道它是左闭右开区间,也就是说,当a=b的时候,它是不成立的。我们举个例子,当a=b=1的时候,区间为[1,1),显然不成立,怎么可能存在区间即包含1,又不包含1。所以当在比较left++和right--的过程中,他们是不可以相等的,所以这时候就应该选择while(left<right)的写法。
接下来就是right的选择了。依然有mid>val,这里的区间是左闭右开。假设这里选择了right=mid-1。对于mid-1的大小我们并不明确,区间右边又是开的,是不包含的,那么下一个区间它就不在判断范围内,大家想想这合理吗?显然这是不合理的!如果我们选择right=mid,mid的大小我们知道,右区间是开的,mid此时不在下一个区间判断范围内,大小未知的mid-1在,这就十分合理。所以这里我们要选择right=mid;
{
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right)
{
// 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (nums[middle] > target)
{
right = middle; // target 在左区间,在[left, middle)中
}
else if (nums[middle] < target)
{
left = middle + 1; // target 在右区间,在[middle + 1, right)中
}
else return middle; // 数组中找到目标值,直接返回下标
}
// 未找到目标值
return -1;
}
总结:
- 对于区间我们的开闭我们要从头到尾贯彻到底,从数学区间的角度去思考需不需要包含,问题也就迎刃而解,一定一定不要死记硬背!!!
我的表达可能没那么清晰。关于排版,表达以及其他问题大家可以多多提建议,我会吸取努力改正进步的!
同时更加希望大家可以真正弄懂二分查找,实现零错误使用!!那将是我的荣幸