LeetCode hot100——74.搜索二维矩阵:Java 二分模板

一句话说明核心方法

题目两个条件保证了矩阵按行展开后就是一条严格有序的一维数组 ,所以不必真的展平,直接对虚拟下标 [0, m·n - 1] 做二分,用 mid / nmid % n 把一维下标映射回二维坐标取值比较即可。


思路推导

题意转化 :在 m×n 矩阵里找 target,要求 O(log(m·n))------这个复杂度上限明示了做法:整个矩阵当作一个长度为 m·n 的有序数组,做一次标准二分

关键观察 1:为什么"虚拟展开"是合法的?把矩阵按行首尾相接拼成一条:

复制代码
[1,3,5,7 | 10,11,16,20 | 23,30,34,60]
  • 条件一(每行从左到右递增)保证每段内部有序;
  • 条件二(每行第一个 > 前一行最后一个)保证段与段的接缝处也递增。

两条拼起来,整条序列全局严格递增------这就是二分的前提。

关键观察 2:一维下标 ↔ 二维坐标的映射是纯算术------

  • 展开序列中第 idx 个元素位于第 idx / n 行、第 idx % n 列(n 是列数);
  • 例如 n=4 时,idx=5 → 行 5/4=1,列 5%4=1,即 matrix11

二分过程示意(matrix 3×4 = 展开长度 12,target = 13)

复制代码
展开序列(虚拟):  idx: 0  1  2  3 | 4  5  6  7 | 8  9  10 11
                 val: 1  3  5  7 |10 11 16 20 |23 30 34 60

初始:  left = 0, right = 11            区间 [0, 11]

第1轮:  mid = 0 + (11-0)/2 = 5
        row = 5/4 = 1, col = 5%4 = 1 → val = 11
        11 < 13 → left = mid + 1 = 6

        idx: 0  1  2  3 | 4  5  6  7 | 8  9  10 11
                              L   R          ← 区间 [6, 11]

第2轮:  mid = 6 + (11-6)/2 = 8
        row = 8/4 = 2, col = 8%4 = 0 → val = 23
        23 > 13 → right = mid - 1 = 7        ← 区间 [6, 7]

第3轮:  mid = 6 + (7-6)/2 = 6
        row = 6/4 = 1, col = 6%4 = 2 → val = 16
        16 > 13 → right = mid - 1 = 5        ← 区间 [6, 5] 为空

left(6) > right(5),循环结束 → return false ✓

13 确实不在矩阵里,二分三轮就把它"夹"没了------如果 target 是 16,第 3 轮 mid=6 正好命中,直接 return true。坐标映射每轮都现算,整个过程像在一条一维数组上搜索,但一个额外格子都没开


Java 完整代码

复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length;
        int n = matrix[0].length;
        int left = 0;
        int right = m * n - 1;            // 左闭右闭:最后一个合法下标是 m*n - 1

        while (left <= right) {           // 闭区间非空条件:带等号
            int mid = left + (right - left) / 2;   // 防溢出写法
            int row = mid / n;            // 一维下标 → 行号
            int col = mid % n;            // 一维下标 → 列号
            int val = matrix[row][col];

            if (val == target) {
                return true;              // 命中,直接返回(本题无重复,可提前返回)
            } else if (val > target) {
                right = mid - 1;          // mid 位置确定偏大,闭区间丢弃 mid 本身
            } else {                      // val < target
                left = mid + 1;
            }
        }
        return false;
    }
}

关键代码逐行解释

  • int right = m * n - 1 ------左闭右闭约定的起点:把矩阵看成长度 m·n 的数组,最后一个下标是 m·n−1。注意这里没有真的分配 m·n 的数组,right 只是个虚拟边界,矩阵在内存里还是 m 行 n 列。

  • row = mid / n, col = mid % n ------本题独有的"坐标反解"。除法算"前面已经放满了几个完整行",取模算"在当前行里偏移几个"。除数必须是列数 n 而不是行数 m,写反了映射就全错(高频手误,一跑样例就露馅)。

  • while (left <= right)right = mid - 1 ------左闭右闭的完整套装。mid 位置的值已经比较过且确定不是答案,所以两侧收缩都要把 mid 本身 排除(mid ± 1);区间为空的条件是 left > right,循环条件带等号。对照 35/34 的右开写法(right = mid,不带等号),差异全部源于"mid 是否还可能是答案":本题每次比较都明确排除 mid,右闭合适;上一题 mid 要留给边界当候选,右开合适。

  • val == target 提前返回 ------本题矩阵元素唯一 (由两条递增条件可推出整条展开序列无重复),命中即是最终答案,提前返回是安全且省轮次的。对照 34:那里有重复且要找边界,提前返回就是 bug------"能不能提前 return"取决于重复性,不取决于个人习惯

  • left + (right - left) / 2------m·n ≤ 10⁴ 本题不会溢出,但矩阵维度不受限制时(比如 m、n 各 10⁵),left + right 可能到 4×10¹⁰ 超过 int 上限,防溢出写法是硬要求。


