欢迎阅读阳明Coding的博客,这一章带来了力扣 hot 100关于二分查找问题的解决方法。这期在代码上加上了详细注释,看讲解很迷糊的朋友可以直接copy代码结合注释看更好理解。

目录
二分查找算法
在开始题目教程之前,先讲解一下二分查找算法是什么。
二分查找是一种在有序数组中快速查找目标值的算法 ,核心是每次将搜索的范围缩小一半。时间复杂度为 O(log n),效率远远高于线性查找的 O(n)。
基本前提:
- 数组必须是有序的(升序 或者降序)
- 用于静态查找表(没有频繁插入删除的场景)
算法步骤

基于上面的分析,当我们看到有序数组这个字眼,和寻找具体和某个值相关的问题时,可以考虑套用二分查找算法。
搜索插入的位置

为什么选择二分查找?
从条件上看:
必须使用 O(log n) 算法 → 直接指向二分查找
数组是有序的 → 二分查找的前提条件
从题目看:
找目标值位置 → 标准二分查找
找不到时找插入位置 → 其实就是找第一个大于等于 target 的位置
这是二分查找的变种(lower_bound)
代码流程
初始化左边界 left = 0,右边界 right = nums.length - 1
进入循环,条件:left <= right
计算中间位置 mid = left + (right - left) / 2
判断:
如果 nums[mid] == target:找到目标,直接返回 mid
如果 nums[mid] < target:目标在右侧,left = mid + 1
如果 nums[mid] > target:目标在左侧,right = mid - 1
循环结束后,left 指向的就是插入位置
原因:循环结束时 left 是第一个大于 target 的元素位置
代码
class Solution {
public int searchInsert(int[] nums, int target) {
// 定义二分查找的左右边界
int left = 0;
int right = nums.length - 1;
// 标准二分查找循环条件
while (left <= right) {
// 计算中间位置,避免整数溢出
int mid = left + (right - left) / 2;
// 找到目标值,直接返回索引
if (nums[mid] == target) {
return mid;
}
// 中间值大于目标值,目标值在左半部分
else if (nums[mid] > target) {
right = mid - 1;
}
// 中间值小于目标值,目标值在右半部分
else {
left = mid + 1;
}
}
// 循环结束时,left指向目标值应该插入的位置
// 这是因为:
// 1. 如果target不存在,left会停在第一个大于target的元素位置
// 2. 或者停在数组末尾(当target大于所有元素时)
return left;
}
}
以 nums = [1,3,5,6], target = 2 手绘代码执行图如下

搜索二维矩阵

思路分析
为什么用二分查找?
1. 数据特性利用:
每行递增,且下一行首元素大于上一行末元素
整个矩阵可以展开成一个有序的一维数组2. 降维转换:
二维坐标 [i][j] ↔ 一维索引 idx
公式:行号 = idx / n,列号 = idx % n(n为列数)
代码流程
获取矩阵的行数 m 和列数 n
将二维矩阵虚拟成一维数组,总长度 = m * n
初始化 left = 0,right = m*n - 1
进入二分循环:
计算 mid 索引
将一维索引转换为二维坐标:
行号 = mid / n
列号 = mid % n比较 matrix[行号][列号] 与 target:
相等:返回 true
小于:left = mid + 1
大于:right = mid - 1循环结束未找到,返回 false
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
// 获取矩阵的行数和列数
int m = matrix.length;
int n = matrix[0].length;
// 将二维矩阵视为一维有序数组进行二分查找
// 总元素个数为 m*n
int left = 0, right = m * n - 1;
// 标准二分查找循环
while (left <= right) {
// 计算中间位置,避免整数溢出
int mid = left + (right - left) / 2;
// 将一维索引 mid 转换为二维矩阵的行列索引
// 行索引 = mid / 列数n (整除得到行号)
// 列索引 = mid % 列数n (取余得到列号)
int i = mid / n;
int j = mid % n;
// 找到目标值,返回 true
if (target == matrix[i][j]) {
return true;
}
// 目标值大于中间值,在右半部分继续查找
else if (target > matrix[i][j]) {
left = mid + 1;
}
// 目标值小于中间值,在左半部分继续查找
else if (target < matrix[i][j]) {
right = mid - 1;
}
}
// 循环结束未找到目标值,返回 false
return false;
}
}
以示例一画一下代码执行流程图

具体自己可以推算一遍

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

