二分查找的边界问题

考虑一个问题:给定正序排列的数组,A,有 n 个元素,找出不大于 M 的元素中最大的元素的下标。

这个问题要考虑以下几个边界条件:

1.不大于 M 的元素中最大的元素可能就是 M 

2.不大于 M 的元素中最大的元素不是M

另外,还有隐形的边界问题

1.查找何时结束。在二分查找中,一般是以等于查找元素或者待查找的部分为空作为结束条件。在这个问题中,实际上也是以为这两个条件的或作为结束条件。

2.查找的过程中,设当前查找部分为 i ~ j,我们用 A[(i + j)/2]去定位下次应该在左边部分,还是右边部分查找。因此会有下面的代码:

while(...)
	{
		mid = (i + j) >> 1;
		if (M == A[mid])
		{
			pos =  mid;    //记录找到的位置
			break;
		}
		else if(M < A[mid])
		{
			j = mid - 1;    //下次去左边部分查找
		}
		else
		{
			i = mid + 1;    //下次去右边部分查找
		}
	}


但是这个代码忽略了一种情况,我们设想一下,如果现在等查找的部分是下标为13 ~ 17 的元素,值是 11,23,44,100,200, 令 M = 70;此时 A[mid] = A[(13 + 17)/2] = A[15] = 44,按上面的代码,下一次查找的

范围是 16 ~ 17 元素。但我们看上面的值,它们都大于 M,因此,不可能在剩下的元素中找到不大于 M 的元素中最大的元素。很明显,说明上一次划分下一次查找部分的地方出错了。

我们先这样考虑,当 A[mid] < M时,将 mid 元素也纳入下一次查找的范围,因为 A[mid] 是小于 M的,这样就保证了划分到的范围必然能够找到解。

但如果我们考虑查找的部分是下标为 4 ~ 5 的元素,值是 4,7,令 M = 70,A[mid] = A[(4 + 5)/2] = A[4] < M,如果按上面的思路,我们将 A[mid] 纳入到下一次查找的范围,因此,下次查找的范围是 4 ~ 5,又是 4 ~5 !于是,按这个处理会陷入死循环!

所以,上面的思路是行不通的!

所以,当 A[mid] < M时,不能将 mid 元素纳入下一次查找的范围。(结论一)

我们再考虑 A[mid] > M 时,显然没有必要将 mid 元素纳入下一次查找的范围,因为我们查找的元素是小于 M的,没有必要将一个不可能成为结果的元素纳入查找。实际上,如果纳入查找,也可能形成死循环。读者可以自行举例。

所以,当A[mid] > M时,不能将 mid 元素纳入下一次查找的范围。(结论二)

因此,我们循环里面的执行流程结构是不会改变的,还是上面给出代码的结构。

由结论一,不将 mid 加入下次查找的范围,那么,下一次查找的范围的元素都是大于 M 的,此时,我们有两种处理手段:

1.让其继续寻找,依据结论一和结论二,我们避开了死循环的机会,因此,继续寻找最终必定会处理完成。

2.我们每次检查下次查找的范围的首个元素 A[mid + 1] (注意判断 mid + 1 是不是有效的下标),若 A[mid + 1] > M,则 mid 元素就是小于 M 的最大元素。若 A[mid + 1] = M ,直接返回结果。剩下的 A[mid + 1] < M 继续查找。

但是上面还没有回答一个问题,结果如何给出?

实际上,我们的处理过程,每次都可以找出 A[mid] < M ,那么,它就是一个潜在的答案,我们只需要保存下来,并且去更新就行了,最终一定可以得到答案。给出代码。

int maxLessThanEmePos(int M)
{
	int pos = -1;
	int mid;
	int i = 0,j = ARRAYSIZE-1;
	bool continue_ = true;
	while(continue_)
	{
		mid = (i + j) >> 1;
		continue_ = (!(i == j));	//一旦寻找的部分只有一个元素,只需要往后再寻找一次了
		if (M == A[mid])
		{
			pos = mid;
			break;
		}
		else if(M < A[mid])
		{
			j = mid - 1;
		}
		else
		{
			i = mid + 1;
			pos = mid;	//每个 A[mid] < M 都是一个满足条件的答案,且慢慢地变大
		}
	}
	return pos;
}

最后,还有一个问题。

查找结束的条件,真的是“等于查找元素或者待查找的部分为空”吗?也就是上面代码中的,continue_ = (!(i == j)) 是正确的处理吗?答案是,不正确!

按代码的逻辑,可能出现在一堆大于 M 的元素中寻找结果。假设现在还剩下两个元素 i ~ j ,值是 100,200。A[mid] 必将大于 M,因为这一堆元素都比 M 大。mid = (i + j)/2 = i,下一次查找的范围是 i ~ mid - 1,即 i ~ i-1,此时,应该退出查找,因为查找的部分为负范围,是无效的范围。因此,正确的代码是将上面那句改成: continue_ = (!(i <= j))。


毫无疑问,上面的第 2 种处理效率更高,因为它能避开很多无畏的查找,因此,最终给出一个效率稍高的实现。

int A[ARRAYSIZE] = {1,2,3,4,5,7,24,44,100,200};
int maxLessThanEmePos(int M)
{
	if(A[0] > M)
	{
		return -1;
	}
	else if(A[0] == M)
	{
		return 0;
	}
	else
	{
		//往后继续处理
	}
	int pos = -1;
	int mid;
	int i = 0,j = ARRAYSIZE-1;
	bool continue_ = true;
	while(continue_)
	{
		mid = (i + j) >> 1;
		continue_ = (!(i >= j));	//一旦寻找的部分只有一个元素,只需要往后再寻找一次了
		if (M == A[mid])
		{
			pos = mid;
			break;
		}
		else if(M < A[mid])
		{
			j = mid - 1;
		}
		else
		{
			i = mid + 1;
			if(A[i] > M)
			{
				pos = mid;
				break;
			}
			else if(A[i] == M)
			{
				pos = i;
				break;
			}
			else
			{
				pos = mid;	//每个 A[mid] < M 都是一个满足条件的答案,且慢慢地变大
			}
		}
	}
	return pos;
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值