问题描述:
对于书中提到的二分查找中常见错误,见代码:
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;
}