给定一个双调数组和数组中的元素 x,在 2log(n) 时间内找到 x 的索引

Posted

技术标签:

【中文标题】给定一个双调数组和数组中的元素 x,在 2log(n) 时间内找到 x 的索引【英文标题】:Given a bitonic array and element x in the array, find the index of x in 2log(n) time 【发布时间】:2013-10-22 18:25:51 【问题描述】:

首先,这个问题的双调数组被定义为这样一个数组,对于长度为N的数组中的某个索引K,其中0 < K < N - 1和0到K是一个单调递增的整数序列,而K到N - 1 是一个单调递减的整数序列。

例如:[1, 3, 4, 6, 9, 14, 11, 7, 2, -4, -9]。它从 1 单调增加到 14,然后从 14 减少到 -9。

这个问题的前兆是在3log(n)中解决它,这要容易得多。一次修改二分查找找到最大值的索引,然后二分查找分别为 0 到 K 和 K + 1 到 N - 1。

我认为2log(n) 中的解决方案要求您在不找到最大值索引的情况下解决问题。我曾考虑过重叠二进制搜索,但除此之外,我不确定如何继续前进。

【问题讨论】:

我认为一个可行的方法是进行两次二进制搜索,假设中间元素是最大值。然后,在两个二分搜索中,我们都包含一个约束来注意一些不一致,即在某些时候两半中的一个不是单调递增/递减。一旦找到,对它进行的二分搜索就会相应地进行调整。这里的问题是要注意什么。 但是,如果中间不是最大值,则两个二分搜索可能会收敛到一侧,这意味着一个将是多余的。在这种情况下,我们必须强制其中一个二进制搜索以另一种方式进行,直到它自行解决并且不会转向另一个二进制搜索。我想这就是如何去做。 如何在没有常数因子的情况下搜索最大值以精确记录 n? @JudgeMental flexaired.blogspot.com/2013/06/… 【参考方案1】:

不幸的是,其他答案(this 和 this)中提出的算法不正确,它们不是 O(logN)!

递归公式 f(L) = f(L/2) + log(L/2) + c 不会导致 f(L) = O(log(N)) 但会导致 f(L) = O((log(N))^2)

确实,假设 k = log(L),那么 log(2^(k-1)) + log(2^(k-2)) + ... + log(2^1) = log(2 )*(k-1 + k-2 + ... + 1) = O(k^2)。因此,log(L/2) + log(L/4) + ... + log(2) = O((log(L)^2))。

及时解决问题的正确方法~2log(N)如下进行(假设数组先升序后降序):

    取数组中间 比较中间元素和它的一个邻居,看看最大值是在右边还是在左边 将中间元素与所需值进行比较 如果中间元素小于所需值且最大值在左侧,则对左侧子数组进行双调搜索(我们确定该值不在右侧子数组中) 如果中间元素小于期望值且最大值在右侧,则对右侧子数组进行双调搜索 如果中间元素大于期望值,则对右子数组进行降序二分查找,对左子数组执行升序二分查找。

在最后一种情况下,对可能是双调的子数组进行二进制搜索可能会令人惊讶,但它确实有效,因为我们知道顺序不正确的元素都大于所需的值。例如,对数组 [2, 4, 5, 6, 9, 8, 7] 中的值 5 进行升序二分搜索将起作用,因为 7 和 8 大于所需的值 5。

这是一个完整的双音搜索及时实现(C++)~2logN

#include <iostream>

using namespace std;

const int N = 10;

void descending_binary_search(int (&array) [N], int left, int right, int value)

  // cout << "descending_binary_search: " << left << " " << right << endl;

  // empty interval
  if (left == right) 
    return;
  

  // look at the middle of the interval
  int mid = (right+left)/2;
  if (array[mid] == value) 
    cout << "value found" << endl;
    return;
  

  // interval is not splittable
  if (left+1 == right) 
    return;
  

  if (value < array[mid]) 
    descending_binary_search(array, mid+1, right, value);
  
  else 
    descending_binary_search(array, left, mid, value);
  


void ascending_binary_search(int (&array) [N], int left, int right, int value)

  // cout << "ascending_binary_search: " << left << " " << right << endl;

  // empty interval
  if (left == right) 
    return;
  

  // look at the middle of the interval
  int mid = (right+left)/2;
  if (array[mid] == value) 
    cout << "value found" << endl;
    return;
  

  // interval is not splittable
  if (left+1 == right) 
    return;
  

  if (value > array[mid]) 
    ascending_binary_search(array, mid+1, right, value);
  
  else 
    ascending_binary_search(array, left, mid, value);
  


