浅谈二分查找
二分思想简单 , 但细节是魔鬼.
学习博客(传送门)
正如原文所说的 , 二分中情况多变 , 代码习惯很重要 , 比如建议全打 else if .
以下在原文的基础上写了些自己的看法.
样例 : 1 2 2 3 3 3 4 5
\(①\) \(:\) 正常的二分查找 ( 如果目标数在数列中不止一个 , 输出的位置是随机的. )
int binarySearch(int target){
int left = 1,right = n;
while(left<=right){
int mid = (left + right)/2;
if(a[mid] == target) return mid;
else if(a[mid] < target) left = mid+1;
else if(a[mid] > target) right = mid-1;
}
return -1;
}
自己可以手模几组数据.
如果目标数存在 , 会 return mid .
如果目标数不存在 , 由于left = mid+1 或 right = mid-1 , 会退出 , 不会卡死.
\(②\) \(:\) 目标数不止一个 , 输出目标数最左边的位置.
先上代码吧
int left_Search(int target){
int left = 1,right = n;
while(left<right){
int mid = (left + right)>>1;
if(a[mid] == target) right = mid;
else if(a[mid] < target) left = mid+1;
else if(a[mid] > target) right = mid-1;
}
return a[left] == target?left:-1;
}
前置 \(:\) 样例 \(①\) 1 2 3 3 3 3 4 5 , 样例 \(②\) 1 2 2 3 3 3 4 5.
\(①\) \(:\) 为什么是left<right , 而不是 left<=right.
因为我们不能像普通二分查找( left<=right )到目标值就立即返回位置 , 找最左边就要贪心地认为左边还有目标值 , 找最右边就要贪心地认为右边还有目标值 , 如果是 left<=right , 则会陷入死循环 , 这是因为我们处理a[mid] == target的方法是令 right = mid (因为 mid 可能已经是答案了,是最左边了) , 而不是直接返回位置.
\(target : 2\) (假设是 left<=right )
样例 \(①\) \(:\) 会在 $ left = 2 , right = 2 , mid = 2$ 陷入死循环.
样例 \(②\) \(:\) 会在 $ left = 3 , right = 3 , mid = 3$ 陷入死循环.
原因都是最后会缩为一个数 , left == right 需要条件 left < right 终止.
\(②\) \(:\) if(a[mid] == target) right = mid; 在 \(①\) 已经提及了原因.
\(③\) \(:\) 与原作不同的是 \(:\) right = n+1 else if(a[mid] > target) right = mid;
原作引入 [left,right) 的概念 , 它始终认为 right 不是答案 , right = n+1 else if(a[mid] > target) right = mid; 自然成立 . 其实这里 right = mid-1 与 right = mid 最终效果相同 , 因为 if(a[mid] == target) right = mid; 会 "帮助" right = mid 貌似不正确的答案 "前移" , 这里我的表达能力有限 , 对比一下试一试样例就知道了 , 所以我一开始选择的范围是 [1,n] .
\(③\) \(:\) 目标数不止一个 , 输出目标数最右边的位置.
先上代码吧
int Search(int target){
int left = 1,right = n;
while(left<right){
int mid = (left + right)/2;
if(a[mid] == target) left = mid+1;
else if(a[mid] < target) left = mid+1;
else if(a[mid] > target) right = mid;
}
//return left-1;
//updata : 有人提出hack : 1 2 3 5 5 5 5 5 , target = 5 , 找出来是7 , ans = 8.
//这是因为我们希望left = mid+1 , 一直移动到right(最近的错误答案位置);
//但hack数据right一直没动 , 它一直在正确答案位置 , 进行一下修改.
if(a[left] == target) return left;
if(a[left-1] == target && left-1 != 0) rturn left - 1;
return -1;
}
前置 \(:\) 样例 \(①\) 1 2 3 3 3 3 4 5 , 样例 \(②\) 1 2 2 3 3 3 4 5.
\(①\) \(:\) 为什么是 if(a[mid] == target) left = mid+1; , 再 return left-1; , 这不是多此一举 \(?\)
为什么不跟前面求左边的情况一样去保留 \(mid\) \(?\)
先看样例 \(②\) : 1 2 2 3 3 3 4 5 , 会发现会在[3,4]区间卡死 , ( \(3+4\) ) / \(2\) = \(3\) , a[3] = 2 = target ,
left = mid 两个相邻的数相加除 \(2\) 会偏向前者 , 在最后两个数中出答案会导致出现死循环 , 把求左边第一个数的代码改为 left = mid , 一样会出现这种情况的死循环.
\(②\) \(:\) else if(a[mid] > target) right = mid; 一定不能是 right = mid-1 , 因为 left 的位置是 ans+1 , right = mid - 1可能会提前导致left跳到区间 [1,ans] , 一下子 left = ans , 一下子 left = ans + 1 肯定不合理 , 反正是右逼近 , right = mid 是没有问题的 , (有 mid = left+1的“帮助” ).
上述情况参考样例 \(:\) 1 3 3 4 4 4 5 6 ,\(target =\) 3或4 时 , 两者对比即可.
追更 \(:\)
针对于第二种情况 , 算法竞赛进阶指南指出中 \(mid =\) \((l+r)>>1\) 不会取到 \(r\) , \(mid =\) \((l+r+1)>>1\) 不会取到 \(l\) .
对此 , 我们对第二种情况给出更好理解的代码.
int Search(int target){
int left = 1 , right = n;
while(left<right){
int mid = (left+right+1)>>1;
if(a[mid] == target) left = mid;
else if(a[mid] < target) left = mid+1;
else if(a[mid] > target) right = mid-1;
}
return a[left] == target ? left : -1;
}

浙公网安备 33010602011771号