二分搜索手动实现

附上参考的大佬的文章链接https://baobaobear.github.io/post/20210507-binsearch/
看了大佬写的二分搜索杂谈,感觉瞬间就顿悟了,大佬真的写的非常非常清楚!orz (原来一直对二分里循环,判断时的等于要不要加,l=mid/l=mid+1,r=mid/r=mid-1分的不是很清楚,写的时候也觉得挺混乱的) 然后在此参考大佬文章总结了一部分,做个笔记。

一: 二分搜索找值

1:递归写法

int binary_search(const int arr[], int start, int end, int key)
{
	if (start > end)
		return -1;
	int mid = start + end >> 1;
	if (arr[mid] == key)
		return mid;
	else if (arr[mid] > key)
		binary_search(arr, start, mid - 1, key);
	else
		binary_search(arr, mid + 1, end, key);
}

2:非递归写法

//非递归写法
int binary_search(int arr[], int start, int end, int key)
{
	while (start <= end)
	{
		int mid = start + end >> 1;
		if (arr[mid] == key)
			return mid;
		else if (arr[mid] > key)
			end = mid - 1;
		else
			start = mid + 1;
	}
	return -1;
}

二:二分查找边界

有四种边界情况:借用大佬的例子。对于不同的边界,我们二分的写法也会有区别。

1 1 1 2 2 5 5 5 5 7 9
        ^ 1 小于的最右元素
          ^ 2 大于等于的最左元素
                ^ 3 小于等于的最右元素
                  ^ 4 大于的最左元素

实现代码如下(闭区间写法)

/*
#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
int book[200005];
int main()
{
	int t;
	cin >> t;
	while (t--)
	{
		int n, m;
		int num;
		cin >> n >> m;
		memset(book, 0, sizeof(book));
		for (int i = 0; i < m; i++)
		{
			cin >> num;
			book[num]++;
		}
		int l = 1, r = 2 * m, mid;
		while (l < r)
		{
			mid = (l + r) / 2;
			int a = 0, b = 0;//以a,b记录每个时间到选定中间时间所差的时间还能处理的事件数
			for (int i = 1; i <= n; i++)
			{
				if (book[i] > mid)
					a += book[i] - mid;//当处理擅长事务的时间(处理每件擅长事务所需时间为1(事务数=时间数))大于选定中间时间时,说明是以时间1处理每件事情,a记录能处理的事务数
				else
					b += (mid - book[i]) / 2;//当处理擅长事务的时间小于选定中间时间时,说明剩下的时间能处理从别的员工匀出的多的事务,由于此时处理一件事务需要时间2,所以能处理的事务数=时间数/2
			}
			if (a > b)
				l = mid + 1;
			else
				r = mid;
		}
		cout << l << endl;
	}
	return 0;
}
*/
#include<iostream>
using namespace std;
//1:找到小于val的最右元素
int bin_search_1(int arr[], int len, int val)
{
	int l = 0, r = len - 1;
	while (l < r)
	{
		int mid = l + r + 1 >> 1;
		if (arr[mid] >= val)
			r = mid - 1;
		else
			l = mid;
	}
	return l;
}

//找到大于等于val的最左元素
int bin_search_2(int arr[], int len, int val)
{
	int l = 0, r = len - 1;
	while (l < r)
	{
		int mid = l + r >> 1;
		if (arr[mid] < val)
			l = mid + 1;
		else
			r = mid;
	}
	return l;
}

//找到小于等于val的最右元素
int bin_search_3(int arr[], int len, int val)
{
	int l = 0, r = len - 1;
	while (l < r)
	{
		int mid = l + r + 1 >> 1;
		if (arr[mid] > val)
			r = mid - 1;
		else
			l = mid;
	}
	return l;
}

//找到大于val的最左元素
int bin_search_4(int arr[], int len, int val)
{
	int l = 0, r = len - 1;
	while (l < r)
	{
		int mid = r + l >> 1;
		if (arr[mid] <= val)
			l = mid + 1;
		else
			r = mid;
	}
	return l;
}

//找到大于等于val的最左元素(2)
//当终止条件为l<=r时,l要等于mid+1,r要等于mid-1
int bin_search_5(int arr[], int len, int val)
{
	int l = 0, r = len - 1;
	while (l <= r)
	{
		int mid = l + r >> 1;
		if (arr[mid] < val)
			l = mid + 1;
		else
			r = mid - 1;
	}
	return l;
}

int main()
{
	int arr[11] = { 1,1,1,2,2,5,5,5,5,7,9 };
	cout << bin_search_1(arr, 11, 5) << endl;
	cout << bin_search_2(arr, 11, 5) << endl;
	cout << bin_search_3(arr, 11, 5) << endl;
	cout << bin_search_4(arr, 11, 5) << endl;
	cout << bin_search_5(arr, 11, 5) << endl;
	return 0;
}

三:上述闭区间写法坑点

(大佬原文,略有一点改动(表示字符改成和上面代码中一致了),整体思路不变)
1:区间表示
如果采用闭区间,那么循环的条件就是l < r,当这个条件满足时,这个区间就表示了两个或以上的元素,同理,如果采用半开半闭区间,那么循环的条件就是l + 1 < r,如果采用开区间,那么循环条件就是l + 2 < r。
2:比较
比较的方式取决于你的边界是左边界还是右边界,如果是左边界,那么边界左边的数均为小于,而右边的数是大于等于,那么你应该用 arr[mid] < val作为判断条件;同理地,如果是右边界,那么边界左边的数均为小于等于,而右边的数是大于,这时应该用arr[mid] <= val作为判断条件。
3:更新区间
在比较后,更新区间时,有的+1有的-1有的不需要加减,这是怎么决定要不要加呢?这个由你所查找的区间是否包含这个数决定。举个具体例子,在4 5 5 6中,给出要查找的数值5,要查找到小于5的最大那个数,即4,那么比较方式是arr[mid] < val。如果这个小于号满足,比如说arr[mid]是4,那么这个数是可能在查找区间内,所以不应该+1或-1,但如果arr[mid]是5,这个数不应该在查找区间内,那么就果断要+1或-1。在边界查找写法里,必然有一边是需要+1或-1,而另一边不需要,这个会影响下面要介绍的中间数选择。
4:中间数的选择
中间数选择,即以上代码中的变量mid,有时取mid=l+r>>1,有时取mid=l+r+1>>1,这个写法取决于当r-l=1(即l,r取到两个相邻的数时),此时m要么等于l,要么等于r。为了这个循环能结束,如果下方代码是l = m,那么m要取r,即要+1,如果下方代码是r = m,那么m要取l,不需要+1。这个如果写错就会导致死循环的发生。
另外,还有一个更多人犯的错误,很多人会写m = (l + r) / 2,当然,这个其实在不少情况下确实也不会怎么样,但是,如果我们要做的事情并不是在数组中查找,而是在一个区间里面,比如找一个方程的整数解,或者求3次方根的整数部分,这就会产生问题了,当 l和r是负数的时候,与它们是正数的时候,除以2的含义是不一样的,C语言中的除法实际上是向0取整,比如说,-7/2 == -3,但是,在二分搜索时,我们必须要么向上取整,要么向下取整,但存在负数时,这样做除法可能会导致二分死循环。采用(r - l) / 2(我理解是l+(r-l)/2简写了)可以避免这个问题,或者,你还可以使用位运算技巧(r - l) >> 1。当然,为了减少中坑起见,写m = l + (r - l) / 2最保险。(我较习惯写l+r>>1)
大佬还写了更不容易出错的代码,这里就不展示了,强烈建议看大佬原文!真的很优秀!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值