算法笔记 18 二分查找

之前学得有点迷迷糊糊、囫囵吞枣,现在重新学一遍。

1 核心框架

零、二分查找框架

cpp 复制代码
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 = ...
        }
    }
}

为什么 mid = left + (right - left) / 2 而不是 (left + right) / 2

核心原因:避免整数溢出

  • 假设 leftright 都是接近 int 最大值的数(比如 left = 2^30right = 2^30),那么 left + right 会超过 int 的取值范围(int 最大值约 2^31-1),导致整数溢出(结果变成负数)。
  • left + (right - left) / 2 等价于 (left + right) / 2(数学上完全相等),但计算过程中 right - left 不会溢出(因为 right >= left,且差值远小于 int 最大值),因此更安全。
  • 补充:在 C++11 及以上,也可以用 mid = (long long)left + right / 2 强制类型转换避免溢出,但 left + (right - left)/2 是更通用的写法。

2 边界处理

二分查找的核心痛点就是边界处理,绝大多数错误都源于「区间定义」和「更新规则」不一致。想要彻底掌握边界细节,关键是先固定「区间定义」,再严格遵循对应的规则。以下是一套可落地的边界注意事项和避坑指南:

一、先明确:二分查找的 2 种核心区间定义(二选一,全程不变)

所有边界规则都围绕「区间是否包含端点」展开,先选一种风格,不要混用:

区间风格 核心定义 初始值 循环条件 核心特点
闭区间 [L, R] 包含左、右边界 L=0,R=len-1 L ≤ R 需检查最后一个元素(L==R 时)
左闭右开 [L, R) 包含左边界,不包含右边界 L=0,R=len L < R R 永远是 "越界 / 不检查" 的位置

二、按区间风格,牢记「边界更新 + 终止处理」规则(避坑核心)

1. 闭区间 [L, R] 规则(新手优先)

核心:每一步都要明确「排除已检查的 mid」,避免死循环 / 漏查

  • ✅ 循环条件:L <= R(因为 [L, R] 非空时都要检查,比如 L=R 时还有 1 个元素);
  • ✅ 当 nums[mid] < target:目标在右侧,排除 mid → L = mid + 1
  • ✅ 当 nums[mid] > target:目标在左侧,排除 mid → R = mid - 1
  • ✅ 当 nums[mid] == target:按需处理(找值返回 mid / 找左边界继续左移 R / 找右边界继续右移 L);
  • ✅ 循环终止后:L > R,此时 L 是「第一个大于 target 的位置」,R 是「最后一个小于 target 的位置」。

避坑点

  • ❌ 不要写 L = midR = mid:比如 L=2、R=3 时,mid=2,若更新 L=mid,循环永远无法终止;
  • ❌ 循环条件写 L < R:会漏掉最后一个元素(比如数组只有 1 个元素时,循环直接不执行)。
2. 左闭右开 [L, R) 规则(进阶)

核心:R 是 "不检查的位置",更新时保留候选 mid

  • ✅ 循环条件:L < R(因为 [L, R) 为空时终止,比如 L=R 时无元素);
  • ✅ 当 nums[mid] < target:目标在右侧,排除 mid → L = mid + 1
  • ✅ 当 nums[mid] > target:mid 是候选,保留 → R = mid(因为 R 不检查,无需 - 1);
  • ✅ 当 nums[mid] == target:按需处理(找左边界→R=mid / 找右边界→L=mid+1);
  • ✅ 循环终止后:L == R,指向候选位置(若 L=len 则说明无符合条件的元素)。

避坑点

  • ❌ 初始 R 不要写 len-1:比如数组长度为 3,R=3(而非 2),才能覆盖「0,1,2」三个元素;
  • ❌ 当 nums[mid] > target 时写 R = mid - 1:会排除候选 mid,导致漏查;
  • ❌ 循环终止后直接返回 L:需先判断 L 是否等于 len(越界则按题意处理,比如返回 0)。

三、通用边界注意事项(所有场景都要遵守)

1. mid 计算:避免溢出,统一写法

