[编程之美] PSet3.11 程序改错:二分查找与扩展

问题描述:

        对于书中提到的二分查找中常见错误,见代码:

int binSearch(int num[] ,int val ,  int begin , int end)//num数组已按照从小到大顺序排列
{
	while(begin < end){
		int mid = (begin+end)/2;
		if(val >= num[mid])
			begin = mid;
		else
			end = mid-1;
	}
	return num[begin] == val ? begin:-1;
}

       
分析:本程序有两个错误,

        第一个是mid求解时容易发生上溢,因为begin+end很容易超过int所能代表的最大值,可以考虑改为int mid = min+(max-min)/2;

        第二个是当程序运行到begin = end-1的时候,计算出的mid就与begin相等,如果这时不幸的是val >= num[mid],那么将会陷入死循环,不停的计算mid的值,mid一直等于begin,又令begin = mid相当于begin = begin死循环。

        主要的问题出在begin与end相邻的时候,mid = begin (由int除2截断取整决定的),可见如果发现有begin = mid赋值的情况一定要当心死循环,end=mid就不会出现死循环,分析可知跳出循环时end = mid = begin。于是单独的分离出相邻时的情况,单独处理:

int binSearch(int num[] ,int val ,  int begin , int end)//num数组已按照从小到大顺序排列
{
	while(begin < end-1){//剔除了相邻的情况
		int mid = (begin+end)/2;
		if(val >= num[mid])
			begin = mid;
		else
			end = mid-1;
	}
	if(num[begin] == val)
		return begin;
	else if(num[end] == val)
		return end;
	else 
		return-1;
}

      接下来总结一下二分查找常见问题:

      二分查找需要遵循一定的区间开闭原则,如果输入begin与end是[begin,end]型或是[begin,end),循环体内也要按照这样的方式去赋值,否则会造成错误。下面给出几个例子:

int binSearch(int num[] ,int val ,  int begin , int end)//num数组已按照从小到大顺序排列
{
	//---如果end = numLen的话,给出的区间是[begin , end)
	while(begin < end){
		int mid = begin+(end-begin)/2;
		if(val == num[mid])
			return mid;
		else if(val < num[mid])
			
			end = mid-1;//本处按照右端点闭区间赋值,与
			//end = mid;//对区间右端点赋值,注意按照开区间(与输入一致)来赋值
		else
			begin = mid+1;
	}
	return-1;
}

       这个例子输入区间为半开半闭,但是循环体内赋值是按照全闭空间来赋值的。当val<num[mid]时,查找空间应该缩减为[begin , mid),而不是[begin,mid-1),如果刚好num[mid-1]==val则恰好找不到这个元素。

int binSearch(int num[] ,int val ,  int begin , int end)//num数组已按照从小到大顺序排列
{
	//---如果end = numLen的话,给出的区间是[begin , end]
	while(begin < end){
		int mid = begin+(end-begin)/2;
		if(val == num[mid])
			return mid;
		else if(val < num[mid])
			end = mid;//应该为[begin,mid-1]
		else
			begin = mid;//应该为[mid+1,end]
	}
	return-1;
}

       本例中,依代码中注释部分所言,两个边界的选择都出了问题。可能造成某次查找始终在这个范围查找,造成程序的死循环。举个例子,假设num[]={1,3},如果要查找3这个数,begin=0,end=1则算得mid=(0+1)/2=0=begin;此时num[mid]=num[begin]<val,如果按照程序又会令mid=begin造成死循环。

       扩展问题:

       给定一个有序(升序)数组arr

      1.求任意一个i使得arr[i]等于v

      2.求最小的i使得arr[i]等于v

      3.求最大的i使得arr[i]等于v

      4.求最大的i使得arr[i]小于v

      5.求最小的i使得arr[i]大于v

      如果不存在则返回-1;

     代码如下(统一按全闭空间输入):