void bitonic_search(int (&array) [N], int left, int right, int value)

  // cout << "bitonic_search: " << left << " " << right << endl;

  // empty interval
  if (left == right) 
    return;
  

  int mid = (right+left)/2;
  if (array[mid] == value) 
    cout << "value found" << endl;
    return;
  

  // not splittable interval
  if (left+1 == right) 
    return;
  

  if(array[mid] > array[mid-1]) 
    if (value > array[mid]) 
      return bitonic_search(array, mid+1, right, value);
    
    else 
      ascending_binary_search(array, left, mid, value);
      descending_binary_search(array, mid+1, right, value);
    
  

  else 
    if (value > array[mid]) 
      bitonic_search(array, left, mid, value);
    
    else 
      ascending_binary_search(array, left, mid, value);
      descending_binary_search(array, mid+1, right, value);
    
  


int main()

  int array[N] = 2, 3, 5, 7, 9, 11, 13, 4, 1, 0;
  int value = 4;

  int left = 0;
  int right = N;

  // print "value found" is the desired value is in the bitonic array
  bitonic_search(array, left, right, value);

  return 0;

【讨论】:

这里的“dichotomic_search”是指“二分搜索”吗? 绝对!我已经编辑了帖子并将“dichotomic_search”替换为“binary_search”以与解释保持一致。 你能解释一下你的重复关系吗? 二分查找的假设不成立?说整数数组[2, 4, 5, 6, 9, 8, 1] ? @Sohaib:只要值围绕搜索值进行分区,(自定义)二进制搜索就可以工作。我们确实不能使用标准一(std::binary_searchstd::lower_bound),因为它将是 UB。 【参考方案2】:

该算法通过结合双调和二分搜索递归地工作:

def bitonic_search (array, value, lo = 0, hi = array.length - 1)
  if array[lo] == value then return lo
  if array[hi] == value then return hi
  mid = (hi + lo) / 2
  if array[mid] == value then return mid
  if (mid > 0 & array[mid-1] < array[mid])
     | (mid < array.length-1 & array[mid+1] > array[mid]) then
    # max is to the right of mid
    bin = binary_search(array, value, low, mid-1)
    if bin != -1 then return bin
    return bitonic_search(array, value, mid+1, hi)
  else # max is to the left of mid
    bin = binary_search(array, value, mid+1, hi)
    if bin != -1 then return bin
    return bitonic_search(array, value, lo, mid-1)        

所以时间的递归公式是f(l) = f(l/2) + log(l/2) + c,其中log(l/2) 来自二进制搜索,c 是在函数体中进行比较的成本。

【讨论】:

我假设该方法以 lo 为第一个元素,hi 为最后一个元素? 我不熟悉双调搜索,所以我必须问:&amp;| 是二进制操作还是 &amp;&amp;|| 的简写? 是的,这些是标准的逻辑运算andor(这是伪代码)【参考方案3】:

提供的答案的时间复杂度为 (N/2)*logN。因为最坏的情况可能包括太多不必要的子搜索。一个修改是在搜索之前将目标值与子序列的左右元素进行比较。如果目标值不在单调级数的两端或小于双调级数的两端,则后续搜索是多余的。这种修改导致 2lgN 复杂度。

【讨论】:

【参考方案4】:

根据数组的最大元素在哪里,以及中间元素是否大于期望值,主要有5种情况

计算中间元素。 比较中间元素所需的值,如果它匹配搜索结束。否则继续下一步。

    比较中间元素和邻居,看看最大元素是在左边还是右边。如果两个邻居都小于中间元素,则数组中不存在元素,因此退出。(问题中提到的数组将首先遇到这种情况,因为最大元素 14 位于中间)

    如果中间元素小于期望值且最大元素在右侧,则在右侧子数组中进行双调搜索

    如果中间元素小于期望值且最大元素在左侧,则在左侧子数组中进行双调搜索

    如果中间元素大于期望值且最大元素在左侧,则在右侧子数组中进行降序二分查找

    如果中间元素大于期望值且最大元素在右侧,则在左侧子数组中进行升序二分查找

在最坏的情况下,每次将数组分成两半时,我们将进行两次比较,因此复杂度将是 2*logN

【讨论】:

【参考方案5】:
    public int FindLogarithmicGood(int value)
    
        int lo = 0;
        int hi = _bitonic.Length - 1;
        int mid;
        while (hi - lo > 1)
        
            mid = lo + ((hi - lo) / 2);
            if (value < _bitonic[mid])
            
                return DownSearch(lo, hi - lo + 1, mid, value);
            
            else
            
                if (_bitonic[mid] < _bitonic[mid + 1])
                    lo = mid;
                else
                    hi = mid;
            
        

        return _bitonic[hi] == value 
            ? hi
            : _bitonic[lo] == value 
                ? lo
                : -1;
    