无论哪种区间风格,mid 都要写:mid = L + (R - L) / 2(而非 (L+R)/2)。

  • 原因:L 和 R 接近 int 最大值时,L+R 会溢出(变成负数),而 R-L 不会;
  • 补充:C++ 中也可写 mid = (long long)L + R / 2,但前者更通用。
2. 重复元素的边界处理(找第一个 / 最后一个目标值)

题目要求 "找第一个大于 target 的字符""找第一个等于 target 的下标" 时,核心是「不直接返回 mid,而是收缩区间保留候选」:

  • 找第一个≥target 的位置:闭区间风格下,nums[mid] >= targetR = mid - 1,循环结束返回 L;
  • 找最后一个≤target 的位置:闭区间风格下,nums[mid] <= targetL = mid + 1,循环结束返回 R。
3. 越界处理(无符合条件元素时)
  • 闭区间:循环结束后 L 可能 = len(比如 target 比所有元素大),R 可能 =-1(比如 target 比所有元素小);
  • 左闭右开:循环结束后 L 可能 = len(无大于 target 的元素),需按题意返回(比如返回数组第一个元素)。
4. 死循环排查(边界错误的典型表现)

如果出现死循环,90% 是区间更新规则错误:

  • 闭区间下:检查是否写了 L=midR=mid(比如 L=2、R=3,mid=2,L=mid 会让 L 永远 = 2);
  • 左闭右开下:检查是否在 nums[mid] > target 时写了 R=mid-1(导致 R<L,循环提前终止,或后续死循环)。

四、实战案例:用规则验证边界(以 744 题为例)

题目要求:找大于 target 的最小字符,无则返回第一个字符。

  1. 选左闭右开风格:
    • 初始:L=0,R=len(比如数组长度 3,R=3);
    • 循环条件:L < R;
    • target < letters[mid] → R=mid(保留候选);
    • target >= letters[mid] → L=mid+1(排除 mid);
    • 循环结束:若 L=len(无符合条件),返回 letters [0],否则返回 letters [L]。
  2. 错误对比:
    • 若初始 R=len-1,循环结束后 L 最大 = len-1,无法判断是否 "所有元素≤target",需额外检查 letters[L] > target
    • 若更新 R=mid-1,会漏掉候选 mid,导致返回错误字符。

五、总结:边界处理的 "口诀"

  1. 先定区间(闭 / 左闭右开),再定规则,全程不换;
  2. 闭区间:L≤R,更新 ±1,终止看 L/R;
  3. 左闭右开:L<R,R 不 - 1,终止查越界;
  4. mid 计算防溢出,重复元素不早返;
  5. 死循环看更新,越界场景提前想。

核心原则:让每一次区间更新都 "排除已确定无关的元素",保留候选元素,边界错误本质是 "要么漏了候选,要么没排除无关元素"。

3 逻辑统一

第一个,最基本的二分查找算法

bash 复制代码
因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1

因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回

第二个,寻找左侧边界的二分查找

bash 复制代码
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid

因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界

第三个,寻找右侧边界的二分查找

bash 复制代码
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid

因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界

又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一
相关推荐
C雨后彩虹1 小时前
机器人活动区域
java·数据结构·算法·华为·面试
MarkHD1 小时前
车辆TBOX科普 第53次 三位一体智能车辆监控:电子围栏算法、驾驶行为分析与故障诊断逻辑深度解析
算法
van久1 小时前
.Net Core 学习: Razor Pages -- EF Core简介
学习·.netcore
苏小瀚2 小时前
[算法]---路径问题
数据结构·算法·leetcode
以孝治家行动2 小时前
学习无止境 行动在进行—以孝治家义工暖心守护独居老人
学习·以孝治家
月明长歌2 小时前
【码道初阶】一道经典简单题:多数元素(LeetCode 169)|Boyer-Moore 投票算法详解
算法·leetcode·职场和发展
wadesir2 小时前
C语言模块化设计入门指南(从零开始构建清晰可维护的C程序)
c语言·开发语言·算法
t198751282 小时前
MATLAB水声信道仿真程序
开发语言·算法·matlab
colus_SEU2 小时前
【编译原理笔记】5.2 SDT Schemes
笔记·编译原理