原理
本题要求生成一个给定大小 n
的螺旋矩阵,矩阵中的元素按照螺旋顺序从 1 开始依次递增填充。整体思路是通过模拟螺旋的填充路径,一圈一圈地向矩阵内部填充数字,每一圈的填充过程都按照顺时针方向,依次填充上侧行、右侧列、下侧行和左侧列,直到整个矩阵被填满。对于奇数边长的矩阵,最后还需要单独处理正中间的那个元素。
步骤
- 初始化相关变量
- 创建一个
n * n
大小的二维数组res
,并将所有元素初始化为 0,用于存储最终生成的螺旋矩阵。 - 定义
startx
和starty
,它们表示每循环填充一圈时的起始坐标位置,初始都设为 0,即从矩阵的左上角开始填充第一圈。 - 计算
loop
,它代表总共需要循环填充完整几圈,其值为n / 2
,比如n
为 4 时,需要完整循环 2 圈;n
为 5 时,完整循环 2 圈后还剩下中间一个元素单独处理。 - 确定
mid
,它表示矩阵中间的位置坐标,用于后续处理奇数边长矩阵中间元素的赋值,计算方式为n / 2
。 - 设定
count
初始值为 1,用于按顺序给矩阵中的元素赋值,每填充一个位置,count
的值就递增 1。 - 初始化
offset
为 1,这个变量用于控制每一圈中每条边遍历填充的长度,随着圈数的增加,每条边需要填充的长度会逐渐减少,每完成一圈,offset
的值就增加 1。
- 创建一个
- 循环填充矩阵(外层
while
循环部分)- 进入
while
循环,只要loop
的值大于 0,就代表还需要继续填充完整的圈数。在每次循环中:- 先将当前圈的起始填充坐标
i
和j
分别赋值为startx
和starty
。 - 填充上侧行(从左到右):通过
for (j; j < n - offset; j++)
循环,从当前起始列starty
开始,向右填充元素,直到达到本圈右侧边界(n - offset
,每一圈右边界会收缩一位,由offset
控制),每次填充一个元素就将count
的值赋给当前位置res[i][j]
,然后count
自增 1,模拟按螺旋顺序填充数字的过程。 - 填充右侧列(从上到下):接着通过
for (i; i < n - offset; i++)
循环,从当前行(此时i
已经在刚才填充上侧行结束时所在行)开始,向下填充元素,直到达到本圈下侧边界,同样每次填充元素时更新res[i][j]
和count
的值。 - 填充下侧行(从右到左):再通过
for (; j > starty; j--)
循环,从本圈最右侧列(刚才填充右侧列结束时所在列)开始,向左填充元素,直到回到本圈起始列位置(starty
),过程中持续更新对应矩阵位置的值和count
。 - 填充左侧列(从下到上):最后通过
for (; i > startx; i--)
循环,从本圈最下侧行(填充下侧行结束时所在行)开始,向上填充元素,直到回到本圈起始行位置(startx
),同样更新矩阵元素值和count
。 - 更新下一圈的起始位置和
offset
:完成一圈填充后,将startx
和starty
的值都加 1,使下一圈的起始位置往矩阵内部移动一位;同时将offset
的值加 1,使得下一圈每条边遍历填充的长度再收缩一位,以符合螺旋向内填充的规律。
- 先将当前圈的起始填充坐标
- 进入
- 处理奇数边长矩阵中间元素(
if
判断部分)
如果n
为奇数,意味着在完成所有完整圈的填充后,矩阵正中间还有一个元素未赋值,此时通过res[mid][mid] = count;
将count
的当前值赋给矩阵中间位置的元素,完成整个螺旋矩阵的填充。
图示法表示步骤(以 n = 5
为例)
- 初始化矩阵和变量
初始矩阵(5 * 5,全为0):
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
变量情况:startx = 0, starty = 0, loop = 2, mid = 2, count = 1, offset = 1
- 第一圈填充
- 填充上侧行(从左到右):
1 2 3 4 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
此时 j
从 0 循环到 3(n - offset = 5 - 1 = 4
),依次填充元素,count
从 1 递增到 4。
- 填充右侧列(从上到下):
1 2 3 4 0
0 5 6 7 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
i
从 0 循环到 3,填充对应位置元素,count
从 5 递增到 8。
- 填充下侧行(从右到左):
1 2 3 4 0
0 5 6 7 0
0 0 8 9 0
0 0 0 0 0
0 0 0 0 0
j
从 4 循环到 1,填充元素,count
从 9 递增到 11。
- 填充左侧列(从下到上):
1 2 3 4 0
0 5 6 7 0
0 0 8 9 0
0 0 10 11 0
0 0 0 0 0
i
从 4 循环到 1,填充元素,count
从 11 递增到 13。
完成第一圈填充后,startx = 1, starty = 1, offset = 2
。
- 第二圈填充
- 填充上侧行(从左到右):
1 2 3 4 0
0 5 6 7 0
0 0 13 14 0
0 0 10 11 0
0 0 0 0 0
j
从 1 循环到 2(n - offset = 5 - 2 = 3
),填充元素,count
从 13 递增到 14。
- 填充右侧列(从上到下):
1 2 3 4 0
0 5 6 7 0
0 0 13 14 0
0 0 10 15 0
0 0 0 0 0
i
从 1 循环到 2,填充元素,count
从 14 递增到 15。
- 填充下侧行(从右到左):
1 2 3 4 0
0 5 6 7 0
0 0 13 14 0
0 0 10 15 0
0 0 0 16 0
j
从 2 循环到 1,填充元素,count
从 15 递增到 16。
- 填充左侧列(从下到上):
1 2 3 4 0
0 5 6 7 0
0 0 13 14 0
0 0 10 15 0
0 0 0 16 0
i
从 2 循环到 1,填充元素,count
从 16 递增到 17。
完成第二圈填充后,此时 loop
变为 0,循环结束。
- 处理中间元素(因为
n = 5
为奇数)
1 2 3 4 0
0 5 6 7 0
0 0 13 14 0
0 0 10 15 0
0 0 0 16 17
通过 res[mid][mid] = count;
(即 res[2][2] = 17
)完成整个螺旋矩阵的填充。
代码关键行注释
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组,初始化n * n大小的矩阵,所有元素初始为0,用于存储螺旋矩阵结果
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置坐标,初始为左上角(0, 0)
int loop = n / 2; // 每个圈循环几次,根据n的值计算完整循环的圈数(n为奇数时最后中间元素单独处理)
int mid = n / 2; // 矩阵中间的位置坐标,用于后续奇数情况处理中间元素赋值
int count = 1; // 用来给矩阵中每一个空格赋值,从1开始按顺序递增
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位,初始为1
int i, j;
while (loop--) { // 只要还需要循环填充完整的圈数,就继续循环
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开),按螺旋顺序从左到右填充当前圈的上侧行元素
for (j; j < n - offset; j++) {
res[i][j] = count++;
}
// 模拟填充右列从上到下(左闭右开),从上到下填充当前圈的右侧列元素
for (i; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开),从右到左填充当前圈的下侧行元素
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开),从下到上填充当前圈的左侧列元素
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1,更新下一圈的起始坐标位置,往矩阵内部移动一位
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度,每完成一圈,每条边遍历长度收缩一位,增加1
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值,因为奇数边长矩阵在完成圈填充后中间还有一个元素未处理
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
完整代码程序
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
int startx = 0, starty = 0;
int loop = n / 2;
int mid = n / 2;
int count = 1;
int offset = 1;
int i, j;
while (loop--) {
i = startx;
j = starty;
// 填充上行从左到右
for (j; j < n - offset; j++) {
res[i][j] = count++;
}
// 填充右列从上到下
for (i; i < n - offset; i++) {
res[i][j] = count++;
}
// 填充下行从右到左
for (; j > starty; j--) {
res[i][j] = count++;
}
// 填充左列从下到上
for (; i > startx; i--) {
res[i][j] = count++;
}
startx++;
starty++;
offset += 1;
}
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
int main() {
Solution solution;
int n = 5; // 这里可自行修改n的值来生成不同大小的螺旋矩阵
vector<vector<int>> result = solution.generateMatrix(n);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cout << result[i][j] << " ";
}
cout << endl;
}
return 0;
}
时间复杂度分析
- 整体循环部分:
主要的循环操作是外层的while
循环,它控制了填充完整圈数的次数,循环次数为n / 2
(向下取整),即最多执行n / 2
次。在每次循环中,又包含了四个for
循环,分别用于填充每一圈的四条边,每个for
循环最多执行n
次(在第一圈时,四条边的填充长度都接近n
,后续随着圈数增加会逐渐减少,但按最坏情况考虑与n
相关),而循环内部的操作(给矩阵元素赋值、变量更新等)时间复杂度都是常数级别O(1)
。所以这部分的时间复杂度大致为 ,因为整体的循环次数与n
的平方相关。 - 处理奇数边长矩阵中间元素部分:
如果n
为奇数,最后有一个单独的赋值操作给矩阵中间元素赋值,时间复杂度为O(1)
,相比于前面的循环填充部分,其时间复杂度可以忽略不计。
综合来看,整个算法的时间复杂度为 ,它与生成的螺旋矩阵的边长 n
的平方成正比,随着 n
的增大,生成螺旋矩阵所需的时间会相应增加。
总结
- 算法特点及适用场景:
- 特点:
- 该算法通过模拟螺旋的填充路径,逻辑清晰直观,易于理解和实现,按照一圈一圈向内填充的方式,能够准确地生成满足要求的螺旋矩阵。并且代码结构相对简洁,通过合理控制循环和变量,有效地处理了不同边长(包括奇数和偶数)情况下的矩阵填充问题。
- 时间复杂度为 ,在处理中等规模的螺旋矩阵生成任务时,具有较好的效率表现,能够在合理的时间内完成矩阵填充。
- 适用场景:
常用于需要按照特定螺旋顺序生成二维数据结构的场景,比如在图像处理中,可能需要按照螺旋顺序对图像像素进行采样、处理或者赋值;在一些数学建模、算法设计等领域,当涉及到以螺旋形式组织数据或者进行数据遍历等操作时,也可以借助此算法来构建相应的基础数据结构(螺旋矩阵),方便后续的计算和分析。
- 特点:
- 注意事项及可能遇到的问题:
- 边界情况处理:代码中对于每条边填充的边界控制(如
for
循环的结束条件)以及圈数的计算、奇数边长矩阵中间元素的处理等边界情况需要特别留意,一旦处理不当,很容易导致生成的矩阵不符合螺旋矩阵的要求,出现元素缺失、重复填充或者顺序错误等问题。例如,如果offset
的计算或使用有误,可能会使每条边的填充长度出错,进而影响整个矩阵的填充结果。 - 代码可扩展性和通用性:虽然当前算法能够很好地解决 LeetCode 中给定规则的螺旋矩阵生成问题,但如果要对矩阵的填充规则进行修改,比如改变螺旋的方向(变为逆时针)、按照不同的递增规律填充元素(不是简单的连续整数递增)等,可能需要对代码的循环结构和逻辑进行较大幅度的调整,代码的可扩展性和通用性相对有限,在实际应用中如果有多样化的需求变动,可能需要重新设计部分代码逻辑来满足新的要求。
- 边界情况处理:代码中对于每条边填充的边界控制(如