题目
将一个给定字符串 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
算法一:初始化矩阵
class Solution {
public:
string convert(string s, int numRows) {
int n = s.size(), r= numRows;
if(r == n || r == 1){
return s;
}
int t = r + r-2; //一个周期
int k = (n+t-1) / t; //周期个数,向上取整
//每个周期占用1+r-2 = r-1 列
int c = k * (r-1); //矩阵列数
vector<string> mat(r, string(c, 0));
for(int i = 0,x = 0,y = 0; i < n; i++){
mat[x][y] = s[i];
if(i%t < r - 1){
x++;
}
else{
x--;
y++;
}
}
string ans;
for(string &row : mat){
for(char ch : row){
if(ch){
ans += ch;
}
}
}
return ans;
}
};
空间复杂度:O(r⋅n)。矩阵需要 O(r⋅n) 的空间。
几个点提一下:
一个是周期个数k的取整方式,这里用到的是常见的取整方式,因为在C++中,int类型是向下取整,所以说只要给他的分子加上周期减一也就是t-1,(n+t-1) / t
。
另外一点是vector<string> mat(r, string(c, 0));
构造了一个行数为r的矩阵,每一行都为长度为c的字符串,其中每个字符初始化为0。
第三点是 for(string &row : mat)
中row前面使用&, row 是对 mat 中每个字符串的引用。使用 & 确保我们直接操作 mat 中的字符串,而不是创建它们的副本。这样做可以节省内存和提高性能。
这样做有一个问题,就是要消耗大量内存用于矩阵的创建和遍历。那是否可以节省掉矩阵空白的浪费部分,减少内存消耗?实际上会发现,在最后遍历矩阵得到ans的过程中,if(ch){ ans += ch; }
有这么一个语句,说明遇到空白部分就会跳过。或许在创建矩阵的时候可以不需要创建这些不被使用的部分。算法二将会对其进行优化。
算法二:矩阵压缩
class Solution {
public:
string convert(string s, int numRows) {
int n = s.size(), r= numRows;
if(r == n || r == 1){
return s;
}
int t = r + r-2; //一个周期
int k = (n+t-1) / t; //周期个数,向上取整
//每个周期占用1+r-2 = r-1 列
vector<string> mat(r);
for(int i = 0,x = 0; i < n; i++){
mat[x] += s[i];
if(i%t < r - 1){
x++;
}
else{
x--;
}
}
string ans;
for(string &row : mat){
for(char ch : row){
ans += ch;
}
}
return ans;
}
};
时间复杂度:O(n)。
空间复杂度:O(n)。
这段代码初始化后的矩阵,每一行都是空列表,不会对他进行赋值一个字符串。并且省去了对列序号y的操作,不管在哪一列,只要保证在哪一行加上对应的字符即可。
方法三:直接构造Z字形
class Solution {
public:
string convert(string s, int numRows) {
int n = s.size(), r= numRows;
if(r == n || r == 1){
return s;
}
int t = r + r-2; //一个周期
string ans;
for(int i = 0;i< r;i++){
for(int j = 0; j+i<n; j+=t){
ans += s[j+i];
if(0 < i && i < r-1 && j+t-i<n){
ans += s[j + t - i];
}
}
}
return ans;
}
};
时间复杂度:O(n),其中 n 为字符串 s 的长度。s 中的每个字符仅会被访问一次,因此时间复杂度为 O(n)。
空间复杂度:O(1)。返回值不计入空间复杂度。
这个算法的核心是弄明白字符串每个字符所对应的下标和行数以及周期之间的关系。
在这里感谢作者hy。,给出的例子。
0 0+t 0+2t 0+3t
1 t-1 1+t 0+2t-1 1+2t 0+3t-1 1+3t
2 t-2 2+t 0+2t-2 2+2t 0+3t-2 2+3t
3 3+t 3+2t 3+3t
通过观察,在一个周期中,除了头尾两行,在中间行都会有两个字符,头尾两行是一个字符。
在每一个周期中,第一列都可以表示为【行号+周期】,而剩下的列可以表示为【周期+1 - 行号】。
for(int i = 0;i< r;i++){
for(int j = 0; j+i<n; j+=t){
ans += s[j+i];
if(0 < i && i < r-1 && j+t-i<n){
ans += s[j + t - i];
}
}
}
举个例子,i=1时,即第二行中,依次插入
第一个周期: s[0+1] ,s[0 + t - 1]
第二个周期:s[t + 1] ,s[t + t - 1]
第三个周期: s[2t + 1],s[2t + t -1]