【问题标题】:Searching for an element in a circular sorted array在循环排序数组中搜索元素
【发布时间】:2011-02-19 13:12:13
【问题描述】:

我们想在复杂度不大于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);
    }
}

希望这会奏效。

【问题讨论】:

  • 你应该澄清你是否事先知道循环数组从哪里开始。通常在您会知道的实际应用程序中。
  • 不,我不知道循环数组从哪里开始,如果我知道,那么我不需要转换算法,而是直接应用上述关系并进行二进制搜索。
  • 您需要知道元素是否不同。否则最坏的情况是 Omega(n)。
  • 考虑到问题的限制,它从来没有说数组是排序的,也没有给出任何部分排序的建议。使用排序转换数组最多会使您减少 n 次(计算具有额外空间复杂度的排序),并且在大多数情况下为 nlgn 时间。数组可能完全未排序,使得二分搜索方案无法使用(对于 lgn 时间)。请参阅 stackoverflow.com/a/7694019/2812818

标签: algorithm binary-search circular-buffer


【解决方案1】:

虽然批准的答案是最佳的,但我们也可以使用类似且更简洁的算法。

  1. 运行二分搜索以查找枢轴元素(数组旋转的位置)。 O(logn)
  2. 枢轴的左半部分将按降序排序,在此处运行向后二进制搜索键。 O(logn)
  3. 枢轴的右半部分将按升序排序,在这半部分运行正向二进制搜索以查找键。 O(logn)
  4. 从第 2 步和第 3 步返回找到的键索引。

总时间复杂度:O(logn)

欢迎提出想法。

