在这里插入图片描述

简要介绍

二分算法是我们写算法题目中比较常见的一种优化算法,这个算法很好地体现了分治的思想,我们的二分算法不管是在算法题目中屡见不鲜,在实际工程当中也是十分热门的一个算法,比如我们的数据库索引、文件系统、甚至是我们游戏AI的路径规划都或多或少地用到了我们的二分算法,在我们的算法题目中常见的几种二分算法分别是二分答案、二分区间和二分浮点数。

二分答案

这是一个在有单调性的区间中找值的一个算法,通过二分思想来调整我们的查找范围。

二分区间

这个其实和我们的二分答案类似,只不过这里是搜索的一个区间而不是一个值,这种方法常应用于在区间内查找某一个解的情况。

二分浮点数

这个就比较冷门了,这里其实和二分答案类似,只是这里的处理方式不同而已。

相关例题

二分查找

题目描述

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果 target 存在返回下标,否则返回 -1

你必须编写一个具有 O(log n) 时间复杂度的算法。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。
题目分析

这个题目是一道相当经典的一道二分答案的题目,我们这里的思想就是二分。

实现思路

定义两个指针,一个左指针left,一个右指针right,分别指向我们的数组的左右端,计算出我们的中间点mid,然后就是三种情况的讨论了:
1、arr[mid] == target说明找到了,直接返回即可。

2、arr[mid] > target说明我们的区间[mid, right]都是要大于我们的target的,所以我们要舍弃这个区间的值,在我们的左边[left, mid - 1]继续查找,也就是让我们的right = mid - 1

3、arr[mid] < target说明我们的区间[left, mid]这个区间的值都是要小于我们的target的,所以我们这里要舍弃这一段区间,在我们的区间[mid + 1, right]继续进行查找,也就是让我们的left = mid + 1

以上三个操作都是在while(left < right)条件下进行的,跳出循环说明没有找到。

实现代码
class Solution {
public:
int search(vector<int>& nums, int target) {
  int n=nums.size();
  int left=0,right=n-1;
  while(left<=right)
  {
  int mid=left+(right-left+1)/2;
  if(nums[mid]<target) left=mid+1;
  else if(nums[mid]>target) right=mid-1;
  else return mid;
  }
  return -1;
  }
  };

在排序数组中查找元素的第一个和最后一个位置

题目描述

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109
题目分析

这个题目就是我们之前说的二分区间的经典例题,这里其实还是比较容易混淆的,这里我们做一个区分(使用基础二分,通过基础二分模板然后保留特定的左或是右下标的方式的做法其实是找到的值不一定是等于我们的target的,这里的就是必须要是等于target的算法)。

实现思路

我们这里的基本思路就是找到我们的左边界和我们的右边界,这是两种不同的情况,我们这里需要分开讨论着两种情况的:

找左边界:

我们这里需要明确几个区间的特点:

我们的左区间[left, k - 1]都是要小于我们的target的。

我们的右区间[k, right]都是要大于等于我们的target的。

所以这里我们的mid的位置右有了下面的两种情况:
当我们的mid[left, k - 1]的时候,也就是说arr[mid] < target,说明[left, mid]都是可以舍弃的,我们这里和我们的二分答案的更新是一样的就是将left移到mid + 1的位置上。

但是当我们这里的mid[k, right]的时候就和我们的二分答案的地方不一样了,这里我们是大于等于我的target值,我们要找的就是tatget值在的最左边的下标,所以这里的k我们是不能舍弃的,我们这里可以将我的[left + 1, right]舍弃掉,所以这里我们可以将right更新到mid位置。

需要特别注意的是(也是特别容易出错的):我们这里一定是向下取整。

示例(如果是向上取整):

我们这里图示的情况就是陷入了死循环的状态,所以这里我们采取的应该是向下取整,这里我们也是比较好理解的,相较于我们的二分答案我们这里的代码主要还是没有让我们的右指针往左靠,于是我们这里采用向下调整整体上是使得我们的mid更加向我们的left靠近了,这也就弥补了右指针没往左靠的问题。

