Searching in an sorted and rotated array
在准备技术面试时,我偶然发现了一个有趣的问题:
已经为您提供了一个排序后旋转的数组。
例子
让排序后旋转的
现在,如何才能最好地搜索这个排序+旋转数组?
可以先取消数组的汇总,然后进行二进制搜索。但这并不比在输入数组中进行线性搜索更好,因为两者都是最坏的情况O(N)。
请提供一些指针。我在谷歌上搜索了很多关于这个的特殊算法,但找不到任何。
我理解C和C++
这可以在
排序+旋转数组的有趣特性是,当您将其分成两半时,两半中的至少一个始终会被排序。
1 2 3 4 5 6 7 | Let input array arr = [4,5,6,7,8,9,1,2,3] number of elements = 9 mid index = (0+8)/2 = 4 [4,5,6,7,8,9,1,2,3] ^ left mid right |
似乎左子数组排序时右子数组未排序。
如果中间恰好是旋转点,则左、右子数组都将被排序。
1 2 | [6,7,8,9,1,2,3,4,5] ^ |
但在任何情况下,都必须对一半(子数组)进行排序。
通过比较每一半的开始元素和结束元素,我们可以很容易地知道哪一半是排序的。
一旦我们找到哪一半是排序的,我们就可以看到键是否存在于与极值的半简单比较中。
如果键在那一半中,我们递归地调用那一半上的函数否则,我们递归地调用另一半的搜索。
我们将在每个调用中丢弃数组的一半,这使得该算法成为
伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | function search( arr[], key, low, high) mid = (low + high) / 2 // key not present if(low > high) return -1 // key found if(arr[mid] == key) return mid // if left half is sorted. if(arr[low] <= arr[mid]) // if key is present in left half. if (arr[low] <= key && arr[mid] >= key) return search(arr,key,low,mid-1) // if key is not present in left half..search right half. else return search(arr,key,mid+1,high) end-if // if right half is sorted. else // if key is present in right half. if(arr[mid] <= key && arr[high] >= key) return search(arr,key,mid+1,high) // if key is not present in right half..search in left half. else return search(arr,key,low,mid-1) end-if end-if end-function |
这里的关键是始终要对一个子数组进行排序,使用它我们可以丢弃该数组的一半。
您可以进行2次二进制搜索:首先查找索引
显然,
然后,如果是
复杂度
编辑:代码。
当数组中存在重复的元素时,所选答案有一个错误。例如,我们正在寻找的是
这一采访问题在《破解编码采访》一书中有详细讨论。在那本书中特别讨论了重复元素的条件。因为OP在评论中说数组元素可以是任何东西,所以我在下面给出了伪代码的解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | function search( arr[], key, low, high) if(low > high) return -1 mid = (low + high) / 2 if(arr[mid] == key) return mid // if the left half is sorted. if(arr[low] < arr[mid]) { // if key is in the left half if (arr[low] <= key && key <= arr[mid]) // search the left half return search(arr,key,low,mid-1) else // search the right half return search(arr,key,mid+1,high) end-if // if the right half is sorted. else if(arr[mid] < arr[low]) // if the key is in the right half. if(arr[mid] <= key && arr[high] >= key) return search(arr,key,mid+1,high) else return search(arr,key,low,mid-1) end-if else if(arr[mid] == arr[low]) if(arr[mid] != arr[high]) // Then elements in left half must be identical. // Because if not, then it's impossible to have either arr[mid] < arr[high] or arr[mid] > arr[high] // Then we only need to search the right half. return search(arr, mid+1, high, key) else // arr[low] = arr[mid] = arr[high], we have to search both halves. result = search(arr, low, mid-1, key) if(result == -1) return search(arr, mid+1, high, key) else return result end-if end-function |
我的第一个尝试是使用二进制搜索找到应用的旋转次数-这可以通过使用常用的二进制搜索机制找到索引n,其中a[n]>a[n+1]。然后进行常规的二进制搜索,同时旋转找到的每个班次的所有索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | int rotated_binary_search(int A[], int N, int key) { int L = 0; int R = N - 1; while (L <= R) { // Avoid overflow, same as M=(L+R)/2 int M = L + ((R - L) / 2); if (A[M] == key) return M; // the bottom half is sorted if (A[L] <= A[M]) { if (A[L] <= key && key < A[M]) R = M - 1; else L = M + 1; } // the upper half is sorted else { if (A[M] < key && key <= A[R]) L = M + 1; else R = M - 1; } } return -1; } |
如果您知道数组已经向右旋转了s,您可以简单地执行二进制搜索,将s右移。这是O(LG n)
通过这个,我的意思是,初始化左极限到s,右极限到(s-1)mod n,然后在它们之间进行二进制搜索,注意在正确的区域工作。
如果不知道数组旋转了多少,可以使用二进制搜索确定旋转的大小,即o(lg n),然后执行移位二进制搜索,o(lg n),总共还有o(lg n)。
如果你知道它旋转了多远,你仍然可以进行二进制搜索。
诀窍是你得到两个级别的索引:你在虚拟的0..n-1范围内做b.s.,然后在实际查找值时取消旋转它们。
您不需要首先旋转数组,可以在旋转的数组上使用二进制搜索(进行一些修改)。
假设n是您搜索的数字:
读取第一个数字(arr[开始])和数组中间的数字(arr[结束]):
如果arr[start]>arr[end]->前半部分未排序,但后半部分排序:
如果arr[end]>n->数字在索引中:(middle+n-arr[end])
如果n在数组的第一部分重复搜索(请参见end to be the middle of the first half of the array etc.)。
(如果第一部分已排序,第二部分未排序,则相同)
回复上述帖子"本访谈问题在《破解编码访谈》一书中详细讨论"。在那本书中特别讨论了重复元素的条件。由于op在注释中说数组元素可以是任何东西,所以我在下面给出了伪代码的解决方案:"
你的解决方案是O(N)!!(在最后一个if条件中,您检查数组的两部分是否存在单个条件,这使其成为线性时间复杂性的sol)
比起在一轮编码过程中陷入由错误和分段错误组成的迷宫,我更擅长进行线性搜索。
我认为没有比O(N)更好的解决方案来搜索旋转排序数组(有重复项)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class PivotedArray { //56784321 first increasing than decreasing public static void main(String[] args) { // TODO Auto-generated method stub int [] data ={5,6,7,8,4,3,2,1,0,-1,-2}; System.out.println(findNumber(data, 0, data.length-1,-2)); } static int findNumber(int data[], int start, int end,int numberToFind){ if(data[start] == numberToFind){ return start; } if(data[end] == numberToFind){ return end; } int mid = (start+end)/2; if(data[mid] == numberToFind){ return mid; } int idx = -1; int midData = data[mid]; if(numberToFind < midData){ if(midData > data[mid+1]){ idx=findNumber(data, mid+1, end, numberToFind); }else{ idx = findNumber(data, start, mid-1, numberToFind); } } if(numberToFind > midData){ if(midData > data[mid+1]){ idx = findNumber(data, start, mid-1, numberToFind); }else{ idx=findNumber(data, mid+1, end, numberToFind); } } return idx; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | short mod_binary_search( int m, int *arr, short start, short end) { if(start <= end) { short mid = (start+end)/2; if( m == arr[mid]) return mid; else { //First half is sorted if(arr[start] <= arr[mid]) { if(m < arr[mid] && m >= arr[start]) return mod_binary_search( m, arr, start, mid-1); return mod_binary_search( m, arr, mid+1, end); } //Second half is sorted else { if(m > arr[mid] && m < arr[start]) return mod_binary_search( m, arr, mid+1, end); return mod_binary_search( m, arr, start, mid-1); } } } return -1; } |
首先,你需要找到移位常数k。这可以在O(lgn)时间内完成。从常量shift k中,可以很容易地找到要使用的元素一个常数k的二进制搜索。增广的二进制搜索也需要O(lgn)时间。总运行时间为o(lgn+lgn)=o(lgn)
要找到常量移位,只需在数组中查找最小值。数组最小值的索引告诉您常量移位。考虑排序数组[1,2,3,4,5]。
1 2 3 4 5 6 7 | The possible shifts are: [1,2,3,4,5] // k = 0 [5,1,2,3,4] // k = 1 [4,5,1,2,3] // k = 2 [3,4,5,1,2] // k = 3 [2,3,4,5,1] // k = 4 [1,2,3,4,5] // k = 5%5 = 0 |
要在O(lgn)时间内执行任何算法,关键是始终找到将问题除以一半的方法。一旦这样做,其余的实现细节就很容易了
下面是C++中的算法代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | // This implementation takes O(logN) time // This function returns the amount of shift of the sorted array, which is // equivalent to the index of the minimum element of the shifted sorted array. #include <vector> #include <iostream> using namespace std; int binarySearchFindK(vector<int>& nums, int begin, int end) { int mid = ((end + begin)/2); // Base cases if((mid > begin && nums[mid] < nums[mid-1]) || (mid == begin && nums[mid] <= nums[end])) return mid; // General case if (nums[mid] > nums[end]) { begin = mid+1; return binarySearchFindK(nums, begin, end); } else { end = mid -1; return binarySearchFindK(nums, begin, end); } } int getPivot(vector<int>& nums) { if( nums.size() == 0) return -1; int result = binarySearchFindK(nums, 0, nums.size()-1); return result; } // Once you execute the above, you will know the shift k, // you can easily search for the element you need implementing the bottom int binarySearchSearch(vector<int>& nums, int begin, int end, int target, int pivot) { if (begin > end) return -1; int mid = (begin+end)/2; int n = nums.size(); if (n <= 0) return -1; while(begin <= end) { mid = (begin+end)/2; int midFix = (mid+pivot) % n; if(nums[midFix] == target) { return midFix; } else if (nums[midFix] < target) { begin = mid+1; } else { end = mid - 1; } } return -1; } int search(vector<int>& nums, int target) { int pivot = getPivot(nums); int begin = 0; int end = nums.size() - 1; int result = binarySearchSearch(nums, begin, end, target, pivot); return result; } |
1 2 3 | Hope this helps!=) Soon Chee Loong, University of Toronto |
问题:在旋转排序数组中搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public class SearchingInARotatedSortedARRAY { public static void main(String[] args) { int[] a = { 4, 5, 6, 0, 1, 2, 3 }; System.out.println(search1(a, 6)); } private static int search1(int[] a, int target) { int start = 0; int last = a.length - 1; while (start + 1 < last) { int mid = start + (last - start) / 2; if (a[mid] == target) return mid; // if(a[start] < a[mid]) => Then this part of the array is not rotated if (a[start] < a[mid]) { if (a[start] <= target && target <= a[mid]) { last = mid; } else { start = mid; } } // this part of the array is rotated else { if (a[mid] <= target && target <= a[last]) { start = mid; } else { last = mid; } } } // while if (a[start] == target) { return start; } if (a[last] == target) { return last; } return -1; } } |
这里有一个简单的(时间、空间)高效的非递归O(log n)python解决方案,它不修改原始数组。将旋转的数组切成两半,直到我只有两个要检查的索引,如果一个索引匹配,则返回正确的答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | def findInRotatedArray(array, num): lo,hi = 0, len(array)-1 ix = None while True: if hi - lo <= 1:#Im down to two indices to check by now if (array[hi] == num): ix = hi elif (array[lo] == num): ix = lo else: ix = None break mid = lo + (hi - lo)/2 print lo, mid, hi #If top half is sorted and number is in between if array[hi] >= array[mid] and num >= array[mid] and num <= array[hi]: lo = mid #If bottom half is sorted and number is in between elif array[mid] >= array[lo] and num >= array[lo] and num <= array[mid]: hi = mid #If top half is rotated I know I need to keep cutting the array down elif array[hi] <= array[mid]: lo = mid #If bottom half is rotated I know I need to keep cutting down elif array[mid] <= array[lo]: hi = mid print"Index", ix |
另一种处理重复值的方法是找到旋转,然后在每次访问数组时应用旋转进行常规的二进制搜索。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | test = [3, 4, 5, 1, 2] test1 = [2, 3, 2, 2, 2] def find_rotated(col, num): pivot = find_pivot(col) return bin_search(col, 0, len(col), pivot, num) def find_pivot(col): prev = col[-1] for n, curr in enumerate(col): if prev > curr: return n prev = curr raise Exception("Col does not seem like rotated array") def rotate_index(col, pivot, position): return (pivot + position) % len(col) def bin_search(col, low, high, pivot, num): if low > high: return None mid = (low + high) / 2 rotated_mid = rotate_index(col, pivot, mid) val = col[rotated_mid] if (val == num): return rotated_mid elif (num > val): return bin_search(col, mid + 1, high, pivot, num) else: return bin_search(col, low, mid - 1, pivot, num) print(find_rotated(test, 2)) print(find_rotated(test, 4)) print(find_rotated(test1, 3)) |
试试这个解决方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | bool search(int *a, int length, int key) { int pivot( length / 2 ), lewy(0), prawy(length); if (key > a[length - 1] || key < a[0]) return false; while (lewy <= prawy){ if (key == a[pivot]) return true; if (key > a[pivot]){ lewy = pivot; pivot += (prawy - lewy) / 2 ? (prawy - lewy) / 2:1;} else{ prawy = pivot; pivot -= (prawy - lewy) / 2 ? (prawy - lewy) / 2:1;}} return false; } |
对于具有重复的旋转数组,如果需要找到元素的第一个出现,可以使用下面的过程(Java代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public int mBinarySearch(int[] array, int low, int high, int key) { if (low > high) return -1; //key not present int mid = (low + high)/2; if (array[mid] == key) if (mid > 0 && array[mid-1] != key) return mid; if (array[low] <= array[mid]) //left half is sorted { if (array[low] <= key && array[mid] >= key) return mBinarySearch(array, low, mid-1, key); else //search right half return mBinarySearch(array, mid+1, high, key); } else //right half is sorted { if (array[mid] <= key && array[high] >= key) return mBinarySearch(array, mid+1, high, key); else return mBinarySearch(array, low, mid-1, key); } } |
这是对上述CodAddict程序的改进。注意附加的if条件如下:
1 | if (mid > 0 && array[mid-1] != key) |
我的简单代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public int search(int[] nums, int target) { int l = 0; int r = nums.length-1; while(l<=r){ int mid = (l+r)>>1; if(nums[mid]==target){ return mid; } if(nums[mid]> nums[r]){ if(target > nums[mid] || nums[r]>= target)l = mid+1; else r = mid-1; } else{ if(target <= nums[r] && target > nums[mid]) l = mid+1; else r = mid -1; } } return -1; } |
时间复杂度o(对数(n))。
C++中的代码应该适用于所有的情况,虽然它与复制品一起工作,如果这个代码中有bug,请告诉我。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | #include"bits/stdc++.h" using namespace std; int searchOnRotated(vector<int> &arr, int low, int high, int k) { if(low > high) return -1; if(arr[low] <= arr[high]) { int p = lower_bound(arr.begin()+low, arr.begin()+high, k) - arr.begin(); if(p == (low-high)+1) return -1; else return p; } int mid = (low+high)/2; if(arr[low] <= arr[mid]) { if(k <= arr[mid] && k >= arr[low]) return searchOnRotated(arr, low, mid, k); else return searchOnRotated(arr, mid+1, high, k); } else { if(k <= arr[high] && k >= arr[mid+1]) return searchOnRotated(arr, mid+1, high, k); else return searchOnRotated(arr, low, mid, k); } } int main() { int n, k; cin >> n >> k; vector<int> arr(n); for(int i=0; i<n; i++) cin >> arr[i]; int p = searchOnRotated(arr, 0, n-1, k); cout<<p<<" "; return 0; } |