首页
/ CS-Notes 剑指 Offer 题解 29:顺时针打印矩阵——四边界螺旋遍历的完整实现

CS-Notes 剑指 Offer 题解 29:顺时针打印矩阵——四边界螺旋遍历的完整实现

2026-09-04 22:59:55作者:瞿蔚英Wynne

本篇技术指南基于 CS-Notes 仓库的剑指 Offer 题解 - 29. 顺时针打印矩阵展开,讲解如何用四个边界变量逐层剥开矩阵、按顺时针顺序打印全部元素的经典解法。读完之后,你将掌握"行边界 + 列边界"这一螺旋遍历模型的完整推导过程,包括防重复打印的边界条件、单层执行轨迹的逐行验证,以及时间/空间复杂度分析,能够独立处理单行、单列、奇数阶矩阵等边界场景。

题目描述

题目要求:按顺时针的方向,从外到里打印矩阵的值。原文档给出的 4x4 示例矩阵(即文档配图中对应的矩阵)及其标准输出为:

1   2   3   4
5   6   7   8
9  10  11  12
13  14  15  16

打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10

从输出序列可以直接观察出螺旋轨迹:先横向走完第一行,再纵向走完最后一列,然后反向走最后一行,再反向走第一列,完成一圈后向内收缩到子矩阵继续同样的过程,直到所有元素打印完毕。这正是"从外到里逐层打印"的直观体现。

该题收录于 剑指 Offer 题解 - 目录 的"数组与矩阵"分类中,是面试中考察二维数组遍历的必刷题之一。

解题思路:用四个变量定义"当前最外层"

原文档的核心思想是:一层一层从外到里打印,每一层的处理步骤完全相同,唯一不同的是上下左右边界。因此引入四个变量 r1, r2, c1, c2 分别存储上、下、左、右边界值,从而在每一轮循环中定义当前最外层:

  • r1:当前层的上边界(起始行号)
  • r2:当前层的下边界(结束行号)
  • c1:当前层的左边界(起始列号)
  • c2:当前层的右边界(结束列号)

打印当前最外层的顺序固定为四步:

  1. :从左到右打印最上一行(第 r1 行,列从 c1c2);
  2. :从上到下打印最右一行(第 c2 列,行从 r1+1r2,起点跳过 r1 行避免与"上"边重复);
  3. :从右到左打印最下一行(第 r2 行,列从 c2-1c1,起点跳过 c2 列避免与"右"边重复);
  4. :从下到上打印最左一行(第 c1 列,行从 r2-1r1+1,起点跳过 r2 行避免与"下"边重复)。

原文档特别强调了一个关键细节:只有 r1 != r2 时才打印最下一行,也就是在当前最外层行数大于 1 时才执行这一步。原因是当前最外层只有一行时,"上"边已经把这行完整打印过了,若继续打印最下一行(此时 r1 == r2,两条边重合)会导致元素重复输出。"左"边同理,需要 c1 != c2 才执行。这两个判断条件是本题区别于简单套路的易错点。

完整解法代码与逐层执行追踪

原文档给出的 Java 完整实现如下(与 29. 顺时针打印矩阵 中的代码一致):

public ArrayList<Integer> printMatrix(int[][] matrix) {
    ArrayList<Integer> ret = new ArrayList<>();
    int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1;
    while (r1 <= r2 && c1 <= c2) {
        // 上
        for (int i = c1; i <= c2; i++)
            ret.add(matrix[r1][i]);
        // 右
        for (int i = r1 + 1; i <= r2; i++)
            ret.add(matrix[i][c2]);
        if (r1 != r2)
            // 下
            for (int i = c2 - 1; i >= c1; i--)
                ret.add(matrix[r2][i]);
        if (c1 != c2)
            // 左
            for (int i = r2 - 1; i > r1; i--)
                ret.add(matrix[i][c1]);
        r1++; r2--; c1++; c2--;
    }
    return ret;
}

逐行解读关键结构:

  • 边界初始化r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1,即整个矩阵的外边界;
  • 循环条件 r1 <= r2 && c1 <= c2:只要上下边界没有交叉、左右边界没有交叉,就说明还存在未打印的行/列,需要继续向内收缩;
  • 收缩步骤 r1++; r2--; c1++; c2--;:每打印完一圈,四个边界各向中心推进一格,下一轮处理的就是"当前最外层"的内一层。

用题目中的 4x4 矩阵完整追踪一遍,可以验证每个环节:

第 1 轮r1=0, r2=3, c1=0, c2=3):

  • 上:第 0 行第 0~3 列 → 1, 2, 3, 4
  • 右:第 3 列第 1~3 行 → 8, 12, 16
  • 下(r1 != r2 成立):第 3 行第 2~0 列 → 15, 14, 13
  • 左(c1 != c2 成立):第 0 列第 2~1 行 → 9, 5

收缩后边界变为 r1=1, r2=2, c1=1, c2=2,指向内部的 2x2 子矩阵。

第 2 轮r1=1, r2=2, c1=1, c2=2):

  • 上:第 1 行第 1~2 列 → 6, 7
  • 右:第 2 行第 2 列 → 11
  • 下(r1 != r2 成立):第 2 行第 1 列 → 10
  • 左(c1 != c2 成立,但循环 ir2-1=1 开始且要求 i > r1,即 i > 1,不满足)→ 不输出任何元素。这里正是防重复机制的体现:2x2 子矩阵的左列 (1,1) 已在"上"边打印、(2,1) 已在"下"边打印,确实无需再打。

收缩后 r1=2, r2=1r1 <= r2 不成立,循环结束。拼接结果恰好为 1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10,与题目给出的标准输出完全一致。

边界场景分析:单行、单列与奇数阶中心

r1 != r2c1 != c2 两个判断并非多余,它们保证了算法在退化形态的矩阵上依然正确。从源码结构看,可以针对三类典型输入逐一验证:

单行矩阵(例如 1x4):初始 r1 = r2 = 0。"上"边打印整行 4 个元素后,"右"边循环 ir1+1=1 开始、条件 i <= r2=0 直接不成立;"下"边因 r1 == r2 被跳过(否则这 4 个元素会被再打一遍);"左"边因 c1 != c2 进入,但 ir2-1=-1 开始、i > r1 不成立,同样不输出。结果正确且无重复。

单列矩阵(例如 3x1):初始 c1 = c2 = 0。"上"边打印第 1 个元素,"右"边打印剩余 2 个元素,"下"边循环 ic2-1=-1 起不成立而空转,"左"边因 c1 == c2 被跳过。结果正确。

奇数阶矩阵(例如 3x3):外圈打印完后,收缩到 r1 = r2 = 1, c1 = c2 = 1,此时"当前最外层"退化为单元素中心。"上"边打印该中心元素,其余三条边分别被 r1 == r2 / c1 == c2 判断或空循环挡住,中心元素恰好只输出一次。

另外需要注意一个适用前提:上述实现直接以 matrix.length - 1matrix[0].length - 1 初始化边界,意味着它假设了矩阵非空(行数、列数均大于 0)。从原文档代码看并未包含空矩阵防御,实际面试或工程使用时建议在函数开头补充 if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return new ArrayList<>(); 之类的判空逻辑——这与同仓库4. 二维数组中的查找题解开头先判空再访问元素的防御式写法是一致的。

复杂度与同系列矩阵题的关联

时间复杂度O(m × n)。每个元素恰好被访问、输出一次,四段循环在每一圈的总长度不超过该圈的周长,全部圈层加总即为矩阵总元素数。

空间复杂度:除必须返回的结果列表(O(m × n),题目要求输出完整序列,这是输出本身决定的)之外,工作空间只有 r1, r2, c1, c2 四个变量和若干循环变量,为 O(1)

这道题与仓库题解中另一道二维数组题形成了很好的对照:4. 二维数组中的查找考察的是利用行列有序性从右上角线性逼近目标(O(m + n)),而本题考察的是在无有序性的任意矩阵上按几何结构(螺旋层)遍历。两者共同覆盖了面试中二维数组问题的两大高频方向——"利用单调性搜索"与"按结构遍历"。更完整的题目索引可参考 剑指 Offer 题解 - 目录。

小结

顺时针打印矩阵的解法可以浓缩为三个要点:

  1. r1, r2, c1, c2 四个边界变量动态定义"当前最外层",每圈向内收缩一格;
  2. 每圈按"上 → 右 → 下 → 左"四段打印,后三段的起点均跳过已被前一段覆盖的端点;
  3. r1 != r2c1 != c2 两个判断防止单行/单列/单元素层发生重复打印,循环条件 r1 <= r2 && c1 <= c2 保证所有元素恰好输出一次。

掌握这一模式后,类似的"螺旋遍历"问题(如按圈读取、螺旋填充)都可以复用同一套四边界骨架,只需替换四段循环内部的读写逻辑即可。

登录后查看全文
热门项目推荐
相关项目推荐