【发布时间】:2020-09-13 23:08:53
【问题描述】:
这两个公式有什么区别
mid = low + (high - low) / 2;
mid = (high + low) / 2;
【问题讨论】:
-
您可以通过单击分数下方的灰色复选标记来接受其中一个答案。
标签: c++ c binary-search
这两个公式有什么区别
mid = low + (high - low) / 2;
mid = (high + low) / 2;
【问题讨论】:
标签: c++ c binary-search
在第二个版本中,如果high + low 大于int 的最大值(假设high 是int),那么它可能会溢出,调用未定义的行为。第一版解决了这个特殊的错误。
第一个版本仍然存在问题,例如如果low 是一个很大的负数,差值仍然会溢出。
从 c++20 开始,您应该为此使用 std::midpoint,它可以处理一大堆极端情况,并为所有情况做正确的事情。
这个看似简单的功能实际上实现起来却出奇的难,事实上,Marshall Clow 在 cppcon 2019 上给出的一个小时的talk 就涵盖了这个功能的实现。
【讨论】:
第一个更胜一筹(虽然还不完美,见Binary Search: how to determine half of the array):
它适用于未为 high 和 low 定义添加但定义为向 low 添加间隔的情况。指针就是这样一个例子,日期类型的对象可以是另一个。
high + low 可以溢出类型。对于有符号整数类型,行为未定义。
【讨论】:
两者都存在潜在的溢出问题。有符号整数溢出是未定义的行为 (UB)。
使用 unsigned 数学(常用于数组索引),那么当 low <= high 时,low + (high - low) / 2; 不会像 (high + low) / 2 那样溢出。
与low <= high 和0 <= low 时的签名 数学相同。
为了避免 any 溢出有符号数学(或 unsigned 数学与 low > high)并且仍然只使用 int/unsigned 数学,我认为下面会起作用。
mid = high/2 + low/2 + (high%2 + low%2)/2;
但是,当 high/2 + low/2 的符号与 (high%2 + low%2) 的符号不同时,这可能会失败。
下面是一个更强大且经过测试的版本。也许我稍后会简化。
#include <limits.h>
#include <stdio.h>
int midpoint(int a, int b) {
int avg = a/2 + b/2;
int small_sum = a%2 + b%2;
avg += small_sum/2;
small_sum %= 2;
if (avg < 0) {
if (small_sum > 0) avg++;
} else if (avg > 0) {
if (small_sum < 0) avg--;
}
return avg;
}
int midpoint_test(int a, int b) {
intmax_t lavg = ((intmax_t)a + (intmax_t)b)/2;
int avg = midpoint(a,b);
printf("a:%12d b:%12d avg_wide_math:%12jd avg_midpoint:%12d\n", a,b,lavg,avg);
return lavg == avg;
}
int main(void) {
int a[] = {INT_MIN, INT_MIN+1, -100, -99, -2, -1, 0, 1, 2, 99, 100, INT_MAX-1, INT_MAX};
int n = sizeof a/ sizeof a[0];
for (int i=0; i<n; i++) {
for (int j=0; j<n; j++) {
if (midpoint_test(a[i], a[j]) == 0) {
puts("Oops");
return 1;
}
}
}
puts("Success");
return 0;
}
【讨论】:
mid、low`和high具有相同的类型,high >= low和low >= 0,那么mid = low + (high - low) / 2;怎么会导致溢出呢?它应该为有符号和无符号类型完全定义。在二分查找中,这些变量是索引值,因此 >= 0`。
high >= low >= 0 是很常见的,但是当 p 作为指针指向数组中间的某个位置时,p[some_negative] 是允许的。
midpoint()-like 函数所需的代码似乎很有趣,因为存在各种极端情况。我的与 std::midpoint(a,b) 不同,因为它朝 0 舍入而不是朝 a 舍入。嗯,也许我应该叫我的average() 而不是midpoint()?
两个公式不同:
low 和high 的值,两者都可能溢出。在接下来的讨论中,我们假设low、mid 和high 具有相同的类型。我们正在寻找一种安全的方法来找到low 和high 之间的中点或平均值,它始终在类型的范围内。
第一个公式,mid = low + (high - low) / 2; 如果类型已签名,则向 low 舍入,如果类型已签名且 high 和 low 距离太远,则可能会溢出。
第二个公式mid = (high + low) / 2; 向0 舍入,但对于有符号和无符号类型的high 和/或low 的较大值可能会溢出。
在您的特定应用程序中,计算排序数组的中间元素的索引以执行二分搜索,索引值low 和high 是非负数和low <= high。有了这个约束,两个公式计算的结果相同,但是第二个可以溢出,而第一个不能。
因此,对于您的情况,您应该使用 mid = low + (high - low) / 2; 作为mid = (high + low) / 2; 的安全替代品。
在一般情况下,计算没有溢出的平均值(第二个公式)是一个棘手的问题。下面是一组平均公式的解决方案,以及一个受 chux 回答启发的测试程序。它们可以适用于任何有符号整数类型:
#include <limits.h>
#include <stdio.h>
#include <stdint.h>
int average_chqrlie(int a, int b) {
if (a <= b) {
if (a >= 0)
return a + ((b - a) >> 1);
if (b < 0)
return b - ((b - a) >> 1);
} else {
if (b >= 0)
return b + ((a - b) >> 1);
if (a < 0)
return a - ((a - b) >> 1);
}
return (a + b) / 2;
}
int average_chqrlie2(int a, int b) {
if (a > b) {
int tmp = a;
a = b;
b = tmp;
}
if (a >= 0)
return a + ((b - a) >> 1);
if (b < 0)
return b - ((b - a) >> 1);
return (a + b) / 2;
}
int average_chqrlie3(int a, int b) {
int half, mid;
if (a < b) {
half = (int)(((unsigned)b - (unsigned)a) / 2);
mid = a + half;
if (mid < 0)
mid = b - half;
} else {
half = (int)(((unsigned)a - (unsigned)b) / 2);
mid = b + half;
if (mid < 0)
mid = a - half;
}
return mid;
}
int average_chux(int a, int b) {
int avg = a / 2 + b / 2;
int small_sum = a % 2 + b % 2;
avg += small_sum / 2;
small_sum %= 2;
if (avg < 0) {
if (small_sum > 0)
avg++;
} else if (avg > 0) {
if (small_sum < 0)
avg--;
}
return avg;
}
int run_tests(const char *name, int (*fun)(int a, int b)) {
int array[] = { INT_MIN, INT_MIN+1, -100, -99, -2, -1, 0, 1, 2, 99, 100, INT_MAX-1, INT_MAX };
int n = sizeof(array) / sizeof(array[0]);
int status = 0;
printf("Testing %s:", name);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
int a = array[i], b = array[j];
intmax_t lavg = ((intmax_t)a + (intmax_t)b) / 2; // assuming sizeof(intmax_t) > size(int)
int avg = fun(a, b);
if (lavg != avg) {
printf("\na:%12d b:%12d average_wide:%12jd average:%12d", a, b, lavg, avg);
status = 1;
}
}
}
puts(status ? "\nFailed" : " Success");
return status;
}
int main() {
run_tests("average_chqrlie", average_chqrlie);
run_tests("average_chqrlie2", average_chqrlie2);
run_tests("average_chqrlie3", average_chqrlie3);
run_tests("average_chux", average_chux);
return 0;
}
【讨论】:
与第二个不同,第一个不会导致较大的低/高值溢出。总是首选使用mid = low + (high - low) / 2。
【讨论】: