在循环有序数组中查找元素

28
我们希望在不超过 O(log n) 的时间复杂度内搜索循环排序数组中的一个元素。
例如:在 {5,9,13,1,3} 中搜索 13
我的想法是将循环数组转换为常规排序数组,然后在结果数组上执行二分查找,但是我遇到的问题是我想出的算法在最坏情况下需要 O(n) 的时间复杂度:
for(i = 1; i < a.length; i++){
    if (a[i] < a[i-1]){
        minIndex = i; break;
    }
}

那么第i个元素的对应索引将根据以下关系确定:

(i + minInex - 1) % a.length

很明显,我的转换(从圆形到常规)算法可能需要O(n)的时间,因此我们需要一个更好的算法。

根据ire_and_curses的想法,这是Java中的解决方案:

public int circularArraySearch(int[] a, int low, int high, int x){
    //instead of using the division op. (which surprisingly fails on big numbers)
    //we will use the unsigned right shift to get the average
    int mid = (low + high) >>> 1;
    if(a[mid] == x){
        return mid;
    }
    //a variable to indicate which half is sorted
    //1 for left, 2 for right
    int sortedHalf = 0;
    if(a[low] <= a[mid]){
        //the left half is sorted
        sortedHalf = 1;
        if(x <= a[mid] && x >= a[low]){
            //the element is in this half
            return binarySearch(a, low, mid, x);
        }
    }
    if(a[mid] <= a[high]){
        //the right half is sorted
        sortedHalf = 2;
        if(x >= a[mid] && x<= a[high] ){
            return binarySearch(a, mid, high, x);
        }
    }
    // repeat the process on the unsorted half
    if(sortedHalf == 1){
        //left is sorted, repeat the process on the right one
        return circularArraySearch(a, mid, high, x);
    }else{
        //right is sorted, repeat the process on the left
        return circularArraySearch(a, low, mid, x);
    }
}

希望这会有效。


你应该澄清一下你是否事先知道循环数组的起始位置。通常在实际应用中,你会知道。 - Christopher Barber
不,我不知道循环数组从哪里开始,如果我知道的话,我就不需要转换算法,而是直接应用上述关系并进行二分查找。 - guirgis
你需要知道元素是否不同。否则最坏情况是Omega(n)。 - Aryabhatta
2
请参见https://dev59.com/nXE85IYBdhLWcg3wZymt。 - starblue
鉴于问题的限制,它从未说明数组已排序,也没有给出任何部分排序的建议。对数组进行排序会将您最多降至n时间(具有额外空间复杂度的计数排序),对于大多数情况而言是nlgn时间。该数组可能完全未排序,使二进制搜索方案无法使用(对于lgn时间)。请参见https://stackoverflow.com/a/7694019/2812818。 - Droid Teahouse
16个回答

57

你可以利用数组已经排序的事实,除了特殊情况:基准值及其相邻元素。

  • 找到数组a的中间值。
  • 如果a[0] < a[mid],那么数组前半部分的所有值都是有序的。
  • 如果a[mid] < a[last],那么数组后半部分的所有值都是有序的。
  • 取出有序的那一半,检查你的值是否在其中(与该半部分的最大索引进行比较)。
  • 如果是,则只需在该半部分进行二分搜索。
  • 如果不是,则它必须在无序的那一半中。取出那一半并重复此过程,确定那一半的哪一半是有序的,依此类推。

5
+1 非常好。我相信这就是面试官想要的答案。这个答案利用了数组中任何一段至少有一半是有序的事实,因此我们可以使用它来决定继续哪一半,从而每一步将搜索空间减少2,正如所需的那样。 - Eyal Schneider
2
需要不同的元素,否则算法会失败。奇怪的是,我们可以基于渐近运行时间给出证明! - Aryabhatta

9

虽然不太优雅,但是我能想到的方法是使用二分查找来找到旋转数组的中心点,然后再进行一次二分查找,补偿中心点的偏移量。这样做可能有些愚蠢,因为需要进行两次完整的查找,但是它确实满足条件,因为O(log n) + O(log n) == O(log n)。保持简单和愚蠢(tm)!


