横向归一: 观K神,Carl神题解有感---leetcode 6.Z字形变换 & 螺旋矩阵II

125 阅读6分钟

前言

题目的相似性 ,完全是笔者的个人理解 。在刷题的过程中 , 体会到的一些解法上的相似性 , 然而题目一般都是一题多解的 , 笔者更多的是抽取其中一种解法 , 然后发散性的去联系和挖掘 , 难免有错误之处 🤡。

题目 Z 字形变换

6. Z 字形变换

将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

P   A   H   N
A P L S I I G
Y   I   R

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);

示例 1:

输入:s = "PAYPALISHIRING", numRows = 3
输出:"PAHNAPLSIIGYIR"

示例 2:

输入:s = "PAYPALISHIRING", numRows = 4
输出:"PINALSIGYAHRPI"
解释:
P     I    N
A   L S  I G
Y A   H R
P     I

示例 3:

输入:s = "A", numRows = 1
输出:"A"

提示:

  • 1 <= s.length <= 1000
  • s 由英文字母(小写和大写)、',''.' 组成
  • 1 <= numRows <= 1000

题目理解:

仿 K 神代码如下 : (思路见:解法相似性)

class Solution {
public:
    string convert(string s, int numRows) {
        if (numRows == 1) return s;
        vector<string> rows(numRows);
        // 行转向标志,极妙
        int flag = 1;  
        // 行下标索引
        int idxRows = 0;   
        //循环不变量
        for (int i = 0; i < s.size(); i++) {
            rows[idxRows].push_back(s[i]);
            // 更新行下标
            idxRows += flag;  
            if (idxRows == numRows - 1 || idxRows == 0) {
                // 转向,上——>下 | 下——>上
                flag = -flag;
            }
        }
        string res;
        for (auto row : rows) {
            // 拿到答案
            res += row;
        }
        return res;
    }
};

题目 螺旋矩阵 II

螺旋矩阵 II

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

示例:

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

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
        int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
        int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
        int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
        int count = 1; // 用来给矩阵中每一个空格赋值
        int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
        int i,j;
        while (loop --) {
            i = startx;
            j = starty;

            // 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (j; j < n - offset; j++) {
                res[i][j] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (i; i < n - offset; i++) {
                res[i][j] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (; j > starty; j--) {
                res[i][j] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)
            for (; i > startx; i--) {
                res[i][j] = count++;
            }

            // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
            startx++;
            starty++;

            // offset 控制每一圈里每一条边遍历的长度
            offset += 1;
        }

        // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n % 2) {
            res[mid][mid] = count;
        }
        return res;
    }
};

思路细节参考 Carl 大神 :www.programmercarl.com/0059.%E8%9E…

分析相似性

二刷的倔友可能更容易理解 , 一刷的倔友可以先去刷刷这两道题目 ,之后再来看看我分析的相似性 ,也许和笔者产生同样的理解🤡 , 也许你有自己更好的想法 🤡

题目相似性

给你一个矩阵(矩阵可能是显式的 , 也可能是样隐式的)

  • 螺旋矩阵 II 是显式的矩阵
  • Z 字形 转换 是隐式的矩阵 (因为 Z 字形遍历后 ,"Z"型的字符串可以看成在矩阵中 , 解法其实也是抽象成矩阵 ,写出来的)

然后该矩阵不按"规矩"

"规矩" : 矩阵每个位置都有元素 , 遍历顺序一般是一行一行 或者 一列一列 , 没有"花式"遍历

解法相似性

  • 循环不变量
    • 螺旋矩阵是行列不断变换 ,存在一种周期 , 我们就是要使用 for , if else 等一些逻辑去捕捉这周期性的变化 ,封印在小小的代码片段中
    • Z 字形变换 是“Z”字不断循环 , 所以也存在周期 , 同样使用基本的逻辑去捕捉 ,抓住一段重复的逻辑 ,并且可以统一后面的所有做法 ,那我们就循环这段逻辑!
  • 使用一个或多个变量来控制矩阵"转折点" (比如遍历完矩阵一列后,怎么进行下一个花式遍历, 这可能需要一个专门的变量来控制)
    • 螺旋矩阵 II 顺时针旋转,一直旋转到最中间的元素 , 这个过程就是矩阵由大到小的过程, 比如图中的 1->2 ,每次迭代的过程中 , 矩阵不断向内缩一层 , 使得遍历的起始位置变化 , 需要遍历的长度(行和列)也在变化 , 所以为了控制这个"转折点" 带来的变化,我们需要使用多个变量来, 比如 Carl 代码中 ,使用 stratx 和 starty 表示所有矩阵的起始点 , 又用 offset 来表示所有矩阵的边长 , 在每一次矩阵内缩的时候 , 更新这三个变量就行 ,于是达到遍历逻辑不变 , 只是遍历的起始和长度变了 。
    • Z 字形变换 也是这样的 (看懂上面代码可以更好理解) , 看下图 , 题目要求的是 , Z 型变换后 , 输出字符串 : 即"PAYPALISHIRING" -> PAHNAPLSIIGYIR

思路是这样的 : 遍历原字符串 , 在遍历的过程中 , 用一个数组来收集 ,

比如遍历 PAYPALISHIRING 这个字符串 , 在经过"Z 字形变换后" , 我希望能够收集到这样一个数组 ["PAHN" , "APLSIIG","YIR"] ,如此我就可以通过拼接直接得到正确答案 。

那么根据上图可以发现 ,Z 字形路径,遍历的顺序 ,不断地变化 ,有可能是从上到下 ,也有可能是从下到上的 。

那么这个转折点怎么控制呢 ?

其实想法和上面一致 , 需要一个变量来控制 ,代码中采用 flag

  //循环不变量
        for (int i = 0; i < s.size(); i++) {
            rows[idxRows].push_back(s[i]);
            // 更新行下标
            idxRows += flag;  
            if (idxRows == numRows - 1 || idxRows == 0) {
                // 转向,上——>下 | 下——>上
                flag = -flag;
            }
        }
  • flag 变量作为行转向的标志,初始化为 1。它用于控制在填充字符到各行时,行索引 idxRows 的变化方向。当 flag 为 1 时,表示行索引要递增(向下移动行);当 flag 为 -1 时,表示行索引要递减(向上移动行)。
  • idxRows 是行下标索引,用于指定当前要将字符添加到 rows 中的哪一行,初始化为 0,表示从第一行开始。

总结

循环不变量 和 巧妙使用变量控制转折点的思想很重要

代码附录

python 代码

class Solution:
    def convert(self, s: str, numRows: int) -> str:
        if numRows == 1:
            return s
        rows = [""] * numRows
        flag = 1
        idxRows = 0
        for c in s:
            rows[idxRows]+=c
            idxRows+=flag
            if idxRows == numRows-1 or idxRows==0 :
                flag = -flag
        res = ""
        for i  in rows : 
            res+=i
        return res

javaScript代码

/**
 * @param {string} s
 * @param {number} numRows
 * @return {string}
 */
var convert = function(s, numRows) {
    if(numRows === 1)return s;
    let rows = new Array(numRows).fill("");
    let flag = 1;
    let idx = 0;
    for(let i=0; i< s.length;i++){
        rows[idx]+=s[i];
        idx+=flag;
        //注意是绝对相等 !!!
        if(idx === numRows-1 || idx === 0){
            flag = -flag ;
        }
    }

    let res = "" ;
    for(let row of rows){
        res+=row;
    }

    return res;
};