欢迎光临
我们一直在努力

二分查找的魔鬼细节:5 道 LeetCode 题踩坑实录

文章目录

  • 二分查找的魔鬼细节:5 道 LeetCode 题踩坑实录
    • 一、流派一:排除法 — 标准查找
      • LeetCode 35:搜索插入位置
      • LeetCode 34:在排序数组中查找第一个和最后一个位置
      • LeetCode 74:搜索二维矩阵
    • 二、流派二:收敛法 — 保留候选
      • LeetCode 153:寻找旋转排序数组中的最小值
      • LeetCode 162:寻找峰值
      • LeetCode 278:第一个错误的版本
    • 三、流派混合:旋转数组搜 target(LeetCode 33)
    • 四、两个流派对比
    • 五、边界判断:二分最容易炸的地方
    • 总结

二分查找的魔鬼细节:5 道 LeetCode 题踩坑实录

你以为二分查找就是 while (l <= r) 然后 return mid?我刷完 5 道题,发现边界、返回值、循环条件,每一条都能出 bug。

这篇文章从一个标准模板出发,覆盖 LeetCode 35 / 34 / 74 / 33 / 153 / 162 六道题,把二分查找的写法分成两个流派,一次性讲清楚。


一、流派一:排除法 — 标准查找

适用场景:给定 target,找到返回下标,找不到返回 -1。

int binarySearch(int[] nums, int target) {
int l = 0, r = nums.length 1;
while (l <= r) {
int mid = l + (r l) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) l = mid + 1;
else r = mid 1;
}
return 1;
}

三条铁律:

  • while (l <= r):mid 确定不是答案就彻底排除,两边都 mid ± 1
  • 区间始终是 [l, r],l == r 时还有一个元素要检查
  • 循环结束 l > r,说明找不到

一句话:mid 不是答案,跳过。配 while (l <= r)。


LeetCode 35:搜索插入位置

要找第一个 >= target 的位置。把标准模板改一行:

int lowerBound(int[] nums, int target) {
int l = 0, r = nums.length 1;
while (l <= r) {
int mid = l + (r l) / 2;
if (nums[mid] < target) l = mid + 1;
else r = mid 1;
}
return l; // 第一个 >= target
}

循环结束 l > r,通常 r = l – 1。此时 l 指向第一个 >= target 的位置,r 指向最后一个 < target 的位置。

条件返回值含义return
nums[mid] < target → l = mid + 1 第一个 >= target(插入位置) l
nums[mid] < target → l = mid + 1 最后一个 < target r
nums[mid] <= target → l = mid + 1 第一个 > target l
nums[mid] <= target → l = mid + 1 最后一个 <= target r

记住这个表,34 题直接秒。


LeetCode 34:在排序数组中查找第一个和最后一个位置

分两步:找左边界(第一个 >= target),找右边界(最后一个 <= target)。

class Solution {
public int[] searchRange(int[] nums, int target) {
int first = findFirst(nums, target);
if (first == 1) return new int[]{1, 1};
int last = findLast(nums, target);
return new int[]{first, last};
}

int findFirst(int[] nums, int target) {
int l = 0, r = nums.length 1;
while (l <= r) {
int mid = l + (r l) / 2;
if (nums[mid] < target) l = mid + 1;
else r = mid 1;
}
// l 可能越右界(target 比所有数都大)
if (l == nums.length || nums[l] != target) return 1;
return l;
}

int findLast(int[] nums, int target) {
int l = 0, r = nums.length 1;
while (l <= r) {
int mid = l + (r l) / 2;
if (nums[mid] <= target) l = mid + 1;
else r = mid 1;
}
// r 可能越左界(target 比所有数都小)
if (r < 0 || nums[r] != target) return 1;
return r;
}
}

关键:findFirst 返回 l,findLast 返回 r。两个函数只有条件里的 = 不一样。


LeetCode 74:搜索二维矩阵

每行递增,且下一行首元素 > 上一行末元素。当成一维数组二分:

class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int l = 0, r = m * n 1;
while (l <= r) {
int mid = l + (r l) / 2;
int val = matrix[mid / n][mid % n];
if (val == target) return true;
else if (val < target) l = mid + 1;
else r = mid 1;
}
return false;
}
}

mid / n 是行,mid % n 是列。转换完就是标准二分。


二、流派二:收敛法 — 保留候选

适用场景:没给 target,要找极值或边界。mid 满足条件时,它自己可能就是答案。

