LeetCode【刷题日记】:螺旋矩阵逆向全过程,边界缩进优化

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:今天是数组的最后一章节,最近还在整理苍穹外卖,前面我们刚学完关于滑动窗口的方法,这一章节我们学习螺旋矩阵,这是面试中常见的题目,主要考察的不是思维,而是我们能否正确顺畅的手撕出来,是一个中等题。

题目背景:LeetCode 59

给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

示例:

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

题目理解:

看到这个题目,我们第一眼可能不知道题目要干什么,但是仔细地看一眼,我们就能理解了,根据我们输入的正整数n,会生成一个正方形的矩阵,如果所示:

而且还是按照顺时针方向的排列方向,思路其实很简单,但是在具体操作的时候往往会让我们纠结或者搞混,就和二分法一样,难的不是思路,而是我们能否清晰的把这道题写出来,而不是漏掉一些,或者写错一些地方。下面我们会讲解具体的方法

题目答案:

java 复制代码
class Solution {
    public int[][] generateMatrix(int n) {
        int[][] nums = new int[n][n];
        int startX = 0, startY = 0;  // 每一圈的起始点
        int offset = 1;
        int count = 1;  // 矩阵中需要填写的数字
        int loop = 1; // 记录当前的圈数
        int i, j; // j 代表列, i 代表行;

        while (loop <= n / 2) {

            // 顶部
            // 左闭右开,所以判断循环结束时, j 不能等于 n - offset
            for (j = startY; j < n - offset; j++) {
                nums[startX][j] = count++;
            }

            // 右列
            // 左闭右开,所以判断循环结束时, i 不能等于 n - offset
            for (i = startX; i < n - offset; i++) {
                nums[i][j] = count++;
            }

            // 底部
            // 左闭右开,所以判断循环结束时, j != startY
            for (; j > startY; j--) {
                nums[i][j] = count++;
            }

            // 左列
            // 左闭右开,所以判断循环结束时, i != startX
            for (; i > startX; i--) {
                nums[i][j] = count++;
            }
            startX++;
            startY++;
            offset++;
            loop++;
        }
        if (n % 2 == 1) { // n 为奇数时,单独处理矩阵中心的值
            nums[startX][startY] = count;
        }
        return nums;
    }
}

题目讲解:

在写这道题的时候,我们要遵循一个选择,就是循环不变量原则,跟写二分法一样,我们要根据一个固定的原则来写循环,在二分法中,我们是事先固定了区间范围,之后再写内部逻辑

在这里,我们要先固定每次循环的起始位置,这里我们固定的是从起始开始,而不遍历每一行的最后一个元素,这样执行一圈之后,正好全部遍历到,而不是一会遍历一会不遍历的,类似于数学中的控制变量法,因为循环本来就是一个动态的过程,如果我们连条件都没固定,那岂不是很容易混乱。

底层逻辑已经讲完了,下面我们来说一下代码的实现逻辑:

顺时针排列,那起始位置就是原点,我们定义起始位置startX,startY代表原点,和一个二维数组,代表i,j轴,这个位置就是记录添加到哪个位置 的,我们还定义了loop,是代表要循环几圈

如果是奇数圈,那么最后一圈就不满足我们while的条件,就执行到了if,最后一圈就是内部的一个点,直接把count添加到里面即可。

那我们在核心循环的代码,实现思路:我们一开始是从j轴开始的,就是最顶部的一行,循环的截止条件是n-offset,代表不去遍历最后一个位置,然后就是第二次for循环,我们是遍历最右侧,j轴不变,i轴加加,不遍历最后一个,把遍历到的每个位置,count的值都赋给当前的位置,count++,第三次for循环的时候,注意条件,条件是i轴不变,j轴--,而此时j的值就是n,已经在最底部的位置了,因此一直--,直到等于0时截止。下面的也同理。

当循环完这一圈之后,我们进行的一系列++操作,首先就是对原点位置的向内收缩,都是加一,偏移量offset自然也就是加1(因为内圈的行列数都少了一格),圈数加1,以此类推。

下面我们来讲解这道题相反过程的:

题目背景:LeetCode 54

给你一个 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 复制代码
class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> result = new ArrayList<>();
        
        if (matrix == null || matrix.length == 0) {
            return result;
        }
        
        int m = matrix.length;      // 行数
        int n = matrix[0].length;   // 列数
        int startX = 0, startY = 0; // 起始位置
        int offset = 1;              // 偏移量
        int loop = 1;               // 当前圈数
        int i, j;
        
        // 需要遍历多少圈:取行和列的最小值除以2
        int circles = Math.min(m, n) / 2;
        
        while (loop <= circles) {
            // 顶部:从左到右
            for (j = startY; j < n - offset; j++) {
                result.add(matrix[startX][j]);
            }
            
            // 右列:从上到下
            for (i = startX; i < m - offset; i++) {
                result.add(matrix[i][j]);
            }
            
            // 底部:从右到左
            for (; j > startY; j--) {
                result.add(matrix[i][j]);
            }
            
            // 左列:从下到上
            for (; i > startX; i--) {
                result.add(matrix[i][j]);
            }
            
            startX++;
            startY++;
            offset++;
            loop++;
        }
        
        // 处理中间剩余的行或列
        if (Math.min(m, n) % 2 == 1) {
            if (m <= n) {  // 行数少,剩余一行
                for (j = startY; j <= n - offset; j++) {
                    result.add(matrix[startX][j]);
                }
            } else {       // 列数少,剩余一列
                for (i = startX; i <= m - offset; i++) {
                    result.add(matrix[i][startY]);
                }
            }
        }
        
        return result;
    }
}

