

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:今天是数组的最后一章节,最近还在整理苍穹外卖,前面我们刚学完关于滑动窗口的方法,这一章节我们学习螺旋矩阵,这是面试中常见的题目,主要考察的不是思维,而是我们能否正确顺畅的手撕出来,是一个中等题。
题目背景:LeetCode 59
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
题目理解:
看到这个题目,我们第一眼可能不知道题目要干什么,但是仔细地看一眼,我们就能理解了,根据我们输入的正整数n,会生成一个正方形的矩阵,如果所示:

而且还是按照顺时针方向的排列方向,思路其实很简单,但是在具体操作的时候往往会让我们纠结或者搞混,就和二分法一样,难的不是思路,而是我们能否清晰的把这道题写出来,而不是漏掉一些,或者写错一些地方。下面我们会讲解具体的方法
题目答案:
java
class Solution {
public int[][] generateMatrix(int n) {
int[][] nums = new int[n][n];
int startX = 0, startY = 0; // 每一圈的起始点
int offset = 1;
int count = 1; // 矩阵中需要填写的数字
int loop = 1; // 记录当前的圈数
int i, j; // j 代表列, i 代表行;
while (loop <= n / 2) {
// 顶部
// 左闭右开,所以判断循环结束时, j 不能等于 n - offset
for (j = startY; j < n - offset; j++) {
nums[startX][j] = count++;
}
// 右列
// 左闭右开,所以判断循环结束时, i 不能等于 n - offset
for (i = startX; i < n - offset; i++) {
nums[i][j] = count++;
}
// 底部
// 左闭右开,所以判断循环结束时, j != startY
for (; j > startY; j--) {
nums[i][j] = count++;
}
// 左列
// 左闭右开,所以判断循环结束时, i != startX
for (; i > startX; i--) {
nums[i][j] = count++;
}
startX++;
startY++;
offset++;
loop++;
}
if (n % 2 == 1) { // n 为奇数时,单独处理矩阵中心的值
nums[startX][startY] = count;
}
return nums;
}
}
题目讲解:
在写这道题的时候,我们要遵循一个选择,就是循环不变量原则,跟写二分法一样,我们要根据一个固定的原则来写循环,在二分法中,我们是事先固定了区间范围,之后再写内部逻辑。
在这里,我们要先固定每次循环的起始位置,这里我们固定的是从起始开始,而不遍历每一行的最后一个元素,这样执行一圈之后,正好全部遍历到,而不是一会遍历一会不遍历的,类似于数学中的控制变量法,因为循环本来就是一个动态的过程,如果我们连条件都没固定,那岂不是很容易混乱。
底层逻辑已经讲完了,下面我们来说一下代码的实现逻辑:
顺时针排列,那起始位置就是原点,我们定义起始位置startX,startY代表原点,和一个二维数组,代表i,j轴,这个位置就是记录添加到哪个位置 的,我们还定义了loop,是代表要循环几圈
如果是奇数圈,那么最后一圈就不满足我们while的条件,就执行到了if,最后一圈就是内部的一个点,直接把count添加到里面即可。
那我们在核心循环的代码,实现思路:我们一开始是从j轴开始的,就是最顶部的一行,循环的截止条件是n-offset,代表不去遍历最后一个位置,然后就是第二次for循环,我们是遍历最右侧,j轴不变,i轴加加,不遍历最后一个,把遍历到的每个位置,count的值都赋给当前的位置,count++,第三次for循环的时候,注意条件,条件是i轴不变,j轴--,而此时j的值就是n,已经在最底部的位置了,因此一直--,直到等于0时截止。下面的也同理。
当循环完这一圈之后,我们进行的一系列++操作,首先就是对原点位置的向内收缩,都是加一,偏移量offset自然也就是加1(因为内圈的行列数都少了一格),圈数加1,以此类推。
下面我们来讲解这道题相反过程的:
题目背景:LeetCode 54
给你一个
m行n列的矩阵matrix,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5]示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] 输出:[1,2,3,4,8,12,11,10,9,5,6,7]提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 10-100 <= matrix[i][j] <= 100
题目理解:
这道题的背景跟上一题一摸一样,仅仅是过程相反了,我们完全可以用上面相同的方式来处理,但是有细微的差别,下面具体说明,同时附上另一种优化的解法。
第一种解法:
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int m = matrix.length; // 行数
int n = matrix[0].length; // 列数
int startX = 0, startY = 0; // 起始位置
int offset = 1; // 偏移量
int loop = 1; // 当前圈数
int i, j;
// 需要遍历多少圈:取行和列的最小值除以2
int circles = Math.min(m, n) / 2;
while (loop <= circles) {
// 顶部:从左到右
for (j = startY; j < n - offset; j++) {
result.add(matrix[startX][j]);
}
// 右列:从上到下
for (i = startX; i < m - offset; i++) {
result.add(matrix[i][j]);
}
// 底部:从右到左
for (; j > startY; j--) {
result.add(matrix[i][j]);
}
// 左列:从下到上
for (; i > startX; i--) {
result.add(matrix[i][j]);
}
startX++;
startY++;
offset++;
loop++;
}
// 处理中间剩余的行或列
if (Math.min(m, n) % 2 == 1) {
if (m <= n) { // 行数少,剩余一行
for (j = startY; j <= n - offset; j++) {
result.add(matrix[startX][j]);
}
} else { // 列数少,剩余一列
for (i = startX; i <= m - offset; i++) {
result.add(matrix[i][startY]);
}
}
}
return result;
}
}
题目解析:
底层逻辑是一样的,需要注意的细节是,我们第一提到的是正方形矩阵,但这道题目并没有规定,而是,矩形,那么这就默认了行和列并不是完全相等的,因此我们需要单独注意这一部分,首先被牵扯到的就是循环的圈数了,我们需要从行和列中找一个较小值来计算圈数,而不是像第一那样不用考虑,其次就是内圈的处理问题,在结束了外层圈的循环,在最后一圈的时候,我们就需要判断到底是行少还是列少,因为这决定了我们执行最后一次循环的方向,到底是按i方向,还是j方向,这里的循环条件为什么是<=n-offset,因为 我们的startX,或则startY,都已经自增,向内缩进了,我们要遍历完这行或者列,不好解释,如图吧:
完整过程:
原始矩阵: [1, 2, 3, 4] [5, 6, 7, 8] [9, 10, 11, 12] 第1圈遍历后(最外层): 已遍历:1,2,3,4,8,12,11,10,9,5 剩余: [6, 7] ← 这是1行2列 此时: startX = 1 startY = 1 m - offset = 3 - 2 = 1 n - offset = 4 - 2 = 2 剩余的是第1行,从第1列到第2列: for (j = startY; j <= n - offset; j++) { // j从1到2 result.add(matrix[1][1]) = 6 result.add(matrix[1][2]) = 7 }图解:
第1圈后: [1, 2, 3, 4] [5, ← 中心区域 → 8] [9,10,11,12] 中心区域(剩余一行): startX=1 [6, 7] ← 一行 startY=1 到 n-offset=2
这样看下来,当我们执行完外层的圈数时,这时还剩下一行6,7没有添加,原点位置被缩进了,其实就执行了索引1,和2,刚好添加完。
第二种优化解法:
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// 1. 从左到右遍历上边
for (int j = left; j <= right; j++) {
result.add(matrix[top][j]);
}
top++; // 上边界下移
// 2. 从上到下遍历右边
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--; // 右边界左移
// 3. 从右到左遍历下边(需要检查是否还有行)
if (top <= bottom) {
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
bottom--; // 下边界上移
}
// 4. 从下到上遍历左边(需要检查是否还有列)
if (left <= right) {
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++; // 左边界右移
}
}
return result;
}
}
题目解析:
这种解法的核心逻辑就是实时更新边界,而不用手动的去处理最后一圈,这种解法用来解决我们正向59题也是同样优雅的,避免了offset的使用
首先就是变量的定义:
int botton = matrix.length; // 有多少行 = 3
int right = matrix[0].length; // 第一行有多少列 = 3
就是记录二维数组的上下左右的边界,while循环的判断条件,这个条件是边界检查,确保在遍历过程中不会越界,并且能正确处理各种形状的矩阵(正方形、长方形、单行、单列)。直到执行完之后,才自动跳过。因为这个遍历的过程就是边界的收缩过程,知道最后重合到一个点,或者是一行,一列时,才结束while。
下面的for循环大差不大。
结语:
如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!


