LeetCode (6): ZigZag Conversion
【题目】LeetCode(6): ZigZag Conversion
URL: https://leetcode.com/problems/zigzag-conversion/
【描述】
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"
Write the code that will take a string and make this conversion given a number of rows:
string convert(string text, int nRows);
convert("PAYPALISHIRING", 3)
should return "PAHNAPLSIIGYIR"
.
【中文描述】给一个String比如"PAYPALISHIRING",要求返回zigzag写法后,一行一行输出的结果。比如上面结果就是从第一行PAHN,到第二行APLSIIG,到第三行YIR结束。所以最后的输出是:“PAHNAPLSIIGYIR”。
————————————————————————————————————————————————————————————
【初始思路】
刚开始看了10分钟没看懂题目,汗~~~
后来自己写了个,一提交错了,看了下测试用例,才明白所以然。
其实所谓zigzag意思是,你的输入字符串,从上往下先竖写一列,然后从下往右上写一斜线,然后再竖着写下来,再往右上,如此反复,如下图所示。
然后,就brute写了一个,思想是直接硬按照规律找位置从string里挑字符出来拼。
过程很郁闷,因为首尾两行情况特殊考虑,中间行又要找规律,找了好久都没找出个所以然来。最后正面刚写了个程序出来,很难读懂。。:
1 public String convert(String s, int numRows) { 2 if(numRows == 1 || numRows >= s.length()) { 3 return s; 4 } 5 StringBuilder sb = new StringBuilder(); 6 int length = s.length(); 7 int r = length % (2 * numRows - 2); // total remain number, indicate that how many rows need to add more element 8 int c = length / (2 * numRows - 2); // columns number 9 int excol = 0; 10 int remain = 0; // total remain - numRow = remain 11 if(r >= numRows) { 12 excol = 1; 13 remain = r - numRows; 14 } else { 15 excol = 0; 16 remain = r; 17 } 18 19 for(int i = 0; i < numRows; i++) { 20 if (i == 0) { 21 for(int j = 0; j < c + (r > 0 ? 1:0); j++) { 22 sb.append(s.charAt((2 * numRows - 2) * j + i)); 23 } 24 } else if(i == numRows - 1){ 25 for(int j = 0; j < c + excol; j++) { 26 sb.append(s.charAt((2 * numRows - 2) * j + i)); 27 } 28 } else { 29 int j = 1; 30 sb.append(s.charAt(i)); 31 while(j * (2 * numRows - 2) - i < length) { 32 sb.append(s.charAt(j * (2 * numRows - 2) - i)); 33 if(j * (2 * numRows - 2) + i < length) { 34 sb.append(s.charAt(j * (2 * numRows - 2) + i)); 35 } 36 j++; 37 } 38 } 39 } 40 return sb.toString(); 41 }
【反思】
然后看了网上大神的思路(汗,easy题都搞不出来。。。),豁然开朗,考虑如下图(原谅我,懒得用photoshop画图了):
显然,第一眼就能发现的规律是,有一个循环cycle存在(图中以虚线隔开),在numRow=3的时候,这个cycle = 4。如上图,整个string被分成了4个cycle(最后一个不满,待会儿讨论)。
那么很显然,从任一个cycle 任一个字符到下一个cycle相同位置的字符,需跳跃cycle个位置。比如,第一个字符P,到第一行第二个字符A,需跳跃cycle个字符。所以A的位置为 0 + cycle = 4。
到这里,首尾两行已经基本分析完了。算出cycle数,以 i 为行号,然后在每一行内以 k 循环,依次输出 k + cycle位置的字符(如果此位置小于String长度)即可。
这个题实际的难点在于z字中间行的规律寻找。找到迎刃而解,找不到就慢慢正面刚代码吧。这是这个题easy的原因,也是我认为它很无聊的原因。。。
还是看上面图,中间行为"APLSIIG",这一行的规律是什么呢?
我们可以看到,这一行每个cycle其实有2个字符(除了最后未满的cycle):AP/LS/II。 这是一个很重要的发现:
首先,无论numRows为多少,这个规律是不变的;
其次,从这2个字符中第一个字符跳到下一个2个字符的第一个字符,跳跃数也是cycle。比如,图上A到L,其实就是 A的位置+cycle = L的位置。
好,如果我们把这一行也纳入到上面首尾两行的循环中去,可以轻松找到所有2字符组的第一个字符,我们称2字符组第一个字符位置为j,所处行行号为 i, 有如下规律:
j = i + cycle * k, k = 0, 1, 2, 3, ......
同时可以看到,对于中间行,在循环中对每个 k, 要找2个字符。找到j的同时,再找到第二个字符的位置 second_j,一直循环到结束,这一行不就输出了么?
那么second_j有何规律?
For numRows ==3的情况, APLSIIG的规律看似很简单,因为他们的位置是:1、3、5、7、9、11、13。那么second_j = j + 2?,我们来验证一下。
假设numRows==5,那么题目string的zigzag结果应该是:
P H A S I Y I R
P L I G
A N
比如,对于i=1(第二行)来看,第一个cycle的j=1,second_j = 7。对于i=2, 第一个cycle的j=2, 而second_j = 6。显然上面的结论是错误的。
我们可以观察到,i越大,它的 second_j 和 j 之间的差距越小。 所以,可以假设second_j = N - c * i。N为某数字, c应当为 i 的系数。同时显然可以猜测,N应当是j的线性函数。那我们假设N = a * j + b。
所以,得到式子: second_j = a * j + b - c * i。 接下来,我们试试解这个方程组。
根据上面的位置数字(1-7, 2-6, 3,5),分别代入second_j、j、i,得到以下方程组:
a * 1 + b - 1*c = 7
a * 2 + b - 2*c = 6
a * 3 + b - 3*c = 5
上面三式可轻松求出,b = 8, 并且 a-c = -1. 继续,根据上面数字(3-5, 11-13) 解如下方程组:
11*a + 8 - 3c = 13
3*a + 8 - 3c = 5
解得 a = 1, c = 2
由此,得到second_j = j + 8 - 2 * i 。
回到numRows = 3的情况,second_j = j + 8 - 2*i,我们来验证一下:3 = 1 + 8 - 2*1。 这明显不对,哪里出了问题?
思考一下,a是j的参数,c是i的参数,参数必然不会随着numRows的变化而变化,他们肯定是固定的。唯一可变的只有b。那么在numRow=5的时候,b=8, 在numRows=3的时候, b=4,上面式子才能成立。
到这里,我们发现了b的规律, b = cycle!!!
所以, second_j = j + cycle - 2* i。 代入上面各种情况验证,全部验证成功!
这样,我们就得到了second_j 和 j 的关系,那么对于每一行 i,以 k 为循环变量,每一个cycle第一个字符位置为j, 第二个位置就为 second_j = j * cycle - 2 * i。
代码呼之即出!
【Show me the Code!!!】
1 if(numRows <= 1) return s; 2 StringBuilder sb = new StringBuilder(); 3 //the size of a cycle(period) 4 int cycle = 2 * numRows - 2; 5 for(int i = 0; i < numRows; ++i) 6 { 7 for(int j = i; j < s.length(); j = j + cycle){ //j = j + cycle is wonderful!!! 8 sb.append(s.charAt(j)); 9 int secondJ = (j - i) + cycle - i; 10 if(i != 0 && i != numRows-1 && secondJ < s.length()) 11 sb.append(s.charAt(secondJ)); 12 } 13 } 14 return sb.toString();
此外,应当看到,其实,首尾两行可以纳入通盘考虑。当i = 0 或者 i = numRows的时候,不予考虑second_j,因为他们也没有。所以,inner-for循环始终保持跳跃 j + cycle
此外,second_j必须检查是否会越界。
最后,我想说的是,这种题好无聊~~~