int binary_search(int *a,int x,int y,int v) //经典二分查找 闭区间
{
int m;
while(y>=x)
{
m=x+(y-x)/2;
if(a[m]==v) return m;
else if(a[m]>v) y=m-1;
else x=m+1;
}
return -1;
}
思想:对于以排序的闭区间[x,y],计算出中间下标m,将这个m对应的元素与查找值v做比较
1.如果相等 查找成功 返回m
2.若a[m]<v 则查找区间变为[m+1,y]
3.若a[m]>v 则查找区间变为[x,m-1]
重复以上3步 直到查找成功或区间为空
这样 每次查找的区间都能缩小一半 效率高 为O(logn)
变形1:给出元素互不相同的以排序(升序)数组a 返回小于或等于给定key的最大元素
分析一下
设数组区间为[x,y] 计算出中间下标m 与给定key做比较
如果a[m]<=key 则更新左边界 x=m;
如果a[m]>key 则更新右边界 y=m-1;
重复以上步骤 直到x==y 找到所求元素
代码实例
#include<iostream>
using namespace std;
int binary_search_ex1(int *a,int x,int y,int v)
{
int m;
while(x<y)
{
m=x+(y-x)/2;
if(a[m]<=v) x=m;
else y=m-1;
}
return a[x];
}
int main()
{
int A[] = {-1, 2, 3, 5, 6, 8, 9, 10} ;
int key=7;
printf("%d\n",binary_search_ex1(A,0,sizeof(A)/4-1,key));
return 0;
}
期望结果是6 但实际运行结果为只有光标一直闪 应该是出现了死循环
问题在哪里 我们先改一下代码 看看区间
#include<iostream>
using namespace std;
int binary_search_ex1(int *a,int x,int y,int v)
{
int m;
while(x<y)
{
printf("[%d,%d]\n",x,y);
m=x+(y-x)/2;
if(a[m]<=v) x=m;
else y=m-1;
}
return x;
}
int main()
{
int A[] = {-1, 2, 3, 5, 6, 8, 9, 10} ;
int key=7;
printf("%d\n",binary_search_ex1(A,0,sizeof(A)/4-1,key));
return 0;
}
经过测试 我们发现区间一直在[3,4]循环
原因很简单 自己照着代码算一遍就知道了
其实这是“地板除”导致的 也就是说每次中间下标都会偏向左边界x(因为是数值低的一边 形象比喻为地板),如果某次计算中间下标正好等于了左边界x,那么在你接下来的程序分支中 一定不能出现x=m 因为新区间[m,y]和[x,y]是一样的 循环的开始
分析:原程序怎么改 我们要保证新区间和原区间不同 让中间值偏向右边界y 所以把原本的闭区间变成[x,y+1) 这样不管区间为多大 多不会使得左边界等于中间下标
例如[3,4] m=3+(4-3)/2=3 改成[3,5) m=3+(5-3)/2=4 我们可以放心大胆的执行x=m这个赋值语句
<同时因为左闭右开 y=m-1 要改为y=m 循环条件改为while(y-x>1)
好了 既然有了y=m这个语句 我们就有必要考虑是否会出现计算出的中间下标等于右边界了 这个留给大家去思考
变形2:给出元素互不相同的以排序(升序)数组a 返回大于或等于给定key的最小元素
这里只给出代码
#include<iostream>
using namespace std;
int binary_search_ex2(int *a,int x,int y,int v)
{
int m;
while(x<y)
{
m=x+(y-x)/2;
if(a[m]>=v) y=m;
else x=m+1;
}
return a[x];
}
int main()
{
int A[] = {-1, 2, 3, 5, 6, 8, 9, 10} ;
int key=7;
printf("%d\n",binary_search_ex2(A,0,sizeof(A)/4-1,key));
return 0;
}
2015/03/27更新
变形3:求出给定数字key在数组a的右下界(即给定数字key在a中第一次出现的位置,若不存在,返回-1)
用一个变量mark表示数字key在数组中a的位置 初始化为-1
计算出中间下标m
1.如果a[m]==key 左边可能还有 记录mark=m 更新y=m-1;
2.如果a[m]>key m及其右边的元素不可能了 更新 y=m-1;
3.如果a[m]<key m及其左边的元素不可能了 更新 x=m-1;
重复以上步骤直到区间为空
要知道循环的情况是怎么都不会出现了 毕竟没出现y=m或者x=m
代码示例:
int lower_bound(int *a,int x,int y,int v)
{
int m,mark=-1;
while(x<=y)
{
m=x+(y-x)/2;
if(a[m]>v) y=m-1;
else if(a[m]<v) x=m+1;
else
{
mark=m;
y=m-1;
}
}
return mark;
}
变形4:求出给定数字key在数组a的左下界的后一个位置
int upper_bound(int *a,int x,int y,int v)
{
int m,mark=-1;
while(x<=y)
{
m=x+(y-x)/2;
if(a[m]<v) x=m+1;
else if(a[m]>v) y=m-1;
else
{
mark=m+1;
x=m+1;
}
}
return mark;
}
变形5:统计一个数字在排序数组中出现的次数
思路:upper_bound-lower_bound就行了
为什么变形4里面不直接直接返回左下界 而返回它的后一个位置
如果upper_bound返回的是左下界 那数字key在数组a中存在的情况下 出现次数为upper_bound-lower_bound+1
但如果不出现的话 出现次数为0 但是 upper_bound-lower_bound=(-1)-(-1)+1=1
原因就是这样
变形5的实战 http://ac.jobdu.com/problem.php?pid=1349
AC代码:
#include<iostream>
#include<cstdio>
using namespace std;
int A[1000005];
int lower_bound(int *a,int x,int y,int v)
{
int m,mark=0;
while(x<=y)
{
m=x+(y-x)/2;
if(a[m]>v) y=m-1;
else if(a[m]<v) x=m+1;
else
{
mark=m;
y=m-1;
}
}
return mark;
}
int upper_bound(int *a,int x,int y,int v)
{
int m,mark=0;
while(x<=y)
{
m=x+(y-x)/2;
if(a[m]<v) x=m+1;
else if(a[m]>v) y=m-1;
else
{
mark=m+1;
x=m+1;
}
}
return mark;
}
int main()
{
int n,cnt,key,i;
while(cin>>n)
{
for(i=0;i<n;i++)
scanf("%d",&A[i]);
scanf("%d",&cnt);
while(cnt--)
{
scanf("%d",&key);
printf("%d\n",upper_bound(A,0,n-1,key)-lower_bound(A,0,n-1,key));
}
}
return 0;
}
注意:在输入输出频率高的地方不要用cin和cout 输入输出时间远大于scanf和printf
变形6:
有一个已排序的数组(无相同元素)在未知的位置进行了旋转操作,找出在新数组中的最小元素所在的位置。
例如:原数组 {1,2,3,4,5,6,7,8,9,10}, 旋转后的数组可能是 {6,7,8,9,10, 1,2,3,4,5 },也可能是 {8,9,10,1,2,3,4,5,6,7 }
思路:别说直接找了 如果时间只有1s n=10e9话 O(n)都可能救不了你 当然得O(logn) 上二分 只不过这次比较条件变了
观察可以知道 旋转后左边的一部分要比右边一部分全大 所以比右边最大一个都大
当区间元素大于1个时
1.计算出中间下标m
2.a[m]>a[r] x=m+1
3.a[m]<a[r] y=m
重复以上步骤直到条件不满足
代码
#include<iostream>
using namespace std;
int solve(int *a,int x,int y)
{
int m;
while(y-x>0)
{
m=x+(y-x)/2;
if(a[m]<a[y])
y=m;
else x=m+1;
}
return a[x];
}
int main()
{
int a[]={6,7,8,9,10, 1,2,3,4,5 };
cout<<solve(a,0,sizeof(a)/4-1)<<endl;
return 0;
}
暂时总结一下:1.找出中点 2.按照不同的条件 3.将区间缩小为原区间一半