查找排序数组中大于目标的第一个元素

Posted

技术标签:

【中文标题】查找排序数组中大于目标的第一个元素【英文标题】:Find the first element in a sorted array that is greater than the target 【发布时间】:2011-09-27 02:14:15 【问题描述】:

在一般的二分搜索中,我们正在寻找出现在数组中的值。然而,有时我们需要找到大于或小于目标的第一个元素。

这是我丑陋的、不完整的解决方案:

// Assume all elements are positive, i.e., greater than zero
int bs (int[] a, int t) 
  int s = 0, e = a.length;
  int firstlarge = 1 << 30;
  int firstlargeindex = -1;
  while (s < e) 
    int m = (s + e) / 2;
    if (a[m] > t) 
      // how can I know a[m] is the first larger than
      if(a[m] < firstlarge) 
        firstlarge = a[m];
        firstlargeindex = m;
      
      e = m - 1; 
     else if (a[m] < /* something */) 
      // go to the right part
      // how can i know is the first less than  
    
  

对于这类问题有更优雅的解决方案吗?

【问题讨论】:

数组排序了吗?如果是则二分查找,如果不是则线性查找... 它是一个排序数组,因此我们可以进行二进制搜索。在上面的代码中,我使用比较来查找数组中的第一个更大或更小的元素 为什么不使用upper_bound c++ STL 【参考方案1】:

考虑这个问题的一种方法是考虑对数组的转换版本进行二进制搜索,其中数组已通过应用函数进行了修改

f(x) = 1 if x > target
       0 else

现在,我们的目标是找到该函数在值1 上的第一个位置。我们可以使用二进制搜索来做到这一点,如下所示:

int low = 0, high = numElems; // numElems is the size of the array i.e arr.size() 
while (low != high) 
    int mid = (low + high) / 2; // Or a fancy way to avoid int overflow
    if (arr[mid] <= target) 
        /* This index, and everything below it, must not be the first element
         * greater than what we're looking for because this element is no greater
         * than the element.
         */
        low = mid + 1;
    
    else 
        /* This element is at least as large as the element, so anything after it can't
         * be the first element that's at least as large.
         */
        high = mid;
    

/* Now, low and high both point to the element in question. */

要查看此算法是否正确,请考虑进行的每个比较。如果我们找到一个不大于目标元素的元素,那么它和它下面的所有东西都不可能匹配,所以不需要搜索那个区域。我们可以递归搜索右半部分。如果我们找到一个大于所讨论的元素的元素,那么它之后的任何东西也必须更大,所以它们不能是更大的 first 元素,所以我们不需要搜索他们。因此,中间元素是它可能出现的最后一个位置。

请注意,在每次迭代中,我们都会放弃至少一半的剩余元素。如果顶部分支执行,那么[low, (low + high) / 2]范围内的元素都被丢弃,导致我们丢失floor((low + high) / 2) - low + 1 &gt;= (low + high) / 2 - low = (high - low) / 2 elements

如果底部分支执行,则[(low + high) / 2 + 1, high] 范围内的元素都将被丢弃。这让我们失去了high - floor(low + high) / 2 + 1 &gt;= high - (low + high) / 2 = (high - low) / 2 elements

因此,我们最终会在此过程的 O(lg n) 次迭代中找到大于目标的第一个元素。

这是在数组 0 0 1 1 1 1 上运行的算法的踪迹。

最初,我们有

0 0 1 1 1 1
L = 0       H = 6

所以我们计算mid = (0 + 6) / 2 = 3,所以我们检查位置3的元素,它的值是1。由于1 &gt; 0,我们设置high = mid = 3。我们现在有

0 0 1
L     H

我们计算mid = (0 + 3) / 2 = 1,因此我们检查元素1。由于它的值为0 &lt;= 0,我们设置mid = low + 1 = 2。我们现在只剩下L = 2H = 3

0 0 1
    L H

现在,我们计算 mid = (2 + 3) / 2 = 2。索引2 处的元素是1,并且由于10,我们设置H = mid = 2,此时我们停止,实际上我们正在查看大于0 的第一个元素。

【讨论】:

能否请您运行一个解决方案示例,例如我们输入了 0,0,1,1,1,1。让我们找到第一个大于 0 的元素。 类似地,我们可以组合 func 来找到小于 t 的第一个元素: if(a[m]>=t) high = m-1;否则低 = m; 类似“找到最后一个小于target的元素” @SecureFish:只是补充一点:对于这个相反的问题,还需要调整mid的计算。由于除法和减法中的舍入效应相结合,无需修改即可获得负高值。这可以通过在此计算中更改为向上取整行为来解决,例如通过再次添加模 2 项。 如果我们做一个正常的二分查找,当array[mid]==target,也low=mid+1,那么low总是指向第一个大于target的元素,对吧?跨度> 【参考方案2】:

如果数组已排序,则可以使用std::upper_bound(假设n 是数组a[] 的大小):

int* p = std::upper_bound( a, a + n, x );
if( p == a + n )
     std::cout << "No element greater";
