【问题标题】:Extension of Binary search algo to find the first and last index of the key value to be searched in an array二分搜索算法的扩展,以查找要在数组中搜索的键值的第一个和最后一个索引
【发布时间】:2011-01-14 04:55:56
【问题描述】:

问题是扩展二分搜索算法,以最有效的方式在排序数组中查找目标值的所有出现。 具体来说,该算法的输入是(1)一个排序好的整数数组,其中一些数字可能出现多次,以及(2)要搜索的目标整数。算法的输出应该是一对索引值,指示数组中整数的第一次和最后一次出现(如果确实出现的话)。 源代码可以是c#、c、c++。

另外,我们可能需要查找索引的最大和最小比较次数是多少?

【问题讨论】:

  • 如果这是家庭作业,请将其标记为这样,并展示您到目前为止所获得的内容。
  • 您是否已经了解二分搜索,或者您也需要解释一下?
  • 是的,我了解二进制搜索算法。这不是家庭作业。它是课堂讨论的一部分。
  • ...这不是功课吗?
  • 好吧,把它当作作业吧!!

标签: c# c++ c algorithm binary-search


【解决方案1】:

我创建了两种二分搜索方法,分别返回第一个和最后一个匹配项。

public static void main(String[] args) {
    int a[] ={1,2,2,2,2,2,5,5,6,8,9,10};

    System.out.println(5+" first = "+first(a, 5, 0, a.length-1));
    System.out.println(5+" last = "+right(a, 5, 0, a.length-1));

    System.out.println(1+" first = "+first(a, 1, 0, a.length-1));
    System.out.println(1+" last = "+right(a, 1, 0, a.length-1));

    System.out.println(2+" first = "+first(a, 2, 0, a.length-1));
    System.out.println(2+" last = "+right(a, 2, 0, a.length-1));

    System.out.println(10+" first = "+first(a, 10, 0, a.length-1));
    System.out.println(10+" last = "+right(a, 10, 0, a.length-1));

    System.out.println(8+" first = "+first(a, 8, 0, a.length-1));
    System.out.println(8+" last = "+right(a, 8, 0, a.length-1));

    System.out.println(11+" first = "+first(a, 11, 0, a.length-1));
    System.out.println(11+" last = "+right(a, 11, 0, a.length-1));


}

private static int first(int [] a, int x, int l, int h){
    if(l>h){
        return -1;
    }
    int mid = (h-l)/2+l;
    if(a[mid] == x && (mid==0 || a[mid-1] != x) ){
        return mid;
    }else if(a[mid] == x){
        return first(a, x, l, mid-1);
    }else if(a[mid]>x){
        return first(a, x, l, mid-1);
    }else{
        return first(a, x, mid+1, h);
    }
}


private static int right(int [] a, int x, int l, int h){
    if(l>h){
        return -1;
    }
    int mid = (h-l)/2+l;
    if(a[mid] == x && (mid==a.length-1 || a[mid+1] != x) ){
        return mid;
    }else if(a[mid] == x){
        return right(a, x, mid+1, h);
    }else if(a[mid]>x){
        return right(a, x, l, mid-1);
    }else{
        return right(a, x, mid+1, h);
    }
}

Output:
    1 first = 0
    1 last = 0
    2 first = 1
    2 last = 5
    10 first = 11
    10 last = 11
    8 first = 9
    8 last = 9
    11 first = -1
    11 last = -1

