Java详解LeetCode 热题 100(21):LeetCode 240. 搜索二维矩阵 II(Search a 2D Matrix II)详解

文章目录

    • [1. 题目描述](#1. 题目描述)
    • [2. 理解题目](#2. 理解题目)
      • [2.1 矩阵特性分析](#2.1 矩阵特性分析)
      • [2.2 关键观察](#2.2 关键观察)
    • [3. 解法一:从右上角开始搜索(推荐)](#3. 解法一:从右上角开始搜索(推荐))
      • [3.1 思路分析](#3.1 思路分析)
      • [3.2 图解演示](#3.2 图解演示)
      • [3.3 Java代码实现](#3.3 Java代码实现)
      • [3.4 代码详解](#3.4 代码详解)
      • [3.5 复杂度分析](#3.5 复杂度分析)
      • [3.6 适用场景](#3.6 适用场景)
    • [4. 解法二:从左下角开始搜索](#4. 解法二:从左下角开始搜索)
      • [4.1 思路分析](#4.1 思路分析)
      • [4.2 Java代码实现](#4.2 Java代码实现)
      • [4.3 图解演示](#4.3 图解演示)
      • [4.4 复杂度分析](#4.4 复杂度分析)
    • [5. 解法三:逐行二分搜索](#5. 解法三:逐行二分搜索)
      • [5.1 思路分析](#5.1 思路分析)
      • [5.2 Java代码实现](#5.2 Java代码实现)
      • [5.3 代码详解](#5.3 代码详解)
      • [5.4 复杂度分析](#5.4 复杂度分析)
      • [5.5 优化版本:智能剪枝](#5.5 优化版本:智能剪枝)
    • [6. 解法四:分治法](#6. 解法四:分治法)
      • [6.1 思路分析](#6.1 思路分析)
      • [6.2 Java代码实现](#6.2 Java代码实现)
      • [6.3 复杂度分析](#6.3 复杂度分析)
    • [7. 详细步骤分析与示例跟踪](#7. 详细步骤分析与示例跟踪)
      • [7.1 示例 1:目标值存在的情况](#7.1 示例 1:目标值存在的情况)
      • [7.2 示例 2:目标值不存在的情况](#7.2 示例 2:目标值不存在的情况)
      • [7.3 边界情况分析](#7.3 边界情况分析)
    • [8. 常见错误与优化](#8. 常见错误与优化)
      • [8.1 常见错误](#8.1 常见错误)
      • [8.2 性能优化](#8.2 性能优化)
    • [9. 扩展题目与应用](#9. 扩展题目与应用)
      • [9.1 相关LeetCode题目](#9.1 相关LeetCode题目)
      • [9.2 实际应用场景](#9.2 实际应用场景)
    • [10. 完整的Java解决方案](#10. 完整的Java解决方案)
      • [10.1 多种解法的性能对比](#10.1 多种解法的性能对比)
    • [11. 总结与技巧](#11. 总结与技巧)
      • [11.1 解题要点](#11.1 解题要点)
      • [11.2 算法选择建议](#11.2 算法选择建议)
      • [11.3 面试技巧](#11.3 面试技巧)
      • [11.4 学习收获](#11.4 学习收获)
    • [12. 参考资料](#12. 参考资料)

1. 题目描述

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列
  • 每列的元素从上到下升序排列

示例 1:

复制代码
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true

示例 2:

复制代码
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 14
输出:true

示例 3:

复制代码
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= n, m <= 300
  • -10^9 <= matrix[i][j] <= 10^9
  • 每行的所有元素从左到右升序排列
  • 每列的所有元素从上到下升序排列
  • -10^9 <= target <= 10^9

2. 理解题目

这道题的关键在于理解矩阵的特殊性质:

2.1 矩阵特性分析

给定的矩阵有两个重要特性:

  1. 行有序:每一行从左到右递增
  2. 列有序:每一列从上到下递增

让我们看一个具体的矩阵例子:

复制代码
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

观察这个矩阵:

  • 第一行:1 < 4 < 7 < 11 < 15
  • 第一列:1 < 2 < 3 < 10 < 18
  • 每个位置的值都比其左边和上边的值大

2.2 关键观察

这种特殊的排列方式给我们提供了重要的搜索线索:

  1. 右上角特性:矩阵右上角的元素(如15)是该行最大值,该列最小值
  2. 左下角特性:矩阵左下角的元素(如18)是该行最小值,该列最大值
  3. 搜索方向:从特殊位置开始,可以根据比较结果确定搜索方向

3. 解法一:从右上角开始搜索(推荐)

3.1 思路分析

从矩阵的右上角开始搜索是最优雅和高效的解法。

核心思想

  • 从右上角位置 (0, n-1) 开始
  • 如果当前值等于目标值,返回 true
  • 如果当前值大于目标值,向左移动(排除当前列)
  • 如果当前值小于目标值,向下移动(排除当前行)

为什么从右上角开始?

  • 右上角元素是该行的最大值,该列的最小值
  • 这个特性让我们能够明确地决定搜索方向
  • 每次比较都能排除一行或一列,确保搜索效率

3.2 图解演示

让我们用示例矩阵搜索目标值 14:

复制代码
初始状态:从右上角(0,4)开始,当前值=15
[
  [1,  4,  7,  11, 15*] ← 当前位置
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
15 > 14,向左移动

步骤1:移动到(0,3),当前值=11
[
  [1,  4,  7,  11*, 15]
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
11 < 14,向下移动

步骤2:移动到(1,3),当前值=12
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12*, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
12 < 14,向下移动

步骤3:移动到(2,3),当前值=16
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16*, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
16 > 14,向左移动

步骤4:移动到(2,2),当前值=9
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9*, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
9 < 14,向下移动

步骤5:移动到(3,2),当前值=14
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14*, 17, 24],
  [18, 21, 23, 26, 30]
]
14 == 14,找到目标!返回true

3.3 Java代码实现

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 处理边界情况
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        int m = matrix.length;    // 行数
        int n = matrix[0].length; // 列数
        
        // 从右上角开始搜索
        int row = 0;         // 起始行:第一行
        int col = n - 1;     // 起始列:最后一列
        
        // 当行和列都在有效范围内时继续搜索
        while (row < m && col >= 0) {
            int current = matrix[row][col];
            
            if (current == target) {
                // 找到目标值
                return true;
            } else if (current > target) {
                // 当前值大于目标值,向左移动(排除当前列)
                col--;
            } else {
                // 当前值小于目标值,向下移动(排除当前行)
                row++;
            }
        }
        
        // 搜索完毕,未找到目标值
        return false;
    }
}

3.4 代码详解

让我们逐行分析代码的每个部分:

java 复制代码
// 处理边界情况
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
    return false;
}
  • 检查矩阵是否为空或者没有元素
  • 这是防御性编程的重要实践
java 复制代码
int m = matrix.length;    // 行数
int n = matrix[0].length; // 列数
  • 获取矩阵的维度信息
  • m 表示行数,n 表示列数
java 复制代码
// 从右上角开始搜索
int row = 0;         // 起始行:第一行
int col = n - 1;     // 起始列:最后一列
  • 初始化搜索起点为右上角
  • row = 0:第一行
  • col = n - 1:最后一列
java 复制代码
while (row < m && col >= 0) {
    int current = matrix[row][col];
    
    if (current == target) {
        return true;
    } else if (current > target) {
        col--;  // 向左移动
    } else {
        row++;  // 向下移动
    }
}
  • 循环条件:确保行和列索引都在有效范围内
  • 获取当前位置的值
  • 根据比较结果决定移动方向:
    • 相等:找到目标,返回 true
    • 大于:向左移动(col--)
    • 小于:向下移动(row++)

3.5 复杂度分析

  • 时间复杂度: O(m + n),其中 m 是行数,n 是列数

    • 最坏情况下,我们从右上角走到左下角
    • 每次移动都会排除一行或一列
    • 总移动次数不超过 m + n - 1
  • 空间复杂度: O(1)

    • 只使用了常数个额外变量
    • 不需要额外的数据结构

3.6 适用场景

这种解法是最推荐的,因为:

  1. 代码简洁易懂
  2. 时间复杂度最优
  3. 空间复杂度最优
  4. 逻辑清晰,不容易出错

4. 解法二:从左下角开始搜索

4.1 思路分析

从左下角开始搜索的思路与从右上角开始类似,只是方向相反。

核心思想

  • 从左下角位置 (m-1, 0) 开始
  • 如果当前值等于目标值,返回 true
  • 如果当前值大于目标值,向上移动(排除当前行)
  • 如果当前值小于目标值,向右移动(排除当前列)

4.2 Java代码实现

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 处理边界情况
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        int m = matrix.length;    // 行数
        int n = matrix[0].length; // 列数
        
        // 从左下角开始搜索
        int row = m - 1;     // 起始行:最后一行
        int col = 0;         // 起始列:第一列
        
        // 当行和列都在有效范围内时继续搜索
        while (row >= 0 && col < n) {
            int current = matrix[row][col];
            
            if (current == target) {
                // 找到目标值
                return true;
            } else if (current > target) {
                // 当前值大于目标值,向上移动(排除当前行)
                row--;
            } else {
                // 当前值小于目标值,向右移动(排除当前列)
                col++;
            }
        }
        
        // 搜索完毕,未找到目标值
        return false;
    }
}

4.3 图解演示

让我们用示例矩阵从左下角搜索目标值 14:

复制代码
初始状态:从左下角(4,0)开始,当前值=18
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14, 17, 24],
  [18*, 21, 23, 26, 30] ← 当前位置
]
18 > 14,向上移动

步骤1:移动到(3,0),当前值=10
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10*, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
10 < 14,向右移动

步骤2:移动到(3,1),当前值=13
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13*, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
13 < 14,向右移动

步骤3:移动到(3,2),当前值=14
[
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14*, 17, 24],
  [18, 21, 23, 26, 30]
]
14 == 14,找到目标!返回true

4.4 复杂度分析

  • 时间复杂度: O(m + n),与解法一相同
  • 空间复杂度: O(1),与解法一相同

5. 解法三:逐行二分搜索

5.1 思路分析

由于每一行都是有序的,我们可以对每一行进行二分搜索。

核心思想

  • 遍历矩阵的每一行
  • 对每一行使用二分搜索查找目标值
  • 如果在某一行找到目标值,返回 true
  • 如果所有行都搜索完毕仍未找到,返回 false

5.2 Java代码实现

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 处理边界情况
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        int m = matrix.length;    // 行数
        int n = matrix[0].length; // 列数
        
        // 对每一行进行二分搜索
        for (int i = 0; i < m; i++) {
            if (binarySearch(matrix[i], target)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 在有序数组中进行二分搜索
     * @param row 有序数组(矩阵的一行)
     * @param target 目标值
     * @return 是否找到目标值
     */
    private boolean binarySearch(int[] row, int target) {
        int left = 0;
        int right = row.length - 1;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            
            if (row[mid] == target) {
                return true;
            } else if (row[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        
        return false;
    }
}

5.3 代码详解

java 复制代码
// 对每一行进行二分搜索
for (int i = 0; i < m; i++) {
    if (binarySearch(matrix[i], target)) {
        return true;
    }
}
  • 遍历矩阵的每一行
  • 对每一行调用二分搜索函数
  • 如果在某一行找到目标值,立即返回 true
java 复制代码
private boolean binarySearch(int[] row, int target) {
    int left = 0;
    int right = row.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (row[mid] == target) {
            return true;
        } else if (row[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return false;
}
  • 标准的二分搜索实现
  • 在有序数组中查找目标值
  • 时间复杂度 O(log n)

5.4 复杂度分析

  • 时间复杂度: O(m log n)

    • 需要对 m 行进行搜索
    • 每行的二分搜索时间复杂度为 O(log n)
    • 总时间复杂度为 O(m log n)
  • 空间复杂度: O(1)

    • 只使用了常数个额外变量

5.5 优化版本:智能剪枝

我们可以对逐行二分搜索进行优化,利用矩阵的列有序特性进行剪枝:

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        int m = matrix.length;
        int n = matrix[0].length;
        
        for (int i = 0; i < m; i++) {
            // 剪枝:如果当前行的第一个元素大于目标值,后续行也不可能包含目标值
            if (matrix[i][0] > target) {
                break;
            }
            
            // 剪枝:如果当前行的最后一个元素小于目标值,跳过当前行
            if (matrix[i][n - 1] < target) {
                continue;
            }
            
            // 在当前行进行二分搜索
            if (binarySearch(matrix[i], target)) {
                return true;
            }
        }
        
        return false;
    }
    
    private boolean binarySearch(int[] row, int target) {
        int left = 0;
        int right = row.length - 1;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            
            if (row[mid] == target) {
                return true;
            } else if (row[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        
        return false;
    }
}

优化说明

  1. 提前终止:如果某行的第一个元素大于目标值,由于列有序,后续行的第一个元素也会更大,可以提前终止
  2. 跳过无效行:如果某行的最后一个元素小于目标值,该行不可能包含目标值,直接跳过

6. 解法四:分治法

6.1 思路分析

分治法利用矩阵的有序特性,将搜索空间递归地分割成更小的子问题。

核心思想

  • 选择矩阵的中心点作为分割点
  • 根据中心点的值与目标值的比较结果,排除不可能包含目标值的区域
  • 递归搜索剩余的区域

6.2 Java代码实现

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        return searchHelper(matrix, target, 0, 0, matrix.length - 1, matrix[0].length - 1);
    }
    
    /**
     * 分治搜索辅助函数
     * @param matrix 矩阵
     * @param target 目标值
     * @param row1 搜索区域的起始行
     * @param col1 搜索区域的起始列
     * @param row2 搜索区域的结束行
     * @param col2 搜索区域的结束列
     * @return 是否找到目标值
     */
    private boolean searchHelper(int[][] matrix, int target, int row1, int col1, int row2, int col2) {
        // 边界条件:搜索区域无效
        if (row1 > row2 || col1 > col2) {
            return false;
        }
        
        // 边界条件:搜索区域只有一个元素
        if (row1 == row2 && col1 == col2) {
            return matrix[row1][col1] == target;
        }
        
        // 选择中心点
        int midRow = row1 + (row2 - row1) / 2;
        int midCol = col1 + (col2 - col1) / 2;
        int midValue = matrix[midRow][midCol];
        
        if (midValue == target) {
            return true;
        } else if (midValue > target) {
            // 目标值在左上区域,搜索三个子区域
            return searchHelper(matrix, target, row1, col1, midRow - 1, col2) ||  // 上半部分
                   searchHelper(matrix, target, midRow, col1, row2, midCol - 1);   // 左下部分
        } else {
            // 目标值在右下区域,搜索三个子区域
            return searchHelper(matrix, target, row1, midCol + 1, midRow, col2) ||  // 右上部分
                   searchHelper(matrix, target, midRow + 1, col1, row2, col2);      // 下半部分
        }
    }
}

6.3 复杂度分析

  • 时间复杂度: O(n^1.58),其中 n 是矩阵的较大维度

    • 每次递归将问题规模减少约 3/4
    • 递归深度约为 log n
    • 总时间复杂度介于 O(n) 和 O(n^2) 之间
  • 空间复杂度: O(log n)

    • 递归调用栈的深度为 O(log n)

7. 详细步骤分析与示例跟踪

让我们通过几个具体例子来详细跟踪不同算法的执行过程。

7.1 示例 1:目标值存在的情况

输入

复制代码
matrix = [
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
target = 5

使用解法一(从右上角搜索)

  1. 初始化:row = 0, col = 4, current = 15
  2. 步骤1:15 > 5,向左移动,col = 3, current = 11
  3. 步骤2:11 > 5,向左移动,col = 2, current = 7
  4. 步骤3:7 > 5,向左移动,col = 1, current = 4
  5. 步骤4:4 < 5,向下移动,row = 1, current = 5
  6. 步骤5:5 == 5,找到目标!返回 true

搜索路径 :(0,4) → (0,3) → (0,2) → (0,1) → (1,1)
总步数:5步

7.2 示例 2:目标值不存在的情况

输入

复制代码
matrix = [
  [1,  4,  7,  11, 15],
  [2,  5,  8,  12, 19],
  [3,  6,  9,  16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
target = 20

使用解法一(从右上角搜索)

  1. 初始化:row = 0, col = 4, current = 15
  2. 步骤1:15 < 20,向下移动,row = 1, current = 19
  3. 步骤2:19 < 20,向下移动,row = 2, current = 22
  4. 步骤3:22 > 20,向左移动,col = 3, current = 16
  5. 步骤4:16 < 20,向下移动,row = 3, current = 17
  6. 步骤5:17 < 20,向下移动,row = 4, current = 24
  7. 步骤6:24 > 20,向左移动,col = 2, current = 23
  8. 步骤7:23 > 20,向左移动,col = 1, current = 21
  9. 步骤8:21 > 20,向左移动,col = 0, current = 18
  10. 步骤9:18 < 20,向下移动,row = 5,超出边界,搜索结束

搜索路径 :(0,4) → (1,4) → (2,4) → (2,3) → (3,3) → (4,3) → (4,2) → (4,1) → (4,0) → 越界
结果:false

7.3 边界情况分析

情况1:目标值小于矩阵最小值

复制代码
matrix = [[1, 2], [3, 4]]
target = 0

从右上角开始:(0,1) → (0,0) → 越界,返回 false

情况2:目标值大于矩阵最大值

复制代码
matrix = [[1, 2], [3, 4]]
target = 5

从右上角开始:(0,1) → (1,1) → 越界,返回 false

情况3:单元素矩阵

复制代码
matrix = [[1]]
target = 1

从右上角开始:(0,0),1 == 1,返回 true

8. 常见错误与优化

8.1 常见错误

  1. 边界条件处理不当

    java 复制代码
    // 错误:没有检查矩阵是否为空
    public boolean searchMatrix(int[][] matrix, int target) {
        int row = 0;
        int col = matrix[0].length - 1; // 可能抛出空指针异常
        // ...
    }
    
    // 正确:先检查边界条件
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        // ...
    }
  2. 搜索方向错误

    java 复制代码
    // 错误:从右上角开始但移动方向错误
    if (current > target) {
        row++; // 应该是col--
    } else {
        col--; // 应该是row++
    }
    
    // 正确:从右上角开始的正确移动方向
    if (current > target) {
        col--; // 向左移动
    } else {
        row++; // 向下移动
    }
  3. 循环条件错误

    java 复制代码
    // 错误:循环条件不正确
    while (row < m || col >= 0) { // 应该是&&而不是||
        // ...
    }
    
    // 正确:两个条件都必须满足
    while (row < m && col >= 0) {
        // ...
    }
  4. 数组越界

    java 复制代码
    // 错误:没有检查索引范围
    while (true) {
        int current = matrix[row][col]; // 可能越界
        // ...
    }
    
    // 正确:在循环条件中检查范围
    while (row < m && col >= 0) {
        int current = matrix[row][col];
        // ...
    }

8.2 性能优化

  1. 提前终止优化

    java 复制代码
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        // 优化:检查目标值是否在矩阵范围内
        if (target < matrix[0][0] || target > matrix[matrix.length - 1][matrix[0].length - 1]) {
            return false;
        }
        
        // 从右上角开始搜索
        int row = 0;         // 起始行
        int col = matrix[0].length - 1;     // 起始列
        
        while (row < matrix.length && col >= 0) {
            int current = matrix[row][col];
            
            if (current == target) {
                return true;
            } else if (current > target) {
                // 当前值大于目标值,向左移动
                col--;
            } else {
                // 当前值小于目标值,向下移动
                row++;
            }
        }
        
        return false;
    }
  2. 缓存矩阵维度

    java 复制代码
    // 优化:缓存矩阵维度,避免重复计算
    int m = matrix.length;
    int n = matrix[0].length;
    
    int row = 0;
    int col = n - 1; // 使用缓存的n
    
    while (row < m && col >= 0) { // 使用缓存的m
        // ...
    }
  3. 减少数组访问

    java 复制代码
    // 优化:减少重复的数组访问
    while (row < m && col >= 0) {
        int current = matrix[row][col]; // 只访问一次
        
        if (current == target) {
            return true;
        } else if (current > target) {
            col--;
        } else {
            row++;
        }
    }

9. 扩展题目与应用

9.1 相关LeetCode题目

  1. LeetCode 74. 搜索二维矩阵

    • 矩阵每行有序,且每行第一个元素大于前一行最后一个元素
    • 可以将矩阵看作一维有序数组进行二分搜索
  2. LeetCode 378. 有序矩阵中第K小的元素

    • 在行列都有序的矩阵中找第K小的元素
    • 可以使用二分搜索或堆的方法
  3. LeetCode 1351. 统计有序矩阵中的负数

    • 统计行列都有序的矩阵中负数的个数
    • 可以使用类似的从角落开始搜索的方法

9.2 实际应用场景

  1. 数据库索引查询

    • 多维索引结构中的范围查询
    • 利用索引的有序性快速定位数据
  2. 图像处理

    • 在有序的像素矩阵中查找特定值
    • 图像分割和特征检测
  3. 游戏开发

    • 在有序的地图网格中查找特定位置
    • 路径规划和碰撞检测
  4. 科学计算

    • 在有序的数据矩阵中查找特定数值
    • 数值分析和统计计算

10. 完整的Java解决方案

以下是结合了各种优化和最佳实践的完整解决方案:

java 复制代码
/**
 * LeetCode 240. 搜索二维矩阵 II
 * 
 * 解法:从右上角开始搜索
 * 时间复杂度:O(m + n)
 * 空间复杂度:O(1)
 */
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 边界条件检查
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        int m = matrix.length;    // 行数
        int n = matrix[0].length; // 列数
        
        // 快速排除:检查目标值是否在矩阵范围内
        if (target < matrix[0][0] || target > matrix[m - 1][n - 1]) {
            return false;
        }
        
        // 从右上角开始搜索
        int row = 0;         // 起始行
        int col = n - 1;     // 起始列
        
        while (row < m && col >= 0) {
            int current = matrix[row][col];
            
            if (current == target) {
                return true;
            } else if (current > target) {
                // 当前值大于目标值,向左移动
                col--;
            } else {
                // 当前值小于目标值,向下移动
                row++;
            }
        }
        
        return false;
    }
}

/**
 * 测试类
 */
public class SearchMatrix2DTest {
    public static void main(String[] args) {
        Solution solution = new Solution();
        
        // 测试用例1:目标值存在
        int[][] matrix1 = {
            {1,  4,  7,  11, 15},
            {2,  5,  8,  12, 19},
            {3,  6,  9,  16, 22},
            {10, 13, 14, 17, 24},
            {18, 21, 23, 26, 30}
        };
        
        System.out.println("测试用例1:");
        System.out.println("查找5:" + solution.searchMatrix(matrix1, 5));   // true
        System.out.println("查找14:" + solution.searchMatrix(matrix1, 14)); // true
        System.out.println("查找20:" + solution.searchMatrix(matrix1, 20)); // false
        
        // 测试用例2:边界情况
        int[][] matrix2 = {{1}};
        System.out.println("\n测试用例2:");
        System.out.println("单元素矩阵查找1:" + solution.searchMatrix(matrix2, 1)); // true
        System.out.println("单元素矩阵查找2:" + solution.searchMatrix(matrix2, 2)); // false
        
        // 测试用例3:空矩阵
        int[][] matrix3 = {};
        System.out.println("\n测试用例3:");
        System.out.println("空矩阵查找1:" + solution.searchMatrix(matrix3, 1)); // false
    }
}

10.1 多种解法的性能对比

java 复制代码
/**
 * 性能对比测试类
 */
public class PerformanceComparison {
    
    // 解法1:从右上角搜索
    public boolean searchFromTopRight(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        int row = 0, col = matrix[0].length - 1;
        while (row < matrix.length && col >= 0) {
            if (matrix[row][col] == target) return true;
            else if (matrix[row][col] > target) col--;
            else row++;
        }
        return false;
    }
    
    // 解法2:从左下角搜索
    public boolean searchFromBottomLeft(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        int row = matrix.length - 1, col = 0;
        while (row >= 0 && col < matrix[0].length) {
            if (matrix[row][col] == target) return true;
            else if (matrix[row][col] > target) row--;
            else col++;
        }
        return false;
    }
    
    // 解法3:逐行二分搜索
    public boolean searchWithBinarySearch(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        for (int[] row : matrix) {
            if (binarySearch(row, target)) {
                return true;
            }
        }
        return false;
    }
    
    private boolean binarySearch(int[] row, int target) {
        int left = 0, right = row.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (row[mid] == target) return true;
            else if (row[mid] < target) left = mid + 1;
            else right = mid - 1;
        }
        return false;
    }
    
    public static void main(String[] args) {
        PerformanceComparison pc = new PerformanceComparison();
        
        // 创建测试矩阵
        int[][] matrix = generateMatrix(1000, 1000);
        int target = 500000;
        
        // 测试各种解法的性能
        long startTime, endTime;
        
        // 解法1
        startTime = System.nanoTime();
        boolean result1 = pc.searchFromTopRight(matrix, target);
        endTime = System.nanoTime();
        System.out.println("从右上角搜索:" + result1 + ",耗时:" + (endTime - startTime) + "ns");
        
        // 解法2
        startTime = System.nanoTime();
        boolean result2 = pc.searchFromBottomLeft(matrix, target);
        endTime = System.nanoTime();
        System.out.println("从左下角搜索:" + result2 + ",耗时:" + (endTime - startTime) + "ns");
        
        // 解法3
        startTime = System.nanoTime();
        boolean result3 = pc.searchWithBinarySearch(matrix, target);
        endTime = System.nanoTime();
        System.out.println("逐行二分搜索:" + result3 + ",耗时:" + (endTime - startTime) + "ns");
    }
    
    // 生成测试矩阵
    private static int[][] generateMatrix(int m, int n) {
        int[][] matrix = new int[m][n];
        int value = 1;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                matrix[i][j] = value++;
            }
        }
        return matrix;
    }
}

11. 总结与技巧

11.1 解题要点

  1. 理解矩阵特性:行有序 + 列有序是解题的关键
  2. 选择合适起点:右上角或左下角都是很好的起点
  3. 明确搜索方向:根据比较结果确定唯一的移动方向
  4. 处理边界条件:空矩阵、单元素矩阵等特殊情况

11.2 算法选择建议

  • 推荐解法:从右上角开始搜索(解法一)

    • 代码简洁,逻辑清晰
    • 时间复杂度最优:O(m + n)
    • 空间复杂度最优:O(1)
  • 备选解法:从左下角开始搜索(解法二)

    • 与解法一等价,只是方向不同
    • 复杂度相同
  • 特殊情况:逐行二分搜索(解法三)

    • 当矩阵行数远小于列数时可能更优
    • 时间复杂度:O(m log n)

11.3 面试技巧

如果在面试中遇到此类问题:

  1. 分析题目

    • 明确矩阵的有序特性
    • 理解搜索目标
  2. 提出思路

    • 从暴力搜索开始分析
    • 逐步优化到最优解法
  3. 编写代码

    • 先处理边界条件
    • 实现核心搜索逻辑
    • 注意循环条件和移动方向
  4. 测试验证

    • 用示例数据验证
    • 考虑边界情况
  5. 复杂度分析

    • 分析时间和空间复杂度
    • 讨论可能的优化

11.4 学习收获

通过学习这道题,你可以掌握:

  • 二维矩阵搜索的经典技巧
  • 利用数据结构特性优化算法
  • 从角落开始搜索的思想
  • 分治和二分搜索的应用

这些技巧在解决其他矩阵相关问题时都非常有用。

12. 参考资料

相关推荐
浮灯Foden43 分钟前
算法-每日一题(DAY11)每日温度
开发语言·数据结构·c++·算法·leetcode·面试
闻缺陷则喜何志丹1 小时前
【并集查找】P10729 [NOISG 2023 Qualification] Dolls|普及+
c++·算法·图论·洛谷·并集查找
coding随想4 小时前
Java中间件简介:构建现代软件的“隐形桥梁”
java·中间件
CQ_07125 小时前
自学记录:力扣hot100第三题
算法·leetcode
superkcl20227 小时前
【JAVA】【Stream流】
java·windows·python
mldong8 小时前
mldong 快速开发框架登录模块设计与实现
java·后端·架构
bulucc8 小时前
Maven 或 Gradle 下载和添加 jar 文件的步骤
java·maven·jar
Wilber的技术分享8 小时前
【机器学习实战笔记 12】集成学习:AdaBoost算法
人工智能·笔记·算法·决策树·机器学习·分类·集成学习
我爱Jack8 小时前
@annotation:Spring AOP 的“精准定位器“
java·后端·spring
一ge科研小菜鸡9 小时前
编程语言的演化与选择:技术浪潮中的理性决策
java·c语言·python