二分查找-搜索二维矩阵

矩阵的两个性质保证了:将矩阵按行展开成一维数组后,数组是严格递增的 。因此我们可以直接用二分查找,将二维坐标映射为一维索引,时间复杂度为 O(log(mn)),空间复杂度为 O(1)

坐标映射公式

  • 一维索引 idx 对应二维坐标:
    • 行号:row = idx / nn 为矩阵列数)
    • 列号:col = idx % n
  • 二分查找边界:left = 0right = m * n - 1

为什么要用 (left + right) >>> 1

很多新手在写二分查找时,会用最直观的写法:

复制代码
int mid = (left + right) / 2;

但在工业级代码(如 JDK 源码)中,几乎看不到这种写法,取而代之的是:

复制代码
int mid = (left + right) >>> 1;
// 或
int mid = left + (right - left) / 2;

1. 整数溢出

Java 中 int 类型是 32 位有符号整数,取值范围为 [-2^31, 2^31 - 1],即 [-2147483648, 2147483647]

leftright 都接近 int 最大值时:

复制代码
int left = 2147483647;
int right = 2147483647;
// left + right = 4294967294,超出 int 范围,溢出为负数
int sum = left + right; // sum = -2
int mid = sum / 2; // mid = -1,完全错误!

在二分查找中,错误的负数索引会直接导致数组越界异常(ArrayIndexOutOfBoundsException),程序崩溃。

2. >>> 无符号右移:从根源解决溢出

Java 提供了两种右移运算符:

  • >>有符号右移,高位补符号位(正数补 0,负数补 1),等价于除以 2(向下取整)
  • >>>无符号右移,无论正负,高位统一补 0,将数值视为 32 位无符号整数处理
原理验证

对于溢出后的负数 sum = -2(二进制补码为 11111111 11111111 11111111 11111110):

  • sum >> 1:高位补 1,结果为 -1(错误)
  • sum >>> 1:高位补 0,结果为 2147483647(正确,即 (2147483647 + 2147483647) / 2 = 2147483647

对于非负整数,(left + right) >>> 1 完全等价于 (left + right) / 2,但永远不会出现溢出问题,完美解决了二分查找的索引计算风险。

3. 三种写法的全面对比

写法 优点 缺点 适用场景
(left + right) / 2 可读性高 存在整数溢出风险,不安全 仅适用于 left + right 不超过 int 最大值的场景
left + (right - left) / 2 无溢出,可读性较好 运算步骤多,性能略差 所有场景,新手友好
(left + right) >>> 1 无溢出、代码简洁、性能最优 新手可读性差 工业级代码、JDK 源码、高性能场景

延伸:二分查找的其他安全写法

除了无符号右移,还有一种经典的安全写法:

复制代码
int mid = left + (right - left) / 2;

原理

通过变形避免直接相加:

复制代码
left + (right - left) / 2 = (2left + right - left) / 2 = (left + right) / 2

这种写法从根源上避免了 left + right 的溢出,可读性比 >>> 更高,适合新手理解,也是面试中的标准写法。

性能对比

  • (left + right) >>> 1:仅需一次加法 + 一次位运算,JVM 可直接优化为机器指令,性能最优
  • left + (right - left) / 2:需要一次减法 + 一次除法 + 一次加法,性能略差,但现代 JVM 优化后差异极小

实战:搜索二维矩阵的完整优化

1. 边界处理

代码中必须处理矩阵为空、单行、单列等极端情况,避免空指针异常:

复制代码
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
    return false;
}

2. 二分查找的循环条件

  • while (left <= right):适用于闭区间 [left, right],每次循环都会检查 mid,最终 left > right 时退出,代表未找到
  • while (left < right):适用于开区间 [left, right),需要额外处理最后一个元素,容易出错,不推荐新手使用

3. 示例验证

以题目示例矩阵为例:

复制代码
[
  [1, 3, 5, 7],
  [10, 11, 16, 20],
  [23, 30, 34, 60]
]
  • 展开为一维数组:[1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60]
  • 查找 target = 3mid 最终命中索引 1,对应 row=0, col=1,返回 true
  • 查找 target = 13:遍历完所有区间未命中,返回 false

