这是一个经典的问题,在1986年引起了共鸣,当时Donald Knuth在一个长达8页的程序中通过哈希尝试实现了一种快速解决方案,以说明他的识字编程技术,而Unix管道的教父Doug McIlroy则回应了单线,虽然没有那么快,但是完成了工作:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
当然,McIlroy的解决方案具有时间复杂度O(N log N),其中N是单词总数。有更快的解决方案。例如:
这是一个C ++实现,通常具有上限时间复杂度O((N + k)log k)-接近线性。
下面是使用哈希字典和堆的快速Python实现,其时间复杂度为O(N + k log Q),其中Q是许多唯一的单词:
import collections, re, sys
filename = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv)>2 else 10
text = open(filename).read()
counts = collections.Counter(re.findall('[a-z]+', text.lower()))
for i, w in counts.most_common(k):
print(i, w)
CPU时间比较(以秒为单位):
bible32 bible256
C++ (prefix tree + heap) 5.659 44.730
Python (Counter) 10.314 100.487
Sheharyar (AWK + sort) 30.864 251.301
McIlroy (tr + sort + uniq) 60.531 690.906
笔记:
- bible32是圣经与自身的串联32次(135 MB),分别与bible256 – 256次(1.1 GB)连接。
- Python脚本的非线性减慢完全是由于它完全在内存中处理文件这一事实造成的,因此大文件的开销越来越大。
- 如果有一个Unix工具可以构造一个堆并从堆顶部选择n个元素,则AWK解决方案可以实现近乎线性的时间复杂度,而目前为O(N + Q log Q)。