详解二分查找算法

二分查找是通过将递增序列不断减半的方式寻找目标值的下标。其看似简单,但往往存在一些细节被忽略,即while循环判断条件和区间右边界的取值方式。另外,我们往往只知二分查找搜索目标值的功能,而忽略了二分查找的另外一个功能,即寻找最大的小于目标值的元素和最小的大于目标值的元素。

目录

一、二分查找算法的基本框架

二、二分查找之搜索目标值

三、二分查找之搜索目标值的左右边界

四、总结


一、二分查找算法的基本框架

二分查找充分利用了序列元素的递增性质,采用分治策略搜索目标值(目标值存在于序列中),目标值的左边界和右边界(目标值不存在于序列中),其中左边界指的是最大的小于目标值的元素,右边界指的是最小的大于目标值的元素。例如,有递增数列{ 2,5,7,8,10,15,16},如果搜索目标值8,则返回下标3,如果搜索目标值9,则返回-1;如果搜索目标值9的左右边界,则左边界为8(下标为3),右边界为10(下标为4)。

因此二分查找算法的目标可以分为三个:

1、查找目标值,若存在返回下标;不存在,返回-1

2、查找目标值的左边界,若序列所有元素大于目标值,返回-1

3、查找目标值的右边界,若序列所有元素小于目标值,返回序列长度

给出二分查找算法的基本框架:

int BinarySearch(vector<int>& nums, int target)
{
    int len = nums.size();
    int left = 0, right = ...;//right=nums.size()-1 或者 right=nums.size()

	while (...) //left<=right 或者 left<right,取决于right的取值方式
	{
		int mid = (left + right) / 2;
		if (nums[mid] == target)
			...; //取决于二分查找的目的和right的取值方式
		else if (nums[mid] < target)
			left = mid + 1;
		else if (nums[mid] > target)
			right = ...; //right=mid 或者 right=mid-1,取决于right的取值方式
	}
	return ...; //取决于二分查找的目的
}

如果想要充分理解二分查找程序代码的细节,那么强烈建议将判断条件表示出来,而不是使用else进行省略,熟练之后再进行省略。通过上述代码框架,可以看出通过比较序列中位元素和目标值之间的关系对序列进行分半操作。

二、二分查找之搜索目标值

首先贴出二分查找搜索目标值的两段实现代码。

int BinarySearch(vector<int>& nums, int target)
{
    int left = 0, right = nums.size();
	while (left < right)
	{
		int mid = (left + right) / 2;
		if (nums[mid] == target)
			return mid;
		else if (nums[mid] < target)
			left = mid + 1;
		else if (nums[mid] > target)
			right = mid;
	}
	return -1;
}

int BinarySearch(vector<int>& nums, int target)
{
    int left = 0, right = nums.size() - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (nums[mid] == target)
			return mid;
		else if (nums[mid] < target)
			left = mid + 1;
		else if (nums[mid] > target)
			right = mid - 1;
	}
	return -1;
}

观察上述两段代码,两者的区别在于right的初始化取值和while循环中的取值方式,我们对此进行分析:

1、第一段代码

(1)right的初始化取值为序列的长度,因为下标从0开始,所以序列长度是越界的,则初始化判断区间为[0,len),左闭右开区间,后续判断区间为[left,right)。所以while循环的结束条件为left==right,判断条件为left<right。

(2)在每次进行区间拆半操作时,即对[left,right)进行拆半,中位元素nums[mid]已经搜索完毕,使得下次搜索区间仍然为左闭右开区间,则左半区间为[left,mid),右半区间为[mid+1,right)。所以right=mid。

2、第二段代码

(1)right的初始化取值为序列的长度-1,则初始化判断区间为[0,len-1],左闭右闭区间,后续判断区间为[left,right]。所以while循环的结束条件为left>right,判断条件为left<=right。

(2)在每次进行区间拆半操作时,即对[left,right]进行拆半,中位元素nums[mid]已经搜索完毕,使得下次搜索区间仍然为左闭右闭区间,则左半区间为[left,mid-1],右半区间为[mid+1,right]。所以right=mid-1。

综上,可以得出right的赋值方式和while循环判断条件与right的初始化取值的关系:

right初始化取值right取值方式while循环判断条件
right=nums.size()right=midleft<right
right=nums.zie()-1right=mid-1left<=right

