二分查找中的第一次出现

Posted

技术标签:

【中文标题】二分查找中的第一次出现【英文标题】:First occurrence in a binary search 【发布时间】:2011-10-04 07:37:24 【问题描述】:

我正在修改一些代码,然后我意识到了一些我从来不知道的事情。正常的二分搜索将在数据集中为多次出现的键返回随机索引。如何修改下面的代码以返回第一次出现?这是人们做的事情吗?

//ripped from the JDK
public static int binarySearchValue(InvertedContainer.InvertedIndex[] a, long key) 
    return bSearchVal(a, 0, a.length, key);


private static int bSearchVal(InvertedContainer.InvertedIndex[] a, int fromIndex,
                                 int toIndex, long key) 
    int low = fromIndex;
    int high = toIndex - 1;

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

        if (midVal < key)
            low = mid + 1;
        else if (midVal > key)
            high = mid - 1;
        else
            return mid; // key found
    
    return (low); // key not found. return insertion point

【问题讨论】:

酷 - 我要代表我自己的问题和答案...***.com/questions/4948162/… 解释了一种可以找到第一项 > 或 >= 或最后一项 哈,谢谢!我会看看。每隔一段时间我就会注意到这样的事情,我想“你什么都不知道”。 【参考方案1】:

Jon Skeets 帖子的补充:

潜在更快的实现实际上并不难实现,只添加了 2 行代码,这是我的做法:

    if (midVal < key)
        low = mid + 1;
    else if (midVal > key)
        high = mid - 1;
    else if (low != mid) //Equal but range is not fully scanned
        high = mid; //Set upper bound to current number and rescan
    else //Equal and full range is scanned
        return mid;

【讨论】:

你会如何改变它以适用于最后一个索引而不是第一个索引? 简单地颠倒高低(没有测试是否有效):.... else if (high != mid) low = mid; else return mid; 第一项:mid = (unsigned int) floor((low + high) / 2.0); 最后一项:mid = (unsigned int) ceil((low + high) / 2.0); @bezmax 我需要你的一点帮助...我想知道此代码是否比此链接中的代码运行得更快hackerearth.com/notes/searching-code-monk 请参阅第一次出现的代码 @VaibhavBajpai 在哪里/如何适应?【参考方案2】:

找到一个匹配值后,你基本上需要遍历集合,直到找到一个匹配的条目。

您可以潜在地通过立即获取低于您要查找的键的索引来使其更快,然后在两者之间进行二进制切割 - 但我可能会选择更简单的版本,这可能“足够高效”,除非您有大量相等的条目。

【讨论】:

嗨乔恩 - 所以我需要在 else 子句中使用 while 循环?我理解这个想法,但它看起来很笨拙,不是吗? 顺便说一句,我是忠实粉丝。感谢您到目前为止的回复。 @Amir:这当然是一种方法。另一种方法是保持原样(最好更改名称:),但提供 another 方法,该方法将找到第一个方法,方法是调用原始方法并 then 执行while 循环(如果没有匹配)。请注意,在我使用过的 API 中,插入点返回为负值(使用~)表示找不到该值,但同时也表示插入点。 此解决方案的时间复杂度为 O(N),因为您要搜索的值最多可以有 N 个元素。 @JuanMartinez:因此,答案末尾的“足够有效,除非你有大量相等的条目”。【参考方案3】:

如果您的数据都是完整的,那么这个 hack 可以提供帮助。 它使用一个浮点数组来存储值。

float array[];    //contains all integral values
int searchValue;

int firstIndex = -(binarySearch(array, (float)searchValue - 0.5F) + 1);

基本上,它的作用是在您的搜索值和它之前的整数之间找到一个值的插入索引。由于所有值都是整数,因此它会查找搜索值的第一个匹配项。

这也是 log(n) 时间。

示例:

import java.util.Arrays;

