算法背景
螺旋矩阵(Spiral Matrix)是按顺时针方向从外向内逐层遍历或填充二维数组的经典问题。该问题在面试中高频出现,同时也在图像处理(螺旋采样)、游戏开发(螺旋视野渲染)、迷宫生成等领域有实际应用价值。核心挑战在于如何在遍历过程中正确管理边界,避免重复访问或越界。
核心思想
螺旋遍历可以分解为四个方向的重复移动:右 → 下 → 左 → 上。每完成一个方向的遍历,对应边界向内收缩一层(上边界下移、下边界上移、左边界右移、右边界左移)。当边界交错时,遍历结束。使用方向向量可以优雅地实现转向逻辑。
Java实现
import java.util.ArrayList;
import java.util.List;
/**
* 螺旋矩阵算法实现
* 支持顺时针遍历、螺旋填充与边界收缩策略
*/
public class SpiralMatrix {
/**
* 顺时针螺旋遍历二维矩阵
* 返回按螺旋顺序排列的元素列表
*/
public static List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return result;
}
int rows = matrix.length;
int cols = matrix[0].length;
// 定义四个边界
int top = 0; // 上边界(已遍历的最上行)
int bottom = rows - 1; // 下边界(已遍历的最下行)
int left = 0; // 左边界(已遍历的最左列)
int right = cols - 1; // 右边界(已遍历的最右列)
while (top <= bottom && left <= right) {
// 方向1:从左到右遍历上边界
for (int col = left; col <= right; col++) {
result.add(matrix[top][col]);
}
top++; // 上边界下移
// 方向2:从上到下遍历右边界
for (int row = top; row <= bottom; row++) {
result.add(matrix[row][right]);
}
right--; // 右边界左移
// 方向3:从右到左遍历下边界(需检查是否还有行)
if (top <= bottom) {
for (int col = right; col >= left; col--) {
result.add(matrix[bottom][col]);
}
bottom--; // 下边界上移
}
// 方向4:从下到上遍历左边界(需检查是否还有列)
if (left <= right) {
for (int row = bottom; row >= top; row--) {
result.add(matrix[row][left]);
}
left++; // 左边界右移
}
}
return result;
}
/**
* 生成n x n的螺旋填充矩阵
* 从1开始按顺时针螺旋顺序填充
*/
public static int[][] generateSpiral(int n) {
int[][] matrix = new int[n][n];
int top = 0, bottom = n - 1;
int left = 0, right = n - 1;
int num = 1;
while (top <= bottom && left <= right) {
// 填充上边界(从左到右)
for (int col = left; col <= right; col++) {
matrix[top][col] = num++;
}
top++;
// 填充右边界(从上到下)
for (int row = top; row <= bottom; row++) {
matrix[row][right] = num++;
}
right--;
// 填充下边界(从右到左)
if (top <= bottom) {
for (int col = right; col >= left; col--) {
matrix[bottom][col] = num++;
}
bottom--;
}
// 填充左边界(从下到上)
if (left <= right) {
for (int row = bottom; row >= top; row--) {
matrix[row][left] = num++;
}
left++;
}
}
return matrix;
}
/**
* 使用方向向量实现螺旋遍历
* 更通用的实现,适用于任意起始方向和转向规则
*/
public static List<Integer> spiralOrderWithDirection(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int rows = matrix.length;
int cols = matrix[0].length;
// 访问标记矩阵
boolean[][] visited = new boolean[rows][cols];
// 方向向量:右、下、左、上(顺时针)
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int dirIndex = 0; // 当前方向索引
int row = 0, col = 0;
for (int i = 0; i < rows * cols; i++) {
result.add(matrix[row][col]);
visited[row][col] = true;
// 计算下一个位置
int nextRow = row + directions[dirIndex][0];
int nextCol = col + directions[dirIndex][1];
// 如果下一个位置越界或已访问,则转向
if (nextRow < 0 || nextRow >= rows || nextCol < 0 ||
nextCol >= cols || visited[nextRow][nextCol]) {
dirIndex = (dirIndex + 1) % 4; // 顺时针转向
nextRow = row + directions[dirIndex][0];
nextCol = col + directions[dirIndex][1];
}
row = nextRow;
col = nextCol;
}
return result;
}
/**
* 逆时针螺旋遍历矩阵
*/
public static List<Integer> spiralOrderCounterClockwise(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int rows = matrix.length;
int cols = matrix[0].length;
boolean[][] visited = new boolean[rows][cols];
// 方向向量:右、上、左、下(逆时针)
int[][] directions = {{0, 1}, {-1, 0}, {0, -1}, {1, 0}};
int dirIndex = 0;
int row = rows - 1, col = 0; // 从左下角开始
for (int i = 0; i < rows * cols; i++) {
result.add(matrix[row][col]);
visited[row][col] = true;
int nextRow = row + directions[dirIndex][0];
int nextCol = col + directions[dirIndex][1];
if (nextRow < 0 || nextRow >= rows || nextCol < 0 ||
nextCol >= cols || visited[nextRow][nextCol]) {
dirIndex = (dirIndex + 1) % 4;
nextRow = row + directions[dirIndex][0];
nextCol = col + directions[dirIndex][1];
}
row = nextRow;
col = nextCol;
}
return result;
}
/**
* 打印矩阵
*/
public static void printMatrix(int[][] matrix) {
for (int[] row : matrix) {
for (int val : row) {
System.out.printf("%3d ", val);
}
System.out.println();
}
}
/**
* 按层打印矩阵(从外到内每层分别输出)
*/
public static List<List<Integer>> getLayers(int[][] matrix) {
List<List<Integer>> layers = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return layers;
}
int rows = matrix.length;
int cols = matrix[0].length;
int top = 0, bottom = rows - 1;
int left = 0, right = cols - 1;
while (top <= bottom && left <= right) {
List<Integer> layer = new ArrayList<>();
// 上边界
for (int col = left; col <= right; col++) {
layer.add(matrix[top][col]);
}
// 右边界(不含拐角)
for (int row = top + 1; row <= bottom; row++) {
layer.add(matrix[row][right]);
}
// 下边界(不含拐角,且需与上边界不同行)
if (top < bottom) {
for (int col = right - 1; col >= left; col--) {
layer.add(matrix[bottom][col]);
}
}
// 左边界(不含拐角,且需与右边界不同列)
if (left < right) {
for (int row = bottom - 1; row > top; row--) {
layer.add(matrix[row][left]);
}
}
layers.add(layer);
top++;
bottom--;
left++;
right--;
}
return layers;
}
public static void main(String[] args) {
System.out.println("=== 螺旋遍历示例 ===");
int[][] matrix = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
System.out.println("原始矩阵:");
printMatrix(matrix);
System.out.println("\n顺时针螺旋遍历结果:");
System.out.println(spiralOrder(matrix));
System.out.println("\n方向向量法遍历结果:");
System.out.println(spiralOrderWithDirection(matrix));
System.out.println("\n=== 螺旋填充示例 ===");
int[][] spiral = generateSpiral(5);
System.out.println("5x5 螺旋填充矩阵:");
printMatrix(spiral);
System.out.println("\n=== 非正方形矩阵遍历 ===");
int[][] rectMatrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12}
};
System.out.println("4x3 矩阵:");
printMatrix(rectMatrix);
System.out.println("螺旋遍历: " + spiralOrder(rectMatrix));
System.out.println("\n=== 按层分解 ===");
List<List<Integer>> layers = getLayers(matrix);
for (int i = 0; i < layers.size(); i++) {
System.out.println("第 " + (i + 1) + " 层: " + layers.get(i));
}
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 螺旋遍历(边界法) | $O(m \times n)$ | $O(1)$ | 除结果外不使用额外空间 |
| 螺旋遍历(方向向量) | $O(m \times n)$ | $O(m \times n)$ | 需要访问标记矩阵 |
| 螺旋填充 | $O(n^2)$ | $O(1)$ | 在原矩阵上填充 |
| 按层分解 | $O(m \times n)$ | $O(1)$ | 层结果不算额外空间 |
算法优势与适用场景
优势:
– 边界收缩法空间复杂度为 $O(1)$,极为高效
– 方向向量法易于扩展至任意转向规则
– 代码清晰,四种方向对称处理
适用场景:
– 面试高频考点(字节、腾讯、阿里均曾考察)
– 图像处理的螺旋采样与卷积
– 游戏中的视野渲染与地图探索
– 矩阵按优先级处理(外层优先)