else
     std::cout << "The first element greater is " << *p
               << " at position " << p - a;

【讨论】:

【参考方案3】:

经过多年的算法教学,我解决二分搜索问题的方法是在元素上设置开始和结束,而不是在数组之外。这样我就可以感觉到正在发生的事情,一切都在控制之中,而不会对解决方案感到神奇。

解决二分搜索问题(以及许多其他基于循环的解决方案)的关键点是一组良好的不变量。选择正确的不变量使解决问题变得轻而易举。我花了很多年才掌握不变的概念,虽然我多年前在大学里第一次学会了它。

即使你想通过在数组之外选择开始或结束来解决二分搜索问题,你仍然可以通过适当的不变量来实现它。话虽这么说,我的选择如上所述,总是在数组的第一个元素上设置开始,在数组的最后一个元素上结束。

总结一下,到目前为止我们有:

int start = 0; 
int end = a.length - 1; 

现在是不变量。我们现在拥有的数组是 [start, end]。我们对元素一无所知。它们都可能大于目标,或者都可能更小,或者一些更小,一些更大。所以到目前为止,我们无法对这些元素做出任何假设。我们的目标是找到大于目标的第一个元素。所以我们像这样选择不变量

结束右侧的任何元素都大于目标。 任何 开始左侧的元素小于或等于 目标。

我们可以很容易地看到我们的不变量在开始时是正确的(即在进入任何循环之前)。开始左边的所有元素(基本上没有元素)都小于或等于目标,结束的原因相同。

有了这个不变量,当循环结束时,end 之后的第一个元素将是答案(还记得 end 的右侧都大于目标的不变量吗?)。所以answer = end + 1

另外,我们需要注意,当循环结束时,开始会比结束多一。即 start = end + 1。所以等价地我们可以说 start 也是答案(不变的是 start 左边的任何东西都小于或等于目标,所以 start 本身是第一个大于目标的元素) .

说了这么多,代码如下。

public static int find(int a[], int target) 
    int st = 0; 
    int end = a.length - 1; 
    while(st <= end) 
        int mid = (st + end) / 2;   // or elegant way of st + (end - st) / 2; 
        if (a[mid] <= target) 
            st = mid + 1; 
         else  // mid > target
            end = mid - 1; 
        
    
    return st; // or return end + 1

关于这种解决二分搜索问题的方法的一些额外说明:

这种类型的解决方案总是将子数组的大小至少缩小1。这在代码中很明显。新的开始或结束是中间的+1-1。我更喜欢这种方法,而不是在两侧或一侧都包含中间,然后再解释为什么算法是正确的。这样一来,它就更具体、更无错误。

while 循环的条件是st &lt;= end。不是st &lt; end。这意味着进入 while 循环的最小大小是大小为 1 的数组。这完全符合我们的预期。在解决二分搜索问题的其他方法中,有时最小大小是大小为 2 的数组(如果是 st &lt; end),老实说,我发现始终处理包括大小 1 在内的所有数组大小要容易得多。

所以希望这可以澄清这个问题和许多其他二进制搜索问题的解决方案。将此解决方案视为一种专业地理解和解决更多二分搜索问题的方法,而无需担心算法是否适用于边缘情况。

【讨论】:

感谢您使用不变量来解释它!让一切变得更加清晰。 @apadana 如果您想找到与目标相同的第一个元素,您将如何处理开始和结束?您需要一种方法来指示该元素不在数组中,这通常使用 -1 来完成(但并非必须如此)。 @Seth 像这样定义不变量:开始左侧的任何东西都小于目标。 (所以 end 右边的任何东西都大于或等于)。当循环结束时, end + 1 将在等于或大于目标的第一个元素上。在循环之后,您可以检查“end + 1”是否等于目标。如果不返回 -1。 @Seth 我也有这个关于二分搜索的答案:***.com/a/54374256/3769451,它更清楚地说明了谓词。请记住,在二进制搜索问题中,目标是找到 0 和 1 之间的边界。如果您可以考虑分配 0 和 1(换句话说,一个为每个元素赋予 0 或 1 的好谓词),那么解决二分搜索问题将非常容易。在你提到的问题中,小于目标的所有东西都是0,大于或等于目标的东西都是1。所以目标是找到第一个1。[0 0 0 0 1 1 1]。希望这能澄清这一点。【参考方案4】:

下面的递归方法怎么样:

public static int minElementGreaterThanOrEqualToKey(int A[], int key,
    int imin, int imax) 

    // Return -1 if the maximum value is less than the minimum or if the key
    // is great than the maximum
    if (imax < imin || key > A[imax])
        return -1;

    // Return the first element of the array if that element is greater than
    // or equal to the key.
    if (key < A[imin])
        return imin;

    // When the minimum and maximum values become equal, we have located the element. 
    if (imax == imin)
        return imax;

    else 
        // calculate midpoint to cut set in half, avoiding integer overflow
        int imid = imin + ((imax - imin) / 2);

        // if key is in upper subset, then recursively search in that subset
        if (A[imid] < key)
            return minElementGreaterThanOrEqualToKey(A, key, imid + 1, imax);

        // if key is in lower subset, then recursively search in that subset
        else
            return minElementGreaterThanOrEqualToKey(A, key, imin, imid);
    