时间、空间复杂度

  • 时间复杂度:O(log(m·n))

    • 对长度为 m·n 的虚拟数组二分,每轮区间减半,最多 ⌈log₂(m·n)⌉ 轮;每轮的坐标映射、取值、比较都是 O(1)。由对数性质,log(m·n) = log m + log n,所以它和"先二分行、再二分列"的两次二分(总轮数 log m + log n)渐近相同------殊途同归,但一次二分的代码更短。
  • 空间复杂度:O(1)

    • 只有 left / right / mid / row / col 五个变量;没有复制矩阵,没有展开数组。

易错点

  • 真的展平成新数组再二分 :先双层循环拷出 int[] flat 再二分,时间空间都 O(m·n),直接不满足题目要求。虚拟下标 + 坐标映射就是为绕开这一步。

  • mid / n 写成 mid / m :映射的除数/模数必须是列数 n(每行 n 个元素)。用行数去除,算出来的"行号"毫无意义,样例都过不了。

  • 空矩阵没防护 :题目保证 1 ≤ m, n,但 LeetCode 240 等姊妹题允许 matrix = [],此时 matrix[0].length 直接越界。养成习惯:入口先判 matrix == null || matrix.length == 0 || matrix[0].length == 0(本题可省,迁移时别省)。

  • 两套二分约定混搭 :右闭初始化(right = m*n - 1)配右开的循环条件(left < right)或右开的收缩(right = mid),都会漏掉最后一个元素或死循环。初始化、循环条件、收缩方式三件套必须同源------这是前面几篇反复强调的点,本题换了一套装束,正好检验你是否真懂。

  • 条件一只有"每行递增"就敢展开 (移植到 LeetCode 240 时踩坑):240 的矩阵只保证每行、每列各自递增,行间无大小关系,整体展开不成立,必须换"右上角 elimination"(O(m + n))或逐行二分。看到矩阵题先确认全局有序性,再决定能不能一次二分。


可复用模板

"虚拟展开二分"母版,适用于一切"可线性化为有序序列"的二维结构:

java

复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        int left = 0, right = m * n - 1;          // 左闭右闭

        while (left <= right) {
            int mid = left + (right - left) / 2;
            int val = matrix[mid / n][mid % n];   // ★ 一维下标 → 二维取值,一步到位

            if (val == target) return true;
            else if (val > target) right = mid - 1;
            else left = mid + 1;
        }
        return false;
    }
}

变体提示

  • 行间不保证递增 (LeetCode 240)→ 整体展开失效,改用右上角 elimination :从 matrix[0][n-1] 出发,大于 target 左移、小于 target 下移,O(m + n)。
  • 要找的不只是存在性,还是边界(如矩阵里统计某值次数)→ 命中后不能提前返回,把二分改成上一题的 lower/upper bound 版本,对虚拟下标照样适用。
  • "杨辉三角 / 螺旋"等非线性展开 → 只要能写出 idx → (row, col) 的映射且对应值有序,模板照用;映射写不出来就老老实实按结构遍历。

相似题及区别

  • LeetCode 240 搜索二维矩阵 II :只保证每行、每列各自递增,行间接缝无序,不能整体二分;标准解法是右上角 elimination(O(m + n)),本质是每步排除一整行或一整列。和本题对照,分水岭就是"行首 > 上行行尾"这一条。

  • LeetCode 704 二分查找 :本题的"一维原型",没有任何坐标映射;理解了 704 再看本题,新知识只有 mid / nmid % n 两个算式。

  • LeetCode 35 搜索插入位置 :同样处理一维有序结构,但返回的是插入点(lower bound)而非存在性;若把 35 的要求平移到本题矩阵上,就是把 lower bound 套进虚拟展开框架,返回 (idx / n, idx % n)

  • LeetCode 34 在排序数组中查找元素的第一个和最后一个位置:重复元素下找边界,必须 lower + upper 两次二分;本题矩阵元素唯一,一次二分即可------两题合起来正好覆盖"存在性 vs 边界"两种查询形态。

相关推荐
tyler_download1 小时前
揉扁搓圆transformer架构:NAG优化器算法详解
深度学习·算法·transformer
weixin199701080161 小时前
[特殊字符]《京东POP二手品类对接:B2C订单模型 vs 二手C2C属性的字段转换难题》(附Python源码)
前端·python·算法
2302_1111 小时前
java依赖小结
java·运维·数据库
sunshine22 girl2 小时前
Java学习三 高级语法3, 方法
java·学习
captain3762 小时前
网络原理(3)-TCP核心机制连接管理▲▲▲
java·服务器·网络·ide·网络协议·tcp/ip·java-ee
企业数字化笔记2 小时前
固定资产历史数据怎么导入系统?Excel模板、字段映射和数据校验
android·java·数据库·后端
Jared_devin2 小时前
单头、多头 Self-Attention可视化
人工智能·pytorch·深度学习·算法·机器学习·pycharm
troy1282 小时前
Python 基础语法(一):变量、数据类型与运算符
java·服务器·python
wuyk5553 小时前
18.Kruskal 算法:用最短的边连成一张网
开发语言·算法·图论