二分查找模板总结(区间、条件不再纠结)
   
    二分查找是一种在
    
     有序数组
    
    中查找某一特定元素的搜索算法。元素集合有顺序,元素性质有分界点,二分法就可以用来求分界点,并不一定要求集合中元素是不重复的。
   
    算法思路:假设目标值在闭区间
    
     [left, right]
    
    中, 每次将区间长度缩小一半,当
    
     left = right
    
    时,我们就找到了目标值。
   
常见问题:
- 
     查找区间是该选择左开右闭区间
 
 [left, right)
 
 还是 左闭右闭区间
 
 [left, right]
 
- 
     循环终止条件是
 
 left < right
 
 还是
 
 left <= right
 
- 比较函数该怎么选择
    
    
    常规写法
   
    二分查找需要注意
    
     查找区间
    
    和
    
     终止条件
    
    ,稍不留神可能出现死循环。常见的写法如下:
   
int binarySearch(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1; // 定义 target 在左闭右闭的区间里,[left, right]
  while (left <= right) { // 当 left==right,区间 [left, right] 依然有效,所以用 <=
    int mid = left + (right - left) / 2); // 防止溢出,结果等同于(left + right)/2
    if (nums[mid] > target) {
      right = mid - 1; // target 在左区间,所以更新为 [left, mid - 1]
    } else if (nums[mid] < target) {
      left = mid + 1; // target 在右区间,所以更新为 [mid + 1, right]
    } else {
      // nums[mid] == target
      return mid; // 数组中找到目标值,直接返回下标
    }
  }
  // 未找到目标值
  return -1;
}
    上述写法中区间
    
     [left, right]
    
    的更新操作是
    
     right = mid - 1; left = mid + 1;
    
    边界条件需要注意:二分区间直到长度为
    
     1
    
    ,即
    
     left == right
    
    时,循环条件依然满足,再进行一次比较,所以用
    
     left <= right
    
例题:
对上述写法稍作改造,总结二分模板共有两个:
    
    
    模板一
   
    当区间
    
     [left, right]
    
    的更新操作是
    
     left = mid + 1; right = mid;
    
    时,二分区间计算
    
     mid
    
    不需要加
    
     1
    
    。
   
通用模板写法:
int binarySearch(int left, int right) {
  while (left < right) {
    int mid = (left + right) / 2; // 注意防止溢出
    if (check(mid)) // 判断 mid 是否满足查找条件
      left = mid + 1; // 结果落在 [mid+1, right] 区间
    else
      right = mid; // 结果落在 [left, mid] 区间
  }
  return left;
}
    上述例题【
    
     704. 二分查找
    
    】套用
    
     模板一
    
    的写法:
   
int binarySearch(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1;
  while (left < right) {
    // 防止溢出,结果等同于 (left + right)/2
    int mid = left + ((right - left) / 2);
    // 检查 mid,比较 nums[mid] 和 target
    if (nums[mid] < target)
      // 结果落在 [mid+1, right] 区间
      left = mid + 1;
    else
      // 结果落在 [left, mid] 区间
      right = mid;
  }
  return nums[left] == target ? left : -1;
}
    
    
    模板二
   
    当区间
    
     [left, right]
    
    的更新操作是
    
     left = mid; right = mid - 1;
    
    时,计算
    
     mid
    
    需要加
    
     1
    
    。
   
通用模板写法:
int binarySearch(int left, int right) {
  while (left < right) {
    // 注意防止溢出
    int mid = (left + right + 1) / 2;
    if (check(mid))
      // 结果落在 [left, mid-1]
      right = mid - 1;
    else
      // 结果落在 [mid, right]
      left = mid;
  }
  return left;
}
    上述例题【
    
     704. 二分查找
    
    】套用
    
     模板二
    
    的写法:
   