int findMin(int[] nums) {
int l = 0, r = nums.length 1;
while (l < r) {
int mid = l + (r l) / 2;
if (nums[mid] > nums[r]) l = mid + 1;
else r = mid; // mid 可能是答案,不跳过
}
return nums[l]; // l == r → 答案
}

三条铁律:

  • while (l < r):因为 r = mid,l <= r 会死循环
  • 只跳过确定不是答案的那半边,保留可能是答案的 mid
  • 循环结束 l == r,这就是答案

一句话:mid 可能是答案,保留。配 while (l < r)。

为什么只能 r = mid 不能 l = mid?

mid = l + (r – l) / 2 是向下取整。当 r = l + 1 时,mid = l。如果写 l = mid,l 不动,死循环。r = mid 则一定会收缩,因为 mid 永远 < r。

如果你非要 l = mid,就把 mid 改成向上取整:mid = l + (r – l + 1) / 2。


LeetCode 153:寻找旋转排序数组中的最小值

class Solution {
public int findMin(int[] nums) {
int l = 0, r = nums.length 1;
while (l < r) {
int mid = l + (r l) / 2;
if (nums[mid] > nums[r]) l = mid + 1;
else r = mid;
}
return nums[l];
}
}

比较对象必须是 nums[r],不能是 nums[l]。反例:[1,2,3,4,5] 没有旋转,nums[mid]=3 > nums[l]=1 会跳过最小值下标 0。


LeetCode 162:寻找峰值

class Solution {
public int findPeakElement(int[] nums) {
int l = 0, r = nums.length 1;
while (l < r) {
int mid = l + (r l) / 2;
if (nums[mid] > nums[mid + 1]) r = mid; // mid 可能是峰值
else l = mid + 1;
}
return l;
}
}


LeetCode 278:第一个错误的版本

public int firstBadVersion(int n) {
int l = 1, r = n;
while (l < r) {
int mid = l + (r l) / 2;
if (isBadVersion(mid)) r = mid; // mid 就是 bad,可能是第一个
else l = mid + 1;
}
return l;
}


三、流派混合:旋转数组搜 target(LeetCode 33)

这道题需要两个流派配合:先收敛法找旋转点,再排除法搜 target。

class Solution {
public int search(int[] nums, int target) {
int l = 0, r = nums.length 1;

// 直接在一次二分里判断:哪半有序,target 在不在里面
while (l <= r) {
int mid = l + (r l) / 2;
if (nums[mid] == target) return mid;

if (nums[l] <= nums[mid]) { // 左半有序
if (nums[l] <= target && target < nums[mid]) r = mid 1;
else l = mid + 1;
} else { // 右半有序
if (nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid 1;
}
}
return 1;
}
}

三个 = 号一个都不能丢:nums[l] <= nums[mid] 处理 l == mid 的情况;nums[l] <= target 和 target <= nums[r] 处理 target 在边界的情况。


四、两个流派对比

排除法收敛法
循环条件 while (l <= r) while (l < r)
更新方式 l = mid + 1, r = mid – 1 一边 mid ± 1,一边 = mid
结束状态 l > r,用 l 或 r 当返回值 l == r 就是答案
适用 查找 target / 边界 找极值 / 第一个满足条件的
代表题 704, 35, 34, 74 153, 162, 278

五、边界判断:二分最容易炸的地方

二分返回值当数组下标用时,有 3 种越界可能:

  • return l → l 可能 = n(target > 最大值)
  • return r → r 可能 = -1(target < 最小值)
  • 旋转数组里拿返回值去访问数组——先判 idx >= 0 && idx < n

统一防御写法:

int idx = binarySearch(nums, target);
if (idx < 0 || idx >= nums.length) return 1; // 或 return false


总结

  • 有 target 用排除法:while (l <= r),两边都跳过,返回 l 或 r 做边界
  • 没 target 用收敛法:while (l < r),可能答案是 mid 时用 r = mid,别跳过
  • 返回 l:第一个 >= target(插入位置)。返回 r:最后一个 < target
  • 左中位只能配 r = mid,l = mid 要改用上中位或配合 mid + 1
  • 旋转数组跟 nums[r] 比,别跟 nums[l] 比
  • 返回值当数组下标前先判越界
  • 二分查找代码短,但边界条件一个都不能省。搞清两个流派,六道题一套模板搞定。

    赞(0)
    未经允许不得转载:171主机测评 » 二分查找的魔鬼细节:5 道 LeetCode 题踩坑实录
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址