为什么用二分查找?
O(log n) 时间复杂度要求 → 必须二分
有序数组(非递减) → 满足二分前提
代码执行流程
查找起始位置:
初始化 left=0, right=n-1, res=-1
循环条件:left <= right
计算 mid
判断:
如果 nums[mid] < target:left = mid + 1
否则(>= target):
如果等于 target:记录 res = mid
继续向左搜索:right = mid - 1循环结束,res 就是起始位置
查找结束位置:
重新初始化 left=0, right=n-1, res=-1
循环条件:left <= right
计算 mid
判断:
如果 nums[mid] > target:right = mid - 1
否则(<= target):
如果等于 target:记录 res = mid
继续向右搜索:left = mid + 1循环结束,res 就是结束位置
class Solution {
public int[] searchRange(int[] nums, int target) {
// 初始化结果数组,默认值为[-1, -1]
int[] res = new int[2];
// 分别查找起始位置和结束位置
res[0] = findFirst(nums, target);
res[1] = findEnd(nums, target);
return res;
}
// 查找目标值在数组中的起始位置(第一次出现的位置)
public int findFirst(int[] nums, int target) {
int res = -1; // 默认未找到
int left = 0;
int right = nums.length - 1;
// 二分查找循环
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (nums[mid] < target) {
// 中间值小于目标值,目标值在右半部分
left = mid + 1;
} else {
// 中间值大于等于目标值
if (nums[mid] == target) {
// 记录当前位置,可能是起始位置
res = mid;
}
// 继续向左查找,尝试找到更早出现的目标值
right = mid - 1;
}
}
return res; // 返回起始位置,未找到返回-1
}
// 查找目标值在数组中的结束位置(最后一次出现的位置)
public int findEnd(int[] nums, int target) {
int res = -1; // 默认未找到
int left = 0;
int right = nums.length - 1;
// 二分查找循环
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (nums[mid] > target) {
// 中间值大于目标值,目标值在左半部分
right = mid - 1;
} else {
// 中间值小于等于目标值
if (nums[mid] == target) {
// 记录当前位置,可能是结束位置
res = mid;
}
// 继续向右查找,尝试找到更晚出现的目标值
left = mid + 1;
}
}
return res; // 返回结束位置,未找到返回-1
}
}
搜索旋转排序数组

为什么还能用二分查找?
-
O(log n) 要求 → 只能二分
-
虽然是旋转数组,但局部有序 → 可以利用二分
初始化 left=0, right=n-1
循环条件:left <= right
计算 mid
如果 nums[mid] == target:直接返回 mid
判断哪部分是有序的:
如果 nums[left] <= nums[mid]:左半部分有序
判断 target 是否在 [nums[left], nums[mid]] 范围内:
是:right = mid - 1(在左侧搜索)
否:left = mid + 1(在右侧搜索)否则:右半部分有序
判断 target 是否在 [nums[mid], nums[right]] 范围内:
是:left = mid + 1(在右侧搜索)
否:right = mid - 1(在左侧搜索)循环结束未找到,返回 -1
class Solution {
public int search(int[] nums, int target) {
// 初始化二分查找的左右边界
int left = 0, right = nums.length - 1;
// 二分查找循环
while (left <= right) {
// 计算中间位置,防止整数溢出
int mid = left + (right - left) / 2;
// 找到目标值,直接返回索引
if (nums[mid] == target) {
return mid;
}
// 情况1:左半部分有序(即旋转点不在左半部分)
// 注意:这里使用 <= 是为了处理 left 和 mid 相等的情况
if (nums[left] <= nums[mid]) {
// 目标值在有序的左半部分范围内
if (nums[left] <= target && target <= nums[mid]) {
// 在左半部分继续搜索
right = mid - 1;
} else {
// 目标值不在左半部分,转向右半部分
left = mid + 1;
}
}
// 情况2:右半部分有序(即旋转点不在右半部分)
// 注意:这里使用 <= 是为了处理 mid 和 right 相等的情况
if (nums[mid] <= nums[right]) {
// 目标值在有序的右半部分范围内
if (nums[mid] < target && target <= nums[right]) {
// 在右半部分继续搜索
left = mid + 1;
} else {
// 目标值不在右半部分,转向左半部分
right = mid - 1;
}
}
}
// 循环结束未找到目标值,返回 -1
return -1;
}
}
寻找旋转排序数组中的最小值

为什么用二分查找?
O(log n) 要求 → 只能二分
旋转数组的局部有序性 → 可以通过比较缩小搜索范围
代码流程
初始化 left=0, right=n-1
循环条件:left < right(注意不是<=)
计算 mid
比较 nums[mid] 和 nums[right]:
如果 nums[mid] > nums[right]:
最小值在右侧,left = mid + 1如果 nums[mid] <= nums[right]:
最小值在左侧或就是 mid,right = mid循环结束时 left == right,指向最小值
class Solution {
public int findMin(int[] nums) {
// 初始化左右指针,覆盖整个数组
int left = 0;
int right = nums.length - 1;
// 使用二分查找,循环条件是 left < right,不是 <=
// 当 left == right 时,就找到了最小值的位置
while (left < right) {
// 计算中间位置,防止整数溢出
int mid = left + (right - left) / 2;
// 情况1:中间值大于右端值
// 说明最小值在 mid 的右侧(因为旋转点在右侧)
// 例如: [4,5,6,7,0,1,2] 中,nums[mid]=7 > nums[right]=2
if (nums[mid] > nums[right]) {
// 最小值在 mid 右侧(不包括 mid)
left = mid + 1;
}
// 情况2:中间值小于等于右端值
// 说明最小值在 mid 的左侧或者就是 mid 本身
// 例如: [4,5,6,0,1,2,3] 中,nums[mid]=0 <= nums[right]=3
else {
// 最小值在 mid 左侧(包括 mid)
right = mid;
}
}
// 循环结束时,left == right,指向最小值的位置
return nums[left];
}
}
寻找两个正序数组的中位数