public class BinarySearch 
    // considering array elements are integers
    float ar[] = new float[]  1, 2, 3, 3, 4, 4, 5, 9, 9, 12, 12 ;

    public void returnFirstOccurrence(int key) 
        int firstIndex = -(Arrays.binarySearch(ar, key - 0.5F) + 1);
        if (ar[firstIndex] != key)
            System.out.println("Key doesn't exist");
        else
            System.out.println("First index of key is " + firstIndex);
    

    public static void main(String Args[]) throws Exception 
        new BinarySearch().returnFirstOccurrence(9);
    


输出:7

p.s:我在几次编码比赛中都使用过这个技巧,而且每次都很好用。

【讨论】:

你能解释一下吗?这如何获得第一个出现索引? java Collections 中的二进制搜索实现要么返回数字的索引,要么如果数字不存在,则返回可以插入数字的位置索引。见link。我还进行了编辑以包含一个示例。 知道了。这不仅是 hacky,而且是非常 hacky :) 不仅它仅适用于整数,而且您仍然希望 float[] 来保存它们。如果客户端最初有一个int[],那么创建一个新的float[] 可能需要一些成本。您可能想让这两个条件用粗体写 :) 正如我所说,我只在比赛中使用它。我是一名学生,没有任何行业经验,但我同意在生产代码中使用它是非常不合适且完全令人困惑的。 对我来说不,在生产中使用这种解决方案比在 int[] 上使用一些临时算法要好......如果你这样做一次会有 O(n) 转换成本但是如果您从一开始就将数组创建为 float[] 并在程序的生命周期内对其进行维护并多次运行搜索,则不会有显着的性能差异。【参考方案4】:

您可以实现“下限”算法而不是二进制搜索。该算法用于例如在C++/STL 中,它的Java 成绩单很简单。下界的算法复杂度也是 O(log n) 作为二分查找。这比首先使用二分搜索和线性搜索第一个匹配元素要好 - 这将具有最坏情况的行为 O(n)。

【讨论】:

虽然这在 C++ 库中被称为下限,但它仍然是二进制搜索——至少根据我的 Niklaus Wirth 的算法和数据结构(Modula 2 版)的旧副本。不过,也许这是一个见仁见智的问题,边界在“不同算法”和“同一算法的变体”之间。 许多(大多数?)库(例如 C、C++/STL、Java)实现的“二进制搜索”未指定在存在多个键时返回哪个索引。这也是提出问题中的问题。 “下限”准确地指定了结果。 同意,但是库函数的好名称不一定与算法教科书中的名称相同,尤其是当库可能有多个变体时。顺便说一句,我并不是要声称你对任何事情都错了,我赞成你的回答。【参考方案5】:

您可以通过更清晰的匹配定义来调整现有的搜索算法。您可以看出序列 1,3,5,5,5,9 中突出显示的 5 是第一个,因为它之前的数字 (3) 小于 5。所以如果 mid 落在数组元素等于键,如果 a[mid-1] 小于键,则仅将其视为匹配项,其他相等的数组元素被视为大于元素。现在你的算法变成了(在包括 Jon Skeet 的建议为插入点返回负数之后):

public static int binarySearch(int[] a, int key) 
    int low=0,high=a.length-1;
    while (low<=high) 
        int mid=(low+high) >>> 1;
        int midVal=a[mid];
        if (midVal < key) 
            low=mid+1;
        else if (mid>0 && a[mid-1]>=key) //we already know midval>=key here
            high=mid-1;
        else if (midVal==key) //found the 1st key 
             return mid;
        else
            return ~mid;      //found insertion point
    
    return ~(a.length);       //insertion point after everything

它使用了更多的比较,但在我的基准测试中比 Stev314 的版本更快,这可能是因为缓存效应。

【讨论】:

【参考方案6】:

以下算法使用大于或等于您的搜索关键字的关键字对第一个项目进行二进制搜索...

while (upperbound > lowerbound)

  testpos = lowerbound + ((upperbound-lowerbound) / 2);

  if (item[testpos] >= goal)
  
    //  new best-so-far
    upperbound = testpos;
  
  else
  
    lowerbound = testpos + 1;
  

这不是为 Java 编写的,我对此不太了解,因此可能需要稍作调整。请注意,边界是半开的(下界包含在内,上界不包含在内),这对于正确性很重要。

