算法:螺旋矩阵

857 阅读2分钟
作者: 成北
公众号: 前端下饭菜

题目:给你一个m行n列的矩阵matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素

示例1:
spiral1.1.jpeg

输入矩阵:[[1, 2, 3], [4, 5, 6], [7, 8, 9]
输出: [1, 2, 3, 6, 9, 8, 7, 4, 5]

示例2:
spiral.2.jpeg

输入矩阵: [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
输出:[123481211109567]

说明:
m = matrix.length,
n = matrix[i].length,
1 <= m, n <= 10,
-100 <= matrix[i][j] <= 100

矩阵为螺旋矩阵,并且方向为顺时针,最初想法是从左到右、从上到下、从右到左、从下到上四个方向循环遍历,如下图所示
屏幕快照 2021-08-15 下午5.13.22.png

记录已经遍历过的索引,并循环执行,直到输出的结果元素个数大于等于矩阵元素个数为止。

const spiralOrder = function(matrix) {
    if (!matrix || !matrix.length || matrix.length > 10) {
        throw new Error('matrix为二位数组,1 <= matrix长度 <= 10')
    }

    const m = matrix.length, n = matrix[0].length
    // 0表示从左到右, 1从上到下, 2从右到左,3从下到上
    let directions = 0 

    const MIN_ROW = 0, MAX_COL = 1, MAX_ROW = 2,MIN_COL = 3
    // [[最小行, ↓][最大列, ←][最大行, ↑][最小列, →]]
    const directionIndexs = [[0, 1], [n - 1, -1], [m - 1, -1], [0, 1]] 
    const result = []

    while (true) {
        if (result.length >= m * n) {
            break
        }

        if (directions === 0) { // →
            for (let index = directionIndexs[MIN_COL][0]; index <= directionIndexs[MAX_COL][0]; index++) {
                result.push(matrix[directionIndexs[MIN_ROW][0]][index])

            }

        } else if (directions === 1) { // ↓
            for (let index = directionIndexs[MIN_ROW][0]; index <= directionIndexs[MAX_ROW][0]; index++) {
                result.push(matrix[index][directionIndexs[MAX_COL][0]])
            }
        } else if (directions === 2) { // ←
            for (let index = directionIndexs[MAX_COL][0]; index >= directionIndexs[MIN_COL][0]; index--) {
                result.push(matrix[directionIndexs[MAX_ROW][0]][index])
            }
        } else if (directions === 3) { // ↑
            for (let index = directionIndexs[MAX_ROW][0]; index >= directionIndexs[MIN_ROW][0]; index--) {
                result.push(matrix[index][directionIndexs[MIN_COL][0]])
            }
        }

        directionIndexs[directions][0] = directionIndexs[directions][0] + directionIndexs[directions][1]
        directions = (directions + 1) % 4
    }

    return  result
};

const imatrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
console.log(spiralOrder(imatrix))

代码虽然能运行出结果,但可读性差、扩展性低,每个方向必须维护其最小、最大索引并且要动态调整,自己在实现这部分逻辑时也是调整了几次才运行正常,更不用说其他人能快速地理解代码。另一方面,这样写比较死板,假如现在算法调整为逆时针方向读取元素,那代码就很难支持了。

如果想减少判断,可以考虑用向量表达方向,用[1, 0]、[0, 1]、[-1, 0]、[0, -1]单位向量分别表示东、南、西、北方向。

屏幕快照 2021-08-15 下午5.13.29.png

这样可以把坐标位置的调整和方向向量结合起来计算就可以省去四个方向的逻辑判断,简化代码,核心代码如下:

const m = matrix.length, n = matrix[0].length, result = []
// 通过单位向量控制方向,[1, 0]表示→, [0, 1]表示↓, [-1, 0]表示←, [0, -1]表示↑
const vects = [[1, 0], [0, 1], [-1, 0], [0, -1]]
// 先从左到右遍历, 起始位置为[0, 0]
let vectIndex = 0, [x, y] = [0, 0]
while (result.length < m * n) {
    // 题目限定了元素值从-100100, 已经取过的元素其值设置为-infinity
    if (0 <= x && x <= m - 1 && 0 <= y && y <= n - 1 && matrix[x][y] !== -Infinity) {
        result.push(matrix[x][y])
        matrix[x][y] = -Infinity
        x += vects[vectIndex][1]
        y += vects[vectIndex][0]
    } else {
        // 不撞南墙不回头, 遇到尽头多走了一步需回退回来并调整到下一步起始位置
        x -= vects[vectIndex][1]
        y -= vects[vectIndex][0]
        vectIndex = (vectIndex + 1) % vects.length
        x += vects[vectIndex][1]
            y += vects[vectIndex][0]
    }
}

return  result

题目条件限定了元素值在[-100, 100]之间,可以把已经遍历的元素设置为-inifinity,这样遍历到元素值为-inifinity时就该调整方向了。

这段代码中明显有多处重复给x、y赋值,占用了6行代码,数组元素少看不出问题,当数组元素指数级增长,这几行代码还是会影响效率,考虑是否可以继续优化。另外,使用-inifinity不够灵活并且污染了原数组,所以还是考虑使用动态调整[minRow, maxCol, maxRow, minCol]方法限制边界。

    const m = matrix.length, n = matrix[0].length, result = []
    // 通过单位向量控制方向,[1, 0]表示→, [0, 1]表示↓, [-1, 0]表示←, [0, -1]表示↑
    const vects = [[1, 0], [0, 1], [-1, 0], [0, -1]]
    // 先从左到右遍历, 起始位置为[0, 0], 用[minRow, maxCol, maxRow, minCol]记录边界位置
    let vectIndex = 0, [x, y] = [0, 0], range = [0, n - 1, m - 1, 0]

    // 边界位置如何调整?
    while (result.length < m * n) {
        result.push(matrix[x][y])

        let [tempX , tempY] = [x + vects[vectIndex][1], y + vects[vectIndex][0]]
        // 如果tempX、tempY超出范围,即时调整方向
        if (tempX < range[0] || tempX > range[2] || tempY < range[3] || tempY > range[1]) {
            if (vectIndex === 0 || vectIndex === 3) {
                range[vectIndex] += 1
            } else {
                range[vectIndex] -= 1
            }
            // 调整至下一个方向
            vectIndex = (vectIndex + 1) % 4
            tempX = x + vects[vectIndex][1]
            tempY = y + vects[vectIndex][0]       
        } 
        // 下一个元素确保在边界范围内,可以继续遍历
        [x,  y] = [tempX, tempY]       
    }

    return  result

其中14至18行代码想表达的意思不够明显,相当于每个方向到达尽头后需要调整边界,例如从左往右遍历到结束位置,需要调整minX的值,即range[MIN_ROW] += 1,其他几个方向的边界调整类似。

本题绝大部分解决方案的时间复杂度都为N(mn),空间复杂度可能差异,有的方法是重新声明个一样的矩阵标记元素是否有访问过,那么空间复杂度变为N(mn), 其他方法空间复杂度一般为常数级N(1)。