2
+1:简单就是好。可能同时完成两个任务;查找包装点需要测试当前低位和高位索引的顺序值是否被颠倒,如果没有,则在没有包装点的段中,如果(!)所寻找的值在此段中,则可以使用常规二分搜索进行处理;如果该数字不在此段内或该段内有一个包装点,则将其分割并重复;这样,您要么找到包装点,要么将其缩小为不需查找包装点的常规二分搜索。 - Unreason
1
@Unreason - 你刚才在我的答案中描述了算法。 ;) - ire_and_curses

7

这是一个在Java中可行的示例。由于这是一个排序数组,因此您可以利用它并运行二分查找,但需要稍微修改以适应枢轴的位置。

该方法如下:

private static int circularBinSearch ( int key, int low, int high )
{
    if (low > high)
    {
        return -1; // not found
    }

    int mid = (low + high) / 2;
    steps++;

    if (A[mid] == key)
    {
        return mid;
    }
    else if (key < A[mid])
    {
        return ((A[low] <= A[mid]) && (A[low] > key)) ?
               circularBinSearch(key, mid + 1, high) :
               circularBinSearch(key, low, mid - 1);
    }
    else // key > A[mid]
    {
        return ((A[mid] <= A[high]) && (key > A[high])) ?
               circularBinSearch(key, low, mid - 1) :
               circularBinSearch(key, mid + 1, high);
    }
}

现在为了消除任何疑虑,这里有一个很棒的小类来验证算法:
public class CircularSortedArray
{
    public static final int[] A = {23, 27, 29, 31, 37, 43, 49, 56, 64, 78, 
                                   91, 99, 1, 4, 11, 14, 15, 17, 19};
    static int steps;

    // ---- Private methods ------------------------------------------

    private static int circularBinSearch ( int key, int low, int high )
    {
        ... copy from above ...
    }

    private static void find ( int key )
    {
        steps = 0;
        int index = circularBinSearch(key, 0, A.length-1);
        System.out.printf("key %4d found at index %2d in %d steps\n",
                          key, index, steps);
    }

    // ---- Static main -----------------------------------------------

    public static void main ( String[] args )
    {
        System.out.println("A = " + Arrays.toString(A));
        find(44);   // should not be found
        find(230);
        find(-123);

        for (int key: A)  // should be found at pos 0..18
        {
            find(key);
        }
    }
}

这将为您提供以下输出:

A = [23, 27, 29, 31, 37, 43, 49, 56, 64, 78, 91, 99, 1, 4, 11, 14, 15, 17, 19]
key   44 found at index -1 in 4 steps
key  230 found at index -1 in 4 steps
key -123 found at index -1 in 5 steps
key   23 found at index  0 in 4 steps
key   27 found at index  1 in 3 steps
key   29 found at index  2 in 4 steps
key   31 found at index  3 in 5 steps
key   37 found at index  4 in 2 steps
key   43 found at index  5 in 4 steps
key   49 found at index  6 in 3 steps
key   56 found at index  7 in 4 steps
key   64 found at index  8 in 5 steps
key   78 found at index  9 in 1 steps
key   91 found at index 10 in 4 steps
key   99 found at index 11 in 3 steps
key    1 found at index 12 in 4 steps
key    4 found at index 13 in 5 steps
key   11 found at index 14 in 2 steps
key   14 found at index 15 in 4 steps
key   15 found at index 16 in 3 steps
key   17 found at index 17 in 4 steps
key   19 found at index 18 in 5 steps

5
你有三个值,lmh,它们代表你搜索的低、中、高索引处的值。如果你考虑每种可能性下继续搜索的位置:
// normal binary search
l < t < m    - search(t,l,m)
m < t < h    - search(t,m,h)

// search over a boundary
l > m, t < m - search(t,l,m)
l > m, t > l - search(t,l,m)
m > h, t > m - search(t,m,h)  
m > h, t < h - search(t,m,h)  

这是考虑目标值可能存在的位置,并搜索该空间一半的问题。最多有一半的空间会出现环绕,很容易确定目标值是否在那一半或另一半中。

这有点像元问题 - 你是否认为二分查找是如何呈现的 - 找到两个点之间的值,还是更普遍地将其视为抽象搜索空间的重复分割。


