LC 74 搜索二维矩阵:换皮的二分查找,我居然一开始没看出来

不知道你有没有这种刷题错觉 ------一维数组的二分查找我熟得不行,模板都能背下来了。可一碰到二维矩阵的查找题,脑子瞬间就短路,第一反应就是写两层循环暴力搜。

我第一次刷这道题的时候就是这样,噼里啪啦写完遍历,提交一看通过了,正准备跳下一题,突然瞥见题目要求 O (log (m * n)) 的复杂度。合着我写了个不符合要求的解法,白高兴一场。

其实这题真的是纸老虎,剥开二维的外壳,里面就是最基础的二分查找。

先快速说清题意

给你一个 m×n 的整数矩阵,满足两个规则:

  1. 每一行从左到右严格递增
  2. 下一行的第一个数,一定比上一行最后一个数还要大

再给你一个目标数 target,判断 target 是否存在于矩阵中。核心要求:时间复杂度必须是对数级别,不能暴力遍历。

思路一:先找行,再行内搜(两次二分)

这是我最开始想到的路子,比较符合直觉,脑子不用拐太多弯。既然每一行的第一个数都是递增的,那第一步先用二分,锁定 target 可能落在哪一行。

具体怎么找行呢?不是找行首等于 target 的,那大概率找不到。我们要找的是:行首元素小于等于 target 的最后一行

比如示例里 target = 16,三行的行首分别是 1、10、23。小于等于 16 的行首有 1 和 10,最后一个是第 1 行,那 target 就只可能在第 1 行里。如果 target 比第一行的行首还小,那直接就可以返回 false 了。

这个方法我踩过一个小坑:最开始找完行,我直接就进去二分了,忘了判断 target 是不是比这一行的最后一个元素还大。比如 target = 21,按规则会分到第 1 行,但第 1 行最大是 20,这时候直接返回 false 就行,不用再做无用的二分。加这个小判断既能省时间,也能避免一些边界错误。

锁定行之后,这一行本身就是个有序数组,再做一次普通的二分查找就搞定了。

这个写法好处是思路顺,一步步来不容易乱;坏处就是要写两套二分逻辑,代码稍长一点。

两次二分代码实现

javascript 复制代码
var searchMatrix = function(matrix, target) {
    const m = matrix.length;
    if (m === 0) return false;
    const n = matrix[0].length;

    // 第一次二分:找目标所在的行
    let top = 0, bottom = m - 1;
    let targetRow = -1;
    while (top <= bottom) {
        const midRow = Math.floor((top + bottom) / 2);
        const firstNum = matrix[midRow][0];
        if (firstNum === target) {
            // 刚好等于行首,直接命中
            return true;
        } else if (firstNum < target) {
            // 这行有可能,先记下来,再往下看看有没有更合适的
            targetRow = midRow;
            top = midRow + 1;
        } else {
            bottom = midRow - 1;
        }
    }

    // 没找到符合条件的行,说明target比所有数都小
    if (targetRow === -1) return false;
    // 小优化:target比这行最后一个数还大,直接不用搜了
    if (matrix[targetRow][n - 1] < target) return false;

    // 第二次二分:在目标行里精确查找
    let left = 0, right = n - 1;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const num = matrix[targetRow][mid];
        if (num === target) {
            return true;
        } else if (num < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    return false;
};

找行的二分写法有好几种,我这种是遇到符合条件就记录行号的写法,个人觉得最好理解,不容易写错边界。

思路二:拍平成一维,一次二分搞定

这个是我复盘时发现的更优雅的写法,也是我现在写题时更偏爱的。

你仔细品题目给的两个条件:每行递增 + 下一行开头大于上一行结尾。这不就等于说,把所有行按顺序拼在一起,就是一个从头到尾完全有序的一维数组吗?

既然是有序数组,那不就是标准二分查找的主场?唯一要处理的,就是把一维数组的索引,换算回二维矩阵的行和列。

换算公式很简单,假设总列数是 n,一维索引是 mid:

  • 行号 = Math.floor (mid /n)
  • 列号 = mid % n

这个坑我实打实踩过!一开始手滑写成了除以行数 m,结果直接数组越界,debug 了五分钟才反应过来。记住,是除以列数,因为一行有 n 个元素,每满 n 个就换下一行。

一次二分代码实现

javascript 复制代码
var searchMatrix = function(matrix, target) {
    const m = matrix.length;
    // 空矩阵直接返回,边界情况别漏了
    if (m === 0) return false;
    const n = matrix[0].length;
    
    let left = 0;
    let right = m * n - 1; // 虚拟一维数组的右边界
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        // 核心:一维索引转二维坐标
        const row = Math.floor(mid / n); 
        const col = mid % n;
        const num = matrix[row][col];
        
        if (num === target) {
            return true;
        } else if (num < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    // 循环跑完都没找到
    return false;
};

复杂度分析

两种写法的时间复杂度其实是等价的。两次二分是 O (log m + log n),一次二分是 O (log (m * n)),根据对数运算法则,这俩完全相等,都符合题目 O (log (m * n)) 的要求。空间复杂度都是 O (1),只用到了几个指针变量。

最后唠两句

这道题说穿了,考的根本不是什么二维数组高级算法,而是你能不能识别出 "全局有序" 这个隐藏条件。很多人一看到矩阵就发怵,觉得肯定有复杂玩法,其实都是纸老虎。

刷题刷多了你就会发现,很多中等题都是 "换皮题",把你熟悉的知识点换个场景考你。能不能看破本质,就是能不能快速解题的关键。

至于两种写法选哪个?面试的时候如果你紧张,怕坐标换算搞混,老老实实写两次二分绝对没问题,一样是满分答案,思路清晰还不容易出错。如果想写得简洁帅气,那就用拍平成一维的写法,代码短,看起来也更有水平。

两个我踩过的坑再提醒大家一遍:

  1. 边界情况别漏,空矩阵、只有一行、只有一列都要考虑到
  2. 用一次二分的话,坐标映射一定是除以列数 n,别和行数 m 搞反

你第一次做这题的时候,第一反应是暴力还是二分?更习惯用两次二分还是一次拍平的写法?评论区唠唠你的刷题趣事~觉得有用的话点个赞,后面继续更更多二分变种题的题解!

相关推荐
文心快码BaiduComate42 分钟前
文心快码荣获“中国优秀软件产品”,实力再获国家级认可
算法·代码规范
橘子星43 分钟前
一篇文章搞懂 useRef:聚焦 DOM、模拟 forceRender、管理 Worker 都靠它
前端·javascript
玖玥拾1 小时前
LeetCode 189 轮转数组
算法·leetcode
前端Hardy1 小时前
Vue 终于杀进终端界!这个开源项目让 CLI 开发像写网页一样简单
前端·javascript·后端
ZhangShao06071 小时前
题解:CF2249B
c++·算法
小玮看世界1 小时前
[Python]线段树与二分法
数据结构·算法
一次旅行1 小时前
多智能体编排实战:拆解Plan-and-Execute范式+三层记忆架构,手写无依赖轻量Agent调度引擎
前端·javascript·架构
sugar__salt3 小时前
跟着 Demo 学 Pinia:两种仓库写法 + 完整 TodoList 复现
前端·javascript·vue.js·前端框架·vue
Tisfy4 小时前
LeetCode 3345.最小可整除数位乘积 I:暴力枚举(从n开始尝试)
数学·算法·leetcode·题解·枚举