从零开始写算法——矩阵类题:图像旋转 + 搜索二维矩阵 II

在力扣(LeetCode)等算法平台上,矩阵操作是考察频率极高的一类题目。这类题目往往不依赖复杂的高级数据结构,而是考验我们对数组下标变换 的数学敏感度以及逻辑剪枝的能力。

今天我们通过两道经典题目------"旋转图像"和"搜索二维矩阵 II",来探讨如何用优雅的思路解决看似麻烦的矩阵变换与查找问题。


第一题:旋转图像 (Rotate Image)

题目背景

给定一个 n × n 的二维矩阵,要求将其顺时针旋转 90 度。 核心要求 :必须在原地旋转图像,这意味着你不能新建一个矩阵来存储结果,只能修改原矩阵。

原理解析:数学变换的魅力

如果我们要把一个矩阵顺时针旋转 90 度,直接寻找下标 (i, j) 变换到 (j, n-1-i) 的规律并原地修改是非常容易出错的,因为会覆盖掉还没处理的数据。

这里有一个经典的数学技巧,可以将旋转操作拆解为两个简单的步骤:

  1. 转置(Transpose):将矩阵的行列互换,即沿主对角线翻转。

  2. 镜像翻转(Reverse):将每一行的数据进行左右翻转。

图解演示: 假设我们有以下矩阵:

Plaintext

cpp 复制代码
1 2 3
4 5 6
7 8 9

第一步:沿主对角线对折(转置) 我们交换 matrix[i][j]matrix[j][i]

Plaintext

cpp 复制代码
1 4 7
2 5 8
3 6 9

第二步:水平翻转每一行 对每一行使用 reverse

Plaintext

cpp 复制代码
7 4 1
8 5 2
9 6 3

神奇的事情发生了:矩阵完美地顺时针旋转了 90 度!

代码实现

C++代码实现:

cpp 复制代码
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        // 1. 先对主对角线进行翻转(转置矩阵)
        // 注意 j 从 i 开始遍历,避免重复交换复原
        for (int i = 0; i < matrix.size(); ++i) {
            for (int j = i; j < matrix.size(); ++j) {
                swap(matrix[i][j], matrix[j][i]);
            }
        }
        // 2. 然后对每一行进行反转
        for (int i = 0; i < matrix.size(); ++i) {
            reverse(matrix[i].begin(), matrix[i].end());
        }
    }
};

复杂度分析

  • 时间复杂度:O(N^2)。

    • 转置操作遍历了大约一半的元素,操作次数为 N(N+1)/2。

    • 翻转操作遍历了所有元素。

    • 总体量级依然是 O(N^2),这是处理 N × N 矩阵不可避免的下限。

  • 空间复杂度:O(1)。

    • 我们只利用了常数级的临时变量进行交换,没有申请额外的矩阵空间,符合"原地"修改的要求。

第二题:搜索二维矩阵 II (Search a 2D Matrix II)

题目背景

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

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

  • 每列的元素从上到下升序排列。

原理解析:站在"十字路口"的选择

如果我们遍历整个矩阵,时间复杂度是 O(MN),这显然没有利用到"有序"这个特性。 如果我们对每一行进行二分查找,时间复杂度是 O(M log N),虽然快了不少,但还不够极致。

为什么选择右上角? 这道题的最优解法是利用**二叉搜索树(BST)**的思想。我们需要找到一个"起点",使得从这个点出发,向一个方向走数值变小,向另一个方向走数值变大。

  • 左上角 (0, 0) :向右变大,向下也变大。如果 target 比当前值大,我们不知道该往右还是往下,无法排除行列

  • 右上角 (0, n-1):这是最佳起点!

    • 向左看:数值变小。

    • 向下看:数值变大。

算法流程:

  1. 初始化指针指向矩阵的右上角

  2. 如果当前值 == target:找到了,返回 true

  3. 如果当前值 > target:说明这一列剩下的数(下面的数)都比当前值大,更肯定比 target 大,因此排除当前列,指针向左移。

  4. 如果当前值 < target:说明这一行左边的数都比当前值小,更肯定比 target 小,因此排除当前行,指针向下移。

代码实现

C++

cpp 复制代码
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        // 从右上角作为起始点
        int m = 0; // 行索引
        int n = matrix[0].size() - 1; // 列索引
        
        // 只要没走出矩阵边界
        while(m < matrix.size() && n >= 0) {
            if (matrix[m][n] == target) {
                return true;
            } 
            // 当前值比目标大,说明这一列都太大了,往左移
            else if (matrix[m][n] > target) {
                n--;
            } 
            // 当前值比目标小,说明这一行都太小了,往下移
            else {
                m++;
            }
        }
        return false;
    }
};

复杂度分析

  • 时间复杂度:O(M + N)。

    • 在最坏的情况下(例如目标值位于左下角,或者不存在),我们的指针要么向左移动一步,要么向下移动一步。

    • 最多移动 M 次下,N 次左,总步数为 M + N。这远优于 O(MN)。

  • 空间复杂度:O(1)。

    • 我们只使用了 mn 两个指针变量,没有使用额外空间。

总结

这两道题目虽然针对的是不同的应用场景,但都体现了**"观察规律,化繁为简"**的算法美学:

  1. 旋转图像利用数学上的转置与镜像,避免了复杂的坐标计算。

  2. 矩阵搜索利用行列排序的单调性,巧妙选取右上角作为切入点,将二维搜索转化为线性的指针移动。

相关推荐
rit84324992 小时前
基于MATLAB的多变量动态矩阵控制(DMC)仿真实现
开发语言·matlab·矩阵
罗湖老棍子2 小时前
Knight Moves(信息学奥赛一本通- P1257)
c++·算法·bfs
西猫雷婶2 小时前
CNN计算|矩阵扩充方法变化和卷积核移动步长变化
人工智能·pytorch·深度学习·神经网络·矩阵·cnn
AuroraWanderll3 小时前
C++11(二)核心突破:右值引用与移动语义(上)
c语言·数据结构·c++·算法·stl
CoderYanger3 小时前
第 479 场周赛Q1——3769. 二进制反射排序
java·数据结构·算法·leetcode·职场和发展
广府早茶3 小时前
机器人重量
c++·算法
sin_hielo3 小时前
leetcode 1925
数据结构·算法·leetcode
CoderYanger3 小时前
A.每日一题——1925. 统计平方和三元组的数目
java·开发语言·数据结构·算法·leetcode·哈希算法
小白程序员成长日记3 小时前
2025.12.08 力扣每日一题
java·算法·leetcode