【讨论】:

    【解决方案2】:

    我想正常的算法会有这样的东西:

    if(value == test) return;
    if(value < test) min = i;
    if(value > test) max = i;
    

    一旦您使用它来查找其中一个值,请使用您当前必须查找提示的最小值和最大值执行两个稍微模式化的二进制搜索。

    要找到最上面的替换为:

    if(value <= test) min = i;
    if(value > test) max = i;
    

    最底部的替换为:

    if(value >= test) max = i;
    if(value < test) min = i;
    

    请注意,使用此方法不会提前返回,您只需继续直到 min 和 max 相隔一个或某个值,我想您可以通过另一个检查添加一个

    if(value == test and arr[i-1] != test) return;
    

    等等

    【讨论】:

    • 感谢您的建议.....我正在使用以下方法:BinarySearch(A[0..N-1], value, low, high) { if (high value) return BinarySearch(A, value, low, mid-1) else if (A[mid]
    【解决方案3】:

    问题中最有效的部分没有明确的答案。这将取决于预期有多少具有相同值的条目。如果它是一些在找到一个元素后在数组的两个方向上的线性搜索将是你最快的选择,但如果你期望有很多具有相同值的条目,你可以做一种二进制搜索来找到开始结束索引。

    免责声明:未经测试;它是为了展示这个想法,而不是直接用作生产代码

    int org = binarySearch(array,value) //do the binary search and find on element
    int min = org-delta; //delta is some constant based on how many elemts are to be expected
    int max = org;
    min = min < 0 ? 0 : min;
    int search= min;
    bool latestWasHit = false;
    while(search > 0)
    {
      if(search+1 == max)
         return max;
      if(array[search] != value)
      {
         min = search;
         search = search + (max-search)/2
      }
      else
      {
         max = search;
         search = (search-min)/2;
      } 
    }
    

    然后是上限的反向。但是,它需要相当多的元素才能比简单的线性搜索更快。

    【讨论】:

      【解决方案4】:

      您可以在 Bentley Programming Pearls 和 Knuth 的 Vol.3:排序和搜索中找到关于此的讨论。

      这是 C++ 中的一种实现:http://the-algo-blog.blogspot.com/2011/06/binary-search-to-find-last-and-first.html

      【讨论】:

        【解决方案5】:

        这很容易做到,无需编写自己的二分搜索算法,只需重复调用标准算法即可。

        // some curly-bracket language:
        
        // int BinarySearch(sortedList, searchIndex, searchLength, valueToFind)
        // returns the zero-based index of the item in the list, or a negative value
        // if the item is not found
        
        int inner = BinarySearch(list, 0, listSize, value);
        if(inner < 0){
            // handle case where value is not found in list
        }
        
        int bottom = inner, top = inner;
        while(true){
            int i = BinarySearch(list, 0, bottom, value);
            if(i < 0)
                break;
            bottom = i;
        }
        while(true){
            int i = BinarySearch(list, top + 1, listSize - top - 1, value);
            if(i < 0)
                break;
            top = i;
        }
        
        // bottom and top now hold the bounds of all instances of value in list
        

        这与使用自定义算法获得的效率非常接近,只是您有更多的函数调用开销。

        至于比较的次数,我得想多想才能确定,但​​我认为它只是 2*log2N,其中 N 是列表。


        编辑

        呸!它不是 2*log2N,因为与您可以使用自定义算法执行的操作不同,它不会逐步排除列表的某些部分。 1 最大比较计数为 (log2N - 0.5) * log2N。对于具有 230 元素的列表,这仍然只有 885 次比较(对于 220 N 进行 390 次比较,对于 210 N 进行 95 次比较),但我们可以做得更好。

        // int Compare(a, b)
        // returns 0 if a and b are equal,
        //         a negative value if a < b, or
        //         a positive value if a > b
        
        int start = 0, end = listSize, inner;
        
        while(true){
            if(end == start){
                // handle case where value is not found in list
            }
            inner = (start + end) / 2;
            int cmp = Compare(list[inner], value);
            if(cmp == 0)
                break;
            if(cmp < 0)
                start = inner + 1;
            else end = inner;
        }
        
        int top = inner, bottom = inner;
        
        while(true){
            if(start >= bottom)
                break;
            inner = (start + bottom) / 2;
            int cmp = Compare(list[inner], value);
            if(cmp == 0)
                bottom = inner;
            else start = inner + 1;
        }
        
        while(true){
            if(end - 1 <= top)
                break;
            inner = (top + 1 + end) / 2;
            int cmp = Compare(list[inner], value);
            if(cmp == 0)
                top = inner;
            else end = inner;
        }
        

        这将最多进行 2*log2N 次比较。 230 项最多需要 60 次比较,220 项最多需要 40 次比较,以此类推。


        1 我通过实验确定了这一点。我不够聪明,无法从数学上算出来。

        【讨论】:

          【解决方案6】:

          如果你有点聪明,你可以定义两个不同的二分搜索函数。一个将返回搜索值的第一次出现的索引,另一个将返回搜索值的最后一次出现。根据您对二分搜索的了解,您应该能够确定最大和最小比较次数。

          在我看来,平均而言,使用两次二进制搜索应该是最快的方法。例如,如果您只使用一个二分搜索来查找第一项并随后进行线性搜索,那么最坏的情况是整个函数的值相同。对于长度为 10000 的数组,这将在最坏的情况下进行 10013 次比较,而使用两次二进制搜索在最坏的情况下对同一数组进行 28 次比较。当然,使用相同大小的数组,二分/线性搜索方法的最佳情况是 14 次比较,而两种二分搜索方法的最佳情况是 26 次比较。

          ** 更新

          好的,这是一个二分搜索,用于查找数组中元素的第一次出现。我会给你一个递归函数(你当然可以让它迭代并以其他方式优化它)。这将在 int 数组 a 中搜索 int val。另外,我没有仔细寻找中点(如果数组真的很大,可能会出现问题)。

          int bs1(int a[], int val, int left, int right)
          {
              if(right == left) return left;
              int mid = (right+left)/2;
          
              if(val > a[mid]) return bs1(a, val, mid+1, right);
              else return bs1(a, val, left, mid);
          }
          

          但是,您应该在返回一个索引后检查它实际上引用了正确的值,因为如果 val 不在数组中,则返回的索引将对应于下一个大于 val 的元素。

          对此进行一些小的更改将创建一个查找最后一个元素的函数。这样做的关键是正确使用比较器并记住整数除法总是会截断。

          【讨论】:

          • 你能否详细说明如何使用两个不同的二进制函数来查找第一个和最后一个出现,我的意思是我们如何计算它是第一个还是最后一个值。
          • 我更新了我的答案,给你一个二分搜索功能。现在您可以找到一个找到该值在数组中最后出现的位置。
          • 非常感谢您的帮助!!我假设最大没有。如果我进行 2 次二进制搜索来查找最小和最大索引值的比较次数是 log 2 到 base n 的 2 倍,最好的情况是 log 2 到 base n ......如果我在任何一个方面都错了,请纠正我case.where n 不是。元素。
          • 嗯.. 这不太对。最好的情况是 2*floor(log base 2 of N),最坏的情况是 2*ceil(log base 2 of N)。 floor 函数向下取整; ceil 函数向上取整。以长度为 3 的数组为例。如果我使用上面的二进制搜索函数,并且 if(val > a[mid]) 为真,那么我们找到了只有 1 个比较的索引,但如果它不是真的,则需要进行 2 次比较。这对应于上述最坏和最好的情况(虽然前面没有 2,因为我们只使用了一个二分搜索)。
          【解决方案7】:

          对于 C++,您可以查看 std::equal_range() 及其复杂性要求。只要您对基本算法感兴趣,无论实现时使用何种语言,都应适用相同的一般规则。

          【讨论】:

          • +1:该说的,该说的。
          • 具体来说,查看lower_bound 和upper_bound 的实现可以很好地了解如何正确执行此操作。
          猜你喜欢
          • 2013-02-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-04-06
          • 1970-01-01
          • 2010-11-06
          • 2013-01-03
          • 1970-01-01
          相关资源
          最近更新 更多