编程之美系列之关于数组的二分查找

时间:2022-03-11 03:36:24

有序的数组,其中的元素可以通过二分查找得到,可以如果一个数组是由有序数组左旋得到的,能不能还是用二分查找得到呢?答案是肯定的。
为了简化问题,我先假设数组是由升序的数组通过左旋得到了,比如说{1,2,3,4,5,6}=>{4,5,6,1,2,3}。只要我们找到这个最大值的下标,那么就可以把数组分成两个升序的有序数组了。找这个最大值很简单,但是我想你应该不会说全部遍历一遍数组吧!这里讲的重点是二分查找。
怎么二分查找最大值的下标?由于数组是有序的,定义查找的开始为b,结束点为e,mid=(b+e)/2.OK,二分的必备神器呵!
1、arr[mid]<arr[nLen-1],更新e。
2、arr[mid]>arr[0],更新b。
问题来了,万一数组中有重复的元素怎么比如说{4,5,6,6,1,2,3}.把上面更新的条件加上等号就可以了,但是还是有bug,万一整个数组都只有一个数呢?比如说{6,6,6,6,6,6,6}。OK,我们需要找到的是最右边的那个最大值的下标。所以这里优先更新b就可以了,也就是:
1、arr[mid]>=arr[0],更新b。
2、arr[mid]<=arr[nLen-1],更新e。
好了,给定key之后,由于数组中的元素可能有重复(没有重复的情况很简单,不需要我废话了),所以如果找到key,返回最右的下标。
1、如果key==arr[nLen-1],下标直接就是nLen-1了。
2、如果key<arr[nLen-1],则在前面一部分的数组中查找这个元素。
3、否则,在后半部分查找这个元素。
各位看官们,我敢保证这个是网上绝无仅有的,所以保护一下偶这个弱女子,你也应该转载注明出处吧!小女子跪谢:http://blog.csdn.net/kay_zhyu/article/details/8867095
关于这个部分的代码如下:

#include<stdio.h>
#include<assert.h>
//arr有递增序列旋转得到,找到arr中最右边的最大值的下标
//e.g.{1,2,3,4,5,6,6}->{4,5,6,6,1,2,3};=>ans:3
int IncreaseFindMax(int *arr, int nLen)
{
assert(arr && nLen > 0);
int b = 0;
int e = nLen - 1;
int mid;
while(b < e - 1)
{
mid = (b + e)>>1;
//由于求最右的下标,故优先更新b,否则优先更新e
if(arr[mid] >= arr[0])
b = mid;
else if(arr[mid] <= arr[nLen - 1])
e = mid;
}
return arr[b] > arr[e] ? b : e;
}
//在递增的序列中查找,返回等于key的最大的下标
//e.g.{1,2,3,3,3,4,5,6,6}中找3,ans=4.
int IncreaseFindKey(int *arr, int b, int e, const int key)
{
if(!arr || b > e || b < 0)
return -1;
int mid;
while(b < e - 1)
{
mid = (b + e)>>1;
if(arr[mid] <= key)
b = mid;
else
e = mid;
}
if(arr[e] == key)
return e;
else if(arr[b] == key)
return b;
else
return -1;
}
题目扩展:
1、如果数组是由降序数组左旋得到的呢?
这个好办,依葫芦画瓢就可以了。代码如下:
//arr有递减序列旋转得到,找到arr中最右边的最小值的下标
//e.g.{6,5,4,3,2,1,1}->{3,2,1,1,6,5,4};ans=>3
int DecreseFindMin(int *arr, int nLen)
{
assert(arr && nLen > 0);
int b = 0;
int e = nLen - 1;
int mid;
while(b < e - 1)
{
mid = (b + e)>>1;
//由于求最右的下标,故优先更新b,否则优先更新e
if(arr[mid] <= arr[0])
b = mid;
else if(arr[mid] >= arr[nLen - 1])
e = mid;
}
return arr[b] < arr[e] ? b : e;
}
//在递减的序列中查找,返回等于key的最大的下标
//e.g.{6,5,4,3,3,3,2,1}中找3,ans=5.
int DecreaseFindKey(int *arr, int b, int e, const int key)
{
if(!arr || b > e || b < 0)
return -1;
int mid;
while(b < e - 1)
{
mid = (b + e)>>1;
if(arr[mid] >= key)
b = mid;
else
e = mid;
}
if(arr[e] == key)
return e;
else if(arr[b] == key)
return b;
else
return -1;
}

2、如果数组只告诉你是有有序数组左旋得到的,但没有说有序是升序还是降序。
OK,那就用一个函数来判断数组的升序和降序情况。
分别从数组的两端进行遍历,如果有一对数不同,OK,它们的大小关系就决定了整个数组的序关系。
如果数组中全部元素相同,则默认为升序降序都无所谓。
3、另外,又想到了一种扩展,关于这个二分查找的,记录在这里了。给定排序的数组,给定一个数字,求这个数字在数组中出现的次数。二分查找到这个数字,然后以这个数字向左向右遍历,然后用下标相减即可。

bool Judge(int *arr, int nLen)
{
assert(arr && nLen > 0);
int b,e;
for(b = 0, e = nLen - 1; b <= e; ++b, --e)
{
if(arr[b] != arr[e])//过滤掉相等的元素
return arr[b] > arr[e];
}
return false;//true或false无所谓,自己高兴就好
}
//附上main函数的调用测试
int main()
{
const int N = 30;
int arr[N];
int n,i,ans,key;
bool IsIncrease;
while(scanf("%d", &n) != EOF)
{
for(i = 0; i < n; ++i)
scanf("%d", &arr[i]);
printf("Enter the key:");
scanf("%d", &key);
if(key == arr[n - 1])//如果等于最后一个元素
{
ans = n - 1;
printf("The position is %d\n", ans);
continue;
}
IsIncrease = Judge(arr, n);
if(IsIncrease)
{
printf("序列由递增序列旋转得到\n");
ans = IncreaseFindMax(arr, n);
printf("最右边的最大值的下标为%d\n",ans);
if(key > arr[n - 1])
ans = IncreaseFindKey(arr, 0, ans, key);
else
ans = IncreaseFindKey(arr, ans + 1, n - 1, key);
}
else
{
printf("序列由递减序列旋转得到\n");
ans = DecreseFindMin(arr, n);
printf("最右边的最小值的下标为%d\n",ans);
if(key < arr[n - 1])
ans= DecreaseFindKey(arr, 0, ans, key);
else
ans = DecreaseFindKey(arr, ans + 1, n - 1, key);
}
if(ans > -1)
printf("The position is %d\n", ans);
else
printf("No found!\n");
}
}

测试结果:
编程之美系列之关于数组的二分查找编程之美系列之关于数组的二分查找编程之美系列之关于数组的二分查找