三、二分查找之搜索目标值的左右边界

再次回顾左右边界的概念。左边界指的是递增序列中最大的小于目标值的元素,右边界指的递增序列中最小的小于目标值的元素。

我们首先贴出寻找目标值右边界的代码如下。我们该如何得到目标元素的左边界呢,关键在于对序列中位数与目标元素相等情况即nums[mid]==target时的处理,不再是像之前一样返回mid值,而是向左收缩,对搜索区间右边界right进行赋值操作,不断压缩右边界,直至跳出while循环。

/*right初始化取值为nums.size()-1,while循环判断条件为left<=right*/
void BinarySearchLeftBoundary1(vector<int>& nums, int target)
{
	int left = 0, right = nums.size() - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (nums[mid] == target)
			right = mid - 1;
		else if (nums[mid] < target)
			left = mid + 1;
		else if (nums[mid] > target)
			right = mid - 1;
	}
	if (right >= 0)
	{
		cout << "左边界下标:" << right << endl;
		cout << "左边界数值:" << nums[right] << endl;
	}
	else
		cout << "递增序列的所有元素都比目标元素大" << endl;

}

/*right初始化取值为nums.size(),while循环判断条件为left<right*/
void BinarySearchLeftBoundary2(vector<int>& nums, int target)
{
	int left = 0, right = nums.size();
	while (left < right)
	{
		int mid = (left + right) / 2;
		if (nums[mid] == target)
			right = mid;
		else if (nums[mid] < target)
			left = mid + 1;
		else if (nums[mid] > target)
			right = mid;
	}
	if (right > 0)
	{
		cout << "左边界下标:" << right - 1 << endl;
		cout << "左边界数值:" << nums[right - 1] << endl;
	}
	else
		cout << "递增序列的所有元素都比目标元素大" << endl;
}

同样的道理,搜索目标值的右边界,即不断向右收缩,对搜索区间的左边界left进行赋值操作,不断压缩左边界,直至while循环结束。代码如下:

/*right初始化取值为nums.size()-1,while循环判断条件为left<=right*/
void BinarySearchRightBoundary1(vector<int>& nums, int target)
{
	int left = 0, right = nums.size() - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (nums[mid] == target)
			left = mid + 1;
		else if (nums[mid] < target)
			left = mid + 1;
		else if (nums[mid] > target)
			right = mid - 1;
	}
	if (left < nums.size())
	{
		cout << "右边界下标:" << left << endl;
		cout << "右边界数值:" << nums[left] << endl;
	}
	else
		cout << "递增序列中所有元素都比目标元素小" << endl;
}

/*right初始化取值为nums.size(),while循环判断条件为left<right*/
void BinarySearchRightBoundary2(vector<int>& nums, int target)
{
	int left = 0, right = nums.size();
	while (left < right)
	{
		int mid = (left + right) / 2;
		if (nums[mid] == target)
			left = mid + 1;
		else if (nums[mid] < target)
			left = mid + 1;
		else if (nums[mid] > target)
			right = mid;
	}
	if (left < nums.size())
	{
		cout << "右边界下标:" << left << endl;
		cout << "右边界数值:" << nums[left] << endl;
	}
	else
		cout << "递增序列中所有元素都比目标元素小" << endl;
}

四、总结

1、二分查找算法具有三个作用:搜索目标值,搜索目标值的左边界(序列中最大的小于目标值的元素),搜索目标值的右边界(序列中最小的大于目标值的元素)。

2、while循环的判断条件和搜索区间右边界right的赋值方式与right的初始化取值有关。right初始化为序列长度,则判断条件为left<right,赋值方式为right=mid;right初始化为序列长度-1,则判断条件为left<=right,赋值方式为right=mid-1。

3、二分查找三个作用的实现取决于while循环中对“序列中位数==目标值”这一情况的处理:

1、搜索目标值
if(nums[mid]==target)
    return mid;

2、搜索目标值的左边界
if(nums[mid]==target)
    right=mid;  //最后结果左边界=right-1

或者

if(nums[mid]==target)    
    right=mid-1;//最后结果左边界=right

3、搜索目标值的右边界
if(nums[mid]==target)
    left=mid+1;

参考

1、https://www.cnblogs.com/kyoner/p/11080078.html

2、https://leetcode-cn.com/problems/search-insert-position/

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值