浅谈二分查找

二分思想简单 , 但细节是魔鬼.

学习博客(传送门)


正如原文所说的 , 二分中情况多变 , 代码习惯很重要 , 比如建议全打 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+1right = 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-1right = 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;
} 
posted @ 2021-10-10 17:34  xqy2003  阅读(105)  评论(0)    收藏  举报