Đây là một vấn đề kinh điển gây được tiếng vang vào năm 1986, khi Donald Knuth thực hiện một giải pháp nhanh với hàm băm trong một chương trình dài 8 trang để minh họa kỹ thuật lập trình biết chữ của mình, trong khi Doug McIlroy, cha đỡ đầu của các ống Unix, đã trả lời một lớp lót, điều đó không nhanh như vậy, nhưng đã hoàn thành công việc:
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q
Tất nhiên, giải pháp của McIlroy có độ phức tạp thời gian O (N log N), trong đó N là tổng số từ. Có nhiều giải pháp nhanh hơn. Ví dụ:
Dưới đây là một triển khai C ++ với độ phức tạp thời gian giới hạn trên O ((N + k) log k), thường - gần như tuyến tính.
Dưới đây là một triển khai Python nhanh bằng cách sử dụng từ điển băm và heap với độ phức tạp thời gian O (N + k log Q), trong đó Q là một số từ duy nhất:
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)
So sánh thời gian CPU (tính bằng giây):
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
Ghi chú:
- bible32 là Kinh thánh được nối với chính nó 32 lần (135 MB), kinh thánh256 - 256 lần tương ứng (1,1 GB).
- Sự chậm lại phi tuyến tính của các tập lệnh Python được gây ra hoàn toàn bởi thực tế là nó xử lý các tệp hoàn toàn trong bộ nhớ, do đó, các chi phí ngày càng lớn hơn đối với các tệp lớn.
- Nếu có một công cụ Unix có thể xây dựng một heap và chọn n phần tử từ đầu heap, giải pháp AWK có thể đạt được độ phức tạp thời gian gần tuyến tính, trong khi hiện tại nó là O (N + Q log Q).