前言
题目的相似性 ,完全是笔者的个人理解 。在刷题的过程中 , 体会到的一些解法上的相似性 , 然而题目一般都是一题多解的 , 笔者更多的是抽取其中一种解法 , 然后发散性的去联系和挖掘 , 难免有错误之处 🤡。
题目 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 <= 1000s由英文字母(小写和大写)、','和'.'组成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
给定一个正整数 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;
};