int binarySearch(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1;
  while (left < right) {
    // 防止溢出,结果等同于 (left + right + 1)/2
    int mid = left + ((right - left + 1) / 2);
    // 检查 mid,比较 nums[mid] 和 target
    if (nums[mid] > target)
      // target 落在 [left, mid-1]
      right = mid - 1;
    else
      // target 落在 [mid, right]
      left = mid;
  }
  return nums[left] == target ? left : -1;
}
    
    
    求同存异
   
    使用这两个模板的
    
     优势
    
    :
   
- 
     不用考虑循环结束时返回
 
 left
 
 还是
 
 right
 
 ,因为最后退出时
 
 left == right
 
- 
     不用考虑循环条件是用
 
 <
 
 还是
 
 <=
 
 ,直接都用
 
 <
 
- 非常适用于求一些分界点的问题
    
    
    共同点
   
    这两个模板写法的
    
     共同点
    
    :
   
- 
     查找区间都是
 
 左闭右闭
 
 区间:
 
 [left, right]
 
- 
     循环判断条件都是用
 
 left < right
 
- 
     循环退出条件都是
 
 left == right
 
 。查找过程一定会在
 
 
 
 O( l o g n ) O(logn) 
 
 
 
 
 
 
 O
 
 
 (
 
 
 l
 
 
 o
 
 
 g
 
 
 n
 
 
 )
 
 
 
 
 
 的时间内终止,即使中间遇到
 
 nums[mid] == target
 
 也不会结束,而是继续收缩区间,直到区间长度为
 
 1
 
 。
- 
     在循环终止(查找结束)之后,判断一下
 
 left
 
 或者
 
 right
 
 是否符合要求即可,如果不符合要求,说明答案不存在
    
    
    不同点
   
    共同点比较好理解,但更值得注意的是,
    
     这两个模板写法的不同点
    
    :
   
- 
     更新
 
 mid
 
 的计算方式不同
- 
     
 check(mid)
 
 不同,所以更新
 
 left
 
 和
 
 right
 
 也会有一些差别
    
    
    Q & A
   
    
    
    1. 为什么模板二计算 mid 会加 1
   
    原因:
    
     避免进入死循环
    
    。
   
    首先,可以明确的是,两个模板在查找过程中都是区间长度不断减半,直到区间长度为
    
     1
    
    ,即
    
     left == right
    
    时退出循环,然后在循环外面检查最后的查找结果
    
     nums[left]
    
    是否为
    
     target
    
    。
   
    如果
    
     模板二
    
    更新
    
     mid
    
    采用
    
     mid = (left + right)/2
    
    ,在
    
     最后一次查找时会陷入死循环
    
    。例如,对于
    
     nums = [3,4]
    
    ,
    
     target
    
    为
    
     4
    
    :
   
- 
     第一次查找,
 
 left = 0,right = 1,mid = 0
 
 ,比较得到
 
 nums[mid] < target
 
 ,更新
 
 left = mid
 
- 
     第二次查找,还是进入相同的条件分支,
 
 陷入死循环
 
 。
    而更新
    
     mid
    
    采用
    
     mid = (left + right + 1) / 2
    
    时:
   
- 
     第一次检查
 
 left = 0, right = 1, mid = 1
 
 ,更新
 
 left = mid
 
 ,
 
 退出循环
 
    也可以通过边界条件来理解,这两个模板最后都是要收缩到区间
    
     [left, left+1]
    
    里进行最后一次
    
     check
    
    :
   
- 
     对于模板一,计算得到
 
 mid = left
 
 ,区间往左收缩是进入
 
 right = mid
 
 ,区间往右收缩是进入
 
 left = mid + 1
 
 ,结束循环,所以更新
 
 mid
 
 要用
 
 mid = (left + right)/2
 
 ,
- 
     对于模板二,计算得到
 
 mid = right
 
 ,区间往左收缩是进入
 
 right = mid - 1
 
 ,区间往右收缩是进入
 
 left = mid
 
 ,结束循环,所以更新
 
 mid
 
 要用
 
 mid = (left + right + 1)/2
 
    
    
    2. 如何选择使用模板
   
    一般写二分的思考顺序是这样的:通过题目背景
    
     确定
     
      check(mid)
     
     的逻辑,判断答案落在左半区间还是右半区间
    
    :
   
