我爱学算法之—— 二分查找(上)
了解二分算法
二分查找,想必多多少少有一点了解了,我们了解的二分查找算法:当一个数组有序的时候,我们可以使用二分算法来查找一个值;
直接比较mid(
(left + right)/2)和我们要查找的值target;如果nums[mid] > target就在右边查找,否则在左边查找。
但是二分查找真的如此简单吗?在什么时候才能使用二分查找呢?
使用二分查找的条件:
数组有序;其实本质是是利用二段性;简单来说,将数组分为;两个区间,一个区间内是满足某个条件;而另一个区间是满足其相反的条件的。
现在我们来通过了解二分查找的算法题,来深入探究二分查找,以及什么时候能够使用二分查找。
一、二分查找
题目解析

这道题,想必之前已经见到过了;
给定一个数组
nums和一个值target,让我们在nums数组中查找target,如果存在就返回下标;否则返回-1。
算法思路
对于这道题,我们可以使用暴力查找:让target和数组nums中所有元素一个一个比较;
暴力解法时间复杂度为O(n);从效率上来说也是非常不错的;
但是暴力解法并没有使用到我们数组有序这一个条件;
我们这里想一下,当我们暴力查找到一个位置
mid时,区间[0,mid-1]内的值是不是都是小于mid位置的值的;区间[mid+1,n]内的值是不是都是大于mid位置的值的。简单来说就是:我们任取一个位置,这个位置的值为
x;这个位置左边区间的值都是小于x的,右边区间的值都是大于x的。那我们是不是就可以将区间划分为两部分:
- 左边区间的值都是小于
x的- 右边区间的值都是大于
x的这样我们任取一个位置,如果这个位置的值
x大于我们要找的值target,那就去这个位置左边区间再去找target;如果这个位置的值
x大于我们要找的值target,那就去这个位置右边区间内再去找target。
那我们大致就理解了如何去找target;但是我们可以取二分点也可以取三分点、四分点…,那如何去取mid呢?
这里就不叙述这个问题了,我们取二分点的效率是最高的。
二分整体思路:
首先定义
left、right分别指向区间的开始位置和结束位置。取
mid(mid = (left + right)/2)比较
mid位置和要查找的值target:如果
nums[mid] > target,就去左边区间找,right = mid - 1;如果
nums[mid] < target,就去右边区间找,left = mid + 1;如果
nums[mid] == target,找到了我们要查找的值,返回结果。查找结束还没有返回结果(
left和right错过去了,那就表示数组中不存在target)。

