LeetCode 74. 搜索二维矩阵:两种高效解题思路

2 阅读6分钟

在LeetCode的数组类题目中,「搜索二维矩阵」是一道经典的二分查找应用题,核心考察对有序结构的利用和二分思想的灵活运用。题目给出的矩阵有两个关键特性:每行从左到右非严格递增,且每行第一个元素大于前一行最后一个元素。这两个特性决定了我们可以用高效的二分查找替代暴力遍历,将时间复杂度从O(mn)优化到O(log(mn))或O(logm + logn)。

今天就来拆解这道题的两种主流解题方法,结合代码逐行解析,帮大家理清思路、吃透细节,无论是面试还是日常刷题都能轻松应对。

题目回顾

给定一个m x n的整数矩阵,满足:

  • 每行中的整数从左到右按非严格递增顺序排列。

  • 每行的第一个整数大于前一行的最后一个整数。

给定一个整数target,判断target是否在矩阵中,存在返回true,否则返回false。

示例:若矩阵为[[1,3,5,7],[10,11,16,20],[23,30,34,60]],target=3则返回true,target=13则返回false。

思路一:将二维矩阵“扁平化”为一维数组(最优解)

观察题目给出的矩阵特性,我们会发现一个关键:整个矩阵可以看作是一个“有序的一维数组”。因为每行递增,且下一行的第一个元素大于上一行最后一个,相当于把所有行拼接起来,就是一个严格递增的一维数组。

基于这个特性,我们可以直接对这个“虚拟的一维数组”做二分查找,无需额外处理行和列的关系,只需要将一维数组的索引与二维矩阵的行、列做映射即可。

代码实现(TypeScript)

function searchMatrix_1(matrix: number[][], target: number): boolean {
  // 边界判断:矩阵为空或第一行为空,直接返回false
  if (!matrix || !matrix[0]) {
    return false;
  }
  const m = matrix.length; // 矩阵的行数
  const n = matrix[0].length; // 矩阵的列数
  let low = 0, high = m * n - 1; // 虚拟一维数组的左右边界(索引从0到m*n-1)
  
  // 二分查找核心逻辑
  while (low <= high) {
    let mid = Math.floor((high + low) / 2); // 中间索引(一维数组)
    // 关键:将一维索引mid映射到二维矩阵的行和列
    // 行索引 = Math.floor(mid / n)(相当于mid除以列数取整)
    // 列索引 = mid % n(相当于mid除以列数取余)
    let x = matrix[Math.floor(mid / n)][mid % n];
    
    if (x < target) {
      // 目标值在右半部分,缩小左边界
      low = mid + 1;
    } else if (x > target) {
      // 目标值在左半部分,缩小右边界
      high = mid - 1;
    } else {
      // 找到目标值,直接返回true
      return true;
    }
  }
  // 循环结束仍未找到,返回false
  return false;
};

核心细节解析

  • 边界处理:首先判断矩阵是否为空(matrix为null/undefined)或第一行是否为空(matrix[0]为空),这两种情况直接返回false,避免后续索引越界。

  • 索引映射:这是该思路的核心。假设虚拟一维数组的索引为mid,那么对应的二维矩阵行索引是Math.floor(mid / n)(因为每一行有n个元素,每n个索引对应一行),列索引是mid % n(取余得到当前行内的位置)。

  • 时间复杂度:O(log(mn)),因为二分查找的次数是log2(mn),每次查找仅做一次索引映射和数值比较,时间为O(1)。

  • 空间复杂度:O(1),仅使用了几个变量存储边界和中间值,没有额外开辟空间。

思路二:两次二分查找(先找行,再找列)

如果觉得“扁平化”的索引映射不好理解,还可以采用更直观的两次二分查找:第一步先找到target可能所在的行,第二步在该行中查找target是否存在。

第一步(找行):遍历矩阵的行首元素,找到“最后一个行首元素 ≤ target”的行,因为矩阵每行递增且下一行首元素大于上一行尾元素,target如果存在,一定在这一行。

第二步(找列):在找到的目标行中,用二分查找判断target是否存在。

代码实现(TypeScript)

function searchMatrix_2(matrix: number[][], target: number): boolean {
  // 边界判断:矩阵为空或第一行为空,直接返回false
  if (!matrix || !matrix[0]) {
    return false;
  }
  const m = matrix.length;
  const n = matrix[0].length;

  // 第一步:二分查找目标行(返回target可能所在的行索引,找不到返回-1)
  const searchRow = (): number => {
    let low = 0, high = m - 1;
    let targetRow = -1; // 初始化目标行为-1(表示未找到)
    while (low <= high) {
      const mid = Math.floor((high + low) / 2);
      if (matrix[mid][0] <= target) {
        // 当前行首元素 ≤ target,可能是目标行,继续向右查找更合适的行
        low = mid + 1;
        targetRow = mid; // 更新目标行
      } else if (matrix[mid][0] > target) {
        // 当前行首元素 > target,目标行在左半部分
        high = mid - 1;
      }
    }
    return targetRow;
  }

  const row = searchRow();
  if (row === -1) {
    // 没有找到符合条件的行,直接返回false
    return false;
  }

  // 第二步:在目标行中二分查找target
  const searchCol = (): boolean => {
    let low = 0, high = n - 1;
    while (low <= high) {
      const mid = Math.floor((low + high) / 2);
      if (matrix[row][mid] < target) {
        low = mid + 1;
      } else if (matrix[row][mid] > target) {
        high = mid - 1;
      } else {
        return true;
      }
    }
    return false;
  }

  return searchCol();
};

核心细节解析

  • 找行逻辑:searchRow函数中,我们始终记录“最后一个行首元素 ≤ target”的行,因为如果target存在,必然在这一行(下一行的行首元素大于target,不可能在下行)。如果循环结束后targetRow仍为-1,说明所有行首元素都大于target,target不存在。

  • 找列逻辑:searchCol函数就是标准的一维数组二分查找,在目标行中遍历列,判断target是否存在。

  • 时间复杂度:O(logm + logn),两次二分查找分别消耗O(logm)和O(logn),整体与思路一的O(log(mn))等价(因为log(mn) = logm + logn)。

  • 空间复杂度:O(1),同样没有额外开辟空间,仅使用局部变量。

两种思路对比与总结

思路核心逻辑时间复杂度空间复杂度特点
思路一(扁平化)将矩阵看作一维数组,一次二分查找O(log(mn))O(1)代码简洁,索引映射是关键,稍难理解
思路二(两次二分)先找行,再找列,两次二分查找O(logm + logn)O(1)逻辑直观,分步清晰,容易理解

解题关键总结

这道题的核心是利用矩阵的“有序性”——无论是将其扁平化看作一维有序数组,还是分两步找行、找列,本质都是二分查找思想的应用。解题时需要注意两个关键点:

  1. 边界处理:必须先判断矩阵是否为空、行是否为空,避免索引越界。

  2. 索引映射(思路一)/ 行判断(思路二):这是两种思路的核心,也是容易出错的地方,需要熟练掌握。