在编写二分搜索时,需要考虑几件事情。首先是您要搜索的区间范围,特别是您如何定义它。
例如,它可以包含low 和high,即[low, high],但也可以不包含high、[low, high)。你选择哪一个会改变你算法的其余部分。
明显的含义是初始值。一般来说,high 应该是数组的长度(如果它是独占的,它应该是包含的长度),但它可能是完全不同的东西,具体取决于您要解决的问题。
对于 while 循环,您希望它在搜索间隔为空时终止,这意味着没有更多的候选者可以检查。如果您使用区间[low, high],那么当low 严格大于high(例如,[5,5] 包含 5,但 [6,5] 不包含任何内容)时,这将为空,因此 while 循环将检查相反,low <= high。但是,如果使用区间[low, high),那么当low等于high时这个区间是空的,所以while循环需要检查low < high。
在 while 循环中,检查 mid 后,您希望将其从间隔中删除,以便不再检查它。如果high 包含在内,那么您必须使用比mid 少一个作为新的high 才能将其从区间中排除。但如果high 是独占的,那么将high 设置为等于mid 就足以排除它。
至于何时更新 low 与 high,这取决于您要搜索的内容。除了基本的二分搜索,您只想知道某物是否确实存在于集合中,您还必须考虑在尽可能接近时该怎么做。
例如,在 C++ 中,binary_search 更有用的版本称为 lower_bound 和 upper_bound。如果要搜索的值在容器中不存在,则它们都返回相同的位置,即大于搜索值的第一个位置。这很方便,因为如果要保持容器排序,这是您应该插入该值的位置。但是,如果值在容器中,可能多次出现,那么 lower_bound 将返回该值的第一次出现,而 upper_bound 仍将返回大于该值的第一个位置(或者换句话说,右边界到值的位置)。
要获得这些不同的行为,当mid 等于搜索值时,您可以更新low 或high 绑定。如果你想要下限,那么你想继续搜索你的搜索范围的下半部分,所以你把high向下。如果您想要上限,则将low 提高。在您的示例中,当cnt == mid 时,它会提升low,因此它会找到一个上限。
至于返回什么,这取决于您的搜索间隔和您要查找的内容。在您的示例中,while 循环正在检查(low < high),因此low 和high 在中断时将是相等的,并且您使用哪个并不重要,但即便如此您可能想要返回left - 1 或@ 987654362@ 取决于问题。如果 while 循环是 (low <= high),那么它什么时候会中断 low == high + 1,所以这取决于您要查找的内容。当你有疑问时,你总是可以通过一个例子来思考。
所以为了使用这一切,这里是你提到的解决方案的一个版本,但使用 [low, high] 而不是 [low, high) 的间隔:
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int low = 1, high = nums.size() - 2;
while (low <= high) {
int mid = low + (high - low) * 0.5;
int cnt = 0;
for (auto a : nums) {
if (a <= mid) ++cnt;
}
if (cnt <= mid) low = mid + 1;
else high = mid - 1;
}
return low;
}
};
PS:我没有提到间隔 (low, high] 或 (low, high) 的原因是因为它与计算 mid 索引的数学运算混淆了。因为 int math 会向下取整,所以您最终可能会遇到再次搜索 mid 的情况。比如low是7,high是9,那么low + (high - low) * 0.5就是8。将low更新到8后(因为是排他的就不加1了),low + (high - low) * 0.5还是8,你的循环永远不会终止。您可以通过在除以 2 的部分上加 1 来解决此问题,但通常使用包含 low 的区间会更干净。