在我发现循环不变量和谓词是解决所有二元问题的最佳和最一致的方法之前,我遇到了完全相同的问题。
第 1 点:考虑谓词
一般来说,对于所有这 4 种情况(以及对等式的正常二分搜索),将它们想象为谓词。所以这意味着一些值满足谓词而一些失败。因此,例如考虑这个目标为 5 的数组:
[1、2、3、4、6、7、8]。找到第一个大于 5 的数字基本上等同于在这个数组中找到第一个:[0, 0, 0, 0, 1, 1, 1]。
第 2 点:包含搜索边界
我喜欢两端总是包容的。但我可以看到有些人喜欢开始包容并结束排他性(在 len 而不是 len -1 上)。我喜欢将所有元素都包含在数组中,所以当提到 a[mid] 时,我认为这不会让我的数组越界。所以我的偏好是:包容!!!
第 3 点:While 循环条件
所以我们甚至想在while循环中处理大小为1的子数组,当while循环结束时应该没有未处理的元素。我真的很喜欢这个逻辑。它总是坚如磐石。最初没有检查所有元素,基本上它们是未知的。这意味着不检查 [st = 0, to end = len - 1] 范围内的所有内容。然后当 while 循环结束时,未检查元素的范围应该是大小为 0 的数组!
第 4 点:循环不变量
由于我们定义了 start = 0, end = len - 1,所以不变量将是这样的:
start 剩下的任何东西都小于 target。
任何结束的权利都大于或等于目标。
第 5 点:答案
循环完成后,基本上基于循环不变量,开始左侧的任何内容都较小。所以这意味着 start 是大于或等于目标的第一个元素。
等效地,结束右侧的任何内容都大于或等于目标。所以这意味着答案也等于 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; // or for no overflow start + (end - start) / 2
if (a[mid] < target)
start = mid + 1;
else // a[mid] >= target
end = mid - 1;
}
return start; // or end + 1;
}
变体:
这相当于找到第一个 0。所以基本上只返回更改。
return end; // or return start - 1;
>
将 if 条件更改为 。没有其他变化。
同>,return end; // or return start - 1;
因此,对于所有 5 种变体(、>=、正常二分搜索),一般来说,只有 if 中的条件和 return 语句中的条件发生了变化。当您考虑不变量(第 4 点)和答案(第 5 点)时,计算这些小变化非常容易。
希望这对阅读本文的人有所帮助。如果有什么不清楚的感觉就像魔术,请联系我解释。了解了这个方法之后,二分查找的一切应该就一清二楚了!
额外点:最好也尝试包括开头但不包括结尾。所以数组最初是 [0, len)。如果您可以编写不变量、while 循环的新条件、答案以及清晰的代码,则意味着您了解了这个概念。