今天再来写一道,这是leetcode第6题,难度是easy,题目没有用到高深的算法,但要考虑的细节比较多,要想一次做对还是有难度的,本人就是修修改改了半天才通过验证。。。
原题如下:
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"
.题目比较长,但如果你英文水平可以的话,理解起来并没有什么难度,就是一个所谓的锯齿转换:将输入的字符串摆成一个锯齿形状,然后按特定顺序读出一个新的字符串。这个通过题中的例子应该比较容易理解,就不多费口舌了。
说白了,这道题考察的就是一个找规律的问题,只有你找到锯齿矩阵中每一行中的字母在原字符串中的下标的关系即可。就比如说题中举得例子,第一行中的P是第0个元素,而A是第(0+4)个,H是第(0+4+4)个。。。到了第二行,A是第1个,P是第(1+2)个,L是第(1+2+2)个。。。
只有大家耐心一点儿,这种规律是不难总结的,要注意的是当nRows比较大时,一些行每次跳过的不相同而且跟行数有关,这个大家细心观察下就好。不过为了适应各种水平的读者,我在这里还是把规律大致总结一下:
在锯齿矩阵中:
(1)每一行起始元素的下标都等于行数。
(2)第一行和最后一行跳过的距离相等为2*numRows-2(设为maxSpace).。
(3)第r行每次跳过的距离不同,分别为maxSpace-2*r和2*r(二者相加仍为maxSpace,maxSpace是一个周期性质的数)。
再说一遍,这些规律只有耐心点,都是容易总结出来的。不过在将规律转化为代码会会发现在起始位置或者结尾位置会有些问题,这些细节处还是很容易出错的,大家不要掉以轻心,不多说了,直接看一下本人的代码吧
<span style="font-size:14px;"><span style="font-size:18px;">class Solution { public: string convert(string s, int numRows) { int len=s.size(); if(len<=numRows || numRows==1) return s; int maxSpace=2*numRows-2; string result; for(int row=0;row!=numRows;++row) { for(int j=row;j-2*row<len;j+=maxSpace) { if(row!=0 && row!=numRows-1 && j>maxSpace) { result.push_back(s[j-2*row]); } if(j<len) result.push_back(s[j]); } } return result; } };</span></span>这个代码是第一次写出来的,具体那句话什么意思就不讲了,可以看出里面很多“补丁”,程序的运行时间为20ms,在C++程序中算是比较好的,但跟最好的仍然有算差距。刚开始我以为这是程序中一写判断太多了,浪费了一些时间,所以将代码重新重新整理了一下,结果如下:
<span style="font-size:14px;"><span style="font-size:18px;">class Solution { public: string convert(string s, int numRows) { int len=s.size(); if(len<=numRows || numRows==1) return s; int maxSpace=2*numRows-2; string result; for(int i=0;i<len;i+=maxSpace) result.push_back(s[i]); for(int row=1;row!=numRows-1;++row) { result.push_back(s[row]); for(int j=row+maxSpace-2*row;j<len;j=j+maxSpace-2*row) { result.push_back(s[j]); j+=2*row; if(j<len) result.push_back(s[j]); } } for(int i=numRows-1;i<len;i+=maxSpace) result.push_back(s[i]); return result; } };</span></span>整理后代码稍微好看了一些,但不幸的是运行时间并没有改变,仍然是20ms。看来是我低估了VS的优化能力,上下两份代码的优化结果是大同小异的,用当前流行的话说:并没有优化什么。
那问题在哪儿呢?可能大神已经看出来了,其实程序关键地方就是最内层循环里的四句话,决定程序运行时间的也就是这四句话(因为执行了最多次嘛),那这四句话有什么问题呢?这样如果还不理解说明你可能不知道或者没有考虑过这点:push_back()操作的运行机制。我们知道,刚定义的string对象是不能直接用下标的额,因为还没有“位置”,所以要用push_back()。但这种机制和所以的顺序表容器一样有一个缺点那就是空间连续性的问题。因为sting对象的空间是在push_back()的过程中“动态分配”的,而又要求空间必须连续,这就涉及到了空间不够时的整体复制问题,另外分配空间本身也是需要时间的,所以时间就在这个地方浪费了。知道了这一点那就好办了,我们可以吧result先reserve()一下:在result定义后面加上一句result.reserve(len);将空间分配一次完成,这样在执行一下程序,发现时间缩短到了16ms,达到了最好水平!
另外程序中仍然有边缘情况特殊处理的应用,这里就不多说了,今天也不再粘贴大神的代码了,因为我们已经和大神一个水平了,呵呵
最好还是奉劝大家动手自己编一下,程序中的细节还是挺多的。