我发现循环不变量和谓词是处理所有二进制问题的最佳和最一致的方法。
要点1:考虑谓词
对于所有这4种情况(以及正常的二进制搜索相等性),将它们想象成一个谓词。这意味着一些值符合谓词,而一些值不符合。例如,考虑这个目标为5的数组:
[1, 2, 3, 4, 6, 7, 8]。找到第一个大于5的数字基本上等同于在这个数组中找到第一个1:[0, 0, 0, 0, 1, 1, 1]。
要点2:搜索边界包含
我喜欢两端都包含。但我知道有些人喜欢起始点包含而结束点不包含(在长度上而不是长度-1上)。我喜欢将所有元素都放在数组内部,这样在引用a[mid]时,我不用担心是否会超出数组范围。所以我的偏好是:包含!
第三点:While循环条件<=
所以我们甚至想要在while循环中处理大小为1的子数组,而当while循环结束时,应该没有未处理的元素。我真的很喜欢这个逻辑。它总是坚如磐石。最初,所有的元素都没有被检查过,基本上它们是未知的。意味着在[st = 0, to end = len - 1]范围内的所有元素都没有被检查过。然后当while循环结束时,未检查元素的范围应该是大小为0的数组!
第四点:循环不变量
由于我们定义了start = 0,end = len - 1,不变量将会是这样的:
start左边的任何元素都小于目标值。
end右边的任何元素都大于或等于目标值。
第五点:答案
一旦循环结束,基于循环不变量,start左边的任何元素都小于目标值。所以这意味着start是第一个大于或等于目标值的元素。
同样地,end右边的任何元素都大于或等于目标值。所以答案也等于end + 1。
代码如下:
public int find(int a[], int target){
int start = 0;
int end = a.length - 1;
while (start <= end){
int mid = (start + end) / 2;
if (a[mid] < target)
start = mid + 1;
else
end = mid - 1;
}
return start;
}
变体:
<
这相当于找到第一个0。所以基本上只返回变化。
return end; // or return start - 1;
>
将if条件改为<=,否则将为>。没有其他改变。
<=
与>相同,return end; // 或 return start - 1;
因此,对于所有5种变体(<=,<,>,>=,普通二分查找),只有if中的条件和return语句发生变化。当您考虑到不变量(第4点)和答案(第5点)时,找出这些小变化非常容易。
理解了这种方法之后,二分查找的一切都应该清晰明了!
额外的一点:尝试包括起始位置但不包括结束位置也是一个很好的实践。因此,数组最初将为[0,len)。如果您能编写不变量、while循环的新条件、答案以及清晰的代码,那就意味着您已经掌握了这个概念。