跳至主要內容

 

labuladong原创数组约 2706 字大约 9 分钟

Info

新版网站会员open in new window 限时优惠;算法可视化编辑器上线,点击体验open in new window

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

LeetCode力扣难度
151. Reverse Words in a Stringopen in new window151. 反转字符串中的单词open in new window🟠
48. Rotate Imageopen in new window48. 旋转图像open in new window🟠
54. Spiral Matrixopen in new window54. 螺旋矩阵open in new window🟠
59. Spiral Matrix IIopen in new window59. 螺旋矩阵 IIopen in new window🟠
61. Rotate Listopen in new window61. 旋转链表open in new window🟠
-剑指 Offer 29. 顺时针打印矩阵open in new window🟢
-剑指 Offer 58 - I. 翻转单词顺序open in new window🟢

有不少读者说,看过很多公众号历史文章之后,掌握了框架思维,可以解决大部分有套路框架可循的题目。

但是框架思维也不是万能的,有一些特定技巧呢,属于会者不难,难者不会的类型,只能通过多刷题进行总结和积累。

那么本文我分享一些巧妙的二维数组的花式操作,你只要有个印象,以后遇到类似题目就不会懵圈了。

顺/逆时针旋转矩阵

对二维数组进行旋转是常见的笔试题,力扣第 48 题「旋转图像open in new window」就是很经典的一道:

48. 旋转图像 | 力扣 open in new window | LeetCode open in new window |

给定一个 × 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]]

提示:

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

题目很好理解,就是让你将一个二维矩阵顺时针旋转 90 度,难点在于要「原地」修改,函数签名如下:

java 🟢
void rotate(int[][] matrix)

如何「原地」旋转二维矩阵?稍想一下,感觉操作起来非常复杂,可能要设置巧妙的算法机制来「一圈一圈」旋转矩阵:

但实际上,这道题不能走寻常路,在讲巧妙解法之前,我们先看另一道谷歌曾经考过的算法题热热身:

给你一个包含若干单词和空格的字符串 s,请你写一个算法,原地反转所有单词的顺序。

比如说,给你输入这样一个字符串:

s = "hello world labuladong"

你的算法需要原地反转这个字符串中的单词顺序:

s = "labuladong world hello"

常规的方式是把 s 按空格 split 成若干单词,然后 reverse 这些单词的顺序,最后把这些单词 join 成句子。但这种方式使用了额外的空间,并不是「原地反转」单词。

正确的做法是,先将整个字符串 s 反转

s = "gnodalubal dlrow olleh"

然后将每个单词分别反转

s = "labuladong world hello"

这样,就实现了原地反转所有单词顺序的目的。力扣第 151 题「颠倒字符串中的单词open in new window」就是类似的问题,你可以顺便去做一下。

上面这个小技巧还可以再包装包装,比如说你可以去看一下力扣第 61 题「旋转链表open in new window」:给你一个单链表,让你旋转链表,将链表每个节点向右移动 k 个位置。

比如说输入单链表 1 -> 2 -> 3 -> 4 -> 5k = 2,你的算法需要返回 4 -> 5 -> 1 -> 2 -> 3,即将链表每个节点向右移动 2 个位置。

这个题,不要真傻乎乎地一个一个去移动链表节点,我给你翻译翻译,其实就是将链表的后 k 个节点移动到链表的头部嘛,反应过来没有?

还没反应过来,那再提示一下,把后 k 个节点移动到链表的头部,其实就是让你把链表的前 n - k 个节点和后 k 个节点原地翻转,对不对?

这样,是不是和前面说的原地翻转字符串中的单词是一样的道理呢?你只需要先将整个链表反转,然后将前 n - k 个节点和后 k 个节点分别反转,就得到了结果。

当然,这个题有一些小细节,比如这个 k 可能大于链表的长度,那么你需要先求出链表的长度 n,然后取模 k = k % n,这样 k 就不会大于链表的长度,且最后得到的结果也是正确的。

有时间的话自己去做一下这个题吧,比较简单,我这里就不贴代码了。

我讲上面这两道题的目的是什么呢?

旨在说明,有时候咱们拍脑袋的常规思维,在计算机看来可能并不是最优雅的;但是计算机觉得最优雅的思维,对咱们来说却不那么直观。也许这就是算法的魅力所在吧。

回到之前说的顺时针旋转二维矩阵的问题,常规的思路就是去寻找原始坐标和旋转后坐标的映射规律,但我们是否可以让思维跳跃跳跃,尝试把矩阵进行反转、镜像对称等操作,可能会出现新的突破口。

我们可以先将 n x n 矩阵 matrix 按照左上到右下的对角线进行镜像对称

然后再对矩阵的每一行进行反转

发现结果就是 matrix 顺时针旋转 90 度的结果

将上述思路翻译成代码,即可解决本题:

java 🟢
// 将二维矩阵原地顺时针旋转 90 度
public void rotate(int[][] matrix) {
    int n = matrix.length;
    // 先沿对角线镜像对称二维矩阵
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            // swap(matrix[i][j], matrix[j][i]);
            int temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }
    // 然后反转二维矩阵的每一行
    for (int[] row : matrix) {
        reverse(row);
    }
}

// 反转一维数组
void reverse(int[] arr) {
    int i = 0, j = arr.length - 1;
    while (j > i) {
        // swap(arr[i], arr[j]);
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
        i++;
        j--;
    }
}

肯定有读者会问,如果没有做过这道题,怎么可能想到这种思路呢?

是的,没做过这类题目,确实不好想到这种思路,但你这不是做过了么?所谓会者不难难者不会,你这辈子估计都忘不掉了。

既然说道这里,我们可以发散一下,如何将矩阵逆时针旋转 90 度呢

