【问题标题】:Binary search start or end is target二分查找开始或结束是目标
【发布时间】:2021-09-29 13:25:49
【问题描述】:

为什么当我看到二进制搜索的示例代码时,从来没有一个 if 语句来检查数组的开头或结尾是否是目标?

import java.util.Arrays;

public class App {

    public static int binary_search(int[] arr, int left, int right, int target) {
        if (left > right) {
            return -1;
        }

        int mid = (left + right) / 2;

        if (target == arr[mid]) {
            return mid;
        }

        if (target < arr[mid]) {
            return binary_search(arr, left, mid - 1, target);
        }

        return binary_search(arr, mid + 1, right, target);
    }

    public static void main(String[] args) {
        int[] arr = { 3, 2, 4, -1, 0, 1, 10, 20, 9, 7 };

        Arrays.sort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.println("Index: " + i + " value: " + arr[i]);
        }

        System.out.println(binary_search(arr, arr[0], arr.length - 1, -1));
    }
}

在此示例中,如果目标是 -1 或 20,则搜索将进入递归。但是它添加了一个 if 语句来检查目标是否是中间,那么为什么不再添加两个语句来检查它是左还是右呢?

【问题讨论】:

    标签: binary-search


    【解决方案1】:

    您似乎有这样的印象,“他们为 mid 添加了额外的检查,所以他们当然也应该为 start 和 end 添加额外的检查”。

    检查“目标是否居中?”实际上不仅仅是他们添加的优化。递归检查“mid”是二分搜索的全部

    当您有一个排序的元素数组时,二分搜索的工作方式如下:

    1. 将中间元素与目标进行比较
      1. 如果中间元素较小,则丢弃前半部分
      2. 如果中间元素较大,则丢弃后半部分
      3. 否则,我们找到了!
    2. 重复直到找到目标或没有更多元素为止。

    检查中间的行为是确定要继续搜索数组的哪一半的基础。

    现在,假设我们还添加了开始和结束检查。这对我们有什么好处?好吧,如果在任何时候目标恰好位于片段的开头或结尾,我们跳过几个步骤并稍早结束。这是一个可能的事件吗?

    对于一些元素的小玩具示例,是的,也许。

    对于包含数十亿条目的海量真实世界数据集?嗯,让我们考虑一下。为了简单起见,我们假设我们知道目标在数组中。

    我们从整个数组开始。第一个元素是目标吗?这种可能性是十亿分之一。不太可能。最后一个元素是目标吗?这种可能性也是十亿分之一。也不太可能。您浪费了两次额外的比较来加速一个极不可能的情况。

    我们将自己限制在上半场。我们再次做同样的事情。第一个元素是目标吗?可能不会,因为几率是十亿分之一。

    ...等等。

    数据集越大,开始/结束“优化”变得越无用。事实上,就(最大优化)比较而言,算法的每一步都有三个比较,而不是通常的一个。非常粗略的估计,这表明算法平均变慢了三倍。

    即使对于较小的数据集,它的用途也很可疑,因为它基本上变成了准线性搜索而不是二分搜索。是的,几率更高,但平均而言,我们可以预期在达到目标之前进行更多的比较。

    二分查找的重点是尽可能少地进行无用的比较来达到目标​​。添加更多不太可能成功的比较通常不是改善这种情况的方法。

    编辑:

    OP 发布的实现也可能会稍微混淆这个问题。该实现选择在目标和中间之间进行两次比较。一个更优化的实现将改为进行单个三向比较(即,将“>”、“=”或“compareTo 或 C++ 的 &lt;=&gt; 通常是这样工作的。

    【讨论】:

      【解决方案2】:

      BambooleanLogic 的回答是正确而全面的。我很好奇这种“优化”使二分搜索慢了多少,所以我写了一个简短的脚本来测试平均执行多少比较的变化:

      Given an array of integers 0, ... , N 
      do a binary search for every integer in the array,
      and count the total number of array accesses made.
      

      为了公平起见,我这样做是为了在检查arr[left] 与目标后,我们将左加 1,右加同样,这样每次比较都尽可能有用。你可以自己试试at Try it online

      结果:

      Binary search on size         10:   Standard         29  Optimized         43   Ratio  1.4828
      Binary search on size        100:   Standard        580  Optimized       1180   Ratio  2.0345
      Binary search on size       1000:   Standard       8987  Optimized      21247   Ratio  2.3642
      Binary search on size      10000:   Standard     123631  Optimized     311205   Ratio  2.5172
      Binary search on size     100000:   Standard    1568946  Optimized    4108630   Ratio  2.6187
      Binary search on size    1000000:   Standard   18951445  Optimized   51068017   Ratio  2.6947
      Binary search on size   10000000:   Standard  223222809  Optimized  610154319   Ratio  2.7334
      

      所以总比较似乎确实是标准数量的三倍,这意味着优化对于更大的数组变得越来越无用。我很好奇限制比率是否正好是 3。

      【讨论】:

      • 很高兴看到一些具体的数字来说明我原本纯理论的论点。 :) 但是,是的,我认为该比率始终严格小于,但渐近接近,恰好是三。除非有一些我想不出来的细节,否则比较的数量永远不会超过三倍,因为“优化”确实会导致搜索深度的微小统计下降,即结束搜索的少数“幸运案例”在“过早”的深度。随着 N 的增长,这种“运气”的重要性会缩小,但不会完全消失。
      【解决方案3】:

      startend 添加一些额外的检查以及 mid 值并不令人印象深刻。

      在任何算法设计中,主要关注的是围绕它的复杂性移动,无论是时间复杂性还是空间复杂性。大多数情况下,时间复杂度被视为更重要的方面

      要了解更多关于 Binary Search Algorithm 在不同用例中的信息,例如 -

      1. 如果数组不包含任何重复

      2. 如果在这种情况下 Array 有重复元素 -

        a) 返回最左边的索引/值

        b) 返回最右边的索引/值

      还有很多点

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-10-18
        • 2023-04-02
        • 2016-07-22
        • 1970-01-01
        • 1970-01-01
        • 2023-03-30
        • 1970-01-01
        • 2015-08-29
        相关资源
        最近更新 更多