目录

二分查找

二分查找的思想-减而治之

  1. 二分查找通过不断缩小区间的范围来查找目标元素,这种"不断缩小区间"就是减而治之的思想

  2. 减而治之听起来高大上,其实就是排除法,我们每一次查找就排除掉一些元素,每一次重复此操作,自然就找到了我们的目标元素

二分查找的应用范围

  1. 在有序数组中寻找一个目标值,有序和数组是重点

  2. 在整数范围内寻找一个目标值,也就是不一定是有序数组,旋转数组和山脉数组都可以用到二分法

二分查找的算法思路

在循环体中查找元素

  1. 简介

    • 此思路很简单,就是直接在数组中查找到目标元素
    • 时间复杂度 O(log N),这里的N是指数组的长度
    • 空间复杂度:由于二分查找算法在执行的过程中只使用到常数个临时变量,因此空间复杂度是 O(1)。
  2. 代码模板

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    int search(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) {
              return mid;
          }
          if (nums[mid] < target) {
              left = mid + 1;
          }
          if (nums[mid] > target) {
              right = mid - 1;
          }
      }
    }
    

在循环体中排除目标元素一定不存在的区间

  1. 简介

    • 这种思路就是一直排除元素不存在的区间,然后剩下来最后一个数 也就是left = right的数,因为这里的循环条件就是while (left < right) {},最后进行这个数的判断是不是我们的目标数
    • 时间复杂度 O(log N),这里的N是指数组的长度
    • 空间复杂度:由于二分查找算法在执行的过程中只使用到常数个临时变量,因此空间复杂度是 O(1)。
  2. 代码模板

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    int search(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) {
              return mid;
          }
          if (nums[mid] < target) {
              left = mid + 1;
          }
          if (nums[mid] > target) {
              right = mid - 1;
          }
      }
    }
    

二分查找的重点

循环条件

循环条件是一个很重要的东西,我们到底需要用left <= right 还是使用left < right,其实这就是跟思路有关了

取中间数的代码

一般来说我们取中间数都是用(left + right) / 2这么使用如果两个数特别大的时候会造成溢出的,所以我们不能这么用

最好的我们是使用left + (right - left) / 2,这个计算方法很好的避免了溢出这个问题,故二分法要这么去写

其实还有一个利用位运算很帅的一个方法,int mid = (left + right) >> 1; 比如这个代码,大家做个了解即可这是因为整数右移 1 位和除以 2(向下取整)是等价的,这样写的原因是因为位运算比整除运算要快一点。但事实上,高级的编程语言,对于 / 2 和除以 2 的方幂的时候,在底层都会转化成为位运算,我们作为程序员在编码的时候没有必要这么做,就写我们这个逻辑本来要表达的意思即可,这种位运算的写法,在 C++ 代码里可能还需要注意优先级的问题

中间数的代码要不要向上取整

int mid = left + (right - left) / 2/ 2表示的含义其实是向下取整,如果最后区间位于中间的有两位数,那么只能取到左边的数,那么上取整和下取整有没有啥区别呢,其实是有的,这种区别我们可以在具体的题目中去感悟,哪一个

案例一 - 二分查找

  1. 在循环体中查找元素

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    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;
          if (nums[mid] == target) {
              return mid;
          }
          if (nums[mid] < target) {
              left = mid + 1;
          }
          if (nums[mid] > target) {
              right = mid - 1;
          }
      }
      return -1;
    }
    };
    
  2. 在循环体中排除目标元素一定不存在区间

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    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;
              if (nums[mid] < target) {
                  left = mid + 1;
              }
              else {
                  right = mid;
              }
          }
          if (nums[left] == target) {
              return left;
          }
          return -1;
      }
    };
    

案例二 - 猜数字大小

  1. 在循环体中查找元素

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    class Solution {
    public:
      int guessNumber(int n) {
          int left = 1, right = n;
          while (left <= right) {
              int mid = left + (right - left) / 2;
              if (0 == guess(mid)) {
                  return mid;
              }
              if (-1 == guess(mid)) {
                  right = mid - 1;
              }
              if (1 == guess(mid)) {
                  left = mid + 1;
              }
          }
    
          return -1;
      }
    };
    
  2. 在循环体中排除目标元素一定不存在区间

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    class Solution {
    public:
      int guessNumber(int n) {
          int left = 1, right = n;
          while (left < right) {
              int mid = left + (right - left) / 2;
              if (1 == guess(mid)) {
                  left = mid + 1;
              }
              else {
                  right = mid;
              }
          }
          return left;
      }
    };
    

课后习题

  1. 搜索插入位置

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

  3. 寻找旋转排序数组中的最小值

  4. 寻找旋转排序数组中的最小值 II

  5. 搜索旋转排序数组

  6. 搜索旋转排序数组 II

  7. 第一个错误的版本

  8. 山脉数组的峰顶索引

  9. 山脉数组中查找目标值

  10. 寻找两个正序数组的中位数

  11. x 的平方根

  12. 寻找重复数

  13. 转变数组后最接近目标值的数组和

  14. 爱吃香蕉的珂珂

  15. 分割数组的最大值

  16. 在 D 天内送达包裹的能力

  17. 制作 m 束花所需的最少天数

  18. 小张刷题计划