这次的练习又是一个单词游戏,规则很简单,判断一个长度为6的单词是否可以由两个短单词组合而成。
举例来说:
al + bums => albums
bar + ely => barely
be + foul => befoul
con + vex => convex
here + by => hereby
jig + saw => jigsaw
tail + or => tailor
we + aver => weaver
作者要求写三次代码:
- 第一次用最直接最易懂的方法
- 第二次用最高效的方法
- 第三次用可扩展性最高的方法
对于我们来说,考虑高效和可扩展性基本已经养成习惯了,强行去写低效直接的代码反而更难做到,于是我们决定不做无用功,直接一步到位。
思路
思路大体来说有正反两种。
正的意思是遍历单词的时候就生成所有可能的长度为6的单词并记录,然后处理长度为6的单词时只要判断是否在记录中即可,显然这样做的复杂度是平方,不可取。
反的意思是先记录所有长度小于6的单词,遇到长度为6的单词时遍历它的所有二分形式并判断二分结果是否存在。对于长度为6的单词来说只有五种二分组合,复杂度是n
,可以接受。
思路出来了,具体写的时候反而遇到了两个小麻烦。
第一个麻烦是思维定式,我和小伙伴在写代码的时候不约而同地使用了Kata06中的hash函数,写到一半才发现根本没必要hash,因为这次的处理过程是位置相关的,hash已经没有意义了,只会损耗性能。
第二个麻烦是语言理解不当,我一开始用数组来存储小于6的单词,但是发现速度很慢,要30多秒才能出结果。我想了半天认为性能瓶颈是二分单词,但是这又和一开始分析的结果矛盾。这时候小伙伴写完来找我讨论,我就说起这个问题,然后我发现我2了,这里显然该用字典而不是数组。。。用数组的话Python内置的in操作其实是遍历数组,这样一来复杂度又变成了平方,自然就慢了。换了字典之后果然有效,直接提升两个数量级,不到0.3秒就跑完了。
代码
上代码,依然很简单:
import collections
all_words = {}
n = 6
def generateSon(word):
for i in range(len(word) - 1):
yield word[:i], word[i:]
a = 0
with open("wordlist.txt") as f:
for i in f.readlines():
word = i.strip()
if len(word) < n:
all_words[word] = 1
elif len(word) == n:
for son1, son2 in generateSon(word):
if son1 in all_words and son2 in all_words:
a += 1
print a
最后输出的是可以由短单词组成的长度为6的单词数量。
扩展性
上面的代码已经提取出了长度限制n
,可以换成任意长度。但是还有一个扩展问题没有解决:划分段数。
这里假设的是二分,那如果要把单词分成三段四段甚至更多该怎么办呢?
讨论了半天递归循环之类的东西,我们最终想到了一个简单的解决办法:封装一个函数,内部多次调用generateSon
。
这个函数很简单,参数是划分段数,然后用generateSon
进行划分,如果划分完数量不够就按顺序再对划分结果进行划分,数量够了就返回。由于是二分,所以可以划分出任意长度,假设要三段,那把第一次二分结果中的任意一段再次二分即可;假设要四段,那把第一次二分的两个结果都进行二分即可,其他段数同理。
思路很清晰了,由于太懒我们没有实现这个函数,大家感兴趣可以自己写一下。
总结
这个练习本质上还是很有用的,对于那些并不习惯从效率和扩展性方面思考的程序员来说可以得到很好的锻炼。对于我们来说,虽然思路是一步到位,但是在具体编程过程中仍然遇到几个小坑,可见还需不断努力。
这已经是第8个Kata,眼看就要到一半了,说实话逐渐发现CodeKata系列并不是非常适合我们。究其原因,还是强度不够。这些练习对于初学者或者初级程序员来说确实可以起到开拓思路锻炼能力的作用,但是对于我们来说有些过于简单。
不过话说回来,收获还是有的,毕竟细节决定成败嘛,多写写代码总没坏处。更重要的是,这次练习让我们养成了每天做练习的习惯,等21个Kata全部做完后我们会选择更合适的练习,那才是真正见效的时候。