使用二分搜索查找多个条目

Posted

技术标签:

【中文标题】使用二分搜索查找多个条目【英文标题】:Finding multiple entries with binary search 【发布时间】:2012-08-22 02:34:58 【问题描述】:

我使用标准二分搜索快速返回排序列表中的单个对象(关于可排序属性)。

现在我需要修改搜索以便返回 ALL 匹配的列表条目。我应该如何做到最好?

【问题讨论】:

什么语言? “标准”二分搜索可能不同或有一些方便的重载。 @ColinD:我目前在 Java 中使用自己的实现。大约有十几行。 【参考方案1】:

好吧,由于列表已排序,您感兴趣的所有条目都是连续的。这意味着您需要找到与找到的项目相等的第一个项目,从二分搜索产生的索引向后看。最后一项也是如此。

您可以简单地从找到的索引向后退,但是如果有很多与找到的项相等的项目,这样解决方案可能会像 O(n) 一样慢。所以你应该更好地使用指数搜索:当你找到更多相等的项目时,你的跳跃加倍。这样你的整个搜索仍然是 O(log n)。

【讨论】:

为什么要后退而不是前进? @Muhammad:当然,你是对的:这同样适用于寻找上限 @Vlad 第二部分错误或缺失。当你的边缘在第 (n-2) 个元素时你会怎么做?您最后一次跳转 (n/2) 将失败(假设 n 为 2^x)。你现在要做什么?如果你扫描它是O(n)。如果您应用相同的技术,它将在 (n/4) 跳转时再次失败。等等……你能证明它是 O(log n) 吗?【参考方案2】:

首先让我们回忆一下幼稚的二分查找代码sn-p:

int bin_search(int arr[], int key, int low, int high)

    if (low > high)
        return -1;

    int mid = low + ((high - low) >> 1);

    if (arr[mid] == key) return mid;
    if (arr[mid] > key)
        return bin_search(arr, key, low, mid - 1);
    else
        return bin_search(arr, key, mid + 1, high);

引用斯基纳教授的话: 假设我们删除相等测试 if (s[middle] == key) 返回(中);从上面的实现中返回索引低 而不是每次不成功搜索时的 -1。现在所有搜索都将 不成功,因为没有平等测试。搜索将继续 每当将键与相同的数组进行比较时,右半部分 元素,最终终止于右边界。重复 反转二进制比较的方向后搜索将 引导我们到左边界。每次搜索都需要 O(lgn) 时间,所以我们可以 无论大小如何,都以对数时间计算出现次数 块。

所以,我们需要两轮binary_search来找到lower_bound(找到不小于KEY的第一个数字)和upper_bound(找到大于KEY的第一个数字)。

int lower_bound(int arr[], int key, int low, int high)

    if (low > high)
        //return -1;
        return low;

    int mid = low + ((high - low) >> 1);
    //if (arr[mid] == key) return mid;

    //Attention here, we go left for lower_bound when meeting equal values
    if (arr[mid] >= key) 
        return lower_bound(arr, key, low, mid - 1);
    else
        return lower_bound(arr, key, mid + 1, high);


int upper_bound(int arr[], int key, int low, int high)

    if (low > high)
        //return -1;
        return low;

    int mid = low + ((high - low) >> 1);
    //if (arr[mid] == key) return mid;

    //Attention here, we go right for upper_bound when meeting equal values
    if (arr[mid] > key) 
        return upper_bound(arr, key, low, mid - 1);
    else
        return upper_bound(arr, key, mid + 1, high);

希望对你有帮助:)

【讨论】:

以 10,000 的数组大小运行此代码时出现 ***Error。 我认为每个“high - 1”应该是“mid - 1”,每个“low + 1”应该是“mid + 1”。 效果很好!尽管我尝试了一个不包含您正在寻找的元素的数组(例如:尝试在数组 [1,1,3 ,4,5]。它将返回低 2 和高 5。所以我在 if (low > high) 之后添加了一个额外的检查,如果低的元素在数组的范围内 (>0 算法的时间复杂度是多少?【参考方案3】:

如果我在关注您的问题,您有一个对象列表,为了进行比较,这些对象看起来像 1,2,2,3,4,5,5,5,6,7,8,8,9。正常搜索 5 会找到比较为 5 的对象之一,但您想全部获取,对吗?

在这种情况下,我建议使用标准的二分搜索,在到达匹配元素时,开始向左看直到它停止匹配,然后再向右看(从第一个匹配)直到它停止匹配。

请注意,您使用的任何数据结构都不会覆盖相同的元素!

或者,考虑使用一种结构来存储与该位置的存储桶比较的元素。

【讨论】:

谢谢,我认为这样的事情会很有成效。但是代码变得非常丑陋。我希望有人有一个简短的递归解决方案或类似的东西...... :) 不需要丑。您应该有一个函数进行二分搜索并返回值的索引,然后进行线性搜索,获取初始索引以返回其余部分,可能递归调用自身进行左右搜索。【参考方案4】:

我会进行两次二分搜索,一次查找比较 >= 值的第一个元素(在 C++ 术语中,lower_bound),然后一次搜索比较 > 值的第一个元素(在 C++ 术语中,upper_bound)。从 lower_bound 到上界之前的元素就是您要查找的元素(根据 java.util.SortedSet、subset(key, key))。

因此,您需要对标准二分搜索进行两个不同的细微修改:您仍然进行探测并使用探测处的比较来缩小您要查找的值必须位于的区域,但现在例如对于 lower_bound 如果你达到相等,你所知道的是你正在寻找的元素(first 相等的值)在范围的第一个元素和你刚刚探测的值之间的某个位置 -你不能马上回来。

【讨论】:

这是正确的做法,函数下限/上限是许多库中实现的函数。【参考方案5】:

找到与 bsearch 匹配后,只需递归 bsearch 双方直到不再匹配

伪代码:

    range search (type *array) 
      int index = bsearch(array, 0, array.length-1);

      // left
      int upperBound = index -1;
      int i = upperBound;
      do 
         upperBound = i;
         i = bsearch(array, 0, upperBound);
       while (i != -1)

      // right
      int lowerBound = index + 1;
      int i = lowerBound;
      do 
         lowerBound = i;
         i = bsearch(array, lowerBound, array.length);
       while (i != -1)

      return range(lowerBound, UpperBound);

但没有涵盖任何极端情况。我认为这将使您的复杂性保持在 (O(logN))。

【讨论】:

【参考方案6】:

这取决于您使用的二进制搜索的实现:

在 Java 和 .NET 中,二分搜索会给你一个任意元素;您必须同时搜索两种方式才能获得所需的范围。 在 C++ 中,您可以使用 equal_range 方法在一次调用中产生您想要的结果。

为了加快在 Java 和 .NET 中搜索相等范围过长而无法进行线性迭代的情况,您可以查找前导元素和后继元素,并在找到的范围中间取值, 不包括结尾。

如果由于第二次二分搜索而太慢,请考虑编写自己的搜索,同时查找两端。这可能有点乏味,但它应该运行得更快。

【讨论】:

谢谢,我正在使用我自己的实现。通常,任何语言提供的实现都只返回一个元素。 @Gruber 不在 C++ 中,它返回整个范围。您可能想看看他们的实现,看看他们是如何做到的,它非常聪明,将equal_range 翻译成您选择的语言应该不会太难。【参考方案7】:

我首先找到给定可排序属性的单个元素的索引(使用“正常”二进制搜索),然后开始查看列表中元素的左右两侧,将找到的所有元素添加到满足搜索条件,当一个元素不满足条件或者没有元素可以遍历时,在一端停止,当左右两端都满足前面提到的停止条件时,完全停止。

【讨论】:

谢谢。似乎是共识。您知道这是否是既定的最佳做法吗? @Gruber 一方面,它是最容易实现的解决方案,以最少的修改重用现有算法。与发明和测试算法的新变体相比,这是一个巨大的优势,而且它的性能成本也可以忽略不计 如果您想保留现有的二进制搜索,您可以创建两个额外的数组,为每个元素提供其左右相等值的数量。使用这些作为复合键的一部分,您可以定位 (key, left(0)) 和 (key, right(0)) - 保存值键的第一个和最后一个元素。可能只有在你想要一个值和一个计数时才值得,因为我猜如果你必须读取所有值,那么左右移动以找到它们的成本相对较小。【参考方案8】:

Java 中的这段代码在 一次传递 中以 O(logN) 时间计算有序数组中目标值的出现次数。很容易修改它以返回找到的索引列表,只需传入 ArrayList。

想法是递归地细化eb 边界,直到它们成为具有目标值的连续块的上下边界;

static int countMatching(int[] arr, int b, int e, int target)
    int m = (b+e)/2;
    
    if(e-b<2)
        int count = 0;
        if(arr[b] == target)
            count++;
        
        if(arr[e] == target && b!=e)
            count++;
        
        return count;
    
    else if(arr[m] > target)
        return countMatching(arr,b,m-1, target);
    
    else if(arr[m] < target)
        return countMatching(arr, m+1, e, target);
    
    else 
        return countMatching(arr, b, m-1, target) + 1 
            + countMatching(arr, m+1, e, target);
    

【讨论】:

【参考方案9】:

你的二分搜索是返回元素还是元素所在的索引?你能得到索引吗?

由于列表已排序,所有匹配的元素都应该相邻出现。如果可以得到标准搜索中返回的项目的索引,则只需从该索引开始双向搜索,直到找到不匹配项。

【讨论】:

索引的使用是算法的核心,所以我明白了。谢谢。【参考方案10】:

试试这个。它的效果惊人。

工作示例,Click here

   var arr = [1, 1, 2, 3, "a", "a", "a", "b", "c"]; // It should be sorted array.
   // if it arr contain more than one keys than it will return an array indexes. 

   binarySearch(arr, "a", false);

   function binarySearch(array, key, caseInsensitive) 
       var keyArr = [];
       var len = array.length;
       var ub = (len - 1);
       var p = 0;
       var mid = 0;
       var lb = p;

       key = caseInsensitive && key && typeof key == "string" ? key.toLowerCase() : key;

       function isCaseInsensitive(caseInsensitive, element) 
           return caseInsensitive && element && typeof element == "string" ? element.toLowerCase() : element;
       
       while (lb <= ub) 
           mid = parseInt(lb + (ub - lb) / 2, 10);

           if (key === isCaseInsensitive(caseInsensitive, array[mid])) 
               keyArr.push(mid);
               if (keyArr.length > len) 
                   return keyArr;
                else if (key == isCaseInsensitive(caseInsensitive, array[mid + 1])) 
                   for (var i = 1; i < len; i++) 
                       if (key != isCaseInsensitive(caseInsensitive, array[mid + i])) 
                           break;
                        else 
                           keyArr.push(mid + i);

                       
                   
               
               if (keyArr.length > len) 
                   return keyArr;
                else if (key == isCaseInsensitive(caseInsensitive, array[mid - 1])) 
                   for (var i = 1; i < len; i++) 

                       if (key != isCaseInsensitive(caseInsensitive, array[mid - i])) 
                           break;
                        else 
                           keyArr.push(mid - i);
                       
                   
               
               return keyArr;

            else if (key > isCaseInsensitive(caseInsensitive, array[mid])) 
               lb = mid + 1;
            else 
               ub = mid - 1;
           
       

       return -1;
   

【讨论】:

【参考方案11】:

最近发现了非常有效的算法。 考虑到两个变量(输入大小和搜索键的数量),该算法具有对数时间复杂度。然而,搜索到的键也必须进行排序。

#define MIDDLE(left, right) ((left) + (((right) - (left)) >> 1))

int bs (const int *arr, int left, int right, int key, bool *found)

    int middle = MIDDLE(left, right);

    while (left <= right)
    
        if (key < arr[middle])
            right = middle - 1;
        else if (key == arr[middle]) 
            *found = true;
            return middle;
        
        else
            left = middle + 1;
        middle = MIDDLE(left, right);
    

    *found = false;
    /* left points to the position of first bigger element */
    return left;


static void _mkbs (const int *arr, int arr_l, int arr_r,
                   const int *keys, int keys_l, int keys_r, int *results)

    /* end condition */
    if (keys_r - keys_l < 0)
        return;

    int keys_middle = MIDDLE(keys_l, keys_r);

    /* throw away half of keys, if the key on keys_middle is out */
    if (keys[keys_middle] < arr[arr_l]) 
        _mkbs(arr, arr_l, arr_r, keys, keys_middle + 1, keys_r, results);
        return;
    
    if (keys[keys_middle] > arr[arr_r]) 
        _mkbs(arr, arr_l, arr_r, keys, keys_l, keys_middle - 1, results);
        return;
    

    bool found;
    int pos = bs(arr, arr_l, arr_r, keys[keys_middle], &found);

    if (found)
        results[keys_middle] = pos;

    _mkbs(arr, arr_l, pos - 1, keys, keys_l, keys_middle - 1, results);
    _mkbs(arr, (found) ? pos + 1 : pos, arr_r, keys, keys_middle + 1, keys_r, results);


void mkbs (const int *arr, int N, const int *keys, int M, int *results)
   _mkbs(arr, 0, N - 1, keys, 0, M - 1, results);   

以下是 C 语言的实现和打算发表的论文草稿: https://github.com/juliusmilan/multi_value_binary_search

你能分享一个用例吗?

【讨论】:

【参考方案12】:

您可以使用以下代码解决您的问题。这里的主要目的是首先找到密钥的下限,然后找到相同的上限。后来我们得到了指数的差异,我们得到了答案。与其拥有两个不同的函数,我们可以使用一个标志来查找同一个函数的上限和下限。

#include <iostream>
#include <bits/stdc++.h>
using namespace std;

int bin_search(int a[], int low, int high, int key, bool flag)
long long int mid,result=-1;
while(low<=high)
    mid = (low+high)/2;
    if(a[mid]<key)
        low = mid + 1;
    else if(a[mid]>key)
        high = mid - 1;
    else
        result = mid;
        if(flag)
            high=mid-1;//Go on searching towards left (lower indices)
        else
            low=mid+1;//Go on searching towards right (higher indices)
    

return result;


int main() 

int n,k,ctr,lowind,highind;
cin>>n>>k;
//k being the required number to find for
int a[n];
for(i=0;i<n;i++)
    cin>>a[i];

    sort(a,a+n);
    lowind = bin_search(a,0,n-1,k,true);
    if(lowind==-1)
        ctr=0;
    else
        highind = bin_search(a,0,n-1,k,false);
        ctr= highind - lowind +1;   

cout<<ctr<<endl;
return 0;

【讨论】:

【参考方案13】:

您可以进行两种搜索:一种用于范围之前的索引,另一种用于范围之后的索引。因为之前和之后可以重复 - 使用 float 作为“唯一”键”

    static int[] findFromTo(int[] arr, int key) 
    float beforeKey = (float) ((float) key - 0.2);
    float afterKey = (float) ((float) key + 0.2);
    int left = 0;
    int right = arr.length - 1;
    for (; left <= right;) 
        int mid = left + (right - left) / 2;
        float cur = (float) arr[mid];
        if (beforeKey < cur)
            right = mid - 1;
        else
            left = mid + 1;
    
    leftAfter = 0;
    right = arr.length - 1;
    for (; leftAfter <= right;) 
        int mid = left + (right - leftAfter) / 2;
        float cur = (float) arr[mid];
        if (afterKey < cur)
            right = mid - 1;
        else
            left = mid + 1;
    
    return new int[]  left, leftAfter ;

【讨论】:

以上是关于使用二分搜索查找多个条目的主要内容,如果未能解决你的问题,请参考以下文章

二分查找法(二分搜索法)

常见搜索算法:二分查找

poj3579 二分搜索+二分查找

LeetCode第3天 - 704. 二分查找 | 35. 搜索插入位置

挖掘算法中的数据结构:二分查找 和 二分搜索树(插入查找深度优先遍历)

算法复习笔记:二分查找