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