第六题
题目要求
将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 "LEETCODEISHIRING"
行数为 3 时,排列如下:
L C I R
E T O E S I I G
E D H N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"
。
The string "PAYPALISHIRING"
is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)
P A H N
A P L S I I G
Y I R
And then read line by line: "PAHNAPLSIIGYIR"
示例
输入: s = "LEETCODEISHIRING", numRows = 3
输出: "LCIRETOESIIGEDHN"
输入: s = "LEETCODEISHIRING", numRows = 4
输出: "LDREOEIIECIHNTSG"
解释:
L D R
E O E I I
E C I H N
T S G
我的思路
首先,使用s=“12345678910”,numRows=3/4梳理一遍。这时就会发现:
1 5 9
2 4 6 8 10
3 7
1 7 13
2 6 8 12 14
3 5 9 11 15
4 10 16
注意到一个’N’子形中,处于相同一行的字母可能有三个也可能有两个(最上方和最下方),但是不论两个还是三个字母,'N’的特点就是上下齐宽,也就说间距是一样的,而这个间距可以分析出为2numRow-2(即2numRow-1-1)。同时这个间距被分为两部分:左边一部分和右边一部分,它们的和就是这个间距。对于同一行的数字,可以发现间距出现的规律为左、右、左、右。所以我们还需要计算出第一个间距的表达式。第i行的第一个字母,其索引为记为i,则其旁边的字母索引为(n-i)+(n-i)-1,于是左边间距为2(n-i)-2;所以第i行的间距规律为:[2(n-i)-2,2n-2-2n+2i+2=2i]。考虑到第一行和第后一行只有一个间距,即可完成任务:
public static String convert(String s, int numRows) {
if(s==null||s.equals("")){
return "";
}
if(numRows==1){
return s;
}
int firstDelta,secondDelta,nextPosition,indicator;
int stringLength=s.length();
int deltaSum=2*numRows-2;//总间距
char[] chars=s.toCharArray();
StringBuilder builder=new StringBuilder(s.length());
for(int i=0;i<numRows;i++){
nextPosition=i;
firstDelta=2*(numRows-i)-2;//第一个间距
secondDelta=deltaSum-firstDelta;//第二个间距
indicator=0;
while(nextPosition<stringLength){//该行尚未结束
builder.append(chars[nextPosition]);
if(indicator==0){//如果下一个间距为左间距
nextPosition+=firstDelta;
if(firstDelta==0){//处理最后一行的情况
nextPosition+=secondDelta;
}
indicator=1;
}else{//下一个间距为右间距
nextPosition+=secondDelta;
if(secondDelta==0){//处理第一行的情况
nextPosition+=firstDelta;
}
indicator=0;
}
}
}
return builder.toString();
}
运行结果为:
优秀解法
//26ms
public static String convert(String s, int numRows) {
if(numRows == 1)
return s;
List<StringBuilder> rows = new ArrayList<StringBuilder>();
int len = Math.min(s.length(), numRows);
for(int i = 0; i < len; i++){
rows.add(new StringBuilder());
}
int curRow = 0;
boolean flag = false;
for(char c : s.toCharArray()){
rows.get(curRow).append(c);
if(curRow == 0 || curRow == len - 1)
flag = !flag;
curRow += flag ? 1 : -1;
}
StringBuilder sb = new StringBuilder();
for(StringBuilder now : rows){
sb.append(now);
}
return sb.toString();
}
上面的这种方法很直白,算是题意的直接翻译。按照’N’的顺序从上到下,然后在从下到上,每一行使用StringBuilder来存储,然后做汇总。没有额外的计算和判断,性能支出主要在List的访问。直接翻译也算是一种不错的解题思路,虽然不见得是最佳,但有好胜于无。在54题螺旋矩阵中,这种思路也有体现。
//12ms
public static String convert(String s, int numRows) {
if (numRows == 1) return s;
StringBuilder ret = new StringBuilder();
int n = s.length();
int cycleLen = 2 *numRows - 2;
for (int i = 0; i < numRows; i++) {
for (int j = 0; j + i < n; j += cycleLen) {
ret.append(s.charAt(j + i));
if (i != 0 && i != numRows - 1 && j + cycleLen - i < n)
ret.append(s.charAt(j + cycleLen - i));
}
}
return ret.toString();
}
这种解法算是对我自己的解法的一种升级,所依赖的规律是一样的,但是利用方式却不同。很好,很强大。
差别在哪里
不说直接翻译题意的解法,我们来看看最后一种解法。
基本思路是一样的,发现排列规律后,利用该规律,但是如何利用,确是各有不同啦。外循环都是一样的,都是对行的遍历,这也是题目要求的。但是在内循环上,却风格不同。
在我的解法中,将间距分为两部分,并通过标记变量标记下一个间距,同时对第一行和第末行做特殊处理;这个变量使用了int,很明显不好,应该用boolean。
在最后的解法中,将间距视为一部分。在读入一个字母后,如果不是首行或者尾行的话,就在加入一个字母。也就是直接加入相隔左间距的那个字母。
这样一来,就内循环的次数来说,肯定是最后解法更少,因为单步跨度大。就判断次数来说,我的解法中需要不断判断下一个间距的类别,判断次数也比后者多。这应该就是时间差了。
在我的解法中,只是注意到了两种间距的差异性,按照左、右、左、右的规律完成代码;而最后一种解法却是看到了它们的一致性,(左、右)、(左、右)。代码不但更加简洁、优美,效率也高,而这两者的区别仅在于看待问题的方法和角度不同。值得学习。