您可以选择不变量。这是从实践中学到的技能。即使有经验,它通常也涉及一些试验和错误。选一个。看看情况如何。寻找机会选择一个需要较少工作来维护的不同的。您选择的不变量会对代码的复杂性和/或效率产生重大影响。
二分查找中的不变量至少有四种合理的选择:
a[lo] < target < a[hi]
a[lo] <= target < a[hi]
a[lo] < target <= a[hi]
a[lo] <= target <= a[hi]
您通常会看到最后一个,因为它最容易解释,并且不涉及使用超出范围的数组索引进行棘手的初始化,其他人会这样做。
现在有理由使用像a[lo] < target <= a[hi] 这样的不变量。如果您希望始终找到目标重复系列中的第一个,则此不变量将在 O(log n) 时间内完成。当hi - lo == 1 时,hi 指向目标的第一次出现。
int find(int target, int *a, int size) {
// Establish invariant: a[lo] < target <= a[hi] || target does not exist
// We assume a[-1] contains an element less than target. But since it is never
// accessed, we don't need a real element there.
int lo = -1, hi = size - 1;
while (hi - lo > 1) {
// mid == -1 is impossible because hi-lo >= 2 due to while condition
int mid = lo + (hi - lo) / 2; // or (hi + lo) / 2 on 32 bit machines
if (a[mid] < target)
lo = mid; // a[mid] < target, so this maintains invariant
else
hi = mid; // target <= a[mid], so this maintains invariant
}
// if hi - lo == 1, then hi must be first occurrence of target, if it exists.
return hi > lo && a[hi] == target ? hi : NOT_FOUND;
}
注意此代码未经测试,但应该可以通过不变逻辑工作。
具有两个<= 的不变量只会找到目标的一些 实例。你无法控制哪一个。
此不变量确实需要使用lo = -1 进行初始化。这增加了证明要求。您必须证明mid 永远不能设置为-1,这会导致超出范围的访问。幸运的是,这个证明并不难。
你引用的文章很差。它有几个错误和不一致之处。在别处寻找例子。 Programming Pearls 是个不错的选择。
您提议的更改是正确的,但可能会慢一些,因为它将只运行一次的测试替换为每次迭代运行一次的测试。