在这里插入图片描述

我们这里分析我们的右边界也是一样的,首先还是要明确我们的区间特点:

左区间[left, k]都是要小于等于target的。

右区间[k + 1, right]都是要大于我们的target的。

我们的mid位置的情况也是两种:

当我们的mid在[left, k]区间的时候,因为这个区间是小于等于我们的target的,所以这里的mid是不能舍弃的,我们这里可以将区间[left, mid - 1]舍弃,于是这里的更新就变成了left = mid

当我们的mid[k + 1, right]的时候,说明我们的[mid, right]都是大于target的,所以这里的区间内的值都是可以舍弃的,于是这里的更新就变成了right = mid - 1

注意:我们这里的区间是要向上取整的。

我们这里还是举出一个向下取整而导致死循环的例子:这里的解释和上面类似。

在这里插入图片描述

实现代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
  int n=nums.size();
  if(n==0) return {-1,-1};
  int left=0,right=n-1;
  while(left<right)
  {
  int mid=left+(right-left)/2;
  if(nums[mid]<target) left=mid+1;
  else right=mid;
  }
  int Left=0;
  if(nums[left]!=target) return {-1,-1}; // 如果找到的下标对应的值不是目标值说明我们整个区间内都没有这个target直接返回{-1, -1}即可
  else Left=left;
  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;
  }
  return {Left,left};
  }
  };
小技巧

这里给一个取整的小技巧,while里面有-1,我们的取整就要+1,反之则没有+1

搜索插入位置

题目描述

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:

输入: nums = [1,3,5,6], target = 7
输出: 4

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums无重复元素升序 排列数组
  • -104 <= target <= 104
题目分析

这个题目实际上可以转换成找出大于等于目标值的最左端,这样我们就可以复用之前写的代码了。

实现思路

这里的实现思路就是上面题目中的找左边界的思路,不过这里我们可以右两种写法,也是上面提到的容易混淆的情况,这里我们其实可以使用二分的基础模板,然后标记大于等于目标值的下标即可。

实现代码
代码一
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
  int ret=-1;
  int left=0,right=nums.size()-1;
  if(nums[nums.size()-1]<target) return nums.size();
  while(left<right)
  {
  int mid=left+(right-left)/2;
  if(nums[mid]<target) left=mid+1;
  else right=mid;
  }
  return left;
  }
  };
代码二
class Solution {
public:
int searchInsert(vector<int>& a, int value) {
  int L = 0;
  int R = a.size() - 1;
  if(a[a.size()-1]<value) return a.size();
  int index = -1;
  while(L <= R) {
  int mid = L + ((R - L + 1)>>1);
  if(a[mid] >= value) {
  index = mid;
  R = mid - 1;
  }else {
  L = mid + 1;
  }
  }
  return index;
  }
  };

x 的平方根

题目描述

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

示例 1:

输入:x = 4
输出:2

示例 2:

输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

提示:

  • 0 <= x <= 231 - 1
题目分析

我们这个题目其实也是可以转换一下,我们这里实际求的是小于等于根号x的最大值,于是这里我们也是可以复用我们第二题的代码的,当然了,这里也是可以复用第三题的代码二的。

实现思路

其实这里就是条件改变了一下,基本的思路和之前还是一模一样的,这里就不再赘述了。

实现代码
代码一
class Solution {
public:
int mySqrt(int x) {
int left=1,right=x;
if(x<1) return 0; // 边界情况
while(left<right)
{
long long mid=left+(right-left+1)/2;
if(mid*mid<=x) left=mid;
else right=mid-1;
}
return left;
}
};
代码二
class Solution {
public:
int mySqrt(int x) {
int left=1,right=x;
if(x<1) return 0; // 边界情况
int index = -1;
while(left <= right)
{
long long mid=left+(right-left+1)/2;
if(mid*mid<=x) {
index = mid;
left = mid + 1;
}
else right=mid-1;
}
return index;
}
};