本章将会讲解3种二分算法的模板为 : 1.朴素二分算法,2.左结点二分算法,3.右结点二分算法。
1.算法模板
1.1朴素二分算法
int bin_search(vector<int> &array,int target)
{
int n = array.size();
int left = 0, right = n - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;//防止越界 == (left + right)/2
if (array[mid] == target) return mid;
else if (array[mid] > target) right = mid - 1;
else if (array[mid] < target) left = mid + 1;
}
return -1;//没有target 值,就返回 - 1
}
2.2寻找区间左、右结点的二分算法
寻找区间左右端点的时候需要将其划分为不同的范围.
区间的左端点:target的左边严格小于target,右边是大于等于target,根据 x也就中间值落入区间的不同进行划分,在中间值小于target时,也就是落到了左边的区间我们不需要,直接将left = mid + 1,但是当落到右边的区间时,是大于等于的情况,可能是需要的也可能不需要所以让 right = mid;
还需要注意的是 mid 是(right - left)/2,通过取到特殊的情况为两个的时候,如果是落在了右边,会导致无线的循环,其次循环结束的条件为 left < right,当left == right时就是我们想要的答案,否则会陷入死循环之中。
区间的右端点:ratget的右边严格大于target,左边是小于等于target,同理如上:大于t不是我们想要的区间直接:right = mid - 1, 小于等于 t时,left = mid;需要注意的是,中间mid的使用条件为:(right - left + 1)/2,这里必须是要加上 + 1,让他落到右边防止left == mid,进入死循环。
int bin_search1(vector<int>& array, int target)
{
//寻找左结点的模板
int n = array.size();
int left = 0, right = n - 1;
while (left < right)
{
int mid = left + (right - left) / 2;
if (array[left] < target) left = mid + 1;
else right = mid;
}
return left;
}
int bin_search2(vector<int>& array, int target)
{
//寻找右结点的模板
int n = array.size();
int left = 0, right = n - 1;
while (left < right)
{
int mid = left + (right - left + 1) / 2;//需要进行 + 1,因为在只有两个数字的时候,+ 1会偏向大的那个数,要不然会发生越界的情况
if (array[left] > target) right = mid - 1;
else left = mid;
}
return left;//或者是 right 都可以
}
2.例题
朴素二分算法不在进行练习,直接来到较难的那一部分。
2.1 34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
非常的简单,直接使用前面的模板即可,需要注意的是需要判断一下 n == 0,的边界情况。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target)
{
int n = nums.size();
int left = 0, right = nums.size() - 1;
vector<int> ret;
if(n == 0)
{
ret.push_back(-1);
ret.push_back(-1);
return ret;
}
//寻找第一个出现的位置
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
//最后left 或者是 right 的位置就是我们结果
if (nums[left] != target) ret.push_back(-1);
else ret.push_back(left);
//寻找第二个位置的结点
left = 0, right = nums.size() - 1;
while (left < right)
{
int mid = left + (right - left + 1) / 2;
if (nums[mid] > target) right = mid - 1;
else left = mid;
}
if (nums[left] != target) ret.push_back(-1);
else ret.push_back(left);
return ret;
}
};
2.235. 搜索插入位置 - 力扣(LeetCode)
根据题意可以将内容划分为两个部分,左边都是小于target的,右边都是大于或者是等于target的,也就相当于去找一个左边界,若果是没有找到的话,会停留在大于那个数的下标位置,所以就是我们想要的结果。
class Solution {
public:
int searchInsert(vector<int>& nums, int target)
{
int left = 0, right = nums.size() - 1;
while(left < right)
{
int mid = left + (right - left) / 2;// 防止因为加法过大导致的溢出
if(nums[mid] < target)
{
left = mid + 1;
}
if(nums[mid] >= target)
{
right = mid;
}
}
if(right == nums.size() - 1 && nums[right] < target) return right + 1;
return right;
}
};
2.3 852. 山脉数组的峰顶索引 - 力扣(LeetCode)
寻找峰值,表示的是在一个数组里面最大的那个,这样就有一个向下下降的趋势。可以很好的分成两段。前面的那一段是后面的数严格的大于前面的数,峰值可能在mid中,让 left = mid,如果后面的大于前面的说明 mid 位置,就一定不是峰值位置。其次 mid 位置的值是要指向右边。因为有 left = right 要不然会陷入死循环。
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr)
{
int left = 0, right = arr.size() - 1;
while(left < right)
{
int mid = left + (right - left + 1)/2;
if(arr[mid] > arr[mid - 1]) left = mid;
else right = mid - 1;
}
return left;
}
};
2.4 162. 寻找峰值 - 力扣(LeetCode)
这道题跟前面的那道题类似。
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int n = nums.size();
int left = 0, right = n - 1;
while(left < right)
{
int mid = left + (right - left + 1)/2;
if(nums[mid] > nums[mid - 1]) left = mid;
else right = mid - 1;
}
return left;
}
};
2.5 153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
不需要管时进行了多少次,他始终是符合这样的一个规律。左侧的AB始终表示者大于右侧的CD同时也是在单调递增的。所以最后的 C 对应的值就是我们的结果。还学要进行标记一下最后一个值,如果是 nums[mid] > nums[right],就说明当前的位置一定不是我想要的区间、需要left = mid + 1。
class Solution
{
public:
int findMin(vector<int>& nums)
{
int left = 0, right = nums.size() - 1;
int x = nums[right];
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] > x) left = mid + 1;
else right = mid;
}
return nums[left];
}
};
3.总结
二分算法其实是一个简单算法,需要记住另外的两个模板,然后答案如果是具有两段的性质就可以进行二分的算法。