这里需要注意:
**循环结束的条件:**我们
left和right指向的位置都是没有查找过的位置,所以当left > right时,循环才能结束。**取
mid:**这里如果数组过大,left + right就可能超出数据范围,我们使用left + (right - left)/2或者left +(right - left + 1)/2来计算;但是有一个问题,对于数组内数据个数是奇数时,这两种计算方式没有什么影响;但如果数组中数据个数是偶数时,第一种left + (right - left)/2求的mid是偏左的,而left + (right - left +1)/2求出的mid是偏右的。在这道题中我们感受不到这两种求法的差别,在下面题目中我们就能感受到这两种求法的差别了。
代码实现
class Solution {
public:int search(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {// int mid = (left+right)/2;int mid = left + (right - left) / 2;if (nums[mid] > target)right = mid - 1;else if (nums[mid] < target)left = mid + 1;elsereturn mid;}return -1;}
};
二、在排序数组中查找元素的第一个和最后一个位置
题目解析

这道题和上面那一道题不同,上一道题目在
nums数组中只存在应该target,而这道题目中可能存在多个target;我们需要找到多个
target的起始位置和结束位置。如果数组中不存在
target就返回-1,-1。
算法思路
首先还是来看暴力解法:从左到右遍历数组,遇到target,就记录target起始位置,然后继续向后遍历直到某个位置的值不等于target;
如果数组中不存在target,那暴力解法最坏情况下的时间复杂度为O(n)。
那现在我们来想如何使用二分查找来解决这个问题:
这里相信有人和博主一样,先利用二分查找查找到
target的某一个位置,然后向左和向右遍历查找target出现的起始位置和结束位置;但是如果数组中的数据都是target,那我们不也是要查找完整个数组,时间复杂度也是O(n);
这里我们就不使用上面二分算法划分区间的方法了,因为我们这里target不一定只出现一次,我们找到target时不能直接返回,因为我们不确定是否还存在其他target;
这里我们要查找的是target的起始位置和结束位置,说白了就是左边界和右边界。
二分算法查找左边界:
这里我们查找到
target不能直接返回,那就试着将nums[mid] == target划分到左边或者右边的情况;简单来说就是这里要找的是大于等于
t区间的左边界,我们将数组划分成两部分:
- 左边区间内的值都是小于
target的。- 右边区间内的值都是大于等于
target的。这样我们在使用二分查找时:
- 如果
nums[mid] < target,那就可以直接舍去[left , mid-1]和mid位置的(left = mid + 1);- 如果
nums[mid] >= target,我们的mid位置的值可能等于target,所以我们只能舍去区间[mid + 1 , right];(right = mid)(这里我们要找的是左边界,如果mid位置的值是等于mid+1位置的值时,我们是可以舍去mid+1位置的)这里我们要求的是大于等于
target区间的左边界,所以划分成小于x和大于等于x的两个区间

这里要注意:
**循环结束条件:**这里我们当
left == right时,循环就结束了;所以循环的条件是left < right而不是left <= right。
这里
left == right时是不需要判断的,因为此时就是最终结果:数组中存在大于等于
target的区间,也存在小于target的区间,此时left和right相等时指向的就是大于等于target区间左端点的位置数组中如果所有数都大于等于
target,此时right最终会指向left的位置也就是数组的起始位置,也是大于等于target区间的左端点的位置。数组中如果所有数都小于
target,此时left最终最指向left的数组的结束位置,也就是right;如果
left == right判断了,可能会陷入死循环因为这里当
nums[mid] >= target时,right = mid;这样如果最后left和right指向的位置是大于等于target的,求出的mid是等于left和right的,那此时就会陷入死循环。求
mid的值:在上面朴素的二分查找算法中,我们利用哪一种求法都可以,但是在这里就不一样了;
如果数据个数是偶数个,利用
mid = left + (right - left)/2求出的mid是偏左的;利用mid = left + (right - left + 1)/2求出来的mid是偏右的;这里我们要找的是区间的左边界,我们要使用
mid = left + (right - left)/2来求mid。因为最后如果
left和right指向两个相邻的位置(left + 1 = right),利用第一种方法求出来的mid是等于left的;利用第二章方法求出来的mid是等于right的;如果我们
right位置的值的大于等于target的,如果求出的mid是等于right的,此时就会陷入死循环;(因为nums[mid] >= target时,right = mid)

二分算法查找右边界:
和查找左边界类似:
我们要查找的是小于等于
target区间的有边界,我们可以根据要查找的位置将数组划分成两部分:
- 左边区间内的值都是小于等于
target的- 右边区间内的值都是大于
target的在二分查找的过程中:
- 如果
nums[mid] <= target,mid位置可能就是最终要查找的结果,所以只能舍去区间[left , mid-1](left = mid);(这里查找的是区间的右边界,所以即使mid-1位置的值等于mid位置的值,也是直接可以舍去的)- 如果
nums[mid] > target,区间[mid , right]内的值都是大于target的,可以舍去区间[mid , right](right = mid - 1)。这里我们要查找的是小于等于
target区间的右边界所以划分为:小于等于target和大于target两区间

这里也要注意:
循环条件是
left < right而不是left<=right。求
mid:在求左边界时使用的是
mid = left + (right - left)/2,这样在偶数个数据时求的是偏左位置的;这里我们要使用
mid = left + (right - left + 1)/2,这样当数组在数据个数是偶数个时,求出的mid是偏右的。因为最后如果
left和right指向两个相邻的位置(left + 1 = right),利用第一种方法求出来的mid是等于left的;利用第二章方法求出来的mid是等于right的;如果我们
left位置的值的小于等于target的,如果求出的mid是等于left的,此时就会陷入死循环;(因为nums[mid] <= target时,left = mid)

代码实现
class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {//数组为空if(nums.empty()) return {-1,-1};int n = nums.size();int begin = 0,end = 0;//求大于等于target区间的左边界int left = 0,right = n-1;while(left < right){int mid = left + (right - left)/2;if(nums[mid] >= target) right = mid;else left = mid + 1;}//判断是否存在targetif(nums[left] != target) return {-1,-1};begin = left;//求小于等于target区间的右边界left = 0,right = n-1;while(left < right){int mid = left + (right - left + 1)/2;if(nums[mid] <= target) left = mid;else right = mid -1;}end = right;return {begin,end};}
};
简单总结
这里两道题,算是最基本的二分算法题,我们一定要理解,理解之后在之后的二分算法题目再深入探究二分算法。