总结

  1. 核心结论int mid = (left + right) >>> 1 是二分查找中安全、高效、无溢出的中间索引计算方法,是 JDK 源码的标准写法。
  2. 原理本质 :利用无符号右移 >>> 将溢出的负数视为无符号整数,保证计算结果正确,同时保持最优性能。
  3. 替代方案left + (right - left) / 2 是新手友好的安全写法,可读性更高,适合面试场景。
  4. 应用场景:所有二分查找算法(如搜索二维矩阵、有序数组查找、旋转数组查找等)都应使用安全写法,避免整数溢出。

拓展思考

1. 为什么题目中的「非严格递增」不影响二分?

虽然每行是「非严格递增」,但「每行第一个元素大于前一行最后一个元素」的性质,保证了整个矩阵展开后是严格递增的,因此二分查找的逻辑完全成立。

2. 其他语言的对应写法

  • Python :Python 整数无溢出,直接用 mid = (left + right) // 2 即可
  • C++ :用 mid = left + (right - left) / 2 避免溢出,无 >>> 运算符
  • JavaScript :用 mid = Math.floor((left + right) / 2),注意位运算会截断为 32 位整数

3. 二分查找的常见坑

  • 整数溢出:必须使用安全写法计算 mid
  • 循环条件错误:left <= rightleft < right 的区别
  • 边界更新错误:left = mid + 1 / right = mid - 1left = mid / right = mid 的选择
  • 坐标映射错误:行号 / 列号的计算逻辑(mid / nmid % n

完整代码

复制代码
/**
 * 搜索二维矩阵
 * @param matrix 满足行内非严格递增、行首严格大于前行尾的矩阵
 * @param target 目标值
 * @return target 是否在矩阵中
 */
public boolean searchMatrix(int[][] matrix, int target) {
    // 边界处理:矩阵为空、行空、列空,直接返回false
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
        return false;
    }
    
    int m = matrix.length; // 矩阵行数
    int n = matrix[0].length; // 矩阵列数
    int left = 0; // 二分左边界(一维索引)
    int right = m * n - 1; // 二分右边界(一维索引)
    
    // 闭区间二分查找:[left, right]
    while (left <= right) {
        // 安全计算中间索引:无符号右移,避免int溢出
        int mid = (left + right) >>> 1;
        // 一维索引转二维坐标
        int row = mid / n;
        int col = mid % n;
        int current = matrix[row][col];
        
        if (current == target) {
            // 找到目标值,返回true
            return true;
        } else if (current < target) {
            // 目标值在右半区间,更新左边界
            left = mid + 1;
        } else {
            // 目标值在左半区间,更新右边界
            right = mid - 1;
        }
    }
    
    // 遍历完所有区间未找到,返回false
    return false;
}
相关推荐
会编程的土豆2 小时前
【数据结构与算法】堆排序
开发语言·数据结构·c++·算法·leetcode
会编程的土豆2 小时前
【数据结构与算法】希尔排序
数据结构·c++·算法·排序算法
邦爷的AI架构笔记2 小时前
GLM-5.1 接入踩坑记录:用免费开源模型搭个 AI 代码审计小工具
后端·算法
苏宸啊2 小时前
哈希扩展问题
算法·哈希算法
汀、人工智能2 小时前
[特殊字符] 第73课:打家劫舍
数据结构·算法·数据库架构·图论·bfs·打家劫舍
别或许2 小时前
2、高数----数列极限(知识总结)
算法
汀、人工智能2 小时前
[特殊字符] 第78课:乘积最大子数组
数据结构·算法·数据库架构·数组·前缀积·乘积最大子数组
tankeven2 小时前
HJ168 小红的字符串
c++·算法
数据知道2 小时前
claw-code 源码分析:cargo 视角的 definitive runtime——会话、压缩、MCP、提示构造如何落到系统语言?
算法·ai·claude code·claw code