DownSearch 在哪里

    public int DownSearch(int index, int count, int mid, int value)
    
        int result = BinarySearch(index, mid - index, value);
        if (result < 0)
            result = BinarySearch(mid, index + count - mid, value, false);
        return result;
    

BinarySearch 是

    /// <summary>
    /// Exactly log(n) on average and worst cases.
    /// Note: System.Array.BinarySerch uses 2*log(n) in the worst case.
    /// </summary>
    /// <returns>array index</returns>
    public int BinarySearch(int index, int count, int value, bool asc = true)
    
        if (index < 0 || count < 0)
            throw new ArgumentOutOfRangeException();
        if (_bitonic.Length < index + count)
            throw new ArgumentException();

        if (count == 0)
            return -1;

        // "lo minus one" trick
        int lo = index - 1;
        int hi = index + count - 1;
        int mid;
        while (hi - lo > 1)
        
            mid = lo + ((hi - lo) / 2);
            if ((asc && _bitonic[mid] < value) || (!asc && _bitonic[mid] > value))
                lo = mid;
            else
                hi = mid;
        

        return _bitonic[hi] == value ? hi : -1;
    

github

【讨论】:

【参考方案6】:

通过标准二分法查找一阶差分之间的符号变化,将采用2Lg(n) 数组访问。

使用称为斐波那契搜索的单峰函数最大值的搜索策略可以做得更好。在每个涉及单个查找的 n 步之后,您将间隔大小减小一个因子 Fn,对应于大约 Log n/Log φ ~ 1.44Lg(n) 访问以找到最大值。

当数组访问是昂贵的函数评估时,这种边际收益更有意义。

【讨论】:

【参考方案7】:

当谈到在 O(log N) 时间内搜索算法时,你必须只考虑二分搜索。 这里的概念是首先找到峰值点, 例如: Array = [1 3 5 6 7 12 6 4 2 ] -> 这里,12 是峰值。一旦检测到并标记为 mid,现在只需在 Array[0:mid] 和 Array[mid:len(Array)] 中进行二进制搜索。

注意:从 mid -> len 开始的第二个数组是一个降序数组,需要在二分查找中做一个小的变化。

用于寻找双调点 :-) [用 Python 编写]

start, end = 0, n-1
while start <= end:
    mid = start + end-start//2
    if (mid == 0 or arr[mid-1] < arr[mid]) and (mid==n-1 or arr[mid+1] < arr[mid]):
    return mid
    if mid > 0 and arr[mid-1] > arr[mid]:
        end = mid-1
    else:
        start = mid+1 

找到索引后,进行相应的二分搜索。呜拉...全部完成:-)

【讨论】:

【参考方案8】:

对于二分法,分三种情况:

    max item 在右边,然后是二分查找左边,bitoinc 查找右边。 max item 在左边,然后是二分查找右边,bitoinc 查找左边。 max item 正好在分割点,然后左右二进制。

注意:左右使用的二分查找因递增/递减顺序不同。

public static int bitonicSearch(int[] a, int lo, int hi, int key) 
    int mid = (lo + hi) / 2;
    int now = a[mid];
    if (now == key)
        return mid;
    // deal with edge cases
    int left = (mid == 0)? a[mid] : a[mid - 1];
    int right = (mid == a.length-1)? a[mid] : a[mid + 1];
    int leftResult, rightResult;
    if (left < now && now < right)  // max item is at right
        leftResult = binarySearchIncreasing(a, lo, mid - 1, key);
        if (leftResult != -1)
            return leftResult;
        return bitonicSearch(a, mid + 1, hi, key);
    
    else if (left > now && now > right)  // max item is at left
        rightResult = binarySearchDecreasing(a, mid + 1, hi, key);
        if (rightResult != -1)
            return rightResult;
        return bitonicSearch(a, lo, mid - 1, key);
    
    else  // max item stands at the split point exactly
        leftResult = binarySearchIncreasing(a, lo, mid - 1, key);
        if (leftResult != -1)
            return leftResult;
        return binarySearchDecreasing(a, mid + 1, hi, key);
    

【讨论】:

max item stands at the split point exactly - 误导性评论

以上是关于给定一个双调数组和数组中的元素 x,在 2log(n) 时间内找到 x 的索引的主要内容,如果未能解决你的问题,请参考以下文章

基于Batcher比较器的双调排序网络

给定一个集合,查找集合中一共多多少种不同的元素

Leetcode练习(Python):数组类:第54题:给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。

对给定数组的 XOR 查询

对于给定的n个元素的数组a[1..n] 要求从中找出第k小的元素,输出这个元素 pascal

双调巡游