二分法代码分析

二分法代码分析

  二分搜索又称为折半搜索。使用二分时,要确保数列是具有有序性的,通过比较中间值,不断将搜索范围缩小为原来的一半,大大缩短了查找的时间,其时间复杂度为 O ( log ⁡ n ) O(\log {n}) O(logn)

  在算法竞赛中,二分使用的频率十分广泛,常见的二分问题包括:判断某个值是否出现在数组中,如果出现则求出坐标;找出第一个比X值大的数的坐标;X值第一次出现在数组中的位置等。这些问题都可以统称为“从有序数组中查找值”,求解的过程大致相似,区别在于二分的迭代条件不同,需要根据具体的情况调整。

  具体调整的内容有四块:
  ① leftright的初始值
  ② while循环的条件
  ③ 边界变换
  ④ 返回值


1、常规二分

  寻找某一特定值,找到即可返回,其过程为:先找到搜索区间[l, r],然后对mid位置值与待查找值的大小。当value[mid]大于待查找的值时,说明待查找的值位于 [l, mid-1]内;当value[mid]小于待查找的值时,说明待查找的值位于[mid+1, r]内;若二者值相等,则成功匹配。代码如下:

int binarySearchNormal(int arr[], int length, int x)	// 数组、数组长度、待查找值
{
	int l = 0, r = length - 1, mid;		// 搜索空间为[l, r],即左闭右闭,因此决定了下方while的跳出条件

	while (l <= r) {					// 当l == r时,搜索空间[l, r]仍不为空,所以要继续循环,因此
		mid = l + (r - l) / 2;
		if (arr[mid] == x) {
			return mid;
		} else if (arr[mid] > x) {		// 此时,x在mid的左边,又因为mid不符合要求,搜索空间应剔除mid,即为[l, mid-1]
			r = mid - 1;
		} else if (arr[mid] < x) {		// 此时,x在mid的右边,右因为mid不符合要求,搜索空间应剔除mid,即为[mid+1, r]
			l = mid + 1;
		}
	}

	return -1;
}

注:常规的二分都知道,就不区分左闭右开,左开右闭,左闭右闭,左开右开的搜索空间了,一般用左闭右闭即可

2、寻找右边界

  见于“最大的最小值 ”问题,如POJ2456,即判断mid值是否满足要求,满足的话则向右搜索,不满足则向左搜索。搜索空间可以是左闭右闭,也可以是左闭右开。

左闭右闭代码:

// 寻找右边界,左闭右闭(CC, Closed Closed)
int binarySearchRigthCC(int l, int r)
{
	int mid;							// 搜索空间为[l, r],寻找右边界 注意:因为是左闭右闭区间,因此输入的l及r都应是可取点,如
										// 当搜索对象为某一数组arr时,应搜索arr[0]~arr[length-1],而如果是左闭右开区间,则可搜索arr[0]~arr[length],详见左闭右开部分
	
	while (l <= r) {					// 当l == r时,搜索空间为[l, r]不为空,可继续搜索,因此这里要加等号,即l <= r,也即l = r + 1时,退出循环
		mid = l + (r - l) / 2;
		if (judge(mid)) {				// judge()函数,用来判断该值是否条件

			l = mid + 1;				/* 寻找右边界,如果该mid可行,说明可以继续向右寻找,可令左边界为mid,即l = mid,然而,
										 * mid = (l + r) / 2,所以mid总是不小于l,如果令l = mid,可能导致后续循环mid值不变,如,
										 * 进入循环后,当l = r时,mid = (l + r) / 2 = l = r,此时令l = mid后,
										 * 下次循环时,mid值不变,仍然满足:judge(mid)为真,及mid = l = r,陷入死循环,
										 * 即便l = r - 1时,mid = (l + r) / 2 = l,如果令l = mid,同样导致死循环,
										 * 因此不能赋值l = mid */
		} else {
			r = mid - 1;				// 既然是寻找右边界,如果该mid不可行,说明需要往回即向左找,因此右边界最大在mid - 1,而搜索空间是右闭区间,所以令r = mid - 1
		}
	}

	return l - 1;						/* 跳出while,说明此时l = r + 1
										 * 如果最后一次循环进入的是if,即judge(mid)为真时,说明mid可行,因此可输出mid或l - 1,而l = r + 1,所以也可以输出r
										 * 如果最后一次循环进入的是else,即mid不可行,因此可以输出mid - 1或r,而l = r + 1,因此也可以输出l - 1
										 * 综上,应输出l - 1或r */
}

左闭右开代码:

