「前端刷题」48. 旋转图像

216 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

题目

给定一个 _n _× n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

 

示例 1:

**输入:**matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2:

**输入:**matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] 输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

示例 3:

**输入:**matrix = [[1]] 输出:[[1]]

示例 4:

**输入:**matrix = [[1,2],[3,4]] 输出:[[3,1],[4,2]]

 

提示:

  • matrix.length == n
  • matrix[i].length == n
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

思路

本文尝试倒序、顺序、层次、螺旋遍历和翻转矩阵,测试解构赋值交换数组元素的性能

一 倒序遍历 · 深拷贝:新 → 原

  • 深拷贝原矩阵。任意顺序遍历矩阵,坐标[i, j] = 原坐标[n - 1 - j][i]

代码

Array.from + slice

var rotate = function (matrix) {
    let n = matrix.length, m = Array.from({length: n}, (_, i) => matrix[i].slice())
    for (let i = n; i--;) 
        for (let j = n; j--;) 
            matrix[i][j] = m[n - 1 - j][i]
};

顺序遍历深拷贝slice运行结果

JSON.stringify + JSON.parse

var rotate = function (matrix) {
    let n = matrix.length, m = JSON.parse(JSON.stringify(matrix))
    for (let i = n; i--;) 
        for (let j = n; j--;) 
            matrix[i][j] = m[n - 1 - j][i]
};

顺序遍历深拷贝JSON运行结果

二 顺序遍历 · 拷贝:原 → 新 → 原

解题思路

  • 任意顺序遍历矩阵,新矩阵[j, n - 1 - j] = 原矩阵坐标[i, j]

代码

var rotate = function(matrix) {
    let n = matrix.length, m = Array.from({length: n}, _ => Array(n))
    for(let i = 0; i < n; i++)
        for(let j = 0; j < n; j++)
            m[j][n - 1 - i] = matrix[i][j]
    for(let i = 0; i < n; i++)
        for(let j = 0; j < n; j++)
            matrix[i][j] = m[i][j]
};

顺序遍历 + 拷贝解法运行结果

三 倒序垂直扫描 · 队列

解题思路

代码

var rotate = function(matrix) {
    let n = matrix.length, queue = []
   for(let i = 0; i < n; i++) 
        for(let j = 0; j < n; j++) 
            queue.push(matrix[i][j])
    for(let j = n; j--;) 
        for(let i = 0; i < n; i++) 
            matrix[i][j] = queue.shift()
};

顺序遍历 · 队列解法运行结果

四 层次遍历 · 旋转

解题思路

  • 每旋转1格, 四格值,存14
  • 0层:0 ~ n - 1
    • 1层:1 ~ n - 1 - 1
      • ...
      • 直到第n / 2取整层:i ~ n - 1 - i

层次遍历

代码

var rotate = function(matrix) {
    let n = matrix.length
    for(let i = 0; i < n >> 1; i++)
        for(let j = i; j < n - 1 - i; j++) {
            let tmp = matrix[i][j]
            matrix[i][j] = matrix[n - 1 - j][i]
            matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]
            matrix[n - 1 - i][n - 1 -j] = matrix[j][n - 1 - i]
            matrix[j][n - 1 - i] = tmp
        }
};

层次遍历 + 旋转解法运行结果

五 翻转

解题思路

  • 垂直翻转 → 以n / 2取整为界,坐标[i, j]垂直翻转[i, n - 1 -j]
  • 对角线翻转 → 以右上到左下为界,坐标[i, j]对角线翻转[n - 1 - j, n - 1 - i]

代码

解构赋值

var rotate = function(matrix) {
    let n = matrix.length
    for(let i = 0; i < n; i++)
        for(let j = 0; j < n >> 1; j++) 
            [matrix[i][n - 1 - j], matrix[i][j]] = [matrix[i][j], matrix[i][n - 1 - j]]
    for(let i = 0; i < n - 1; i++)
        for(let j = 0; j < n - 1 - i; j++)
            [matrix[n - 1 - j][n - 1 - i], matrix[i][j]] = [matrix[i][j], matrix[n - 1 - j][n - 1 - i]]
};

翻转解构赋值解法运行结果

中间变量

var rotate = function(matrix) {
    let n = matrix.length, tmp, i, j
    for(i = 0; i < n; i++)
        for(j = 0; j < n >> 1; j++) {
            tmp = matrix[i][j]
            matrix[i][j] = matrix[i][n - 1 - j]
            matrix[i][n - 1 - j] = tmp
        }
    for(i = 0; i < n - 1; i++)
        for(j = 0; j < n - 1 - i; j++) {
            tmp = matrix[i][j]
            matrix[i][j] = matrix[n - 1 - j][n - 1 - i]
            matrix[n - 1 - j][n - 1 - i] = tmp
        }
};