题目解析:

底层逻辑是一样的,需要注意的细节是,我们第一提到的是正方形矩阵,但这道题目并没有规定,而是,矩形,那么这就默认了行和列并不是完全相等的,因此我们需要单独注意这一部分,首先被牵扯到的就是循环的圈数了,我们需要从行和列中找一个较小值来计算圈数,而不是像第一那样不用考虑,其次就是内圈的处理问题,在结束了外层圈的循环,在最后一圈的时候,我们就需要判断到底是行少还是列少,因为这决定了我们执行最后一次循环的方向,到底是按i方向,还是j方向,这里的循环条件为什么是<=n-offset,因为 我们的startX,或则startY,都已经自增,向内缩进了,我们要遍历完这行或者列,不好解释,如图吧:

完整过程

复制代码
原始矩阵:
[1,  2,  3,  4]
[5, 6, 7,  8]
[9, 10, 11, 12]
第1圈遍历后(最外层):
已遍历:1,2,3,4,8,12,11,10,9,5
剩余:
[6, 7]  ← 这是1行2列
此时:
startX = 1
startY = 1
m - offset = 3 - 2 = 1
n - offset = 4 - 2 = 2
剩余的是第1行,从第1列到第2列:
for (j = startY; j <= n - offset; j++) {
    // j从1到2
    result.add(matrix[1][1]) = 6
    result.add(matrix[1][2]) = 7
}

图解

复制代码
第1圈后:
    [1, 2, 3, 4]
    [5, ← 中心区域 → 8]
    [9,10,11,12]
    
中心区域(剩余一行):
    startX=1
    [6, 7]  ← 一行
    startY=1 到 n-offset=2

这样看下来,当我们执行完外层的圈数时,这时还剩下一行6,7没有添加,原点位置被缩进了,其实就执行了索引1,和2,刚好添加完。

第二种优化解法:

java 复制代码
class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> result = new ArrayList<>();
        
        if (matrix == null || matrix.length == 0) {
            return result;
        }
        
        int top = 0;
        int bottom = matrix.length - 1;
        int left = 0;
        int right = matrix[0].length - 1;
        
        while (top <= bottom && left <= right) {
            // 1. 从左到右遍历上边
            for (int j = left; j <= right; j++) {
                result.add(matrix[top][j]);
            }
            top++; // 上边界下移
            
            // 2. 从上到下遍历右边
            for (int i = top; i <= bottom; i++) {
                result.add(matrix[i][right]);
            }
            right--; // 右边界左移
            
            // 3. 从右到左遍历下边(需要检查是否还有行)
            if (top <= bottom) {
                for (int j = right; j >= left; j--) {
                    result.add(matrix[bottom][j]);
                }
                bottom--; // 下边界上移
            }
            
            // 4. 从下到上遍历左边(需要检查是否还有列)
            if (left <= right) {
                for (int i = bottom; i >= top; i--) {
                    result.add(matrix[i][left]);
                }
                left++; // 左边界右移
            }
        }
        
        return result;
    }
}

题目解析:

这种解法的核心逻辑就是实时更新边界,而不用手动的去处理最后一圈,这种解法用来解决我们正向59题也是同样优雅的,避免了offset的使用

首先就是变量的定义:

复制代码
int botton = matrix.length;        // 有多少行 = 3
int right = matrix[0].length;     // 第一行有多少列 = 3

就是记录二维数组的上下左右的边界,while循环的判断条件,这个条件是边界检查,确保在遍历过程中不会越界,并且能正确处理各种形状的矩阵(正方形、长方形、单行、单列)。直到执行完之后,才自动跳过。因为这个遍历的过程就是边界的收缩过程,知道最后重合到一个点,或者是一行,一列时,才结束while。

下面的for循环大差不大。

结语:

如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!

相关推荐
ALex_zry2 小时前
物联网数据质量控制系统设计:质控算法与实现
物联网·算法·struts
EQUINOX12 小时前
货物运输问题,前缀和优化dp,[牛客周赛137 F-小苯的糖果盒]
算法·动态规划
小此方2 小时前
Re:从零开始的 C++ STL篇(九)AVL树太“较真”,红黑树更“现实”:一文讲透工程中的平衡之道
开发语言·数据结构·c++·算法·stl
地平线开发者2 小时前
多 Batch 量化校准与单 Batch 校准的数值差异
算法·自动驾驶
少许极端2 小时前
算法奇妙屋(三十八)-贪心算法学习之路 5
java·学习·算法·贪心算法
im_AMBER2 小时前
Leetcode 150 最小路径和 | 最长回文子串
数据结构·算法·leetcode
模拟器连接器曾工2 小时前
AI视觉检测设备参数有哪些?从硬件到算法的全面解析
人工智能·算法·视觉检测·ai视觉·ai视觉检测
量子物理学2 小时前
Open CV 边缘检测算法:Canny、Sobel、Scharr与Laplacian对比解析
人工智能·算法·计算机视觉
.柒宇.2 小时前
力扣hot 100之和为 K 的子数组(Java版)
java·算法·leetcode