演算法圖鑑
Array & Hashing · 05 / 05

Matrix二維陣列

旋轉、轉置、螺旋走訪、四方向移動

用在:影像處理、棋盤遊戲、網格地圖

時間複雜度O(mn)
空間複雜度O(mn)
難度入門
前置知識Array & Dynamic Array

01為什麼需要它

把照片轉 90 度

手機拍的照片方向不對,要旋轉。圖片就是一個「高 × 寬」的像素矩陣,記憶體有限,不想再開一張一樣大的圖。

為什麼用它旋轉 90° 可以拆成「轉置」加「每列反轉」兩個原地操作,O(1) 額外空間。這種把幾何變換拆成簡單步驟的思路,影像處理裡到處都是。

棋盤遊戲與地圖

井字遊戲判斷連線、掃雷算周圍幾顆雷、遊戲地圖上找從 A 到 B 的路,都是在二維格子上「看鄰居」。

為什麼用它用方向陣列 [(0,1),(1,0),(0,-1),(-1,0)] 表示上下左右,一個迴圈搞定四方向加邊界檢查。之後圖論的網格 BFS / DFS 都用這個寫法。

試算表與矩陣運算

Excel 的一張表、機器學習的一批資料、線性代數的矩陣,都是二維陣列。要取某一行、轉置、對一整塊區域做運算。

為什麼用它理解「先列後行」的索引、記憶體是一列一列連續放的,就知道為什麼按列走比按行走快(快取友善),也知道怎麼正確建立與走訪。

看到這些關鍵字就想到它:grid、二維、m × n、上下左右、鄰居、旋轉/轉置、螺旋、棋盤、影像。

02核心概念

二維陣列就是「陣列的陣列」:grid[r][c] 先選第 r 列,再選那一列裡的第 c 格。慣例是 r 是列(row,垂直方向)、c 是行(column,水平方向)m = len(grid) 是列數、n = len(grid[0]) 是行數。把 r 和 c 弄反是這類題最常見的 bug。

記憶體裡它其實是一維的:一列接著一列連續放(row-major),所以 grid[r][c] 也可以寫成一維的 flat[r × n + c]。反過來,一維索引 k 對應 (k ÷ n, k mod n)。這個轉換讓「在 m × n 矩陣上二分搜尋」變成普通的一維二分搜尋。

矩陣題有三個固定工具。方向陣列:把四個(或八個)方向寫成 (dr, dc) 列表,一個迴圈走完所有鄰居,邊界檢查只寫一次。邊界收縮:螺旋走訪用 top / bottom / left / right 四條邊往內縮,比記方向轉彎乾淨。原地變換:旋轉 = 轉置 + 反轉;標記資訊可以借用第一列和第一行存,省下 O(mn) 的額外空間。

03演算法步驟

  1. 1先確認 mn 與索引順序:grid[r][c],0 ≤ r < m,0 ≤ c < n。空矩陣要特判。
  2. 2要看鄰居時用方向陣列for dr, dc in DIRS,算出 (nr, nc) 後做邊界檢查再處理。
  3. 3螺旋走訪:右→下→左→上各走一邊,走完一邊就把對應的邊界往內縮一格;每一輪走「左」與「上」之前要再檢查邊界沒交叉,否則單列或單行會重複。
  4. 4旋轉 90°(順時針):對角線上方逐對 swap(a[r][c], a[c][r]) 完成轉置,再把每一列反轉。逆時針則改成每一行上下反轉。
  5. 5需要「標記某列某行」又不能開新空間時,把標記寫在第一列與第一行,但要先另外記下它們本身原本有沒有被標記。

04互動示範

「螺旋走訪」逐格顯示走訪順序與四條邊界怎麼收縮;「旋轉 90°」逐步展示轉置的每一次交換,再看每一列反轉。

11
22
33
44
55
66
77
88
99
1010
1111
1212
1313
1414
1515
1616
目前狀態
方向→ 右top / bottom0 / 3left / right0 / 3

格子裡的數字是走訪順序。灰框是目前的邊界,每走完一邊就往內縮一格。

步驟 0/24四個邊界 top/bottom/left/right 圍住整個矩陣。先沿著 top 那一列往右走。

05程式碼

從建立矩陣與方向陣列開始,接著是螺旋走訪、原地旋轉,以及用邊列邊行當標記的 Set Matrix Zeroes。

# 建立 m × n 的矩陣:注意不能寫 [[0] * n] * m,那會讓每一列是同一個 list
grid = [[0] * 4 for _ in range(3)]
m, n = len(grid), len(grid[0])       # 列數、行數
grid[r][c]                           # 先列後行

# 四方向移動:用方向陣列,不要寫四段 if
DIRS = [(0, 1), (1, 0), (0, -1), (-1, 0)]     # 右、下、左、上

def neighbors(r, c):
    for dr, dc in DIRS:
        nr, nc = r + dr, c + dc
        if 0 <= nr < m and 0 <= nc < n:       # 邊界檢查放在同一個地方
            yield nr, nc


# 螺旋走訪(LeetCode 54):四個邊界往內縮
def spiral_order(matrix):
    out = []
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    while top <= bottom and left <= right:
        for c in range(left, right + 1):  out.append(matrix[top][c])
        top += 1
        for r in range(top, bottom + 1):  out.append(matrix[r][right])
        right -= 1
        if top <= bottom:
            for c in range(right, left - 1, -1): out.append(matrix[bottom][c])
            bottom -= 1
        if left <= right:
            for r in range(bottom, top - 1, -1): out.append(matrix[r][left])
            left += 1
    return out


# 原地順時針旋轉 90°(LeetCode 48):轉置,再每列反轉
def rotate(matrix):
    n = len(matrix)
    for r in range(n):
        for c in range(r + 1, n):                  # 只換對角線上方
            matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c]
    for row in matrix:
        row.reverse()


# 用第一列、第一行當標記,O(1) 額外空間(LeetCode 73)
def set_zeroes(matrix):
    m, n = len(matrix), len(matrix[0])
    first_row_zero = any(matrix[0][c] == 0 for c in range(n))
    first_col_zero = any(matrix[r][0] == 0 for r in range(m))
    for r in range(1, m):
        for c in range(1, n):
            if matrix[r][c] == 0:
                matrix[r][0] = matrix[0][c] = 0    # 記在邊上
    for r in range(1, m):
        for c in range(1, n):
            if matrix[r][0] == 0 or matrix[0][c] == 0:
                matrix[r][c] = 0
    if first_row_zero:
        for c in range(n): matrix[0][c] = 0
    if first_col_zero:
        for r in range(m): matrix[r][0] = 0

06練習題

  • LeetCode 54Spiral MatrixMedium
  • LeetCode 48Rotate ImageMedium
  • LeetCode 73Set Matrix ZeroesMedium
  • LeetCode 36Valid SudokuMedium
  • LeetCode 74Search a 2D Matrix(二維當一維二分)Medium
  • LeetCode 200Number of Islands(先用方向陣列 + DFS 試試)Medium