问题描述
将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 “LEETCODEISHIRING” 行数为 3 时,排列如下:
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:“LCIRETOESIIGEDHN”。
请你实现这个将字符串进行指定行数变换的函数:
示例 1:
示例 2:
解决思路
(1)本题和螺旋矩阵有点相似,都是按照给定的路径从原来的数组或者字符串获得输出顺序,所以必须明白输出的顺序是怎么来的,这就要看路径是怎么走得了
(2)首先形成的这个Z字行数是固定的,不过列数是受行数影响的。我们要看看这个Z字是怎么形成的(最好按照原来的字符串的顺序看,容易发现规律),以字符串"LEETCODEISHIRING"为例,从第一个字母开始看怎么形成Z字的
(3)形成的Z字我们以一个二维矩阵Z[i][j]表示,先找到第一个字符L,在Z[0][0]处,第二个字符为E,(顺着刚刚的L找)位置为Z[0][1],第三个字符为E,位置为Z[0][2],(可以发现初步的规律,好像原来字符的顺序在形成Z时一开始是下降排的),再往后看,第四个字符T方向变化了(应该注意到为什么会变化,因为已经下降到了最大的行位置),开始往右上走了(因为要形成Z字,必须要有一个斜方向),再看第五个字符C,该字符是继续往右上的方向走的,不过已经到达最上方了,下一步就要换方向了,而这个换的过程又回到了上面,可以自行试一试
(4)第三步我们其实已经发现了这个Z字变换的规律,总结一下几点:
- Z字中任何字符的‘前’一个字符,‘后’一个字符和在原字符串中是一样的,即Z字的形成是按照原字符的顺序按照一定的规律来进行的
- Z字的形成的方向是:先下降再右上升,一直这样循环,直到原字符串已经用完
暴力思想解法
经过以上的分析,其实已经给出了暴力思想的解法,因为Z字的形成过程是分为两个阶段循环进行的,所以就可以用类似螺旋矩阵的方法来暴力,把两个过程放在一个死循环中,退出条件是原字符串已经没有可用的字符再形成Z了的后面部分了。具体过程如下:
- 需要创建一个二维矩阵,行数为给定的行数,列数设为最大,为字符串的长度
- 设置一个while(1)死循环,还需要设置三个指针i,k,p分别指向表示原字符串的字符(遍历),Z字形成的行的变化,Z字形成的列的变化,注意边界条件
- 遍历二维数组,获得输出序列
class Solution {
public:
string convert(string s, int numRows) {
if(s.empty() || numRows==1) return s;
int len = s.size(); //暴力法
vector<vector<char>> v(numRows,vector<char>(len,' '));
string str;
int i = 0;
int k = 0;
int p = 0;
while(1){
for(;k<numRows;k++){
v[k][p] = s[i++];
if(i==len) break;
}
k -= 2;
if(k<0 || i==len) break;
p++;
for(;k>=0;k--,p++){
v[k][p] = s[i++];
if(i==len) break;
}
k += 2;
if(k>numRows-1 || i==len) break;
}
for(int i=0;i<numRows;i++)
for(int j=0;j<len;j++){
if(v[i][j]!=' ')
str.push_back(v[i][j]);
}
return str;
}
};
优化解法
暴力解法的空间复杂度和时间复杂度为o(numRows * n),虽然也能通过leetcode的时间限制,但还是一个不让人很满意的方法,这里其实有很多优化的方法,读者可以自行去查找,在这里主要说下本人的思路。
由于最终的要求是让我们输出Z字形成后的每一行,顺序是第一行,第二行,第三行…,所以暴力方法创建了二维数组,但这里能不能直接在原来的字符串中找到某一行字符的所有位置呢,答案是可以的。
我们仍然以字符串"LEETCODEISHIRING"为例,我们看到Z字的第一行输出顺序为L(0)C(4)I(8)R(12),括号内是字符的位置,显然,第一行中每个字符后面的字符的位置是该字符的位置加4。再看第二行输出序列E(1)T(3) O(5) E(7)S(9)I(11)I(13) G(15),可以发现,第二行中每两个相邻的字符的间隔虽然不是4了(因为行数有变化),但是仍然是固定的,这说明了每一行的字符之间是有固定的位置间隔的,找到这个规律就可以直接输出每一行的序列。
下面分析下这个Z字形中第i行里面相邻字符的位置间隔:
第一种情况:先下降再上升
从图中可以看到,这种先下降再上升的情况下同一行两个点间隔为2*(r-i)-1,如果第一个红点在原字符串的位置是 j,则第二个红点位置是 j + 2*(r-i)
第二种情况:先上升后下降
从图中可以看到,这种先上升后下降的情况下同一行两个点间隔为2r-1,如果第二个红点在原字符串的位置是 j,则第三个红点位置是 j+2r
综上:可以得到优化的方法,除了储存输出序列外,空间复杂度为常数空间,如下
- 主循环是每一列,在主循环中,需要计算出该行相邻字符的间隔,分为两种路径:先下降再上升,先上升再下降
- 主循环下判断下是否是第一行和最后一行,这两行只有一种路径,第一行只有先下降再上升,最后一行只有先上升再下降,其他行都有两种,需要设置计数器来判断是哪一种,通过计数器的奇偶性即可判断
class Solution {
public:
string convert(string s, int numRows) {
if(s.empty() || numRows==1) return s;
int len = s.size()-1; //解法2,直接求解每一行每一列位置
string str;
for(int i=0;i<numRows;i++){
int start = i;
int down_up = 2*(numRows-1-i); //先下降再上升的间隔
int up_down = 2*i; //先上升再下降的间隔
if(i==0 || i==numRows-1){
int step = i==0?down_up:up_down;//判断第一行和最后一行
while(start<=len){
str.push_back(s[start]);
start += step;
}
}
else{
int count = 0;
while(start<=len){//需设置计数器来判断是哪种路径
str.push_back(s[start]);
count++;
if(count%2==1) start += down_up;
else start += up_down;
}
}
}
return str;
}
};