这可以适应其他类似的搜索 - 例如找到最后一个键

这是我之前的问答here稍作修改。

【讨论】:

虽然扩展很简单,但只想指出 OP 希望第一次出现“等于”,而不是“大于或等于”。【参考方案7】:

这里是解决方案,我发现使用二分搜索在排序数组中获得多次出现的键的较低索引。

int lowerBound(int[] array,int fromIndex, int toIndex, int key)

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

我们必须在此代码中稍作更改以使用二进制搜索来处理上限,所以这里是代码的工作副本。

 int upperBound(int[] array,int fromIndex, int toIndex, int key)

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

【讨论】:

【参考方案8】:

一种方法是在整个二分搜索中保持一个不变量。在您的特定情况下,不变量将是:

array[low] &lt; key key &lt;= array[high]

然后你可以使用二分搜索来最小化低和高之间的差距。当low + 1 == highhigh 将是答案。 C++ 示例代码:

// check invariant on initial values.
if (array[low] >= key) return low;
if (array[high] < key) return high+1;
// low + 1 < high ensures high is at least low + 2, thus
// mid will always be different from low or high. It will
// stop when low + 1 == high.
while (low + 1 < high) 
  int mid = low + (high - low) / 2;
  if (array[mid] < key) 
    low = mid;   // invariant: array[low] < key
   else 
    high = mid;  // invariant: array[high] >= key
  

return high;

这与您的示例代码之间的主要区别是将lowhigh 更新为仅mid 而不是mid+1mid-1,因为我们已经检查了array[mid] 的值,我们可以保证更新边界时,不变量仍然成立。在开始搜索之前,您还需要检查初始值的不变量。

【讨论】:

【参考方案9】:

在这个thread 中,您可以找到二分查找的完整示例(递归版本),以及其他两个版本(基于原始版本),它们可以让您获得第一个索引和给定键的最后一个索引。

为方便起见,我添加了相关的 Junit 测试。

【讨论】:

【参考方案10】:

这是 scala 中解决方案的一种变体。使用模式匹配和递归而不是 while 循环来获得第一次出现。

def binarySearch(arr:Array[Int],key:Int):Int = 
     def binSearchHelper(lo:Int,hi:Int,mid:Int):Int = 
        if(lo > hi) -1 else 
            if(arr(mid) == key) mid else if(arr(mid) > key)
                binSearchHelper(lo,mid-1,lo + (((mid-1) - lo)/2))
            else
                binSearchHelper(mid+1,hi,(mid+1) + ((hi - (mid+1))/2))
            
        
     
    binSearchHelper(0,arr.size-1,(arr.size-1)/2)


def findFirstOccurrence(arr:Array[Int],key:Int):Int = 
    val startIdx = binarySearch(arr,key)
    startIdx match 
        case 0 => 0
        case -1 => -1
        case _ if startIdx > 0 => 
            if(arr(startIdx - 1) < key) startIdx else 
                    findFirstOccurrence(arr.slice(0,startIdx),key)
            
        
    

【讨论】:

【参考方案11】:

这应该可以解决问题

private static int bSearchVal(InvertedContainer.InvertedIndex[] a, int fromIndex,
                             int toIndex, long key) 
int low = fromIndex;
int high = toIndex - 1;
int result = low;
while (low <= high) 
    int mid = (low + high) >>> 1;
    long midVal = a[mid].val;

    if (midVal < key)
        low = mid + 1;
    else if (midVal > key)
        high = mid - 1;
    else
    
        result = mid;
        high = mid -1; 
    

return result; 

【讨论】:

【参考方案12】:

对于元素的最后一次出现:

static int elementExists(int input[], int element)
    int lo=0;
    int high = input.length-1;
    while(lo<high)
        int mid = (lo + high )/2;
        if(element >input[mid] )
            lo = mid+1;
        
        else if(element < input[mid])
            high= mid-1;
        
        else if (high != input.length-1) //Change for the Occurrence check
            lo = mid;
        else 
            return mid;
        
    
    return -1;

第一次出现:

else if (lo != mid)
        high = mid;

