螺旋矩阵 III 图解题解
这道题到底在问什么
- 输入
- rows=1, cols=4, 起点 (0,0)
- 输出
- [[0,0],[0,1],[0,2],[0,3]]
- 输入
- rows=2, cols=2, 起点 (0,0)
- 输出
- [[0,0],[0,1],[1,1],[1,0]]
最优解:一步一步想明白
- 3记住两根尺子:方向东南西北转圈,步长每两段加一。下面用一张小网格把这两根尺子走一遍。
- 4真实网格是上面两行 0、1;最下面灰带是「界外」第 2 行看这张图。真正的网格是上面绿框这两行,共 2 行 4 列。最下面一条灰带代表「网格外面」,等会儿螺旋会踩进去。绿色的 (1,1) 是起点,先把它记成答案里的第 1 个。
- 5每两段方向用同一个步长,之后步长加一先把步长规律记牢。出发先向东走 1 步,再向南走 1 步;然后向西走 2 步,向北走 2 步;接着向东走 3 步,向南走 3 步。就是每两段用同一个步长,走完两段步长就加一。
- 6向东 第 1/1 步 | 已记录 1 / 8出发了。第一段向东走 1 步,指针落到 (1,2)(橙色)。每走到一格,先问一句:这格在网格里吗?
- 7(1,2) 在网格内,收进答案 | 已记录 2 / 8(1,2) 的行和列都没越界,在网格里。把它涂绿、记成答案的第 2 个,右边 ans 队伍多了一个坐标。
- 8向南 第 1/1 步 | 已记录 2 / 8指针沿着南方向迈一步,落到 (2,2)。还是老规矩,先判断它在不在真实网格里。
- 9(2,2) 越出网格,跳过不记 | 已记录 2 / 8(2,2) 掉到灰带里,也就是网格外面。这一步照样要走,但不能记进答案,所以打个红叉跳过。注意:虽然没记,人还是站在这里继续往前走。
- 10向西 第 1/2 步 | 已记录 2 / 8指针沿着西方向迈一步,落到 (2,1)。还是老规矩,先判断它在不在真实网格里。
- 11(2,1) 越出网格,跳过不记 | 已记录 2 / 8(2,1) 又是界外,继续打叉跳过,ans 不变,但脚步不停,接着按规律往下走。
- 12向西 第 2/2 步 | 已记录 2 / 8指针沿着西方向迈一步,落到 (2,0)。还是老规矩,先判断它在不在真实网格里。
- 13(2,0) 越出网格,跳过不记 | 已记录 2 / 8(2,0) 又是界外,继续打叉跳过,ans 不变,但脚步不停,接着按规律往下走。
- 14向北 第 1/2 步 | 已记录 2 / 8指针沿着北方向迈一步,落到 (1,0)。还是老规矩,先判断它在不在真实网格里。
- 15(1,0) 在网格内,收进答案 | 已记录 3 / 8(1,0) 的行和列都没越界,在网格里。把它涂绿、记成答案的第 3 个,右边 ans 队伍多了一个坐标。
- 16向北 第 2/2 步 | 已记录 3 / 8指针沿着北方向迈一步,落到 (0,0)。还是老规矩,先判断它在不在真实网格里。
- 17(0,0) 在网格内,收进答案 | 已记录 4 / 8(0,0) 同样在界内,收进答案,成为第 4 个。
- 18向东 第 1/3 步 | 已记录 4 / 8指针沿着东方向迈一步,落到 (0,1)。还是老规矩,先判断它在不在真实网格里。
- 19(0,1) 在网格内,收进答案 | 已记录 5 / 8(0,1) 同样在界内,收进答案,成为第 5 个。
- 20向东 第 2/3 步 | 已记录 5 / 8指针沿着东方向迈一步,落到 (0,2)。还是老规矩,先判断它在不在真实网格里。
- 21(0,2) 在网格内,收进答案 | 已记录 6 / 8(0,2) 同样在界内,收进答案,成为第 6 个。
- 22向东 第 3/3 步 | 已记录 6 / 8指针沿着东方向迈一步,落到 (0,3)。还是老规矩,先判断它在不在真实网格里。
- 23(0,3) 在网格内,收进答案 | 已记录 7 / 8(0,3) 同样在界内,收进答案,成为第 7 个。
- 24向南 第 1/3 步 | 已记录 7 / 8指针沿着南方向迈一步,落到 (1,3)。还是老规矩,先判断它在不在真实网格里。
- 25(1,3) 在网格内,收进答案 | 已记录 8 / 8(1,3) 同样在界内,收进答案,成为第 8 个。
- 26全部 8 格按螺旋顺序记齐,任务结束当记录数等于网格总格数 8,就说明全走遍了,立刻收工。右边 ans 这一长串坐标,正是按螺旋访问顺序排好的最终答案。中间踩进灰带的那几步,一个都没混进来。
⚠️ 容易写错的地方
✗ 错:走到界外就停下或跳过这一步不走
✓ 对:界外照样走,只是不记进答案
螺旋必须绕到外圈再拐回来,少走一步整个路径就错位
✗ 错:四段步长都用同一个 k
✓ 对:前两段用 k,后两段用 k+1,然后 k 加 2
步长得照 1,1,2,2,3,3 递增,写错就转不成正确的螺旋
✗ 错:靠走满固定圈数来结束
✓ 对:用「已记录数等于总格数」来结束
不同起点要绕的圈数不一样,只有数够格子才是可靠的终止条件
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def spiralMatrixIII(
self, rows: int, cols: int, rStart: int, cStart: int
) -> List[List[int]]:
ans = [[rStart, cStart]]
if rows * cols == 1:
return ans
k = 1
while True:
for dr, dc, dk in [[0, 1, k], [1, 0, k], [0, -1, k + 1], [-1, 0, k + 1]]:
for _ in range(dk):
rStart += dr
cStart += dc
if 0 <= rStart < rows and 0 <= cStart < cols:
ans.append([rStart, cStart])
if len(ans) == rows * cols:
return ans
k += 2C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
vector<vector<int>> spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
int cnt = rows * cols;
vector<vector<int>> ans;
ans.push_back({rStart, cStart});
if (cnt == 1) return ans;
for (int k = 1;; k += 2) {
vector<vector<int>> dirs = {{0, 1, k}, {1, 0, k}, {0, -1, k + 1}, {-1, 0, k + 1}};
for (auto& dir : dirs) {
int r = dir[0], c = dir[1], dk = dir[2];
while (dk-- > 0) {
rStart += r;
cStart += c;
if (rStart >= 0 && rStart < rows && cStart >= 0 && cStart < cols) {
ans.push_back({rStart, cStart});
if (ans.size() == cnt) return ans;
}
}
}
}
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int[][] spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
int cnt = rows * cols;
int[][] ans = new int[cnt][2];
ans[0] = new int[] {rStart, cStart};
if (cnt == 1) {
return ans;
}
for (int k = 1, idx = 1;; k += 2) {
int[][] dirs = new int[][] {{0, 1, k}, {1, 0, k}, {0, -1, k + 1}, {-1, 0, k + 1}};
for (int[] dir : dirs) {
int r = dir[0], c = dir[1], dk = dir[2];
while (dk-- > 0) {
rStart += r;
cStart += c;
if (rStart >= 0 && rStart < rows && cStart >= 0 && cStart < cols) {
ans[idx++] = new int[] {rStart, cStart};
if (idx == cnt) {
return ans;
}
}
}
}
}
}
}复杂度
时间
O(max(rows, cols)²)
螺旋边长一圈圈变大,含界外空走的步数与最长边的平方同阶
空间
O(rows · cols)
只有答案列表本身的开销,不算它则是 O(1) 的额外变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 螺旋矩阵 III 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么不开一个二维 visited 数组来标记走过的格子?+
没必要。螺旋路径本身保证每个界内格子恰好被访问一次,不会重复踩到同一个界内格,所以用一个计数器记录已收集多少个就够了,数到 rows 乘 cols 就结束。开二维数组反而多花 O(rows·cols) 空间,得不偿失。
界外空走会不会让时间复杂度爆掉?+
不会爆,但确实有浪费。最坏情况起点在角落,螺旋要绕很大的圈才能覆盖远端,空走步数和最长边的平方同阶,也就是 O(max(rows,cols) 的平方)。对 100 乘 100 的网格完全跑得动,工程上无需优化。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 螺旋矩阵 III 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。