CS-Notes 剑指 Offer 题解 29:顺时针打印矩阵——四边界螺旋遍历的完整实现
本篇技术指南基于 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:当前层的右边界(结束列号)
打印当前最外层的顺序固定为四步:
- 上:从左到右打印最上一行(第
r1行,列从c1到c2); - 右:从上到下打印最右一行(第
c2列,行从r1+1到r2,起点跳过r1行避免与"上"边重复); - 下:从右到左打印最下一行(第
r2行,列从c2-1到c1,起点跳过c2列避免与"右"边重复); - 左:从下到上打印最左一行(第
c1列,行从r2-1到r1+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成立,但循环i从r2-1=1开始且要求i > r1,即i > 1,不满足)→ 不输出任何元素。这里正是防重复机制的体现:2x2 子矩阵的左列(1,1)已在"上"边打印、(2,1)已在"下"边打印,确实无需再打。
收缩后 r1=2, r2=1,r1 <= r2 不成立,循环结束。拼接结果恰好为 1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10,与题目给出的标准输出完全一致。
边界场景分析:单行、单列与奇数阶中心
r1 != r2 与 c1 != c2 两个判断并非多余,它们保证了算法在退化形态的矩阵上依然正确。从源码结构看,可以针对三类典型输入逐一验证:
单行矩阵(例如 1x4):初始 r1 = r2 = 0。"上"边打印整行 4 个元素后,"右"边循环 i 从 r1+1=1 开始、条件 i <= r2=0 直接不成立;"下"边因 r1 == r2 被跳过(否则这 4 个元素会被再打一遍);"左"边因 c1 != c2 进入,但 i 从 r2-1=-1 开始、i > r1 不成立,同样不输出。结果正确且无重复。
单列矩阵(例如 3x1):初始 c1 = c2 = 0。"上"边打印第 1 个元素,"右"边打印剩余 2 个元素,"下"边循环 i 从 c2-1=-1 起不成立而空转,"左"边因 c1 == c2 被跳过。结果正确。
奇数阶矩阵(例如 3x3):外圈打印完后,收缩到 r1 = r2 = 1, c1 = c2 = 1,此时"当前最外层"退化为单元素中心。"上"边打印该中心元素,其余三条边分别被 r1 == r2 / c1 == c2 判断或空循环挡住,中心元素恰好只输出一次。
另外需要注意一个适用前提:上述实现直接以 matrix.length - 1 和 matrix[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 题解 - 目录。
小结
顺时针打印矩阵的解法可以浓缩为三个要点:
- 用
r1, r2, c1, c2四个边界变量动态定义"当前最外层",每圈向内收缩一格; - 每圈按"上 → 右 → 下 → 左"四段打印,后三段的起点均跳过已被前一段覆盖的端点;
r1 != r2与c1 != c2两个判断防止单行/单列/单元素层发生重复打印,循环条件r1 <= r2 && c1 <= c2保证所有元素恰好输出一次。
掌握这一模式后,类似的"螺旋遍历"问题(如按圈读取、螺旋填充)都可以复用同一套四边界骨架,只需替换四段循环内部的读写逻辑即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00