我最近一直在玩Ruby,我刚从http://codekata.pragprog.com完成了Anagrams Code Kata。
该解决方案是测试驱动的,并利用独特的素因子化定理,然而它似乎运行得非常慢。就45k文件而言,到目前为止它已经运行了大约10分钟。任何人都可以给我任何关于提高代码性能的指示吗?
class AnagramFinder
def initialize
@words = self.LoadWordsFromFile("dict45k.txt")
end
def OutputAnagrams
hash = self.CalculatePrimeValueHash
@words.each_index{|i|
word = @words[i]
wordvalue = hash[i]
matches = hash.select{|key,value| value == wordvalue}
if(matches.length > 1)
puts("--------------")
matches.each{|key,value|
puts(@words[key])
}
end
}
end
def CalculatePrimeValueHash
hash = Hash.new
@words.each_index{|i|
word = @words[i]
value = self.CalculatePrimeWordValue(word)
hash[i] = value
}
hash
end
def CalculatePrimeWordValue(word)
total = 1
hash = self.GetPrimeAlphabetHash
word.downcase.each_char {|c|
value = hash[c]
total = total * value
}
total
end
def LoadWordsFromFile(filename)
contentsArray = []
f = File.open(filename)
f.each_line {|line|
line = line.gsub(/[^a-z]/i, '')
contentsArray.push line
}
contentsArray
end
def GetPrimeAlphabetHash
hash = { "a" => 2, "b" => 3, "c" => 5, "d" => 7, "e" => 11, "f" => 13, "g" =>17, "h" =>19, "i" => 23, "j" => 29, "k" => 31, "l" => 37, "m" => 41, "n" =>43, "o" =>47, "p" => 53, "q" =>59, "r" => 61, "s" => 67, "t" => 71, "u" => 73, "v" => 79, "w" => 83, "x" => 89, "y" => 97, "z" => 101 }
end
end
答案 0 :(得分:5)
Frederick Cheung有一些好处,但我想我可能会为您提供一些描述性的例子。
我认为你的主要问题是你以一种强迫你在其中进行线性搜索的方式创建索引。
您的单词列表(@words
)似乎如下所示:
[
"ink",
"foo",
"kin"
]
也就是说,它只是一个单词阵列。
然后使用CalculatePrimeValueHash
创建哈希索引,哈希键等于@words
中单词的索引。
{
0 => 30659, # 23 * 43 * 31, matching "ink"
1 => 28717, # 13 * 47 * 47, matching "foo"
2 => 30659 # 31 * 23 * 43, matching "kin"
}
我认为这是一个好的开始,但问题是,如果你保持这样,你将不得不迭代哈希来找到属于一起的哈希键(即@words
中的索引),以及然后迭代那些加入他们。也就是说,这里的基本问题是你做得过于细致。
如果您使用主值作为哈希键来构建此哈希,并让它们指向具有该键的单词数组,那么您将获得这样的哈希索引:
{
30659 => ["ink", "kin"],
28717 => ["foo"]
}
使用这种结构,编写输出时唯一需要做的就是迭代哈希值并打印它们,因为它们已经分组了。
你的代码的另一个原因是它似乎生成了一大堆一次性对象,这将确保你的garbarge收集器忙,这通常是ruby中的一个很大的阻塞点。
找到基准工具和/或分析器来分析代码并查看可以批准的位置也可能是一件好事。
答案 1 :(得分:4)
从根本上说,你的代码很慢,因为对于每个单词(45k)你迭代整个哈希(其中45k)寻找具有相同签名的单词,所以你做45k * 45k的这些比较。另一种措辞方式就是说你的复杂性是单词数量的n ^ 2。
下面的代码实现了你的基本想法,但是在我碰巧躺在的236k字文件上运行几秒钟。它绝对可以更快 - 第二次通过数据来查找>可以删除1个项目,但可读性较差
它比你的代码短得多,大约三分之一,同时保持可读性,主要是因为我使用了更多的标准库函数和惯用的ruby。
例如,load_words方法使用collect
将一个数组转换为另一个数组,而不是迭代一个数组并将内容添加到第二个数组。类似地,签名函数使用inject
而不是迭代字符。最后我使用group_by
进行实际分组。所有这些方法恰好都在Enumerable中 - 非常值得熟悉这些方法。
signature_for_word
可能会变得更加精简
word.each_char.map {|c| CHAR_MAP[c.downcase]}.reduce(:*)
这需要单词,将其分成字符,然后将每个字符映射到正确的数字。 reduce(:*)
(reduce是注入的别名)然后将它们相乘。
class AnagramFinder
CHAR_MAP ={ "a" => 2, "b" => 3, "c" => 5, "d" => 7, "e" => 11, "f" => 13, "g" =>17, "h" =>19, "i" => 23, "j" => 29, "k" => 31, "l" => 37, "m" => 41, "n" =>43, "o" =>47, "p" => 53, "q" =>59, "r" => 61, "s" => 67, "t" => 71, "u" => 73, "v" => 79, "w" => 83, "x" => 89, "y" => 97, "z" => 101 }
def initialize
@words = load_words("/usr/share/dict/words")
end
def find_anagrams
words_by_signature = @words.group_by {|word| signature_for_word word}
words_by_signature.each do |signaure, words|
if words.length > 1
puts '----'
puts words.join('; ')
end
end
end
def signature_for_word(word)
word.downcase.each_char.inject(1) {| total, c| total * CHAR_MAP[c]}
end
def load_words(filename)
File.readlines(filename).collect {|line| line.gsub(/[^a-z]/i, '')}
end
end
答案 2 :(得分:2)
您可以使用基准测试工具开始限制速度。这里有一些例子:
http://www.skorks.com/2010/03/timing-ruby-code-it-is-easy-with-benchmark/
首先,看看运行self.calculate_prime_value_hash
以及calculate_prime_word_value
之后需要多长时间才会很有趣。
慢慢地归结为内部循环运行的次数,因此您还可以记录它们运行的次数。
您可以做的一个非常快速的改进是将主要的alhabet散列设置为常量,因为它根本没有改变:
PRIME_ALPHABET_HASH = { "a" => 2, "b" => 3, "c" => 5, "d" => 7, "e" => 11, "f" => 13, "g" =>17, "h" =>19, "i" => 23, "j" => 29, "k" => 31, "l" => 37, "m" => 41, "n" =>43, "o" =>47, "p" => 53, "q" =>59, "r" => 61, "s" => 67, "t" => 71, "u" => 73, "v" => 79, "w" => 83, "x" => 89, "y" => 97, "z" => 101 }