
一句话说明核心方法题目两个条件保证了矩阵按行展开后就是一条严格有序的一维数组所以不必真的展平直接对虚拟下标[0, m·n - 1]做二分用mid / n和mid % 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 是列数)例如 n4 时idx5 → 行 5/41,列 5%41,即 matrix[1][1]。二分过程示意(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 轮 mid6 正好命中直接 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))或逐行二分。看到矩阵题先确认全局有序性再决定能不能一次二分。可复用模板“虚拟展开二分”母版适用于一切“可线性化为有序序列”的二维结构javaclass 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 / n和mid % n两个算式。LeetCode 35 搜索插入位置同样处理一维有序结构但返回的是插入点(lower bound)而非存在性若把 35 的要求平移到本题矩阵上就是把 lower bound 套进虚拟展开框架返回(idx / n, idx % n)。LeetCode 34 在排序数组中查找元素的第一个和最后一个位置重复元素下找边界必须 lower upper 两次二分本题矩阵元素唯一一次二分即可——两题合起来正好覆盖“存在性 vs 边界”两种查询形态。