【讨论】:

    【解决方案2】:

    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)
    

    【讨论】:

      【解决方案3】:

      简单的二分查找,稍加改动。

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

      pivot 是索引 i+1,其中 a[i]>a[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;
      }
      

      【讨论】:

        【解决方案4】:

        这是 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);
          }
        }
        

        【讨论】:

          【解决方案5】:

          下面是使用二分查找的 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;
          }
          

          【讨论】:

            【解决方案6】:

            我认为您可以使用以下代码找到偏移量:

            public static int findOffset(int [] arr){
                    return findOffset(arr,0,arr.length-1);
                }
            private static int findOffset(int[] arr, int start, int end) {
            
                if(arr[start]<arr[end]){
                    return -1;
                }
                if(end-start==1){
                    return end;
                }
                int mid = start + ((end-start)/2);
                if(arr[mid]<arr[start]){
                    return findOffset(arr,start,mid);
                }else return findOffset(arr,mid,end);
            }
            

            【讨论】:

              【解决方案7】:

              这是一个想法,与二分搜索有关。只需继续备份右数组索引边界的索引,左索引边界存储在步长中:

              step = n
              pos = n
              while( step > 0 ):
                  test_idx = pos - step   #back up your current position
                  if arr[test_idx-1] < arr[pos-1]:
                      pos = test_idx
                  if (pos == 1) break
                  step /= 2 #floor integer division
              return arr[pos]
              

              为了避免 (pos==1) 的事情,我们可以循环备份(进入负数)并取 (pos-1) mod n。

              【讨论】:

                【解决方案8】:

                检查这个 coe,

                    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()
                

                【讨论】:

                  【解决方案9】:

                  这是一个适用于 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
                  

                  【讨论】:

                    【解决方案10】:
                    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;
                    }
                    

                    【讨论】:

                      【解决方案11】:

                      您有三个值,l,m,h 用于搜索的低、中和高索引处的值。如果您认为是,您将继续寻找每种可能性:

                      // 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)  
                      

                      这是一个考虑目标值可能在哪里并搜索那一半空间的问题。至多有一半的空间会有回绕,很容易确定目标值是在那一半还是另一半。

                      这是一个元问题 - 你认为二分搜索它是如何经常呈现的 - 在两点之间找到一个值,或者更一般地作为抽象搜索空间的重复划分。

                      【讨论】:

                      • 那行不通。您的四种情况归结为两种(tm),它们与正常的二分搜索完全相同。
                      • 更正了复制粘贴错误。为我提供具体的服务 - 同一事物的更具描述性的版本被接受。
                      【解决方案12】:

                      guirgis: 发面试题太坑爹了,猜你没得到这份工作:-(

                      使用一个特殊的 cmp 函数,你只需要一次通过常规的二分搜索。比如:

                      def rotatedcmp(x, y):
                        if x and y < a[0]:
                          return cmp(x, y)
                        elif x and y >= a[0]:
                          return cmp(x, y)
                        elif x < a[0]:
                          return x is greater
                        else:
                          return y is greater
                      

                      如果您可以依赖 int 下溢,则在访问每个元素时减去 a[0] - MIN_INT 并使用常规比较。

                      【讨论】:

                        【解决方案13】:

                        您可以利用数组已排序这一事实来做到这一点,但枢轴值及其邻居之一的特殊情况除外。

                        • 求数组 a 的中间值。
                        • 如果a[0] &lt; a[mid],则所有值在 数组的前半部分是 排序。
                        • 如果a[mid] &lt; a[last],那么所有 在下半年的价值 数组已排序。
                        • 取排序 一半,检查你的值是否 位于其中(与 那一半的最大 idx)。
                        • 如果是这样,只是二进制 搜索那一半。
                        • 如果没有,它 必须在未排序的一半。拿 那一半,重复这个过程, 确定那一半的哪一半 已排序等。

                        【讨论】:

                        • +1 不错。我相信这就是面试官正在寻找的答案。它利用了这样一个事实,即在数组的任何段中,至少有一半是排序的,因此我们可以使用它来决定继续哪一半,从而根据需要在每一步中将搜索空间减少 2。
                        • 需要不同的元素,否则算法会失败。而且奇怪的是,我们可以根据渐近运行时间给出证明!
                        【解决方案14】:

                        您只需像使用常规排序数组一样使用简单的二进制搜索。唯一的技巧是您需要旋转数组索引:

                        (index + start-index) mod array-size
                        

                        其中起始索引是循环数组中第一个元素的偏移量。

                        【讨论】:

                        • 我认为这样做的问题是你事先不知道数组中最小值的位置,也就是你所说的开始索引。不过,在 log(n) 时间内找到该值相当简单:只需检查数组中的第一个最后一个值和中间值。如果第一个大于中间,你知道跳跃在它们之间。您可以在数组的那一半上使用相同的技巧来找到它。如果中间大于秒,则在后半部分。如果这些都不为真,那么数组(或当前子数组)的零位置就是起始位置。
                        【解决方案15】:

                        您可以使用二分查找找到最小元素的位置并将其减少到O(Log n)

                        您可以通过以下方式找到位置(这只是算法的草图,它不准确,但您可以从中得到想法):
                        1. 我 2. j 3. 当我 3.1。 k 3.2.如果 arr[k]

                        找到最小元素的位置后,您可以将数组视为两个排序数组。

                        【讨论】:

                          【解决方案16】:

                          不是很优雅,但我的头顶 - 只需使用二进制搜索找到旋转数组的枢轴,然后再次执行二进制搜索,补偿枢轴的偏移量。执行两次完整搜索有点愚蠢,但它确实满足条件,因为 O(log n) + O(log n) == O(log n)。保持简单和愚蠢(tm)!

                          【讨论】:

                          • +1:简单就是好。可能可以同时进行这两项工作;寻找包装点需要测试当前低和高索引的顺序值是否颠倒,如果不是,那么你在没有包装点的段上,如果(!)寻找的值在这个段中,然后是常规二进制搜索可以解决它;如果数字不在此段内,或者该段中有一个包装点,则将其拆分并重复;这样,您要么找到包装点,要么将其简化为常规二进制搜索而不找到它。
                          • @Unreason - 你刚刚在我的回答中描述了算法。 ;)
                          猜你喜欢
                          • 2021-12-18
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 2021-07-24
                          • 2011-12-03
                          • 2016-11-13
                          相关资源
                          最近更新 更多