问题描述
一个循环有序的数组是形如:“12,16,18,20,41,100,1,4,6,9” 这样的数组。
问题分析
对于循环有序数组,一种简单的定义是:
循环有序数组是将一个有序数组切成两段,并交换位置得到引用块内容
比如现将1,4,6,9,12,16,18,20,41,100在9和12处切分,得到两段:1,4,6,9和12,16,18,20,41,100,再交换这两段的位置就得到了一开始的循环有序数组。
另一种比较严格的定义是:
对于一个循环有序数组{A1,A2,……An},存在一个i,满足1 < i < n,使得{A1,A2,……Ai}和{Ai,Ai+1,……An}同为单调不减,或单调不增数组。且{A1,A2,……Ai}中的任意一个元素恒大与等于或恒小于等于{Ai,Ai+1,……An}中的任意一个元素。
算法设计
对于这样一个具有一定单调性的数组,最容易想到的O(n)算法显然不是最佳结果。考虑到有序数列的查找算法,通过二分法,可以实现O(log n)的时间复杂度,因此,我们尝试修改二分法,来实现O(log n)的循环有序数组查找指定元素算法。
理论基础
二分法的本质在于,每次只需要搜索一半的数列,而且很容易判定,是哪一半数列需要被搜素。
本例中,我们依然可以每次只搜索一半的数列,但是,对于选择哪一半数列需要继续被搜素,就不像传统的二分法那样显然了。
但是循环有序数组具有两个非常优良的性质:
1.将一个循环有序数组一分为二,一定得到一个有序数组和另一个循环有序数组
2.长度不超过2的循环有序数组其实就是有序数组。
关于以上两点,我也没有严格的数学证明,读者可以以最初的循环有序数组为例,或重新举例,自行验证。
这两点性质为循环有序数组的二分法查找提供了理论基础:对于一个给定的待搜索元素,现将原数组一分为二,因为循环有序数组比较难以通过代码判定,我们只需要判定,哪一个数组是有序数组,且是否需要在这个有序数组中进行搜索即可。
解题思路
首先我们要判定这个数组,是增加型的还是减少型的。也即是说,如果用之前的定义一来判定,我们要先弄清楚这个循环有序数组的原数组是单调不减的还是单调不增的。
这一点并不难,只要比较数组的第一个元素和最后一个元素即可。也就是说,如果A1 > An,那么A一定是增加型的循环有序数组,如果A1 < An,那么A一定是增加型的循环有序数组。A1 = An是非常麻烦的情况,可以通过比较A2 和 An-1得出结果,这里我就没有考虑。
一旦确定了循环有序数组是增加型的还是减少型的,就要开始判断左边一半和右边一半哪一个是有序的。这里以增加型的举例,减少型的同理。
如果A[middle] >= A[begin],那么左边一定是有序的。因为如果左边是循环有序的,那么最大值点一定出现在左侧,且最大值点左侧的数恒大于最大值点右侧的数。这与A[middle] > A[begin]矛盾。反之同理。
确定了有序的一侧后,就要判断是不是在这一侧立面搜索了。这个判断非常简单,只要确定待搜索的数的值是否在有序数列的两个端点值之间即可。
最后通过循环,就可以类似二分法,找到待搜索的数的位置。
代码实现
int main(int argc, const char * argv[]) {
// int a[] = {12,16,18,20,41,100,1,4,6,9};
int a[] = {9,6,4,1,100,41,20,18,16,12};
int target = 20;
int arrayLength = sizeof(a)/sizeof(a[0]) - 1;
int index = getIndex(a,target,arrayLength);
index == -1 ? printf("not found") : printf("index = %d",index);
return 0;
}
int getIndex(int a[],int target,int arrayLength){
int beginPos = 0;
int endPos = arrayLength;
if (a[beginPos] >= a[endPos]) {
while (beginPos <= endPos) {
int middlePos = beginPos + (endPos - beginPos)/2;
int middleValue = a[middlePos];
//说明这是一个在增加的循环有序数组
if (middleValue >= a[beginPos]) {
//左侧单调递增
if (target == a[middlePos]) {
return middlePos;
}
else if (target < a[middlePos] && target >= a[beginPos]){
//一定是在左侧查找
endPos = middlePos - 1;
}
else{
//在右侧查找
beginPos = middlePos + 1;
}
}
else{
//右侧单调递增,同理
if (target == a[middlePos]) {
return middlePos;
}
else if (target > a[middlePos] && target <= a[endPos]){
//一定是在右侧查找
beginPos = middlePos + 1;
}
else{
//在左侧查找
endPos = middlePos - 1;
}
}
}
//没找到元素
return -1;
}
else{
while (beginPos <= endPos) {
int middlePos = beginPos + (endPos - beginPos)/2;
int middleValue = a[middlePos];
//说明这是一个在减少的循环有序数组
if (middleValue >= a[beginPos]) {
//右侧单调递减
if (target == a[middlePos]) {
return middlePos;
}
else if (target < a[middlePos] && target >= a[endPos]){
//一定是在右侧查找
beginPos = middlePos + 1;
}
else{
//在右侧查找
endPos = middlePos - 1;
}
}
else{
//左侧单调递减,同理
if (target == a[middlePos]) {
return middlePos;
}
else if (target <= a[beginPos] && target > a[middlePos]){
//一定是在左侧查找
endPos = middlePos - 1;
}
else{
//在左侧查找
beginPos = middlePos + 1;
}
}
}
//没找到元素
return -1;
}
}
写在最后
对于出现A0 = An的情况,我们继续判断A1和An-1的大小。因此可以通过循环来实现,因此,在最坏情况下,这个算法依旧可能变为线性的算法。当然,如果已经规定了循环有序数组中不会出现相同的元素,那么就简单多了。
由于仅仅测试了几个数据,不保证代码100%正确,欢迎热心的读者测试以上代码,并告诉我可能存在的bug。谢谢。