二分算法研究
此文主旨是彻底搞清楚二分算法。
在处理查找问题时,如果数据结构是数组,则可考虑二分查找。
下面将用两个模板来讲解二分查找,理解这两个模板之后,遇到问题只需稍加变化即可。
模板一
int search(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size() - 1; // 1. 注意这里
int mid = 0;
while (left <= right) // 2. 注意这里
{
mid = left + (right - left) / 2; // 防止mid溢出
if (nums[mid] == target)
{
return mid;
} else if (nums[mid] < target)
{
left = mid + 1; // 3. 注意这里
} else if (nums[mid] > target) // 好习惯
{
right = mid - 1; // 4.注意这里
}
}
return -1;
}
建议刷题:leetcode 704。
此种模板是确保每一次循环查找都是在左闭右闭的区间,这种可保证最后退出时,整个区间为空。
1号注意点是进行初始的区间设置,如果数组中一个5个数,则初始的区间为[0, 4]。
2号注意点是设定程序退出条件,即当left = right + 1
时才会退出,此时区间为[5, 4]
,整个区间为空的,即整个要查找的区间未有遗漏的数。
3和4注意点是当未找到对应的数时,对区间进行划分。当未找到时,区间便被mid
划分为两个区间,即[left, mid - 1], [mid + 1, right]
。所以下一次查找时,left
和right
均要加一。
一般在写二分查找时,有一个好习惯,便是用else if
代替else
。因为困难的二分问题,将会有很多边界判断条件,使用else if
更容易看边界条件,容易找出错误。想挑战较为困难的同学可以尝试一下这道题leetcode 33。
模板二
int search(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size(); // 1. 注意这里
int mid = 0;
while (left < right) // 2. 注意这里
{
mid = left + (right - left) / 2;
if (nums[mid] == target)
{
return mid;
} else if (nums[mid] < target)
{
left = mid + 1;
} else if (nums[mid] > target)
{
right = mid; // 3. 注意这里
}
}
return left;
}
建议刷题:leetcode 35
此模板主要解决当数组中没有要查找的数时,返回它应该插入的地方。
1号注意点用来设定初始的查找区间,最初的区间即为[0, nums.size())
,若数组有5个数,则为[0, 5)
。
2号注意点用来改变退出条件,当left == right
时,程序退出。而这样会导致漏掉一个数,例如当left = 3
时,整个区间为[3, 3)
,在索引3的位置这个数便未被判断,而程序就会退出,所以此时只需要打一个补丁即可:return nums[left] == target ? left : -1;
。
3号注意点是进行区间划分,当未找到是,划分的两个此间为:[left, mid)
和[mid + 1, right)
。所以left
需要等于mid + 1
。而right
只需要等于mid
。
此模板最大的作用是数组中没有该元素,可以返回需要插入的位置,即第一个比该数大的数的索引。此模板中,右边界指向的数是一定比目标数字大的(不论数组中是否存在这个数),因为要想右边界移动,中间的值一定要比目标值大。所以当循环结束时,left == right
此时均一定指向第一个比目标值大的位置。
那么上一个模板可不可以返回插入的位置呢?也是可以的,由于right = mid - 1
,如果mid
的位置刚好是要插入的位置,则right
就会容易跑到要插入的位置的前一个。所以只需要最后返回right + 1
即可。或者由于循环结束条件为left = righ + 1
,所以直接返回left
也可。
例题一
给定一个按照升序排列的整数数组
nums
,和一个目标值target
。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回
[-1, -1]
。示例:
输入:nums = [5, 7, 7, 8, 8, 10], target = 8 输出:[3, 4]
此题可以在找到target
之后,往前往后一个一个找,但这样就不能完全发挥二分查找的威力,并且时间复杂度最坏情况下也成了O(n)。
目标的左边界,可以理解为向数组中,插入一个比目标小一点点的数,这样插入这个数的位置就是左边界。所以当nums[mid]
等于目标值的时候,就相当于找的值大了一点点,所以让右边界移动。
目标的右边界,可以理解为向数组中,插入一个比目标大一点点的数,这样插入这个数的位置就是右边界的后一个。所以当nums[mid]
等于目标值的时候,就相当于找的值小了一点点,所以让左边界移动。
全部代码就是这样:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
// 存放最后结果
vector<int> ans(2);
// 找左边界
ans[0] = searchLeftRight(nums, target, true);
// 找有边界
ans[1] = searchLeftRight(nums, target, false) - 1; // 1. 注意
// 左边大说明没找到
if (ans[0] > ans[1]) // 2. 注意
{
ans[0] = ans[1] = -1;
}
return ans;
}
// 边界就相当于返回插入的位置,所以用左开右闭
int searchLeftRight(vector<int>& nums, int target, bool isleft)
{
int left = 0;
int right = nums.size();
int mid = 0;
while (left < right)
{
mid = left + (right - left) / 2;
if (nums[mid] == target && isleft) // 3. 注意
{
right = mid;
} else if (nums[mid] == target && !isleft){ // 4. 注意
left = mid + 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left; // 5. 注意
}
};
1号注意点,因为右边边界的位置是插入一个比这个大的数的位置的前一个,所以要减1。
2号注意点,当数组中,没有目标值的时候,此时函数返回的左边界和右边界应该相等,而由于右边界减一了,所以如果最后左边界大于右边界,则说明数组中无目标值。
3,4号注意点,当找左边界时,当nums[mid]
等于目标值时,相当于nums[mid]
比要找的数大了一点点,所以应该是right
移动。查找右边界同理,应该是left
移动。
5号注意点,因为采用的是模板二,所以最后返回left
和right
等价,如果使用模板一也完全可以,最后返回left
或right + 1
。
参考文章:
[ 我作了首诗,保你闭着眼睛也能写对二分查找](我作了首诗,保你闭着眼睛也能写对二分查找 (qq.com))