我爱学算法之—— 二分查找(上)
了解二分算法
二分查找
,想必多多少少有一点了解了,我们了解的二分查找算法:当一个数组有序的时候,我们可以使用二分算法来查找一个值;
直接比较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};}
};
简单总结
这里两道题,算是最基本的二分算法题,我们一定要理解,理解之后在之后的二分算法题目再深入探究二分算法。