关于二分查找

二分查找实现起来确实非常容易出错,下面贴几个我实现过的版本。

实现思想:
二分查找是分治思想应用的一个典型例子,对于递归实现,可以画一个二分查找树来辅助理解,这棵动态生成树并不像归并排序那样需要全部遍历(前序),而是根据条件判定生成结点且仅遍历其中一个结点或者中止遍历。


【版本一】
直观递归(成功则返回查找元素key的下标,不成功则返回-1或者返回小于key的最大元素下标)
递归出口有两个,一个是查找失败则一直回溯,返回的-1是栈顶函数的-1; 一个是查找成功也是一直回溯,返回的mid也是栈顶函数的mid
return -1;前面的else可以略去,比如几次查找后成功从第一个if里面的某条分支中返回一个确定值,分析栈底函数,直接返回这个值后会结束函数而不会继续执行return -1,当然加上else更加清晰一些

这个版本递归思想很直白,我比较喜欢

int
bSearch(int a[], int l, int r, int key)
{
	int mid;
	
	if (l <= r) {
		mid = l + (r-l)/2; //use "mid=(l+r)/2;" may overflow
		if (key == a[mid]) 
			return mid;
		else if (key < a[mid])
			return bSearch(l, mid, key);
		else
			return bSearch(mid+1, r, key);		
	}
	else	
		return -1;
}



【版本二】
也是递归(成功则返回key下标,不成功返回-1)
原理就是把mid初始为-1,如果查找成功则返回的是第一个if语句里面的mid,即mid被修改为非-1;如果查找失败返回没有被修改过的mid或第一个if里面被修改过但最终修改为-1的mid。
语句return mid;前面不可以有else,假如有else,经过几次查找成功后,抵达栈顶函数时mid可能由第一个if里面第二条分支语句所赋值,如果有else则丢弃了mid值函数直接退出,因为此时else后的语句不会执行。
这个版本比【版本一】难理解
int
bSearch(int a[], int l, int r, int key)
{
	int mid = -1;
	
	if (l <= r) {
		mid = l + (l-r)/2;
		if (key == a[mid]) 
			return mid;
		else if (key < a[mid])
			mid =  bSearch(l, mid, key);
		else
			mid =  bSearch(mid+1, r, key);		
	}
	return mid;
}


【版本三】
迭代,这个最容易理解,功能也与【版本一】一样

int
bSearch(int a[], int l, int r, int key)
{
	int mid;
	
	while (l <= r) {
		mid = l + (r-l)/2;
		if (key == a[mid]) 
			return mid;
		else if (key < a[mid])
			r = mid;
		else
			l = mid+1;
	}
	return -1;
}



几个解释:

1、为什么查找失败时不返回大于key的最小元素的下标呢?l不是刚好指到那里吗?

因为如果key > 有序表的最大元素,这时l只能指向小于key的最大元素,而其他时候l指向大于key的最小元素。所以返回小于key的最大元素下标,则任意情况下失败时的返回值,其意义都是统一的。

【更正】:

当采用[l, mid]和[mid+1, r]划分,且a[l] <= key <= a[r]时,如果查找不成功,那么l等于大于key的最小元素下标,l-1也就等于小于key的最大元素下标。

尤其注意前提条件有两个:一是采用[l, mid]和[mid+1, r]划分;二是满足a[l] <= key <= a[r]。

对于key<a[l]和key>a[r]的情况最好另外处理。


2、为什么当前发生a[mid] != key 时, 不查找[l, mid-1]或[mid+1, r], 而是查找[l, mid]或[mid+1, r]?

对于第一种划分,若仅实现查找成功返回key的下标失败返回-1则是正确的,但是不能返回小于key的最大元素下标,比如查找1 3 5 9 11,查找6失败指向9,查找10失败也指向9,即失败时(r == l)时l指向的元素与key的大小关系不是确定的。因此用后者可以同时实现二分查找失败时返回小于key的最大元素下标或大于key的最小元素下标。注意key的范围。


3、为什么不使用mid = (l+r)/2?

防止(l+r)时溢出,改成l/2+r/2没问题,但不如l+(r-h)/2效率好。当然这里已经假定函数接受的参数是合法的,所有没有作参数检查。但是上述情况是合法的参数调进却可能发生溢出,这就是算法实现有bug了。



补充 :今天做题时才发现自己沙茶了,既然key可能大于a[l],也可能小于a[r]

现在的问题是, 给定整数S、E,满足S < E,如何在有序表中找出下标距离最大的两个元素X,Y,满足S<=X<Y<=E?

可以分5类情况:

设X=a[left],Y=a[right],则有——

若E<a[l] || S>a[r], 则找不到;

若S<=a[l] && a[r]<=E,则left = l; right = r;

若a[l]<=S && E<=a[r],则left = i, 第一个a[i]>=S;  right = i-1, 第一个a[i]>E;

若S<=a[l] && a[l]<E<a[r],则left = l; right = i-1, 第一个a[i]>E;

若E>=a[r] && a[l]<S<a[r],则left = i, 第一个a[i]>S;  right = r;


下面假定只考虑情况3——

1、如果用直接查找,对于左边,则查找到>=S时,存当前下标,对于右边,则查找到>E时,存前一个元素的下标

for (i = 0; a[i] < S, i++) NULL;
left = i;
for (i = 0; a[i] <= E, i++) NULL;
right = i-1;
2、用二分查找呢?

对情况3,有a[l] <= key <= a[r]

int
bSearch(int mark, int a[], int l, int r, int key)
{
	int mid;

	while (l <= r) {
		mid = l+(r-l)/2;
		if (key == a[mid])
			return mid;
		else if (key < a[mid])
			r = mid;
		else
			l = mid+1;
	}
	if (mark == 0)
		return l;//返回大于key的最小元素下标
	else
		return l-1;//返回小于key的最大元素下标
}

查找左边则调用

left = bSearch(0, a, l, r, S);


查找右边则调用

right = bSearch(1, a, l, r, E);




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值