- 
     
 target
 
 属于右半区间,则右半区间是
 
 [mid+1, right]
 
 ,左半区间是
 
 [left, mid]
 
 ,区间更新方式是
 
 left = mid + 1; right = mid;
 
 ,此时用第一个模板;
- 
     
 target
 
 属于左半区间,则左半区间是
 
 [left, mid-1]
 
 ,右半区间是
 
 [mid, right]
 
 ,区间更新方式是
 
 right = mid - 1; left = mid;
 
 ,此时用第二个模板;
    这种区间划分方式将
    
     check(mid) == targrt
    
    分支的逻辑合并到
    
     check(mid) > targrt
    
    或
    
     check(mid) < targrt
    
    分支,不断将区间长度减半,具有更好的适用性,尤其适用于求一些分界点的问题。
   
例题:
解释:
- 
     查找元素
 
 target
 
 的第一个位置,相当于查找
 
 大于等于 target
 
 的第一个元素位置:
- 
如果 
 
 nums[mid] < target
 
 ,此时
 
 target
 
 应该落在
 
 右半区间
 
 
 [mid+1, right]
 
 ;
- 
如果 
 
 nums[mid] >= target
 
 ,此时
 
 target
 
 应该落在
 
 左半区间
 
 
 [left, mid]
 
 ,因为
 
 mid
 
 可能就是
 
 target
 
 ,所以还需要进一步比较;因而选择 
 
 check(mid)
 
 为
 
 nums[mid] < target
 
 ,区间更新条件分别是
 
 left = mid + 1; right = mid;
 
int searchFirst(vector<int> &nums, int target) {
    int left = 0, right = nums.size() - 1;
    while (left < right) {
      int mid = (left + right) >> 1;
      if (nums[mid] < target)
        left = mid + 1;
      else
        right = mid;
    }
    return nums[left] == target ? left : -1;
  }
- 
     查找元素
 
 target
 
 的最后一个位置,相当于查找
 
 小于等于 target
 
 的最后一个元素位置:
- 
     如果
 
 nums[mid] > target
 
 ,此时
 
 target
 
 应该落在
 
 左半区间
 
 
 [left, mid-1]
 
 ;
- 
     如果
 
 nums[mid] <= target
 
 ,此时
 
 target
 
 应该落在
 
 右半区间
 
 
 [mid, right]
 
 ,因为
 
 mid
 
 可能就是
 
 target
 
 ,所以还需要进一步比较
    因而选择
    
     check(mid)
    
    为
    
     nums[mid] > target
    
    ,区间更新条件分别是
    
     right = mid-1; left = mid;
    
int searchLast(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1;
  while (left < right) {
    int mid = (left + right + 1) / 2;
    if (nums[mid] <= target)
      left = mid;
    else
      right = mid - 1;
  }
  return nums[left] == target ? left : -1;
}
- 
     计算
 
 x
 
 的算术平方根,结果只保留整数部分:由于
 
 x
 
 平方根的整数部分是满足
 
 k*k <= x
 
 的
 
 最大 k 值
 
- 
     如果
 
 mid * mid > x
 
 ,那么结果应该落在
 
 左半区间
 
 
 [left, mid-1]
 
- 
     如果
 
 mid * mid <= x
 
 ,那么结果应该落在
 
 右半区间
 
 
 [mid, right]
 
    因而选择
    
     check(mid)
    
    为
    
     mid*mid > x
    
    ,区间更新条件分别是
    
     right = mid-1; left = mid;
    
int mySqrt(int x) {
  int left = 0, right = x;
  while (left < right) {
    // 防止值溢出
    int mid = (right + left + 1LL) / 2;
    if (mid > x / mid)
      // 结果在左半区间 [left, mid-1]
      right = mid - 1;
    else
      // 结果在右半区间 [mid, right]
      left = mid;
  }
  return left;
}
    
    
    解决问题
   
 
