【问题标题】:Binary search and invariant relation二分查找和不变关系
【发布时间】:2025-12-15 08:20:03
【问题描述】:

我正在阅读这个post 并试图弄清楚我们如何确定二进制搜索的不变关系。具体来说,在他给出的两个例子中,为什么这两个不变量关系是不同的?有何不同?

部分 A[start]

另一个问题是,我可以简单地将框架更改为:

int binarySearchFramework(int A[], int n, int target) {
    int start = start index of array - 1;
    int end = length of the A;
    while (end - start > 1) {
        int mid = (end - start) / 2 + start;
        if (A[mid] == target) return mid;
        if (A[mid] < target) {
            end = mid;
        } else {
            start = mid;
        }
    }      
   //not found
   ...
}  

这个是不是不如帖子里提供的那个?

非常感谢!

【问题讨论】:

  • @chiastic-security 对不起,我已经添加了帖子链接

标签: algorithm search binary-search


【解决方案1】:

您可以选择不变量。这是从实践中学到的技能。即使有经验,它通常也涉及一些试验和错误。选一个。看看情况如何。寻找机会选择一个需要较少工作来维护的不同的。您选择的不变量会对代码的复杂性和/或效率产生重大影响。

二分查找中的不变量至少有四种合理的选择:

a[lo] <  target <  a[hi]
a[lo] <= target <  a[hi]
a[lo] <  target <= a[hi]
a[lo] <= target <= a[hi]

您通常会看到最后一个,因为它最容易解释,并且不涉及使用超出范围的数组索引进行棘手的初始化,其他人会这样做。

现在有理由使用像a[lo] &lt; target &lt;= 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;
}

注意此代码未经测试,但应该可以通过不变逻辑工作。

具有两个&lt;= 的不变量只会找到目标的一些 实例。你无法控制哪一个。

此不变量确实需要使用lo = -1 进行初始化。这增加了证明要求。您必须证明mid 永远不能设置为-1,这会导致超出范围的访问。幸运的是,这个证明并不难。

你引用的文章很差。它有几个错误和不一致之处。在别处寻找例子。 Programming Pearls 是个不错的选择。

您提议的更改是正确的,但可能会慢一些,因为它将只运行一次的测试替换为每次迭代运行一次的测试。

【讨论】:

    【解决方案2】:

    您的问题的答案是“什么是循环不变量”问题的答案。

    循环不变量的全部意义在于在循环终止之前、期间和(可能最重要的)之后提供有用的属性。例如,插入排序有一个循环不变量,即要排序的数组按从 1 个索引开始的范围的排序顺序(一个项目总是排序),并增长为整个数组。 这样做的用处是,如果它在循环开始之前为真,并且循环没有违反它,那么您可以正确推断出在循环执行之后整个数组都已排序。假设您没有弄乱终止条件,这不会违反循环不变量,因为不变量仅指整个数组的子数组,它可能是也可能不是整个数组。如果提前终止,子数组小于整个数组,但子数组保证按照不变量排序。

    你链接的帖子说的差不多,但如果作者真正解释更多关于他在说什么可能会更好。这篇文章似乎在寻求教导,但仍有许多未说的内容,即使只是为那些好奇或需要更多信息的人提供更深入信息的脚注。

    直接回答你的问题“为什么两个不变量不同”,答案是因为它们解决了两个不同的问题。

    您的链接中的几句话说明了这一点:

    • 我再次强调,不变的关系指导我们编码。
    • 找到问题的不变关系,一切就变得简单了。

    【讨论】:

    • 感谢您的回答。您的回答加深了我对不变关系的理解。您能否详细解释一下这两个问题之间的区别导致两种不同的不变关系?谢谢!
    • 第一个很容易解释。二分查找收敛的方式,start
    • 至于第二个,我真的不得不深入研究它是否真实,以及它是否是最佳选择。我希望它是谨慎选择的。至于“你如何选择一个”,我不得不同意@Gene。要么你知道,要么挑一个试试看。
    【解决方案3】:

    你写的

    部分A[start]

    但这显然是错误的,因为初始值应该是 start = 0,end = N-1(而不是 -1,N)。顺便说一句,对于链接中描述的情况(不同元素的数组),您不需要任何不变量。

    这将毫无问题且易于理解。

    int arr[] = {0,1,2,3,4,5,6,7};
    int N = sizeof (arr) / sizeof (arr[0]);
    int target = 4;
    
    int l = 0, r = N-1;
    while( l <= r ) {
        int mid = (l+r)>>1;
        if( arr[mid] == target )
            return mid;
        if( arr[mid] < target )
            l = mid + 1;
        else
            r = mid - 1;
    }
    return -1; // not found
    

    【讨论】:

    • 我已经添加了帖子链接。该帖子解释了为什么 start = -1 和 end = len(A)。你既不理解也不回答我的问题。
    • @fuiiii 不,你的问题在帖子的最后,我看到了。这就是为什么我试图证明链接的 binsearch 是不好的(即使对于世界上最简单的情况 - 排序的不同元素的数组,它看起来也很棘手)。