hot100 搜索二维矩阵(48)

本题采用双指针剪枝算法(又称"二叉搜索树类比法"或"斜向阶梯搜索法")解决行列均有序的二维矩阵目标值查找问题。其核心本质是将二维网格的单向搜索转化为从特定隅角出发的树状分叉决策,利用单调性在每一步直接排除整行或整列。当前提供的源码实现了在时间复杂度 O(m + n) 和额外空间复杂度 O(1) 条件下的全局最优线性检索,最终走向是精准锁定目标值是否存在。

一、 问题本质与数据模型

对于大小为 m x n 的二维矩阵 matrix,题目给出了两个核心的单调性约束:

  1. 每行元素从左到右严格升序排列:matrixij < matrixij+1

  2. 每列元素从上到下严格升序排列:matrixij < matrixi+1j

如果在网格中心或左上角/右下角启动常规检索,当当前数值不等于目标值时,无法产生具有唯一导向性的剪枝方向(例如在左上角,向右和向下都是递增,无法抉择)。

为了破除这种信息对称的物理困局,算法引入了"隅角选址模型"。通过选择 右上角 (0, n-1) 作为搜索起点,该位置在几何空间上表现出独特的双向对立属性:

  • 向左移动:列下标减小,数值严格递减。

  • 向下移动:行下标增大,数值严格递增。

这种独特的拓扑结构赋予了右上角单元格类似于二叉搜索树(BST)根节点的物理特性,使得每次数值比对都能产生确定性的空间剪枝效应。

二、 算法演进对比

在解决行列有序矩阵的检索问题时,阶梯搜索法在时空资源的利用效率上达到了理论极限:

解法名称 时间复杂度 空间复杂度 核心原理 物理瓶颈 / 缺陷
全矩阵暴力枚举 O(m * n) O(1) 双重循环逐个扫描矩阵中的每一个单元格 完全没有利用行列有序的先验条件,算力大量浪费
逐行二分查找 O(m * log n) O(1) 遍历每一行,对每一行单独执行一维二分查找 仅利用了行单调性,忽略了列单调性,行数极大时效率下降
阶梯搜索法(当前解法) O(m + n) O(1) 利用右上角特异性,每步消除一行或一列,实现线性阶收敛 指针只能沿单向边缘推进,属于边界剪枝极限

三、 核心分支控制逻辑与决策证明

当前源码的控制流完全依赖于 while (i < matrix.length && j >= 0) 所构建的边界防护网,其内部决策分支证明如下:

1. 命中分支:if (matrix[i][j] == target)

  • 执行 :直接返回 true

  • 物理意义:当前坐标成功锁定目标值,检索流程提前终止。

2. 递增剪枝分支:else if (matrix[i][j] < target)

  • 执行i++(行指针下移)。

  • 数学证明 :当前位置 matrix[i][j] 是当前行 i 中从左到右遍历到的最大元素(因为是从右往左缩减列的)。如果连当前行最大的元素都小于目标值 target,那么当前行左侧的所有元素(matrix[i][0 ... j-1])必然都严格小于目标值。

  • 结论 :目标值绝对不可能存在于当前第 i 行中,该行已被完全物理排除,执行 i++ 步进到下一行。

3. 递减剪枝分支:else(即 matrix[i][j] > target

  • 执行j--(列指针左移)。

  • 数学证明 :当前位置 matrix[i][j] 是当前列 j 中从上到下遍历到的最小元素(因为是从上往下推进行的)。如果当前列最小的元素都大于目标值 target,那么当前列下方的所有元素(matrix[i+1 ... m-1][j])必然都严格大或等于该值。

  • 结论 :目标值绝对不可能存在于当前第 j 列中,该列已被完全物理排除,执行 j-- 步进到左侧前一列。

四、 算法执行状态机步进示例

以输入矩阵 matrix = [[1, 4, 7], [2, 5, 8], [3, 6, 9]],目标值 target = 5 为例(规模 3 x 3),指针状态机的演进过程如下表所示:

步骤 当前指针坐标 (i, j) 当前单元格数值 状态判定条件 执行的核心剪枝动作 空间物理剩余状态说明
初始 (0, 2) 7 7 > 5 满足大值分支,执行 j-- (j 变为 1) 排除第 2 列。剩余搜索域缩小为前 2 列
1 (0, 1) 4 4 < 5 满足小值分支,执行 i++ (i 变为 1) 排除第 0 行。剩余搜索域缩小为下方 2 行
2 (1, 1) 5 5 == 5 满足相等条件,触发 return true 检索成功,立即终止流程

五、源码实现

复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 边界安全检查:若矩阵为空或结构不完整,直接判定未找到
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }

        // 初始化行指针 i 指向矩阵最上方(第 0 行)
        int i = 0;
        // 初始化列指针 j 指向矩阵最右侧(最后一列)
        int j = matrix[0].length - 1;

        // 边界控制:确保行指针不触底且列指针不越过最左边界
        while (i < matrix.length && j >= 0) {
            // 条件分支 1:成功命中目标值
            if (matrix[i][j] == target) {
                return true;
            } 
            // 条件分支 2:当前值小于目标值,排除当前行,行指针下移
            else if (matrix[i][j] < target) {
                i++;
            } 
            // 条件分支 3:当前值大于目标值,排除当前列,列指针左移
            else {
                j--;
            }
        }

        // 若超出 while 循环边界仍未命中,说明目标值不存在于矩阵中
        return false;
    }
}

六、 复杂度分析

1. 时间复杂度:O(m + n)

  • 分析 :算法从矩阵的右上角 (0, n-1) 出发。在 while 循环的每一次迭代中,要么行指针 i 递增 1(走向下一行),要么列指针 j 递减 1(走向左一列)。因为指针只能单向步进,i 最多增加 m 次,j 最多减少 n 次。

  • 结论:整个矩阵搜索路径的最大跨度为 m + n 步。总的基本比较操作次数与行数 m 和列数 n 的线性加和呈严格的正比关系。

2. 空间复杂度:O(1)

  • 分析 :算法在执行期间,仅在栈内存中开辟了 ij 两个基础类型的整型局部变量用作物理坐标的定位与移动控制。

  • 结论:没有申请任何与输入矩阵规模相关的外部或临时数据结构,内存空间消耗恒定为常数阶。