利用二分查找在循环递增数组中检索一个元素

这是我在校园招聘面试时被问到的一个问题。问题是:
    在一个循环递增数组中检索一个元素的时间复杂度较低的算法。

所谓循环递增数组就是,假设一个长度为n的数组A,存在一个有效下标r(下标从0开始),使得将子数组A[0...r]拼接到子数组A[r+1...n-1]后面得到一个严格递增数组(A[i...j]表示数组A的从下标i开始到下标j结束的所有元素组成的子数组)。严格递增数组是循环递增数组的一个特例。下面是一个循环递增数组的例子,

      5,6,7,8,9,0,1,2,3,4
将子数组(5,6,7,8,9)拼接到子数组(0,1,2,3,4)后面就得到一个严格递增数组(0,1,2,3,4,5,6,7,8,9)。

在一个数组中检索一个元素,最普通的算法就是时间复杂度为O(n)的顺序检索。要降低时间复杂度,并结合循环递增数组的元素已有一定程度顺序的性质,很容易想到二分查找。

我最初的想法是:朴素的二分查找在是下标区间[0 n-1]内进行二分,那么利用循环递增的性质,我们可以在数组的有效下标区间[0 n-1]的一个偏移区间[r+1 n+r-1]里进行二分(其中r是上述循环递增数组定义中的那个分界下标r)。但是,这个想法的一个最大问题是:给定一个循环递增数组,我如何去确定其分界下标r?最明显的做法就是顺序扫描一遍数组,然后确定其分界下标r。但是,既然你都扫描一遍了,也就能确定检索元素是否在该数组中了,何必再去利用分界下标r去检索呢?!

上述想法不成功,我们再去深入地去思考二分查找方法的一些原理特性。在一个严格递增的数组中,我们将要检索的元素和数组中间的元素进行比较,然后根据要检索的元素与数组中间的元素的大小关系来确定该元素落在那个范围内,然后递归地在该范围内进行检索。

现在在循环递增数组中,我们不能简单地通过与数组中间元素的大小关系来确定要检索的元素所落在的区间范围。要确定范围,我们可以再加上要检索的元素与数组两端的元素的大小关系。

循环递增数组有这么一个性质:以数组中间元素将循环递增数组划分为两部分,则一部分为一个严格递增数组,而另一部分为一个更小的循环递增数组。当中间元素大于首元素时,前半部分为严格递增数组,后半部分为循环递增数组;当中间元素小于首元素时,前半部分为循环递增数组;后半部分为严格递增数组。

记要检索的元素为ele,数组的首元素为b-ele,中间元素为m-ele,末尾元素为e-ele。则当ele不等于m-ele时,
    1.m-ele > b-ele,即数组前半部分为严格递增数组,后半部分为循环递增数组时,若ele小于m-ele并且不小于b-ele时,则ele落在数组前半部分;否则,ele落在数组后半部分。
    2.m-ele < b-ele,即数组前半部分为循环递增数组,后半部分为严格递增数组时,若ele大于m-ele并且不大于e-ele时,则ele落在数组后半部分;否则,ele落在数组前半部分。

通过上面利用数组首元素,中间元素和末尾元素确定要检索的元素所在范围,我们就可以使用修改后的二分查找算法了。

下面是此算法的scheme实现:

 1 (define (recycle-inc-search vec ele)
2 (define (rec begin end)
3 (cond ((> begin end) -1)
4 (else
5 (let ((mid (floor (/ (+ begin end) 2))))
6 (let ((b-ele (vector-ref vec begin))
7 (m-ele (vector-ref vec mid))
8 (e-ele (vector-ref vec end)))
9 (cond ((= ele m-ele) mid)
10 ((> m-ele b-ele)
11 (cond ((and (< ele m-ele)
12 (>= ele b-ele))
13 (rec begin (- mid 1)))
14 (else
15 (rec (+ mid 1) end))))
16 (else
17 (cond ((and (> ele m-ele)
18 (<= ele e-ele))
19 (rec (+ mid 1) end))
20 (else
21 (rec begin (- mid 1)))))))))))
22 (rec 0 (- (vector-length vec) 1)))



posted on 2012-02-15 15:40  lienhua34  阅读(1745)  评论(0编辑  收藏  举报

导航