// 寻找右边界,左闭右开(CO,CLosed,Open)
int binarySearchRigthCO(int l, int r)
{
	int mid;							/* 搜索空间为[l,r),寻找右边界 注意:因为是左闭右开区间,因此输入的r可以是不可取的点,因为,
										 * 即便r是不可取的,但右边是开区间,也会把r排除在外	*/

	while (l < r) {						// 当l == r时,搜索空间为[l, r)为空,不可继续搜索,因此这里是l < r,而不能加等号
		mid = l + (r - l) / 2;
		if (judge(mid)) {
			l = mid + 1;				// 当mid可行时,应继续向右搜索,因此令l = mid + 1。同样地,如果令l = mid,则可能进入死循环
		} else {
			r = mid;					// 因为右边是开区间,所以可以即便当前mid不可行,也可令r = mid,因为搜索空间取不到该值
		}
	}

	return l - 1;						/* 跳出while,说明此时l = r
										 * 如果最后一次循环进入的是if,即judge(mid)为真时,说明mid可行,因此可输出mid,而又l = mid + 1,所以可输出l - 1或r - 1
										 * 如果最后一次循环进入的是else,即judge(mid)为假时,说明mid不可行,因此可输出mid - 1,而r = mid,所以可以输出r - 1或l - 1
										 * 综上,可以输出l - 1或r - 1	*/
}

3、寻找左边界

  见于“最小的最大值 ”问题,即判断mid值是否满足要求,满足的话则向左搜索,不满足则向右搜索。搜索空间可以是左闭右闭,也可以是左开右闭。

左闭右闭代码:

// 寻找左边界,左闭右闭
int binarySearchLeftCC(int l, int r)
{
	int mid;
	while (l <= r) {					// 退出条件为l = r + 1
		mid = l + (r - l) / 2;
		if (judge(mid)) {
			r = mid - 1;				// 此mid可行,则应继续向左寻找,可令右边界为mid,即r = mid,然而,
										// 当l = r时,mid = (l + r) / 2 = l = r,此时如果令r = mid,则会陷入死循环
		} else {
			l = mid + 1;				// 既然该mid不可行,应排除,且应往回即向右找,从mid + 1开始,又因为是左闭区间,所以令l = mid + 1
		}
	}

	return r + 1;						/* 跳出while,说明此时l = r + 1
										 * 如果最后一次进入的是if,即judge(mid)为真时,说明此mid可行,可输出mid,而又r = mid - 1,所以可输出r + 1或l
										 * 如果最后一次进入的是else,说明该mid不可行,可输出mid + 1,而又l = mid + 1,所以可输出l或r + 1
										 * 综上,可以输出r + 1或l	*/
}

左开右闭代码:

// 寻找左边界,左开右闭
int binarySearchLeftOC(int l, int r)
{
	int mid;
	while (l < r) {						// 退出条件为l = r
		mid = l + (r - l) / 2;
		if (judge(mid)) {
			r = mid;					// 此时mid可行,则应继续向左寻找,可令右边界为mid,即r = mid。注意:因为进入while的条件是l < r,所以mid总是小于r,此处不会陷入死循环
		} else {
			l = mid + 1;				// 因为是左开区间,所以即便是mid不可行,也可令l = mid,因为搜索空间搜不到该值,然而,
										// 进入循环后,当l = r - 1时,mid = (l + r) / 2 = l,此时如果令l = mid,则下次循环mid值不变,进而陷入死循环。
		}
	}

	return r;							/* 跳出while,说明l = r
										 * 如果最后一次进入的是if,即judge(mid)为真时,说明此mid可行,可输出mid,而又r = mid,所以可输出r或l
										 * 如果最后一次进入的是else,说明此mid不可行,可输出mid + 1,而又l = mid + 1,所以可输出l或r
										 * 综上,可输出r或l	*/
}

4、总结

  ① 初始值与搜索空间相对应,想要闭区间,就将初始值设为可取值,想要开区间,则取为最边界的一个可取值的下一个。

  ② while循环条件,应根据搜索空间确定:如果搜索空间为左闭右闭,那么l <= r时,搜索空间不为空,可以继续搜索,所以应为while (l <= r);如果搜索空间为一开一闭,那么只有l < r时搜索空间才不为空,可以继续搜索,而l = r时,搜索空间为空,应退出,此时应为while (l < r)

  ③ 边界变换:考虑两个问题,一个是搜索空间的开闭,开区间的话,即便这个值不可取,但也可以用它赋值,因为开区间的话取不到该值,闭区间的话,只有这个值可取时,才能用它赋值,否则需要±1;另一个则是考虑是否会陷入死循环。

  ④ 返回值:返回的应该是最后一个可行的mid值,而mid值是不确定的,从循环退出后能确定的只有lr,因此需要根据具体情况判断应输出l还是r,还是l/r ± 1

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值