WordCount:McIlroy的解决方案效率如何?

时间:2014-09-21 09:36:37

标签: shell sorting word-count knuth

长话短说:1986年,一位采访者要求唐纳德·克努特写一个输入文本和数字N的程序,并列出按频率排序的N个最常用的单词。 Knuth制作了一个10页的Pascal程序,Douglas McIlroy用以下6行shell脚本回复:

tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q

阅读http://www.leancrew.com/all-this/2011/12/more-shell-less-egg/的完整故事。

当然,他们有着截然不同的目标:Knuth展示了他的文学编程概念,并从头开始构建了所有内容,而McIlroy使用了一些常用的UNIX实用程序来实现最短的源代码。

我的问题是:那有多糟糕?
(纯粹从运行时速度的角度来看,因为我很确定我们都同意6行代码比10页更容易理解/维护,有文化编程与否。)

我可以理解sort -rn | sed ${1}q可能不是提取常用词的最有效方法,但tr -sc A-za-z '\n' | tr A-Z a-z有什么问题?它看起来对我很好。 关于sort | uniq -c,这是一种非常慢的确定频率的方法吗?

一些注意事项:

  • tr应为线性时间(?)
  • sort我不确定,但我认为
  • uniq也应该是线性时间
  • 产生过程应该是线性时间(在过程数量上)

2 个答案:

答案 0 :(得分:6)

Unix脚本有一些线性操作和2种排序。它将是计算顺序O(n log(n))

对于仅采用前N的Knuth算法:http://en.wikipedia.org/wiki/Selection_algorithm 您可以在算法的时间和空间复杂性方面有一些选择,但从理论上讲,对于一些具有大量(不同)单词的典型示例,它们可以更快。

所以Knuth可能会更快。当然因为英语词典的大小有限。它可能会使log(n)变成一个大的常量,但可能消耗大量内存。

但也许这个问题更适合https://cstheory.stackexchange.com/

答案 1 :(得分:0)

Doug McIlroy的解决方案具有时间复杂度O(T log T),其中T是单词的总数。这是由于第一个func main() { var err error err = createDirectory("/tmp/test1") if err != nil { log.Print(err) } } // Linux Version func createDirectory(dirPath string) error { var err error err = os.Mkdir(dirPath, 0777) if err != nil { e, ok := err.(*os.PathError) if ok && e.Err == syscall.EEXIST { log.Print("Folder already exists") } } return err } //Windows version func createDirectory(dirPath string) error { var err error err = os.Mkdir(dirPath, 0777) if err != nil { e, ok := err.(*os.PathError) if ok && e.Err == syscall.ERROR_ALREADY_EXISTS { log.Print("Folder already exists") } } return err }

为进行比较,以下是针对同一问题的三种更快的解决方案:

Here是一种C ++实现,具有上限时间复杂度O((T + N)log N),但实际上–几乎是线性的,接近O(T + N log N)。

下面是一个快速的Python实现。在内部,它使用哈希字典和具有时间复杂度O(T + N log Q)的堆,其中Q是唯一单词的数量:

sort

另一个使用AWK的Unix shell解决方案。它具有时间复杂度O(T + Q log Q):

import collections, re, sys

filename = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv)>2 else 10
reg = re.compile('[a-z]+')

counts = collections.Counter()
for line in open(filename):
    counts.update(reg.findall(line.lower()))
for i, w in counts.most_common(k):
    print(i, w)

CPU时间比较(以秒为单位):

awk -v FS="[^a-zA-Z]+" '
{
    for (i=1; i<=NF; i++)
        freq[tolower($i)]++;
}
END {
    for (word in freq)
        print(freq[word] " " word)
}
' | sort -rn | head -10

注意:

  • T> = Q,通常是Q >> N(N是一个小常数)
  • bible32是圣经的32次连接(135 MB),bible256 – 256次分别连接(1.1 GB)

如您所见,即使使用标准的Unix工具,也可以轻松地在CPU时间上击败McIlroy的解决方案。但是,他的解决方案仍然非常优雅,易于调试,而且毕竟性能也不是很糟糕,除非您开始将其用于数千兆字节的文件。在C / C ++或Haskell中对较复杂算法的错误实现可能比其管道运行速度慢得多(我已经看到了!)。