【问题标题】:Binary Search: how to determine half of the array二分查找:如何确定数组的一半
【发布时间】:2020-09-13 23:08:53
【问题描述】:

这两个公式有什么区别

mid = low + (high - low) / 2;


mid = (high + low) / 2;

【问题讨论】:

  • 您可以通过单击分数下方的灰色复选标记来接受其中一个答案。

标签: c++ c binary-search


【解决方案1】:

在第二个版本中,如果high + low 大于int 的最大值(假设highint),那么它可能会溢出,调用未定义的行为。第一版解决了这个特殊的错误。

第一个版本仍然存在问题,例如如果low 是一个很大的负数,差值仍然会溢出。

从 c++20 开始,您应该为此使用 std::midpoint,它可以处理一大堆极端情况,并为所有情况做正确的事情。

这个看似简单的功能实际上实现起来却出奇的难,事实上,Marshall Clow 在 cppcon 2019 上给出的一个小时的talk 就涵盖了这个功能的实现。

【讨论】:

    【解决方案2】:

    第一个更胜一筹(虽然还不完美,见Binary Search: how to determine half of the array):

    1. 它适用于未为 highlow 定义添加但定义为向 low 添加间隔的情况。指针就是这样一个例子,日期类型的对象可以是另一个。

    2. high + low 可以溢出类型。对于有符号整数类型,行为未定义。

    【讨论】:

      【解决方案3】:

      两者都存在潜在的溢出问题。有符号整数溢出是未定义的行为 (UB)。

      使用 unsigned 数学(常用于数组索引),那么当 low <= high 时,low + (high - low) / 2; 不会像 (high + low) / 2 那样溢出。

      low <= high0 <= 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 &gt;= lowlow &gt;= 0,那么mid = low + (high - low) / 2;怎么会导致溢出呢?它应该为有符号和无符号类型完全定义。在二分查找中,这些变量是索引值,因此 >= 0`。
      • @chqrlie “……假设……导致溢出?” ---> 它不能与那些假设。使用已包含在答案中的有序 unsigned 值。针对其他常见受限情况修改了代码。一般情况从“为了避免任何溢出......”开始介绍。是的,high >= low >= 0 是很常见的,但是当 p 作为指针指向数组中间的某个位置时,p[some_negative] 是允许的。
      • @chqrlie IAC,看看执行强大的midpoint()-like 函数所需的代码似乎很有趣,因为存在各种极端情况。我的与 std::midpoint(a,b) 不同,因为它朝 0 舍入而不是朝 a 舍入。嗯,也许我应该叫我的average() 而不是midpoint()
      • 确实是一个有趣而棘手的问题。我找到了具有 2 或 3 个测试但更少和更简单的部门的替代实现。除以 2 通常编译为有效的移位代码,但向 0 截断需要一些额外的指令。
      【解决方案4】:

      两个公式不同:

      • 根据lowhigh 的值,两者都可能溢出。
      • 即使没有溢出,它们也不一定会产生相同的结果:第一个计算中点,第二个计算两个数字的平均值。

      在接下来的讨论中,我们假设lowmidhigh 具有相同的类型。我们正在寻找一种安全的方法来找到lowhigh 之间的中点或平均值,它始终在类型的范围内。

      第一个公式,mid = low + (high - low) / 2; 如果类型已签名,则向 low 舍入,如果类型已签名且 highlow 距离太远,则可能会溢出。

      第二个公式mid = (high + low) / 2;0 舍入,但对于有符号和无符号类型的high 和/或low 的较大值可能会溢出。

      在您的特定应用程序中,计算排序数组的中间元素的索引以执行二分搜索,索引值lowhigh 是非负数和low &lt;= 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;
      }
      

      【讨论】:

        【解决方案5】:

        与第二个不同,第一个不会导致较大的低/高值溢出。总是首选使用mid = low + (high - low) / 2

        【讨论】:

          猜你喜欢
          • 2018-12-17
          • 2023-03-12
          • 2010-09-19
          • 2020-08-30
          • 1970-01-01
          相关资源
          最近更新 更多