关于二分法的深入理解及实例

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.将区间缩小为原区间一半

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值