考虑到问题的简单实现,我正在寻找一种更快的方法来查找Python列表中最常见的单词。作为Python访谈的一部分,我收到的反馈是,这种实现效率很低,基本上都是失败的。后来,我尝试了很多我发现的算法,只有一些基于堆栈的解决方案速度稍微快一些,但不是绝大多数(当缩放到数千万个项目时,heapsearch的速度提高了大约30%;在千万个长度上,这几乎是相同;使用timeit)。
def stupid(words):
freqs = {}
for w in words:
freqs[w] = freqs.get(w, 0) + 1
return max(freqs, key=freqs.get)
由于这是一个简单的问题而且我有一些经验(尽管我不是算法大师或竞争对手)我很惊讶。
当然,我想提高自己的技能并学习解决问题的更好方法,所以您的意见将得到赞赏。
重复状态的澄清:我的观点是要找出实际上是否有更多(渐近)更好的解决方案,而其他类似的问题已经选择了一个不太好的答案。如果这还不足以使问题变得独一无二,那么当然要关闭这个问题。
更新
谢谢大家的意见。关于访谈情况,我仍然认为手写搜索算法是预期的(可能更有效)和/或审阅者从另一种语言的角度评估代码,具有不同的常数因素。当然,每个人都可以拥有自己的标准。
对我来说重要的是验证我是否完全无能为力(我的印象是我不是)或者通常只写不是最好的代码。仍然有可能存在更好的算法,但如果它在这里为社区隐藏了几天,我就可以了。
我正在选择最受欢迎的答案 - 尽管不止一个人提供了有用的反馈,但这样做似乎还算公平。
次要更新
似乎使用defaultdict比使用' get'有明显的优势。方法,即使它是静态别名。
答案 0 :(得分:2)
这听起来像是一个糟糕的面试问题,可能是面试官期待某个答案的一个案例。这听起来好像他/她没有清楚地解释他/她在问什么。
您的解决方案是O(n)
(其中n = len(words)
),并且使用堆不会改变它。
有更快的近似解决方案......
答案 1 :(得分:1)
from collections import Counter
word_counter = Counter(words)
word_counter
是一个字典,其中单词为键,频率为值,并且还有most_common()
方法。
答案 2 :(得分:1)
全局命名空间的函数调用和搜索更加昂贵。
您的stupid
函数在单词列表中为每个元素进行2次函数调用。你的max
调用中的第二个是完全可以避免的,迭代dict的键,然后对于每个键使用dict.get
查找值时,如果你可以遍历键值对,这是一个明显的低效率。 / p>
def stupid(words):
freqs = {}
for w in words:
freqs[w] = freqs.get(w, 0) + 1
return max(freqs, key=freqs.get)
def most_frequent(words):
## Build the frequency dict
freqs = {}
for w in words:
if w in freqs:
freqs[w] += 1
else:
freqs[w] = 1
## Search the frequency dict
m_k = None
m_v = 0
for k, v in freqs.iteritems():
if v > m_v:
m_k, m_v = k, v
return m_k, m_v
使用user1952500的单通道建议,您的大型样本集的票价如何?
def faster(words):
freq = {}
m_k = None
m_v = 0
for w in words:
if w in freq:
v = freq[w] + 1
else:
v = 1
freq[w] = v
if v > m_v:
m_k = w
m_v = v
return m_k, m_v
这具有对多个最常见值稳定的轻微优势。
使用nltk.books
生成样本的所有建议的比较:
def word_frequency_version1(words):
"""Petar's initial"""
freqs = {}
for w in words:
freqs[w] = freqs.get(w, 0) + 1
return max(freqs, key=freqs.get)
def word_frequency_version2(words):
"""Matt's initial"""
## Build the frequency dict
freqs = {}
for w in words:
if w in freqs:
freqs[w] += 1
else:
freqs[w] = 1
## Search the frequency dict
m_k = None
m_v = 0
for k, v in freqs.iteritems():
if v > m_v:
m_k, m_v = k, v
return m_k, m_v
def word_frequency_version3(words):
"""Noting max as we go"""
freq = {}
m_k = None
m_v = 0
for w in words:
if w in freq:
v = freq[w] + 1
else:
v = 1
freq[w] = v
if v > m_v:
m_k = w
m_v = v
return m_k, m_v
from collections import Counter
def word_frequency_version4(words):
"""Built-in Counter"""
c = Counter(words)
return c.most_common()[0]
from multiprocessing import Pool
def chunked(seq,count):
v = len(seq) / count
for i in range(count):
yield seq[i*v:v+i*v]
def frequency_map(words):
freq = {}
for w in words:
if w in freq:
freq[w] += 1
else:
freq[w] = 1
return freq
def frequency_reduce(results):
freq = {}
for result in results:
for k, v in result.iteritems():
if k in freq:
freq[k] += v
else:
freq[k] = v
m_k = None
m_v = None
for k, v in freq.iteritems():
if v > m_v:
m_k = k
m_v = v
return m_k, m_v
# def word_frequency_version5(words,chunks=5,pool_size=5):
# pool = Pool(processes=pool_size)
# result = frequency_reduce(pool.map(frequency_map,chunked(words,chunks)))
# pool.close()
# return result
def word_frequency_version5(words,chunks=5,pool=Pool(processes=5)):
"""multiprocessing Matt's initial suggestion"""
return frequency_reduce(pool.map(frequency_map,chunked(words,chunks)))
def word_frequency_version6(words):
"""Petar's one-liner"""
return max(set(words),key=words.count)
import timeit
freq1 = timeit.Timer('func(words)','from __main__ import words, word_frequency_version1 as func; print func.__doc__')
freq2 = timeit.Timer('func(words)','from __main__ import words, word_frequency_version2 as func; print func.__doc__')
freq3 = timeit.Timer('func(words)','from __main__ import words, word_frequency_version3 as func; print func.__doc__')
freq4 = timeit.Timer('func(words)','from __main__ import words, word_frequency_version4 as func; print func.__doc__')
freq5 = timeit.Timer('func(words,chunks=chunks)','from __main__ import words, word_frequency_version5 as func; print func.__doc__; chunks=10')
freq6 = timeit.Timer('func(words)','from __main__ import words, word_frequency_version6 as func; print func.__doc__')
结果:
>>> print "n={n}, m={m}".format(n=len(words),m=len(set(words)))
n=692766, m=34464
>>> freq1.timeit(10)
"Petar's initial"
3.914874792098999
>>> freq2.timeit(10)
"Matt's initial"
3.8329160213470459
>>> freq3.timeit(10)
"Noting max as we go"
4.1247420310974121
>>> freq4.timeit(10)
"Built-in Counter"
6.1084718704223633
>>> freq5.timeit(10)
"multiprocessing Matt's initial suggestion"
9.7867341041564941
注意:
multiprocessing.Pool
实例作为时间目的作为kwarg作弊,timeit
不允许您指定清理代码。这是在“四核”CPU上运行的,我确信输入数据和CPU计数的某些值可以使多处理更快。n*m
的大值答案 3 :(得分:1)
你必须至少经历一次所有的话,产生Omega(n)。存储当前每个不同单词的值会产生Omega(log n)。
如果您为不同的单词找到Omega(1)的存储(获取/设置),则可以使用Omega(n)创建解决方案。据我所知,我们只有Omega(log n)解决方案用于此类存储(独立于类型:堆,地图,树,字典,集...)。
编辑(检查评论): [你的解决方案是O(n log n),因为字典检查] + O(n)因为max(),使其成为O(n log) n)总计....这很好。
据我所知(复杂性),这是一个很好的解决方案。使用不同类型的存储(如语法树或堆)可能会获得更好的性能,但复杂性应该保持不变。
编辑: 从评论讨论中,您可以使用哈希表获得平均和摊销的Omega(n)。
答案 4 :(得分:0)
你的词典/计数器解决方案对我来说很好。它的优点是可以并行计数步骤。
另一个明显的算法是:
这具有时间复杂度O(n log n),其中n是列表的长度。
答案 5 :(得分:-1)
显然你需要查看words
中的每个单词,所以它只能是问题的最后搜索。是否可以选择对最常用的单词进行额外的引用?类似的东西:
def stupid(words):
freqs = {}
most = None
for w in words:
word_freq = freqs.get(w, 0) + 1
if most is None or word_freq > most[0]:
most = (word_freq, w)
freqs[w] = word_freq
return most if most is None else most[1]
这当然会占用额外的空间,但避免搜索。