为什么用二分查找?
O(log(m+n)) 要求 → 必须二分
两个数组都有序 → 可以利用有序性快速排除元素
代码流程
确保 nums1 是较短的数组(方便处理)
初始化 left=0, right=m(nums1的长度)
循环条件:left <= right
计算分割点:
i = (left+right)/2(nums1左半部分元素个数)
j = (m+n+1)/2 - i(nums2左半部分元素个数)处理边界:
nums1左最大 = i==0 ? 最小整数 : nums1[i-1]
nums1右最小 = i==m ? 最大整数 : nums1[i]
nums2左最大 = j==0 ? 最小整数 : nums2[j-1]
nums2右最小 = j==n ? 最大整数 : nums2[j]检查分割是否有效:
如果 nums1左最大 <= nums2右最小 且 nums2左最大 <= nums1右最小:
分割有效,计算中位数候选值如果 nums1左最大 > nums2右最小:
分割点太靠右,right = i-1否则:
分割点太靠左,left = i+1根据总长度奇偶性返回中位数:
奇数:返回左半部分最大值
偶数:返回(左半部分最大值+右半部分最小值)/2
二分查找算法
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 确保 nums1 是较短的数组,这样可以减少二分查找的范围
// 这是为了优化性能,确保在较短的数组上进行二分查找
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.length; // 较短数组的长度
int n = nums2.length; // 较长数组的长度
int left = 0, right = m; // 在较短数组上进行二分查找
// median1: 中位数的第一个值(奇数时为中位数,偶数时为左中位数)
// median2: 中位数的第二个值(偶数时为右中位数)
int median1 = 0, median2 = 0;
// 二分查找较短数组的分割点
while (left <= right) {
// i: nums1 中左半部分的元素个数(分割点左边有 i 个元素)
// j: nums2 中左半部分的元素个数(分割点左边有 j 个元素)
int i = (left + right) / 2;
int j = (m + n + 1) / 2 - i; // 总左半部分应有 (m+n+1)/2 个元素
// 处理边界情况:
// nums1 分割点左边的最大值,如果 i==0 则为负无穷(表示 nums1 左半部分为空)
int nums_im = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
// nums1 分割点右边的最小值,如果 i==m 则为正无穷(表示 nums1 右半部分为空)
int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
// nums2 分割点左边的最大值,如果 j==0 则为负无穷(表示 nums2 左半部分为空)
int nums_jm = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
// nums2 分割点右边的最小值,如果 j==n 则为正无穷(表示 nums2 右半部分为空)
int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);
// 检查分割点是否满足中位数的条件:
// 左半部分的最大值应小于等于右半部分的最小值
if (nums_im <= nums_j) {
// 当前分割点有效,记录候选的中位数
median1 = Math.max(nums_jm, nums_im); // 左半部分的最大值
median2 = Math.min(nums_i, nums_j); // 右半部分的最小值
// 尝试将分割点向右移动,寻找更优的分割
left = i + 1;
} else {
// 当前分割点无效(nums1 左半部分的最大值太大)
// 需要将分割点向左移动
right = i - 1;
}
}
// 根据总元素个数的奇偶性返回中位数
// 偶数个元素:返回两个中位数的平均值
// 奇数个元素:返回左中位数(median1)
return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
}
}
其他算法
同时遍历两个有序数组,每次取最小值前进,记录中间位置的两个数求中位数。
public double findMedianSortedArrays(int[] A, int[] B) {
int m = A.length;
int n = B.length;
// n 代表总长度,不是代码开头定义的 B 数组长度
int totalLen = m + n;
// left 和 right 用于记录中间的两个数
int left = -1, right = -1;
// aStart 和 bStart 是两个数组的指针
int aStart = 0, bStart = 0;
// 遍历到总长度的一半即可找到中位数
for (int i = 0; i <= totalLen / 2; i++) {
// 将上一轮的 right 赋给 left
left = right;
// 比较两个数组当前指针的值,选择较小的那个
// 条件:A 数组未越界 且 (B 数组已越界 或 A 当前值较小)
if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) {
right = A[aStart++];
} else {
right = B[bStart++];
}
}
// 根据总长度的奇偶性返回中位数
if ((totalLen & 1) == 0) {
// 偶数长度,中位数为中间两个数的平均值
return (left + right) / 2.0;
} else {
// 奇数长度,中位数就是 right
return right;
}
}
总结起来就是,如果算看到有序数组,O log(n)这一个 关键字眼,可以优先考虑使用二分查找解决。当然也要结合题目实际。有疑问或者改进建议的友友可以评论区留言一下。你们的关注,点赞,收藏就是我更新的最大动力。
1406

被折叠的 条评论
为什么被折叠?



