是否有更好的方法来查找列表中最常见的单词(仅限Python)

时间:2015-07-08 09:00:33

标签: python algorithm

考虑到问题的简单实现,我正在寻找一种更快的方法来查找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'有明显的优势。方法,即使它是静态别名。

6 个答案:

答案 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的大值
  • ,应忽略版本6(单行)

答案 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)

你的词典/计数器解决方案对我来说很好。它的优点是可以并行计数步骤。

另一个明显的算法是:

  1. 对列表进行排序
  2. 循环列表计算重复值,记录到目前为止最长的运行
  3. 这具有时间复杂度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]

这当然会占用额外的空间,但避免搜索。

相关问题