翻转中间变量解法运行结果

六 螺旋遍历 · 队列

解题思路

  • 螺旋遍历矩阵,方向d 循环
  • 初始边界b[上,右,左,下] = [1, n - 1, 0, n - 1]
  • 初始位置i = 0 j = 0,遍历格数k = 0,直到k = n * n - 1所有格遍历完
    • j++ 到右边界转i++,右边界收缩-1
    • i++ 到下边界转j--,下边界收缩-1
    • j-- 到左边界转i--,左边界收缩+1
    • i-- 到上边界转j++,上边界收缩+1
  • 队列queue 放入遍历过的值
    • 留空。到右边界时,弹出赋值
    • 弹出赋值
    • 弹出赋值 到左边界,如[[1, 2], [3, 4]],判断到3已到头,填空
    • 弹出赋值 到上边界,如[[1, 2, 3],[4, 5, 6],[7, 8, 9]],到5已到头,填空

螺旋遍历解法图示

螺旋遍历解法填空图示

代码

碰撞转向

var rotate = function(matrix) {
    let n = matrix.length, queue = []
    let k = -1, i = 0, j = 0, b = [1, n - 1, 0, n - 1], d = '→'
    while(++k < n * n) {
        queue.push(matrix[i][j])
        switch(d) {
            case '→':
                if (j < b[1]) j++
                else {
                    matrix[i][j] = queue.shift()
                    i++
                    d = '↓'
                    b[1]--
                }
            break
            case '↓':
                matrix[i][j] = queue.shift()
                if (i < b[3]) i++
                else {
                    j--
                    d = '←'
                    b[3]--
                }
            break
            case '←':
                matrix[i][j] = queue.shift()
                if (j > b[2]) j--
                else {
                    if (k + 1 === n * n) {
                        for(let p = j; p <= b[1]; p++) 
                            matrix[b[0] - 1][p] = queue.shift()
                    }
                    i--
                    d = '↑'
                    b[2]++
                }
            break
            case '↑':
                matrix[i][j] = queue.shift()
                if (i > b[0]) i--
                else {
                    for(let p = j; p <= b[1]; p++) 
                        matrix[b[0] - 1][p] = queue.shift()
                    j++
                    d = '→'
                    b[0]++
                }
        }
    }
};

螺旋遍历 · 碰撞转向解法运行结果

循环

var rotate = function(matrix) {
    const n = matrix.length, queue = []
    let t = 0, r = n - 1, l = 0, b = n - 1, k = 0, i // 边界:t上 r右 l左 b下
    while(k < n * n){
        for(i = l; i <= r; i++) { // →
            k++
            queue.push(matrix[t][i])
            if (i === r) matrix[t][i] = queue.shift()
        }
        t++
        for(i = t; i <= b; i++) { // ↓
            k++
            queue.push(matrix[i][r])
            matrix[i][r] = queue.shift()
        }
        r--
        for(i = r; i >= l; i--) { // ←
            k++
            queue.push(matrix[b][i])
            matrix[b][i] = queue.shift()
            if (i === l && k === n * n) 
                while(queue.length) 
                    matrix[t - 1][l++] = queue.shift()
        }
        b--
        for(i = b; i >= t; i--) { // ↑
            k++
            queue.push(matrix[i][l])
            matrix[i][l] = queue.shift()
            if (i === t) {
                let _l = l
                while(queue.length) 
                    matrix[t - 1][_l++] = queue.shift()
            }
        }
        l++
    }
    return queue
}

螺旋遍历 · 循环解法运行结果

排行

100 * 100矩阵100组,值在安全整数范围随机,每解法求解100次,每秒操作数
100*100矩阵100组测试结果
遍历矩阵:同构数组在内存连续,水平扫描缓存命中率 > 垂直扫描。矩阵越大,性能差距越明显
深拷贝矩阵:浅拷贝其中每个数组即可。JSON适合层多function/undefined/Symbol/环对象
根据规范:解构赋值需要从被解构对象中(数组,对象)获取迭代器,迭代赋值
Safari某些版本外,FirefoxChrome都依规范实现,解构赋值速度 < 索引取值
正如使用异或交换变量,某些环境下不能真正节省内存一样,获取迭代器赋值开销 > 变量
在交换数组元素而不是变量时,性能差距更甚。所以对于JS,临时变量依然较好选择