二分查找用于在一个单调序列中查找指定的值,根据查找的方式,二分查找具有不同的变体,但是无论如何,其基本模板为:

int binarySearch(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;

    while (...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
}

下面讨论对于单调非递减序列中查找指定值的方式。这里的“单调非递减”意味着序列是单调递增的,但是值可能有重复。

二分查找有三个变体:

  • 查找指定的值:在序列中查找指定的值。

  • 查找左侧边界:若指定的值有重复,则查找左侧边界。若 target 不存在则返回大于 target 的第一个值。

  • 查找右侧边界:若指定的值有重复,则查找右侧边界。若 target 不存在则返回小于 target 的第一个值

查找指定的值

最常见的查找方式是在单调递增的序列 \([left, right]\) 中查找指定的值。

简易流程如下:

  1. 在 \([left, right]\) 中查找值为 target 对应的索引。

  2. 若 \(nums[mid] == target\),则返回 \(mid\)。

  3. 若 \(nums[mid] < target\),则在 \([left, mid)\) 中查找,否则在 \((mid, right]\) 中查找。

    这里注意 \([left, mid)\) 代表的区间实际上就是 \([left, mid - 1]\)。

    相应的,\((mid, right]\) 实际上就是 \([mid, right]\)

  4. 考虑到当 \(left == right\) 时依然有一个值未检查到,因此结束条件为 \(left > right\)。也就是循环条件为 \(left <= right\)。

最终实现为:

class Solution {
public:
    // 标准的二分搜索框架,搜索目标元素的索引,若不存在则返回 -1
    int search(vector<int>& nums, int target) {
        int left = 0;
        // 注意
        int right = nums.size() - 1;

        while(left <= right) {
            int 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 - 1;
            }
        }
        return -1;
    }
};

查找左侧边界

在查找左侧边界的时候根本思想为在找到 target 值时不要返回,而是不断收缩右侧边界,直至将区间收缩到 1:

  1. 在 \([left, right]\) 中查找值为 target 对应的索引。

  2. 若 \(nums[mid] == target\),则将区间收缩至 \([left, mid)\)。

  3. 若 \(nums[mid] < target\),则在 \([left, mid)\) 中查找,否则在 \((mid, right]\) 中查找。

  4. 重复上述结果,直至 \(left > right\)。

  5. 返回 left。

最终算法为:

int leftBound(vector<int> &nums, int target) {
  // 搜索区间为 [left, right]
  int left = 0;
  int right = nums.size() - 1;
  while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target) {
      // 收缩右侧边界
      right = mid - 1;
    } else if (nums[mid] < target) {
      // 搜索区间变为 [mid + 1, right]
      left = mid + 1;
    } else if (nums[mid] > target) {
      // 搜索区间变为 [left, mid - 1]
      right = mid - 1;
    }
  }

    // 判断 target 是否存在于 nums 中
    // 如果越界,target 肯定不存在,返回 -1
    if (left < 0 || left >= nums.size()) {
        return -1;
    }
  return left;
}

下面论证当 target 不重复时也不会导致值丢失:

  1. 假设在区间 \([left, right]\) 中查找 target,且 target 不重复、恰好处于 mid 位置。

  2. 第一次搜索时区间被收缩到 \([left, mid)\)。此区间已经不存在 target,且总比 target 小,因此最终区间会收束到 \([right, right]\) 的位置上。

  3. 在最后一轮循环中(也就是 \(left == right == mid - 1\)),由于 \(nums[mid] < target\),因此会导致 \(left + 1\)。

  4. 此时 left 恰好指向 mid,完成循环。

查找右侧边界

查找右侧边界与左侧同理,只是收束方向变为向右收束:

int right_bound(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;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 这里改成收缩左侧边界即可
            left = mid + 1;
        }
    }
    // 最后改成返回 left - 1
    if (right  < 0 || right >= nums.size()) {
        return -1;
    }

    return right;
}

例子

69. x 的平方根

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

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

分析

现假设给定整数 10,10 的算术平方根为 3.16。通过二分查找可以将题目抽象为:

给定单调非递减数组 \([0, 10]\),求给定的 \(target = 3.16\)。若 target 存在,返回 target,若 target 不存在,则返回小于 target 的最大值

根据上文 所提到的可知此题目实际上是在求 target 的右侧边界。和普通二分查找唯一不同的是 target 的计算方式不同。另外还需要注意值溢出的问题:

int Sqrt(int x) {
  if (x <= 0) {
    return 0;
  }

  int l = 0;
  int r = x;

  while (l <= r) {
    unsigned long long mid = (r - l) / 2 + l;
    long long pow = mid * mid;

    if (pow == x) {
      l = mid + 1;
    } else if (pow > x) {
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }

  return (r - l) / 2 + l;
}

搜索旋转排序数组

对二分查找的一个拓展,需要对数组的有序部分进行判断

数组从某个节点对前后两个部分进行了交换,求一个时间复杂度为 \(O(\log n)\) 的算法[1]

题解:

数组被旋转为了两部分,但是我们依然可以通过判断有序部分使用二分搜索。判断部分分为三个指针:l, mid, target。

  • 如果 arr[l] < arr[mid] 说明 [l,mid] 处于有序部分,这时再进行判断:

    • 如果 target > nums[l] 并且 target < nums[mid] 说明 target 处于有序部分,直接将 r = mid - 1 即可

    • 否则说明 target 不在此部分,将 l = mid + 1 即可

  • 如果 arr[l] > arr[mid] 。因为数组默认是升序排列的,所以这种情况说明 arr[l,mid] 是无序部分。这时再进行判断:

    • 如果 target > arr[mid] 并且 target < arr[l] 说明数据位于 arr[l:] 部分,直接将 l = mid + 1 即可

    • 否则说明数据位于 arr[l,mid]。令 r = mid - 1

class Solution {
public:
   int search(vector<int>& nums, int target) {
      if(nums.empty()) return -1;
      if(nums.size() == 1) return nums[0] == target ? 0 : -1;
      int l = 0;
      int r = nums.size() - 1;
      int mid;
      while(l < r) {
            mid = (l + r) / 2;
            if(nums[mid] == target) return mid;
            // 数组前半部分和后半部分进行了交换
            if(nums[0] <= nums[mid]) {
               // 这时说明当前位于前半部分(交换前)
               if(nums[0] <= target && target < nums[mid]) r = mid - 1;
               else
                  l = mid + 1;
            } else {
               if(nums[mid] < target && target <= nums[nums.size() - 1]) l = mid + 1;
               else
                  r = mid - 1;
            }
      }
      return -1;
   }
};
Last moify: 2025-01-29 05:38:44
Build time:2025-07-18 09:41:42
Powered By asphinx