那不起作用。你的四种情况归结为两种(t<m和t>m),与正常的二分查找完全相同。 - Frank
更正了复制粘贴错误。我自作自受,因为一个更详细的版本被接受了。 - Pete Kirkham

0

请检查这段代码,

    def findkey():
    key = 3

    A=[10,11,12,13,14,1,2,3]
    l=0
    h=len(A)-1
    while True:
        mid = l + (h-l)/2
        if A[mid] == key:
            return mid
        if A[l] == key:
            return l
        if A[h] == key:
            return h
        if A[l] < A[mid]:
            if key < A[mid] and key > A[l]:
                h = mid - 1
            else:
                l = mid + 1
        elif A[mid] < A[h]:
            if key > A[mid] and key < A[h]:
                l = mid + 1
            else:
                h = mid - 1

if __name__ == '__main__':
    print findkey()

0
public static int _search(int[] buff, int query){
    int s = 0;
    int e = buff.length;
    int m = 0; 

    while(e-s>1){
        m = (s+e)/2;
        if(buff[offset(m)] == query){
            return offset(m);
        } else if(query < buff[offset(m)]){
            e = m;
        } else{
            s = m;
        }
    }

    if(buff[offset(end)]==query) return end;
    if(buff[offset(start)]==query) return start;
    return -1;
}

public static int offset(int j){
    return (dip+j) % N;
}

0

以下是使用二分查找的C语言实现。

int rotated_sorted_array_search(int arr[], int low, int high, int target)
{
    while(low<=high)
    {
        int mid = (low+high)/2;

        if(target == arr[mid])
            return mid;

        if(arr[low] <= arr[mid])
        {
            if(arr[low]<=target && target < arr[mid])
            {
                high = mid-1;
            }
            else
                low = mid+1;
        }
        else
        {
            if(arr[mid]< target && target <=arr[high])
            {
                low = mid+1;
            }
            else
                high = mid-1;
        }
    }
    return -1;
}

0
这是一个关于JavaScript的解决方案。我已经用几个不同的数组进行了测试,看起来它可以正常工作。它基本上使用了ire_and_curses描述的相同方法:
function search(array, query, left, right) {
  if (left > right) {
    return -1;
  }

  var midpoint = Math.floor((left + right) / 2);
  var val = array[midpoint];
  if(val == query) {
    return midpoint;
  }

  // Look in left half if it is sorted and value is in that 
  // range, or if right side is sorted and it isn't in that range.
  if((array[left] < array[midpoint] && query >= array[left] && query <= array[midpoint])
    || (array[midpoint] < array[right] 
        && !(query >= array[midpoint] && query <= array[right]))) {
    return search(array, query, left, midpoint - 1);
  } else {
    return search(array, query, midpoint + 1, right);
  }
}

0
一个简单的方法在Ruby中。
def CircularArraySearch(a, x)
  low = 0
  high = (a.size) -1

  while low <= high
    mid = (low+high)/2
    if a[mid] == x
      return mid
    end
    if a[mid] <= a[high]
      if (x > a[mid]) && (x <= a[high])
        low = mid + 1
      elsif high = mid -1
      end
    else
      if (a[low] <= x) && (x < a[mid])
        high = mid -1
      else
        low = mid +1
      end
    end
  end
  return -1
end

a = [12, 14, 18, 2, 3, 6, 8, 9]
x = gets.to_i
p CircularArraySearch(a, x)

0

简单的二分查找,稍作修改。

旋转数组的索引= (i+pivot)%size

pivot 是满足 a[i]>a[i+1] 的 i+1 索引。

#include <stdio.h>
#define size 5
#define k 3
#define value 13
int binary_search(int l,int h,int arr[]){

int mid=(l+h)/2;

if(arr[(mid+k)%size]==value)
    return (mid+k)%size;

if(arr[(mid+k)%size]<value)
    binary_search(mid+1,h,arr);
else
    binary_search(l,mid,arr);
}

int main() {
    int arr[]={5,9,13,1,3};
    printf("found at: %d\n", binary_search(0,4,arr));
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接