思路是类似的,只要通过另一条对角线镜像对称矩阵,然后再反转每一行,就得到了逆时针旋转矩阵的结果:

翻译成代码如下:

java 🟢
// 将二维矩阵原地逆时针旋转 90 度
void rotate2(int[][] matrix) {
    int n = matrix.length;
    // 沿左下到右上的对角线镜像对称二维矩阵
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i; j++) {
            // swap(matrix[i][j], matrix[n-j-1][n-i-1])
            int temp = matrix[i][j];
            matrix[i][j] = matrix[n - j - 1][n - i - 1];
            matrix[n - j - 1][n - i - 1] = temp;
        }
    }
    // 然后反转二维矩阵的每一行
    for (int[] row : matrix) {
        reverse(row);
    }
}

void reverse(int[] arr) { /* 见上文 */}

至此,旋转矩阵的问题就解决了。

矩阵的螺旋遍历

我的公众号动态规划系列文章经常需要遍历二维 dp 数组,但难点在于状态转移方程而不是数组的遍历,顶多就是倒序遍历。

但接下来我们讲一下力扣第 54 题「螺旋矩阵open in new window」,看一看二维矩阵可以如何花式遍历:

54. 螺旋矩阵 | 力扣 open in new window | LeetCode open in new window |

给你一个 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

函数签名如下:

java 🟢
List<Integer> spiralOrder(int[][] matrix)

解题的核心思路是按照右、下、左、上的顺序遍历数组,并使用四个变量圈定未遍历元素的边界

随着螺旋遍历,相应的边界会收缩,直到螺旋遍历完整个数组:

只要有了这个思路,翻译出代码就很容易了:

java 🟢
List<Integer> spiralOrder(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    int upper_bound = 0, lower_bound = m - 1;
    int left_bound = 0, right_bound = n - 1;
    List<Integer> res = new LinkedList<>();
    // res.size() == m * n 则遍历完整个数组
    while (res.size() < m * n) {
        if (upper_bound <= lower_bound) {
            // 在顶部从左向右遍历
            for (int j = left_bound; j <= right_bound; j++) {
                res.add(matrix[upper_bound][j]);
            }
            // 上边界下移
            upper_bound++;
        }
        
        if (left_bound <= right_bound) {
            // 在右侧从上向下遍历
            for (int i = upper_bound; i <= lower_bound; i++) {
                res.add(matrix[i][right_bound]);
            }
            // 右边界左移
            right_bound--;
        }
        
        if (upper_bound <= lower_bound) {
            // 在底部从右向左遍历
            for (int j = right_bound; j >= left_bound; j--) {
                res.add(matrix[lower_bound][j]);
            }
            // 下边界上移
            lower_bound--;
        }
        
        if (left_bound <= right_bound) {
            // 在左侧从下向上遍历
            for (int i = lower_bound; i >= upper_bound; i--) {
                res.add(matrix[i][left_bound]);
            }
            // 左边界右移
            left_bound++;
        }
    }
    return res;
}

力扣第 59 题「螺旋矩阵 IIopen in new window」也是类似的题目,只不过是反过来,让你按照螺旋的顺序生成矩阵:

59. 螺旋矩阵 II | 力扣 open in new window | LeetCode open in new window |

给你一个正整数 n ,生成一个包含 1n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

示例 1:

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

示例 2:

输入:n = 1
输出:[[1]]

提示:

  • 1 <= n <= 20

函数签名如下:

java 🟢
int[][] generateMatrix(int n)

有了上面的铺垫,稍微改一下代码即可完成这道题:

java 🟢
int[][] generateMatrix(int n) {
    int[][] matrix = new int[n][n];
    int upper_bound = 0, lower_bound = n - 1;
    int left_bound = 0, right_bound = n - 1;
    // 需要填入矩阵的数字
    int num = 1;
    
    while (num <= n * n) {
        if (upper_bound <= lower_bound) {
            // 在顶部从左向右遍历
            for (int j = left_bound; j <= right_bound; j++) {
                matrix[upper_bound][j] = num++;
            }
            // 上边界下移
            upper_bound++;
        }
        
        if (left_bound <= right_bound) {
            // 在右侧从上向下遍历
            for (int i = upper_bound; i <= lower_bound; i++) {
                matrix[i][right_bound] = num++;
            }
            // 右边界左移
            right_bound--;
        }
        
        if (upper_bound <= lower_bound) {
            // 在底部从右向左遍历
            for (int j = right_bound; j >= left_bound; j--) {
                matrix[lower_bound][j] = num++;
            }
            // 下边界上移
            lower_bound--;
        }
        
        if (left_bound <= right_bound) {
            // 在左侧从下向上遍历
            for (int i = lower_bound; i >= upper_bound; i--) {
                matrix[i][left_bound] = num++;
            }
            // 左边界右移
            left_bound++;
        }
    }
    return matrix;
}

至此,两道螺旋矩阵的题目也解决了。

以上就是遍历二维数组的一些技巧,其他数组技巧可参见之前的文章 前缀和数组差分数组数组双指针算法集合,链表相关技巧可参见 单链表六大算法技巧汇总


引用本文的文章


引用本文的题目

安装 我的 Chrome 刷题插件open in new window 点开下列题目可直接查看解题思路:

LeetCode力扣
1260. Shift 2D Gridopen in new window1260. 二维网格迁移open in new window
1329. Sort the Matrix Diagonallyopen in new window1329. 将矩阵按对角线排序open in new window
867. Transpose Matrixopen in new window867. 转置矩阵open in new window
-剑指 Offer 58 - I. 翻转单词顺序open in new window

《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「全家桶」可下载配套 PDF 和刷题全家桶

上次编辑于: