二分搜索及其扩展(循环递增数组的搜索)
二分搜索需要注意开闭区间的问题,限制条件和边界要保持配对:low<=high , low = mid +1 ,high = mid-1。
二分搜索的模板如下:
二分搜索的扩展:对已排好序的数组A,一般来说可用二分查找 可以很快找到。现有一特殊数组A[],它是循环递增的,如A[]={ 17 19 20 25 1 4 7 9},试在这样的数组中找一元素x,看看是否存在。请写出你的算法,必要时可写伪代码,并分析其空间、时间复杂度。
对于循环有序的数组,它是循环递增的,例如:A[]={ 17 19 20 25 1 4 7 9},也可以使用二分搜索:每次将数组分为2部分,一个是单调递增的,一个是循环递增的。如果是单调递增的话直接使用朴素的二分搜索,如果是循环递增的话继续使用特殊二分搜索下去。
思路分析:
在一个数组中检索一个元素,最普通的算法就是时间复杂度为O(n)的顺序检索。要降低时间复杂度,并结合循环递增数组的元素已有一定程度顺序的性质,很容易想到二分查找。
我最初的想法是:朴素的二分查找在是下标区间[0 n-1]内进行二分,那么利用循环递增的性质,我们可以在数组的有效下标区间[0 n-1]的一个偏移区间[r+1 n+r-1]里进行二分(其中r是上述循环递增数组定义中的那个分界下标r)。但是,这个想法的一个最大问题是:给定一个循环递增数组,我如何去确定其分界下标r?最明显的做法就是顺序扫描一遍数组,然后确定其分界下标r。但是,既然你都扫描一遍了,也就能确定检索元素是否在该数组中了,何必再去利用分界下标r去检索呢?
上述想法不成功,我们再去深入地去思考二分查找方法的一些原理特性。在一个严格递增的数组中,我们将要检索的元素和数组中间的元素进行比较,然后根据要检索的元素与数组中间的元素的大小关系来确定该元素落在那个范围内,然后递归地在该范围内进行检索。
现在在循环递增数组中,我们不能简单地通过与数组中间元素的大小关系来确定要检索的元素所落在的区间范围。要确定范围,我们可以再加上要检索的元素与数组两端的元素的大小关系。
循环递增数组有这么一个性质:以数组中间元素将循环递增数组划分为两部分,则一部分为一个严格递增数组,而另一部分为一个更小的循环递增数组。当中间元素大于首元素时,前半部分为严格递增数组,后半部分为循环递增数组;当中间元素小于首元素时,前半部分为循环递增数组;后半部分为严格递增数组。
记要检索的元素为key,数组的首元素为a[low],中间元素为a[mid],末尾元素为a[high]。则当key不等于a[mid] 时,
1、a[mid] > a[low],即数组前半部分为严格递增数组,后半部分为循环递增数组时,若key小于a[mid]并且不小于a[low]时,则key落在数组前半部分;否则,key落在数组后半部分。
2、a[mid] < a[high],即数组前半部分为循环递增数组,后半部分为严格递增数组时,若key大于a[mid]并且不大于a[high]时,则key落在数组后半部分;否则,key落在数组前半部分。
通过上面利用数组首元素,中间元素和末尾元素确定要检索的元素所在范围,我们就可以使用修改后的二分查找算法了。
实现代码如下:
二分搜索的模板如下:
- // 二分搜索
- int BinarySearch(int *num, int key, int low, int high)
- {
- int mid ;
- while(low <= high) //切记:条件是 <= ,很多次都不小心写成了 < ,导致了N多WA
- {
- mid = (low + high)>>1;
- if(num[mid] == key)
- return mid;
- else if(num[mid] < key)
- low = mid + 1;
- else
- high = mid -1;
- }
- return low; //查找不成功时,返回应该插入的位置,或者直接返回-1,表示查找失败也是可以的
- }
对于循环有序的数组,它是循环递增的,例如:A[]={ 17 19 20 25 1 4 7 9},也可以使用二分搜索:每次将数组分为2部分,一个是单调递增的,一个是循环递增的。如果是单调递增的话直接使用朴素的二分搜索,如果是循环递增的话继续使用特殊二分搜索下去。
思路分析:
在一个数组中检索一个元素,最普通的算法就是时间复杂度为O(n)的顺序检索。要降低时间复杂度,并结合循环递增数组的元素已有一定程度顺序的性质,很容易想到二分查找。
我最初的想法是:朴素的二分查找在是下标区间[0 n-1]内进行二分,那么利用循环递增的性质,我们可以在数组的有效下标区间[0 n-1]的一个偏移区间[r+1 n+r-1]里进行二分(其中r是上述循环递增数组定义中的那个分界下标r)。但是,这个想法的一个最大问题是:给定一个循环递增数组,我如何去确定其分界下标r?最明显的做法就是顺序扫描一遍数组,然后确定其分界下标r。但是,既然你都扫描一遍了,也就能确定检索元素是否在该数组中了,何必再去利用分界下标r去检索呢?
上述想法不成功,我们再去深入地去思考二分查找方法的一些原理特性。在一个严格递增的数组中,我们将要检索的元素和数组中间的元素进行比较,然后根据要检索的元素与数组中间的元素的大小关系来确定该元素落在那个范围内,然后递归地在该范围内进行检索。
现在在循环递增数组中,我们不能简单地通过与数组中间元素的大小关系来确定要检索的元素所落在的区间范围。要确定范围,我们可以再加上要检索的元素与数组两端的元素的大小关系。
循环递增数组有这么一个性质:以数组中间元素将循环递增数组划分为两部分,则一部分为一个严格递增数组,而另一部分为一个更小的循环递增数组。当中间元素大于首元素时,前半部分为严格递增数组,后半部分为循环递增数组;当中间元素小于首元素时,前半部分为循环递增数组;后半部分为严格递增数组。
记要检索的元素为key,数组的首元素为a[low],中间元素为a[mid],末尾元素为a[high]。则当key不等于a[mid] 时,
1、a[mid] > a[low],即数组前半部分为严格递增数组,后半部分为循环递增数组时,若key小于a[mid]并且不小于a[low]时,则key落在数组前半部分;否则,key落在数组后半部分。
2、a[mid] < a[high],即数组前半部分为循环递增数组,后半部分为严格递增数组时,若key大于a[mid]并且不大于a[high]时,则key落在数组后半部分;否则,key落在数组前半部分。
通过上面利用数组首元素,中间元素和末尾元素确定要检索的元素所在范围,我们就可以使用修改后的二分查找算法了。
实现代码如下:
- // 改进后的二分搜索
- int Search(int A[],int n, int num)
- {
- int left = 0 ;
- int right = n - 1 ;
- int mid = 0 ;
- int pos = -1 ; //返回-1,表示查找失败
- while(left <= right)
- {
- mid = (left + right)/2 ;
- if (A[mid] == num)
- {
- pos = mid ;
- break;
- }
- if (A[left] <= A[mid]) //前半部分是严格递增的,后半部分是一个更小的循环递增数组
- {
- if(A[left] <= num && num < A[mid] )
- {
- right = mid - 1 ;
- }
- else
- {
- left = mid + 1 ;
- }
- }
- else //后半部分是严格递增的,前半部分是一个更小的循环递增数组
- {
- if ( A[mid] < num && num <= A[right])
- {
- left = mid + 1 ;
- }
- else
- {
- right = mid - 1 ;
- }
- }
- }
- return pos;
- }