74. 搜索二维矩阵 (Medium)

专题归类: 09-二分查找 · 03-数组与矩阵 LeetCode 链接: https://leetcode.cn/problems/search-a-2d-matrix/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

  • 每行中的整数从左到右按升序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

示例 1:

输入:matrix = [[1,3,5,7],
               [10,11,16,20],
               [23,30,34,60]], target = 3
输出:true

示例 2:

输入:matrix = [[1,3,5,7],
               [10,11,16,20],
               [23,30,34,60]], target = 13
输出:false

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 100
  • -10^4 <= matrix[i][j], target <= 10^4

题目详细分析

数据范围含义:

  • m, n <= 100:暴力 O(mn) 最多 10000 次,也可以过,但题目要求”高效算法”暗示要用 O(log(mn))。
  • 值范围 [-10^4, 10

矩阵的特殊性质:

  • 每行内部升序。
  • 下一行的最小值大于上一行的最大值。
  • 这意味着整个矩阵可以展开成一个一维有序数组!

核心问题转化: 因为矩阵的”下一行最小 > 上一行最大”,整个矩阵实际上是”蛇形有序”的。将二维坐标映射到一维索引,就是一个标准的一维二分查找。

与 240-搜索二维矩阵 II 的区别:

  • 240 题只保证每行升序、每列升序,但不保证”下一行最小 > 上一行最大”,所以不能用一维二分展开,而是用”右上角搜索法”。

小白版直白理解

就像一个大书架,有 m 层(行),每层有 n 本书(列)。书的摆放规则是:

  1. 每一层从左到右越来越厚(编号递增)。
  2. 下一层最薄的书也比上一层最厚的书厚。

你可以在脑子里把这个书架想象成一整条长龙:把第一层的书全部取下来,接上第二层、第三层……就得到了一条完全按厚度排序的长队伍。然后在长队伍里用”翻字典法”(二分查找)找目标书。


解题思路

思路一:将二维展开为一维进行二分查找(推荐)

核心思想:m x n 的矩阵看作长度为 m x n 的一维有序数组。通过数学映射在 O(log(mn)) 时间内完成查找。

映射关系:

  • 一维索引 mid → 二维坐标 (row, col)
    • row = mid // n
    • col = mid % n

可视化(以 3x4 矩阵为例,展平后索引对应关系):

矩阵:
  [ 1,  3,  5,  7]    → 索引 0~3
  [10, 11, 16, 20]    → 索引 4~7
  [23, 30, 34, 60]    → 索引 8~11

展平为一维: [1,3,5,7,10,11,16,20,23,30,34,60]
索引:        0 1 2 3  4  5  6  7  8  9 10 11
def searchMatrix(matrix, target):
    if not matrix or not matrix[0]:
        return False
 
    m, n = len(matrix), len(matrix[0])
    left, right = 0, m * n - 1
 
    while left <= right:
        mid = left + (right - left) // 2
        # 一维索引 → 二维坐标
        row = mid // n
        col = mid % n
        val = matrix[row][col]
 
        if val == target:
            return True
        elif val < target:
            left = mid + 1
        else:
            right = mid - 1
 
    return False

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

核心思想: 先用二分查找确定 target 可能在哪一行(找最后一个首元素 <= target 的行),再在该行中二分查找。

def searchMatrix(matrix, target):
    if not matrix or not matrix[0]:
        return False
 
    m, n = len(matrix), len(matrix[0])
 
    # 第一步:确定目标行
    # 找第一个 matrix[row][0] > target 的行,它的前一行就是目标行
    top, bottom = 0, m - 1
    while top <= bottom:
        mid = top + (bottom - top) // 2
        if matrix[mid][0] == target:
            return True
        elif matrix[mid][0] < target:
            top = mid + 1
        else:
            bottom = mid - 1
 
    # bottom 是最后一个首元素 <= target 的行
    row = bottom
    if row < 0:  # target 比所有行的首元素都小
        return False
 
    # 第二步:在目标行中二分查找
    left, right = 0, n - 1
    while left <= right:
        mid = left + (right - left) // 2
        if matrix[row][mid] == target:
            return True
        elif matrix[row][mid] < target:
            left = mid + 1
        else:
            right = mid - 1
 
    return False

时间复杂度: O(log m + log n) = O(log(mn)) 空间复杂度: O(1)


易错点

  1. 矩阵为空matrix = []matrix = [[]] 时需提前返回 false。
  2. 一维展开时的列数 nmid // nmid % n 中的 n 是列数(不是行数),写反会导致完全错误的坐标。
  3. 两次二分中”找行”的边界bottom 指向最后一行首元素 <= target 的行。如果 bottom < 0,说明 target 比所有行的首元素都小,直接返回 false。
  4. 索引从 0 开始:一维索引范围是 [0, m*n-1]right = m * n - 1 是正确的,不要写成 m * n
  5. 列数为 1 的情况n = 1 时,mid % 1 = 0,此时矩阵退化为列向量,二分仍然正确。

框架提炼

二维矩阵的一维二分模板:

def search_matrix(matrix, target):
    if not matrix or not matrix[0]:
        return False
 
    m, n = len(matrix), len(matrix[0])
    left, right = 0, m * n - 1
 
    while left <= right:
        mid = left + (right - left) // 2
        val = matrix[mid // n][mid % n]
        if val == target:
            return True
        elif val < target:
            left = mid + 1
        else:
            right = mid - 1
 
    return False

适用条件: 此模板仅适用于”下一行最小 > 上一行最大”的严格递增矩阵。

矩阵二分 vs 搜索的两种思路:

方法时间复杂度适用条件
展平一维二分O(log(mn))行间严格递增(本题)
两次二分O(log m + log n)行间严格递增
右上角搜索O(m + n)仅每行/每列递增(240题)

关联题目