【讨论】:

【参考方案13】:

我认为一种更简单的方法是将最新的mid 索引,其中xs[mid] == key 存储到结果变量中,然后继续运行二进制搜索。

这是快速代码:

func first<T: Comparable>(xs: [T], key: T) -> Int 
    var lo = xs.startIndex
    var hi = xs.endIndex - 1
    var res = -1
    while lo <= hi 
        let mid = lo + (hi - lo) >> 1
        if xs[mid] == key  hi = mid - 1; res = mid 
        else if xs[mid] < key  lo = mid + 1
        else if xs[mid] > key  hi = mid - 1 
    

    return res

此外,如果您要找到键的最后一个索引,则需要进行非常小的更改(仅一行)。

func last<T: Comparable>(xs: [T], key: T) -> Int 
    var lo = xs.startIndex
    var hi = xs.endIndex - 1
    var res = -1
    while lo <= hi 
        let mid = lo + (hi - lo) >> 1
        if xs[mid] == key  lo = mid + 1;  res = mid 
        else if xs[mid] < key  lo = mid + 1
        else if xs[mid] > key  hi = mid - 1 
    

    return res

【讨论】:

【参考方案14】:

试试这个 javascript 递归解决方案。从某种意义上说它是最优的,它是 O(log(N))

function solve(A, e) 
  function solve (A, start, end, e, bestUntilNow) 
    if (start > end) 
      if (A[start] === e)
        return start
      return bestUntilNow
     else 
      const mid = start + Math.floor((end - start) / 2)
      if (A[mid] === e) 
        return solve(A, start, mid - 1, e, mid)
       else if (e < A[mid]) 
        return solve(A, start, mid - 1, e, bestUntilNow)
       else 
        return solve(A, mid + 1, end, e, bestUntilNow)
      
    
  
  return solve(A, 0, A.length, e, -1)

【讨论】:

【参考方案15】:

给定一个带有可能重复元素的排序数组,如 [1,1,2,2,2,2,3],以下时间复杂度为 O(logn) 的代码查找元素第一次和最后一次出现的索引在给定的数组中。此方法基于递归 JS 二进制搜索实现,通过在最初匹配元素后与立即较低或立即较高的索引/元素进行比较(如下所示)。

// Find the first occurence of the value using binary search
function binarySearchFirstOccurence(arr, left, right, value) 
  let middle = Math.floor((left+right)/2);
  if (left > right) 
    return -1;
   else if (arr[middle] === value && (arr[middle-1] < value || middle === 0)) 
    return middle;
   else if (arr[middle] < value) 
    return binarySearchFirstOccurence(arr, middle + 1, right, value);
   else 
    // Going lower
    return binarySearchFirstOccurence(arr, left, middle - 1, value);
  


// Find the last occurence of the value using binary search
function binarySearchLastOccurence(arr, left, right, value) 
  let middle = Math.floor((left+right)/2);
  if (left > right) 
    return -1;
   else if (arr[middle] === value && (arr[middle+1] > value || middle === arr.length - 1)) 
    return middle;
   else if (arr[middle] > value) 
    return binarySearchLastOccurence(arr, left, middle - 1, value);
   else 
    // Going higher
    return binarySearchLastOccurence(arr, middle + 1, right, value);
  


function sortedFrequency(arr, value) 
  let left = 0;
  let right = arr.length -1;
  let first = binarySearchFirstOccurence(arr, left, right, value);
  let last = binarySearchLastOccurence(arr, left, right, value);
  if (first === -1 && last === -1) 
    return -1;
   else if (first === -1 && last !== -1) 
    return 1;
   else 
    return last-first+1;
  


let arr = [1,1,2,2,2,2,3];
console.log(sortedFrequency(arr, 3));

最好的 乔治

【讨论】:

以上是关于二分查找中的第一次出现的主要内容,如果未能解决你的问题,请参考以下文章

二分查找

代码题(12)— 二分查找

LintCode 14. 二分查找

二分查找(去哪儿校招题)

LintCode刷题---二分查找

二分查找大集合(妈妈再也不用担心我的二分查找了)