五、59.螺旋矩阵II
题解参考:代码随想录 (programmercarl.com)
题目链接:59. 螺旋矩阵 II - 力扣(LeetCode)
1. 题目描述
给你一个正整数
n
,生成一个包含1
到n2
所有元素,且元素按顺时针顺序螺旋排列的n x n
正方形矩阵matrix
。
示例 1:
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1
输出:[[1]]
提示:
1 <= n <= 20
2. 思路
本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。我们在这篇文章一、704 二分查找中讲解了二分法,提到如果要写出正确的二分法一定要坚持循环不变量原则。对于本题依然要坚持这个原则,否则很容易被各个边界条件弄混,不仅费时,而且还有很大的概率会出错。
模拟顺时针画正方形矩阵步骤如下:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
由外向内一圈一圈这么画下去。可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是很容易被自己绕糊涂。
本题我们坚持使用左闭右开原则,保证每一行(列)的最后一个元素交给下一列(行)来处理,这样就能保证循环不变量原则,也就是统一的处理规则。
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
那么我按照左闭右开的原则,来画一圈,大家看一下:
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。这样就坚持了每条边左闭右开的原则。
3. C语言版本
大致实现步骤如下所示:
- 开辟返回结果数组,注意返回的是二维数组,在C语言中二维数组本质上是由多个一维数组构成。(每一个一维数的大小必须相同),因此我们在开辟数组空间的时候,
returnColumnSizes
需要开辟int*
类型的空间 - 定义循环的起始位置
startX、startY
、循环圈数loop
、偏移量offset
、中间位置mid
、空格值count
- 根据
n
的奇偶性填充数组- n为偶数:直接按照上面的左闭右开原则给数组赋值,每次循环结束,起始位置和偏移量都需要+1,循环圈数-1,空格值+1,
- n为奇数:如果n为奇数,那么在填充完外围的数组后,最后会剩下中间的位置需要单独填充
代码如下所示
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
* 时间复杂度:O(n^2)
* 空间复杂度:O(1)
*/
int** generateMatrix(int n, int* returnSize, int** returnColumnSizes){
// 初始化返回结果数组的大小,n行
*returnSize = n;
// 每行元素都是一个一维数组,各行的数组大小需要用一个数组来存储
*returnColumnSizes = (int*)malloc(sizeof(int) * n);
// 初始化返回结果数组,n个一维数组
int** ans = (int**)malloc(sizeof(int*) * n);
int i, j;
for (i = 0; i < n; i++) {
// 给每一行元素开辟空间
ans[i] = (int*)malloc(sizeof(int) * n);
// 第i行的数组大小为n
(*returnColumnSizes)[i] = n;
}
// 设置每次循环的起始位置
int startX = 0;
int startY = 0;
// 设置循环圈数,例如:n = 3, 循环1圈,n = 4,循环2圈
int loop = n / 2;
// 设置矩阵中间的位置,当n为奇数时,需要给中间位置单独赋值
// 例如:n = 3, 中间位置(1,1); n = 5, 中间位置(2,2)
int mid = n / 2;
// 设置偏移量
int offset = 1;
// 设置每一个空格的值
int count = 1;
while (loop--) {
i = startX;
j = startY;
// 下面4个for循环就是模拟转了1圈
// 上行从左往右填充(左闭右开区间)
for (j = startY; j < n - offset; j++) {
ans[startX][j] = count++;
}
// 右列从上到下填充(左闭右开区间)
for (i = startX; i < n - offset; i++) {
// 此时元素在最右边1列,所以为j
ans[i][j] = count++;
}
// 下行从右往左填充(左闭右开区间)
for (; j > startY; j--) {
ans[i][j] = count++;
}
// 左列从下到上填充(左闭右开区间)
for (; i > startX; i--) {
ans[i][j] = count++;
}
// 下一圈开始时,startX和startY都需要+1
// 例如:第二圈,x和y起始位置都变为1
startX++;
startY++;
// 偏移量也+1
offset++;
}
// 如果n为奇数,单独填充中间位置的元素值
if (n % 2 == 1) {
ans[mid][mid] = count;
}
return ans;
}
-
时间复杂度:
O(n^2)
,模拟遍历二维矩阵的时间。 -
空间复杂度:
O(1)
,除了返回的矩阵以外,空间复杂度是常数。
4. Java版本
4. 1 版本一
这个版本和上面C语言的版本的实现方法一样,只是换用Java编写了上述代码
代码如下所示
class Solution {
public int[][] generateMatrix(int n) {
// 定义返回数组
int[][] ans = new int[n][n];
// 设置每次循环的起始位置
int startX = 0;
int startY = 0;
// 设置循环圈数,例如:n = 3, 循环1圈,n = 4,循环2圈
int loop = n / 2;
// 设置矩阵中间的位置,当n为奇数时,需要给中间位置单独赋值
// 例如:n = 3, 中间位置(1,1); n = 5, 中间位置(2,2)
int mid = n / 2;
// 设置偏移量
int offset = 1;
// 设置每一个空格的值
int count = 1;
int i, j;
while (loop-- > 0) {
i = startX;
j = startY;
// 下面4个for循环就是模拟转了1圈
// 上行从左往右填充(左闭右开区间)
for (j = startY; j < n - offset; j++) {
ans[startX][j] = count++;
}
// 右列从上到下填充(左闭右开区间)
for (i = startX; i < n - offset; i++) {
// 此时元素在最右边1列,所以为j
ans[i][j] = count++;
}
// 下行从右往左填充(左闭右开区间)
for (; j > startY; j--) {
ans[i][j] = count++;
}
// 左列从下到上填充(左闭右开区间)
for (; i > startX; i--) {
ans[i][j] = count++;
}
// 下一圈开始时,startX和startY都需要+1
// 例如:第二圈,x和y起始位置都变为1
startX++;
startY++;
// 偏移量也+1
offset++;
}
// 如果n为奇数,单独填充中间位置的元素值
if (n % 2 == 1) {
ans[mid][mid] = count;
}
return ans;
}
}
-
时间复杂度:
O(n^2)
,模拟遍历二维矩阵的时间。 -
空间复杂度:
O(1)
,除了返回的矩阵以外,空间复杂度是常数。
4.2 版本二
这个版本思路一样,仍然坚持左闭右开原则,实现方式略有改变,大致思路如下所示:
- 这里的循环条件
loop
从0开始计数,用loop
代表当前循环的次数,同时也代表偏移量,即每次循环后loop++
,表示当前循环次数和偏移量都+1,同样能保证循环不变量 - 填充下行和左列时for循环终止条件时
>=
- 这里的起始位置只用一个
start
表示,每次循环结束后+1 - 其他实现逻辑和上面C语言版本的一致
代码如下所示
class Solution {
public int[][] generateMatrix(int n) {
// 定义返回数组
int[][] ans = new int[n][n];
// 设置循环起始位置
int start = 0;
// 循环总圈数,例如:n = 3, 循环1圈,n = 4,循环2圈
int allLoop = n / 2;
// 当前处于循环第几圈
int loop = 0;
// 设置矩阵中间的位置,当n为奇数时,需要给中间位置单独赋值
// 例如:n = 3, 中间位置(1,1); n = 5, 中间位置(2,2)
int mid = n / 2;
// 设置每一个空格的值
int count = 1;
int i, j;
// 判断边界之后,loop从1开始计数
while (loop++ < n / 2) {
// 下面4个for循环就是模拟转了1圈
// 上行从左往右填充(左闭右开区间)
for (j = start; j < n - loop; j++) {
ans[start][j] = count++;
}
// 右列从上到下填充(左闭右开区间)
for (i = start; i < n - loop; i++) {
// 此时元素在最右边1列,所以为j
ans[i][j] = count++;
}
// 下行从右往左填充(左闭右开区间)
for (; j >= loop; j--) {
ans[i][j] = count++;
}
// 左列从下到上填充(左闭右开区间)
for (; i >= loop; i--) {
ans[i][j] = count++;
}
// 下一圈开始时,start+1
// 例如:第二圈,x和y起始位置都变为1
start++;
}
// 如果n为奇数,单独填充中间位置的元素值
if (n % 2 == 1) {
ans[mid][mid] = count;
}
return ans;
}
}
-
时间复杂度:
O(n^2)
,模拟遍历二维矩阵的时间。 -
空间复杂度:
O(1)
,除了返回的矩阵以外,空间复杂度是常数。
5. 总结
这道题目之所以一直写不好,代码越写越乱,就是因为在画每一条边的时候,一会左开右闭,一会又来左闭右开,没有坚持使用同一个填充规则,岂能不乱。因此我们在判断边界条件的时候,一定要坚持循环不变量原则,即坚持使用同一个填充规则,例如这题我们一开始使用左闭右开原则,那么后面要保持一致,这样方便理清逻辑,不仅省力,而且不容易把自己绕糊涂。