二分查找代码实现
以下是完整的代码和解释:
#include <stdio.h>
int binarySearch(int arr[], int length, int target) {
int left = 0;
int right = length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (arr[mid] == target) {
return mid; // 找到目标元素
}
if (arr[mid] < target) {
left = mid + 1; // 目标在右半部分
} else {
right = mid - 1; // 目标在左半部分
}
}
return -1; // 未找到目标元素
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 11};
int target = 7;
int length = sizeof(arr) / sizeof(arr[0]); // 在主函数中计算数组长度
int result = binarySearch(arr, length, target);
if (result == -1) {
printf("元素未找到\n");
} else {
printf("元素找到了,位置为: %d\n", result);
}
return 0;
}
在上面的代码中,使用 int mid = left + (right - left) / 2;
代替 int mid = (left + right) / 2;
是为了防止整数溢出。具体原因和解释如下:
原因
假设 left
和 right
都是非常大的正整数,那么 left + right
可能会超过 int
类型的表示范围,导致整数溢出。然而, left + (right - left) / 2
通过先计算 (right - left) / 2
,确保了不会超过 int
的表示范围。
例子
假设 left = 2147483640
和 right = 2147483646
:
left + right
的值是4294967286
,超过了int
的最大值2147483647
,会导致溢出。right - left
的值是6
,计算(right - left) / 2
得到3
,然后left + 3
的结果是2147483643
,没有溢出。
代码执行流程
-
初始化:
left
初始化为数组的起始索引0
。right
初始化为数组的末尾索引length - 1
。
-
二分查找循环:
- 计算中间位置
mid
,使用int mid = left + (right - left) / 2;
防止溢出。 - 检查
arr[mid]
是否等于目标值target
。如果相等,返回mid
。 - 如果
arr[mid]
小于目标值target
,将left
更新为mid + 1
,继续在右半部分查找。 - 如果
arr[mid]
大于目标值target
,将right
更新为mid - 1
,继续在左半部分查找。
- 计算中间位置
-
返回结果:
- 如果找到了目标元素,返回其索引。
- 如果循环结束后没有找到目标元素,返回
-1
。
总结
通过使用 int mid = left + (right - left) / 2;
,你可以有效避免由于 left
和 right
的和超出 int
范围导致的溢出问题,确保二分查找在所有情况下都能正确执行。
在数学上证明 left + (right - left) / 2
和 (left + right) / 2
得到的结果是相同的。两者之所以相等,归根结底在于它们的计算方式和中间步骤是相同的。
证明过程
令 ( L ) 表示 left
,( R ) 表示 right
。
-
表达式1:
(left + right) / 2
mid = L + R 2 \text{mid} = \frac{L + R}{2} mid=2L+R -
表达式2:
left + (right - left) / 2
mid = L + R − L 2 \text{mid} = L + \frac{R - L}{2} mid=L+2R−L
为了证明这两个表达式是等价的,我们可以将表达式2进行展开和简化:
L + R − L 2 = L + R 2 − L 2 = 2 L 2 + R 2 − L 2 = L + R 2 L + \frac{R - L}{2} = L + \frac{R}{2} - \frac{L}{2} = \frac{2L}{2} + \frac{R}{2} - \frac{L}{2} = \frac{L + R}{2} L+2R−L=L+2R−2L=22L+2R−2L=2L+R
由此可见,两个表达式是相等的。
原理
在数学上,两者的计算结果是一样的,因为加法和减法在整数范围内是可交换和结合的。然而在计算机科学中,特别是在考虑可能存在的整数溢出问题时,使用 left + (right - left) / 2
更为安全。
整数溢出问题
假设 left
和 right
都是非常大的整数,如果直接计算 (left + right) / 2
,有可能导致 left + right
超出整数范围,产生溢出。
比如,在 32 位系统中,最大整数值为 2147483647
。如果 left
和 right
的和超过这个值,就会发生溢出,从而导致计算结果错误。
通过 left + (right - left) / 2
这种方式,避免了直接相加可能带来的溢出问题,因为 right - left
的结果不会超过原来的范围。
实际代码示例
#include <iostream>
#include <limits.h>
int main() {
int left = INT_MAX - 1;
int right = INT_MAX;
// 计算两种方式的结果
int mid1 = (left + right) / 2;
int mid2 = left + (right - left) / 2;
std::cout << "Using (left + right) / 2: " << mid1 << std::endl;
std::cout << "Using left + (right - left) / 2: " << mid2 << std::endl;
return 0;
}
结果验证
当 left
和 right
都接近 INT_MAX
时,(left + right) / 2
可能会导致溢出,而 left + (right - left) / 2
则可以避免这个问题,得到正确的结果。
通过这种方式,我们不仅证明了两者的数学等价性,还理解了在编程中为什么更推荐使用 left + (right - left) / 2
来避免潜在的整数溢出问题。