螺旋矩阵

给你一个 mn 列的矩阵 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.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100

边界模拟:

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();

        int top = 0;
        int bottom = m - 1;
        int left = 0;
        int right = n - 1;

        vector<int> ans;

        while (top <= bottom && left <= right)
        {
            for (int j = left; j <= right; j++)
            {
                ans.push_back(matrix[top][j]);
            }
            top++;

            for (int i = top; i <= bottom; i++)
            {
                ans.push_back(matrix[i][right]);
            }
            right--;

            if (top <= bottom)
            {
                for (int j = right; j >= left; j--)
                {
                    ans.push_back(matrix[bottom][j]);
                }
                bottom--;
            }

            if (left <= right)
            {
                for (int i = bottom; i >= top; i--)
                {
                    ans.push_back(matrix[i][left]);
                }
                left++;
            }
        }

        return ans;
    }
};

核心思想

螺旋遍历的本质,是一圈一圈地访问矩阵。

每一圈的顺序固定为:

  1. 从左到右走上边界。
  2. 从上到下走右边界。
  3. 从右到左走下边界。
  4. 从下到上走左边界。

走完一圈以后,这一圈已经全部访问过。

接下来只需要把边界向内收缩一层,继续处理下一圈。

所以我们用四个变量表示当前还没有访问的矩形区域:

  • top:当前上边界
  • bottom:当前下边界
  • left:当前左边界
  • right:当前右边界

只要满足:

top <= bottom && left <= right

说明还有元素没有访问。

四个边界的含义

初始时:

int top = 0;
int bottom = m - 1;
int left = 0;
int right = n - 1;

这表示整个矩阵都还没有被遍历。

访问完上边界以后:

top++;

说明原来的上边界已经访问完,下一圈的上边界向下移动一行。

访问完右边界以后:

right--;

说明原来的右边界已经访问完,下一圈的右边界向左移动一列。

访问完下边界以后:

bottom--;

说明原来的下边界已经访问完,下一圈的下边界向上移动一行。

访问完左边界以后:

left++;

说明原来的左边界已经访问完,下一圈的左边界向右移动一列。

每一圈怎么遍历

假设当前未访问区域是:

top ... bottom

和:

left ... right

1. 从左到右访问上边界

固定行号 top,列号从 leftright

for (int j = left; j <= right; j++)
{
    ans.push_back(matrix[top][j]);
}
top++;

2. 从上到下访问右边界

固定列号 right,行号从新的 topbottom

for (int i = top; i <= bottom; i++)
{
    ans.push_back(matrix[i][right]);
}
right--;

这里使用新的 top,是因为旧的上边界已经访问过了。

3. 从右到左访问下边界

访问下边界前,需要先判断:

top <= bottom

如果不满足,说明已经没有下边界了。

满足时,固定行号 bottom,列号从 rightleft

if (top <= bottom)
{
    for (int j = right; j >= left; j--)
    {
        ans.push_back(matrix[bottom][j]);
    }
    bottom--;
}

4. 从下到上访问左边界

访问左边界前,需要判断:

left <= right

如果不满足,说明已经没有左边界了。

满足时,固定列号 left,行号从 bottomtop

if (left <= right)
{
    for (int i = bottom; i >= top; i--)
    {
        ans.push_back(matrix[i][left]);
    }
    left++;
}

为什么需要额外判断

这题最容易出错的地方,是矩阵剩下最后一行或者最后一列时,可能会重复访问。

例如:

matrix = [[1,2,3]]

只有一行。

从左到右访问上边界后,top 会增加。

此时已经没有下边界了。

如果不判断 top <= bottom,再去从右到左访问下边界,就会把同一行重复加入答案。

同理,如果矩阵只剩一列,也可能重复访问左边界。

所以在访问下边界和左边界之前,必须分别判断:

top <= bottom

和:

left <= right

这样可以保证每个元素只被访问一次。

正确性证明

我们证明算法返回的数组正好是矩阵的顺时针螺旋遍历结果。

结论 1:每一圈的访问顺序正确

在某一轮循环中,当前未访问区域由四个边界确定:

top, bottom, left, right

算法依次访问:

  1. 上边界:从左到右
  2. 右边界:从上到下
  3. 下边界:从右到左
  4. 左边界:从下到上

这正好就是顺时针访问当前外圈的顺序。

所以每一圈的访问顺序正确。

结论 2:访问完一圈后,边界会正确收缩

访问完上边界后,top++

访问完右边界后,right--

访问完下边界后,bottom--

访问完左边界后,left++

因此已经访问过的外圈会被排除在下一轮循环之外。

下一轮循环处理的正好是内部剩余矩形。

所以边界收缩正确。

结论 3:每个元素只会被访问一次

每次访问一条边界后,都会立即把对应边界向内收缩。

后续循环不会再访问已经收缩出去的边界。

同时,在访问下边界和左边界前,算法会检查当前区域是否仍然存在对应边界。

所以单行、单列的情况不会被重复访问。

因此每个元素最多被访问一次。

结论 4:所有元素都会被访问

只要:

top <= bottom && left <= right

就说明当前仍然存在一个未访问的矩形区域。

算法会访问这个区域的外圈,并继续向内收缩。

当循环结束时,说明:

top > bottom

或者:

left > right

此时已经不存在未访问区域。

所以所有元素都会被访问。

得出结论

由结论 1 可知,算法每一圈的访问顺序符合顺时针螺旋顺序。

由结论 2 可知,算法会逐层向内处理矩阵。

由结论 3 可知,算法不会重复访问元素。

由结论 4 可知,算法不会漏掉元素。

因此算法返回的结果正确。

举例理解

以:

matrix = [[1,2,3],
          [4,5,6],
          [7,8,9]]

为例。

第一圈:

  • 上边界:1, 2, 3
  • 右边界:6, 9
  • 下边界:8, 7
  • 左边界:4

此时答案是:

[1,2,3,6,9,8,7,4]

边界向内收缩后,只剩中间:

5

继续访问,最终得到:

[1,2,3,6,9,8,7,4,5]

复杂度分析

矩阵中每个元素都会被访问一次,且只访问一次。

所以时间复杂度是:

O(mn)

其中 m 是矩阵行数,n 是矩阵列数。

除了返回数组 ans 以外,只使用了四个边界变量。

所以额外空间复杂度是:

O(1)

如果把返回数组也计入空间,则空间复杂度是 O(mn)