【讨论】:

【参考方案5】:
public static int search(int target, int[] arr) 
        if (arr == null || arr.length == 0)
            return -1;
        int lower = 0, higher = arr.length - 1, last = -1;
        while (lower <= higher) 
            int mid = lower + (higher - lower) / 2;
            if (target == arr[mid]) 
                last = mid;
                lower = mid + 1;
             else if (target < arr[mid]) 
                higher = mid - 1;
             else 
                lower = mid + 1;
       
    
    return (last > -1 && last < arr.length - 1) ? last + 1 : -1;

如果我们找到target == arr[mid],那么之前的任何元素都将小于或等于目标。因此,下边界设置为lower=mid+1。此外,last 是“目标”的最后一个索引。最后,我们返回last+1 - 处理边界条件。

【讨论】:

【参考方案6】:

我的实现使用条件bottom &lt;= top,这与answer templatetypedef 不同。

int FirstElementGreaterThan(int n, const vector<int>& values) 
  int B = 0, T = values.size() - 1, M = 0;
  while (B <= T)  // B strictly increases, T strictly decreases
    M = B + (T - B) / 2;
    if (values[M] <= n)  // all values at or before M are not the target
      B = M + 1;
     else 
      T = M - 1;// search for other elements before M
    
  
  return T + 1;

【讨论】:

【参考方案7】:

这里是 JAVA 中经过修改的二分搜索代码,时间复杂度 O(logn)

返回要搜索的元素的索引 如果元素存在 如果数组中不存在搜索的元素,则返回下一个更大元素的索引 如果搜索到的元素大于数组的最大元素,则返回 -1
public static int search(int arr[],int key) 
    int low=0,high=arr.length,mid=-1;
    boolean flag=false;
    
    while(low<high) 
        mid=(low+high)/2;
        if(arr[mid]==key) 
            flag=true;
            break;
         else if(arr[mid]<key) 
            low=mid+1;
         else 
            high=mid;
        
    
    if(flag) 
        return mid;
    
    else 
        if(low>=arr.length)
            return -1;
        else
        return low;
        //high will give next smaller
    


public static void main(String args[]) throws IOException 
    BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
    //int n=Integer.parseInt(br.readLine());
    int arr[]=12,15,54,221,712;
    int key=71;
    System.out.println(search(arr,key));
    br.close();

【讨论】:

【参考方案8】: kind =0:完全匹配 kind=1 : 比 x 更好 kind=-1 : 比 x 小一点; 如果未找到匹配项,则返回 -1
#include <iostream>
#include <algorithm>

using namespace std;


int g(int arr[], int l , int r, int x, int kind)
    switch(kind)
    case 0: // for exact match
        if(arr[l] == x) return l;
        else if(arr[r] == x) return r;
        else return -1;
        break;
    case 1: // for just greater than x
        if(arr[l]>=x) return l;
        else if(arr[r]>=x) return r;
        else return -1;
        break;
    case -1: // for just smaller than x
        if(arr[r]<=x) return r;
        else if(arr[l] <= x) return l;
        else return -1;
        break;
    default:
        cout <<"please give "kind" as 0, -1, 1 only" << ednl;
    


int f(int arr[], int n, int l, int r, int x, int kind)
    if(l==r) return l;
    if(l>r) return -1;
    int m = l+(r-l)/2;
    while(m>l)
        if(arr[m] == x) return m;
        if(arr[m] > x) r = m;
        if(arr[m] < x) l = m;
        m = l+(r-l)/2;
    
    int pos = g(arr, l, r, x, kind);
    return pos;


int main()

    int arr[] = 1,2,3,5,8,14, 22, 44, 55;
    int n = sizeof(arr)/sizeof(arr[0]);
    sort(arr, arr+n);
    int tcs;
    cin >> tcs;
    while(tcs--)
        int l = 0, r = n-1, x = 88, kind = -1; // you can modify these values
        cin >> x;
        int pos = f(arr, n, l, r, x, kind);
        // kind =0: exact match, kind=1: just grater than x, kind=-1: just smaller than x;
        cout <<"position"<< pos << " Value ";
        if(pos >= 0) cout << arr[pos];
        cout << endl;
    
    return 0;

【讨论】:

以上是关于查找排序数组中大于目标的第一个元素的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode习题——在排序数组中查找元素的第一个和最后一个位置(二分查找)

1536. 在排序数组中查找元素的第一个和最后一个位置

[LeetCode]34. 在排序数组中查找元素的第一个和最后一个位置(二分)

在排序数组中查找元素的第一个和最后一个位置

[二分查找] 在排序数组中查找元素的第一个和最后一个位置

《LeetCode之每日一题》:90.在排序数组中查找元素的第一个和最后一个位置