//问题1:给定一个有序(升序)数组arr,求任意一个i使得arr[i]等于v,不存在返回-1
//这是较为常见的解法,由于对下标返回没有限制因此边界条件不需考虑太多
int binSearch_1(int num[] ,int val ,  int begin , int end)
{
	while(begin < end){
		int mid = begin+(end-begin)/2;
		if(val == num[mid])
			return mid;
		else if(val < num[mid])
			end = mid-1;
		else
			begin = mid+1;
	}
	if(num[begin] == val)
		return begin;
	return-1;
}

       问题一解法也可以分离相邻的情况,不过本例较为简单,不需要这样做。下面是网上版本的问题2:

//问题2:求最小(最大)的i使得arr[i]等于v,不存在返回-1
int binSearch_2(int num[] ,int val ,  int begin , int end)
{
	if(end <=0)
		return -1;
	while(begin<end){
		int mid = begin+(end-begin)/2;
		if(val == num[mid]){
			end = mid;//不发生错误,相邻时mid=begin,end=mid=begin,循环结束
			//begin = mid;//发生错误,如果begin和end相邻且元素相等,mid=begin=mid,死循环
		}
		else if(val < num[mid])
			end = mid-1;
		else
			begin = mid+1;
	}
	if(num[begin] == val)//跳出循环一定是begin=end
		return begin;
	else 
		return -1;
}

       网上的版本出现的问题是边界逻辑非常复杂,还有一个这样的版本:

//问题2:求最小的i使得arr[i]等于v,不存在返回-1
int binSearch_2(int num[] ,int val ,  int begin , int end)
{
	if(end <=0)
		return -1;
	int last = -1;//保存上次的节点下标
	while(begin<=end){
		int mid = begin+(end-begin)/2;
		if(val == num[mid]){
			last = mid;
			end = mid-1;//找到了这样的数,向前遍历
		}
		else if(val < num[mid])
			end = mid-1;
		else
			begin = mid+1;
	}
	return last;//弹出循环时begin<end,向前遍历的最后一个等于val的数下标保存在last中
}

       总而言之挺复杂的,下面是我实现的版本,当出现边界逻辑混乱时直接分离相邻的情况就能搞定:

//问题2:(另解)求最小(大)的i使得arr[i]等于v,不存在返回-1。
// 建议:如感觉很难把握边界条件,则尽量分离相邻的情况
int binSearch_3(int num[] ,int val ,  int begin , int end ,bool minIndex)
{
	while(begin < end-1){//当begin=end-1相邻时循环结束
		int mid = begin+(end-begin)/2;
		if(val == num[mid]){
			if(true == minIndex)//如果返回最小下标
				end = mid;
			else//返回最大下标
				begin = mid;
		}
		else if(val < num[mid])
			end = mid-1;
		else
			begin = mid+1;
	}
	int result = -1;//返回的结果下标
	if(true == minIndex){//优先返回较小的下标
		if(num[begin] == val)
			result =  begin;
		else if(num[end] == val)
			result = end;
	}
	else{//优先返回较大的下标
		if(num[end] == val)
			result =  end;
		else if(num[begin] == val)
			result = begin;
	}
	return result;
}

         问题四、五的解法可以直接在上述代码上修改解决,所谓的使得arr[i]小于val的最大下标就是最小的等于v下标减去1,而使得arr[i]大于val的最小下标就是最大的等于v下标加上1,看返回值是否越界就知道是否存在了。代码如下:

int binSearch_3(int num[] ,int val ,  int begin , int end ,bool minIndex)
{
	int tempL = begin;
	int tempR = end;
	while(begin < end-1){//当begin=end-1相邻时循环结束
		int mid = begin+(end-begin)/2;
		if(val == num[mid]){
			if(true == minIndex)//如果返回最小下标
				end = mid;
			else//返回最大下标
				begin = mid;
		}
		else if(val < num[mid])
			end = mid-1;
		else
			begin = mid+1;
	}
	int result = -1;//返回的结果下标
	if(true == minIndex){//返回小于val的最大数下标
		if(num[begin] == val)
			result =  begin;
		else if(num[end] == val)
			result = end;
		result--;
	}
	else{//返回大于val的最小数下标
		if(num[end] == val)
			result =  end;
		else if(num[begin] == val)
			result = begin;
		result++;
	}
	if(result<tempL || result>tempR)
		return -1;
	return result;
}

    原文作者:天剑客
    原文地址: https://blog.csdn.net/spaceyqy/article/details/38744709
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