使用二进制搜索的多个键的最后索引?

Posted

技术标签:

【中文标题】使用二进制搜索的多个键的最后索引?【英文标题】:Last index of multiple keys using binary-search? 【发布时间】:2013-01-03 03:16:29 【问题描述】:

我在排序数组中有多次出现的键,我想对它们执行二进制搜索,正常的二进制搜索会为多次出现的键返回一些随机索引,而我想要最后一次出现的索引那个键。

int data[] = [1,2,3,4,4,4,4,5,5,6,6];
int key = 4;
int index = upperBoundBinarySearch(data, 0, data.length-1, key);

Index Returned = 6

【问题讨论】:

Java和C++是不同的语言,你对哪一种感兴趣? int data[] = [1,2,3,4,4,4,4,5,5,6,6]; 在 Java 中是否正确?我不这么认为。 对于C++,有很多standard algorithms你可以试试。 一般同样的问题:first-occurrence-in-a-binary-search 是的,它的java语法,最好是java 【参考方案1】:

this answer 中的 Java 实现找到键的第一个项。有一个 comment 关于如何更改它以找到 last 的出现,但该建议会导致无限循环。不过,这个想法似乎很合理。

编辑:经过一番研究,我找到了neat solution on The Algo Blog。由于第一个找到的匹配不一定是需要的,因此您需要跟踪迄今为止的“最佳”匹配。当你得到一个匹配时,你存储它并在匹配的右边继续进行二分搜索 (low = mid + 1)。

public static int binarySearch(int[] a, int key) 
    return binarySearch(a, 0, a.length, key);


private static int binarySearch(int[] a, int fromIndex, int toIndex,
        int key) 
    int low = fromIndex;
    int high = toIndex - 1;
    int found = -1;

    while (low <= high) 
        int mid = (low + high) >>> 1;
        int midVal = a[mid];

        if (midVal < key) 
            low = mid + 1;
         else if (midVal > key) 
            high = mid - 1;
         else 
            found = mid;
            // For last occurrence:
            low = mid + 1;
            // For first occurrence:
            // high = mid - 1;
        
    
    return found;

此更改保留了 O(log n) 的复杂性。不过,实际性能取决于应用程序。当数组的长度远大于所寻找键的重复数量时,对最后一次出现的线性搜索可能会更快。但是当有很多重复时,这种修改后的二分搜索可能更可取。

【讨论】:

+1 的想法,对最后一行代码的轻微修改可以返回所谓的插入点:return (found != - 1) ?找到:-(low + 1);【参考方案2】:

大概你想要一个 O(log N) 解决方案? (否则你可以做一个线性搜索。)

在 C++ 中,一种可能性(在几种可能性中)是使用std::upper_bound。这将为您提供一个大于您要求的第一个元素的迭代器,因此您需要检查前一个元素。这确实是 O(log N)

我不知道 Java 是否提供了这个标准库方法。但是,upper_bound 的伪代码在上面的链接中给出,应该很容易重新实现。

【讨论】:

是的,我尝试在 java 中转换该上界,但它总是给我上界索引+1,并且我的转换有一些问题,无论如何我在数组中有 10K 条目,我想要具有多次出现的相同键的上下索引,具有比线性更好的解决方案,二进制对我来说可以正常工作,因为它为我提供了至少 O(log2N) 解决方案【参考方案3】:

好吧,特别感谢@Mattias,这个算法听起来不错。无论如何,我已经用自己的方法完成了,这似乎我给出了更好的结果,但是如果有人可以帮助我衡量我的算法和@Mattias 的复杂性,或者任何人有更好的解决方案,欢迎...... . 无论如何,这是我为该问题找到的解决方案,

int upperBound(int[] array,int lo, int hi, int key)

    int low = lo-1, high = hi;
    while (low+1 != high)
    
        int mid = (low+high)>>>1;
        if (array[mid]> key) high=mid;
        else low=mid;
    
    int p = low;
    if ( p >= hi || array[p] != key )
        p=-1;//no key found
    return p;

这是第一次出现,我也更新了另一个类似的帖子First occurrence in a binary search

int lowerBound(int[] array,int lo, int hi, int key)

    int low = lo-1, high = hi;
    while (low+1 != high)
    
        int mid = (low+high)>>>1;
        if (array[mid]< key) low=mid;
        else high=mid;
    
    int p = high;
    if ( p >= hi || array[p] != key )
        p=-1;//no key found
    return p;

【讨论】:

所以基本上,你拿了Bentley's method from that article 并稍微重写它以找到最后一次出现而不是第一次出现?干得好,虽然我必须同意那篇文章的作者:我还发现该算法更棘手,更难理解。例如,我不知道何时以及如何最终检查“未找到密钥”。 是的,我从宾利找到了较低索引的算法,我只是稍微修改它以适应我的需要。是的,它有点棘手,在降低索引的情况下,它将最后一个匹配的值存储在循环中的高变量中,但最后它确保最后一个存储值实际上是匹配的,因为它在两种情况下都存储 >= @MattiasBuelens 需要进行最终检查。想想数组为空的情况。 当 x = [2, 3] 时,似乎 upperBound(x, 0, 1, 3) 失败了,不是吗?见:ideone.com/qoYsfB @DominicFarolino 实际上这里使用的变量名称令人困惑,它实际上不是这里数组的真正索引,而是这些是从 1 开始的下限和等于元素数量的上限在数组中。请参阅ideone.com/Y9zoxd 无论如何我在这里更新名称以避免误解。谢谢。【参考方案4】:

当你找到钥匙时。而不是返回它对数组进行顺序搜索以获取最后一个。这将是 O(N) 解决方案。

【讨论】:

您想要的复杂度是多少?你可以在里面玩。让我知道你的限制 @RizwanYasin 这是一个不错的解决方案。复杂度不是 O(n),而是 O(log n + k)【参考方案5】:

这是一个递归版本的二分搜索。 稍微调整一下这个版本将为您提供最后一个索引或第一个索引,而零工作量和相同的复杂度 O(log-n)。

原来的二分查找递归版本是这样的:

public static int binarySearch(List<Integer> a, int startIndex, int endIndex, int key) 
    int midIndex = (endIndex - startIndex)/2 + startIndex;
    if (a.get(midIndex) == key) // found!
        return midIndex;
    if (startIndex == endIndex || startIndex == endIndex - 1)
        return -1;
    else if (a.get(midIndex) > key) // Search in the left
        return binarySearch(a, 0, midIndex, key); 
    else if (a.get(midIndex) < key) // Search in the right
        return binarySearch(a, midIndex, endIndex, key);
    else
        return -1; // not found 

对第一个if语句稍作改动,就可以得到第一个索引:

public static int binarySearchLowIndex(List<Integer> a, int startIndex, int endIndex, int key) 
    int midIndex = (endIndex - startIndex)/2 + startIndex;
    if (a.get(midIndex) == key && a.get(midIndex - 1) != key) // found!
        return midIndex;
    if (startIndex == endIndex || startIndex == endIndex - 1)
        return -1;
    else if (a.get(midIndex) >= key) // Search in the left
        return binarySearchLowIndex(a, 0, midIndex, key); 
    else if (a.get(midIndex) < key) // Search in the right
        return binarySearchLowIndex(a, midIndex, endIndex, key);
    else
        return -1; // not found 

最后一个索引也是如此:

public static int binarySearchHighIndex(List<Integer> a, int startIndex, int endIndex, int key) 
    int midIndex = (endIndex - startIndex)/2 + startIndex;
    if (a.get(midIndex) == key **&& a.get(midIndex + 1) != key**) // found!
        return midIndex;
    if (startIndex == endIndex || startIndex == endIndex - 1)
        return -1;
    else if (a.get(midIndex) > key) // Search in the left
        return binarySearchHighIndex(a, 0, midIndex, key); 
    else if (a.get(midIndex) <= key) // Search in the right
        return binarySearchHighIndex(a, midIndex, endIndex, key);
    else
        return -1; // not found 

以下是一些测试示例(基于 Junit):

@Test
public void binarySearchTest() 
    assert(BinarySearch.binarySearch(Arrays.asList(5, 7, 7, 8, 8, 10), 0, 5, 5) == 0);


@Test
public void binarySearchLowIndexTest() 
    assert(BinarySearch.binarySearchLowIndex(Arrays.asList(5, 8, 8, 8, 8, 10), 0, 5, 8) == 1);


@Test
public void binarySearchHighIndexTest() 
    assert(BinarySearch.binarySearchHighIndex(Arrays.asList(5, 8, 8, 8, 8, 10), 0, 5, 8) == 4);

【讨论】:

【参考方案6】:

在二分搜索中,您将键与数组 data[i] 的元素进行比较。要获得最后一个匹配的索引,您应该更改比较函数,以便即使 key 等于 data[i] 并且也等于 data[i+1],它也会给出不等式。

int upperBoundBinarySearch(int data[],int start, int end, int key) 
  while(start < end) 
    int middle = start + (end-start)/2;
    if (data[middle] == key && (middle == end || data[middle+1] != key))
      return middle;
    if (data[middle] > key)
      end = middle;
    else 
      if (start == middle)
        return start;
      start = middle;
    
  
  return start;

【讨论】:

这是行不通的,甚至在大多数搜索框架中都不可能。

以上是关于使用二进制搜索的多个键的最后索引?的主要内容,如果未能解决你的问题,请参考以下文章

我的二进制搜索有效,但返回错误的索引

字符串数组中字符串(多个实例)的递归二进制搜索 - C#

如何在数据库索引中使用二进制搜索

使用C中的递归二进制搜索查找目标索引

如何使用递归创建二进制搜索

使用二进制搜索的 Java 前缀搜索