一句话说明核心方法
题目两个条件保证了矩阵按行展开后就是一条严格有序的一维数组 ,所以不必真的展平,直接对虚拟下标 [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 是列数); - 例如 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 / n和mid % n两个算式。 -
LeetCode 35 搜索插入位置 :同样处理一维有序结构,但返回的是插入点(lower bound)而非存在性;若把 35 的要求平移到本题矩阵上,就是把 lower bound 套进虚拟展开框架,返回
(idx / n, idx % n)。 -
LeetCode 34 在排序数组中查找元素的第一个和最后一个位置:重复元素下找边界,必须 lower + upper 两次二分;本题矩阵元素唯一,一次二分即可------两题合起来正好覆盖"存在性 vs 边界"两种查询形态。