算法:计算单词列表频率的更好方法

Posted

技术标签:

【中文标题】算法:计算单词列表频率的更好方法【英文标题】:Algorithm: A Better Way To Calculate Frequencies of a list of words 【发布时间】:2012-04-17 23:48:32 【问题描述】:

这个问题实际上很简单,但在开始编码之前我想听听一些想法。给定一个每行包含一个单词的文件,计算最常见的 n 个数字。

我想到的第一个也是不幸的唯一一件事是使用std::map。我知道 C++ 同胞会说 unordered_map 会非常合理。

我想知道是否可以在算法方面添加任何内容,或者这基本上只是“选择最佳数据结构的人获胜”类型的问题。我已经在互联网上搜索了它并阅读了该哈希表和优先级队列可能会提供具有 O(n) 运行时间的算法,但是我认为实现起来会很复杂

有什么想法吗?

【问题讨论】:

您想知道存储频率或计算频率的最佳方法吗?我很困惑。 @Jesse 感谢您的评论并在最短的运行时间内计算它们。 std::map等跟计算频率有什么关系? 我想我会这样使用它。地图[字]++;因此索引是单词,计数是它的频率。 @BenjaminLindley 绝对是:D 【参考方案1】:

用于此任务的最佳数据结构是 Trie:

http://en.wikipedia.org/wiki/Trie

它在计算字符串方面会优于哈希表。

【讨论】:

trie 对于字典中的 longest-match 查找非常有用,即不限于单个标记的匹配,但可以跨越无限数量的标记.但是对于查找和插入单个令牌,一次一个,根据我的经验,一个良好实现的哈希(包括std::unordered_map)会快得多。你有数据来证实你的说法吗? @jogojapan:在我链接的页面上有比较的性能图表。符号表存储单个标记并且通常存储为尝试,例如参见 boost::spirit。 好的,承认。 nedtries 尤其令人印象深刻。但还应该指出的是(据我所知)这些测试是针对小键(“指针大小”)的,即使有超过 10k 个条目,哈希也能提供相当的性能。尽管如此,我承认看到自然语言标记和(特别是为了频率计数的目的)的哈希与 nedtrie 比较会很有趣。【参考方案2】:

这个问题有很多不同的方法。它最终将取决于场景和其他因素,例如文件的大小(如果文件有十亿行),那么 HashMap 将不是一种有效的方法。根据您的问题,您可以执行以下操作:

    如果您知道唯一词的数量非常有限,您可以使用TreeMap 或在您的情况下使用std::map。 如果字数非常多,那么您可以构建一个trie 并在另一个数据结构中记录各种字数。这可能是一个大小为n 的堆(最小值/最大值取决于你想要做什么)。因此,您无需存储所有单词,只需存储必要的单词即可。

【讨论】:

感谢您的替代方法!我会牢记这些建议。【参考方案3】:

如果我有很多选择,我会std::map(或unordered_map)开头(尽管我不知道可能适用的其他限制条件)。

你这里有两个数据项,你用一个作为时间的关键部分,而另一个作为另一部分时间的关键。为此,您可能想要Boost Bimap 或Boost MultiIndex 之类的东西。

以下是使用 Bimap 的总体思路:

#include <boost/bimap.hpp>
#include <boost/bimap/list_of.hpp>
#include <iostream>

#define elements(array) ((sizeof(array)/sizeof(array[0])))

class uint_proxy 
    unsigned value;
public:
    uint_proxy() : value(0) 
    uint_proxy& operator++()  ++value; return *this; 
    unsigned operator++(int)  return value++; 
    operator unsigned() const  return value; 
;

int main()     
    int b[]=2,4,3,5,2,6,6,3,6,4;

    boost::bimap<int, boost::bimaps::list_of<uint_proxy> > a;

    // walk through array, counting how often each number occurs:
    for (int i=0; i<elements(b); i++) 
        ++a.left[b[i]];

    // print out the most frequent:
    std::cout << a.right.rbegin()->second;

目前,我只打印出最频繁的数字,但迭代 N 次以打印出最频繁的 N 非常简单。

【讨论】:

优雅的解决方案。激励我在未来使用 boost bimaps。但是,我认为在最终打印出来之前它缺少a.right.sort()。默认情况下,列表视图中的元素将按其插入顺序(而不是按其值)排序,请参阅here。【参考方案4】:

如果您只对前 N 个最常用的词感兴趣,并且不需要精确,那么您可以使用一个非常聪明的结构。我是通过 Udi Manber 听说的,它的工作原理如下:

您创建了一个包含 N 个元素的数组,每个元素跟踪一个值和一个计数,您还保留了一个索引该数组的计数器。此外,您有一个从值到索引到该数组的映射。 每次用一个值(如文本流中的一个词)更新结构时,您首先检查您的地图以查看该值是否已经在您的数组中,如果是,则增加该值的计数。如果不是,则减少计数器指向的任何元素的计数,然后增加计数器。

这听起来很简单,算法的任何内容都使它看起来会产生任何有用的东西,但对于典型的真实数据,它往往做得很好。通常,如果您希望跟踪前 N 个事物,您可能希望使这个结构的容量为 10*N,因为其中会有很多空值。使用 King James Bible 作为输入,以下是该结构列出的最常用词(无特定顺序):

0 : in
1 : And
2 : shall
3 : of
4 : that
5 : to
6 : he
7 : and
8 : the
9 : I

这里是前十个最常用的词(按顺序):

0 : the ,  62600
1 : and ,  37820
2 : of ,  34513
3 : to ,  13497
4 : And ,  12703
5 : in ,  12216
6 : that ,  11699
7 : he ,  9447
8 : shall ,  9335
9 : unto ,  8912

您会看到,前 10 个单词中有 9 个是正确的,而且它只使用了 50 个元素的空间。根据您的用例,此处节省的空间可能非常有用。它也非常快。

这是我用 Go 编写的 topN 的实现:

type Event string

type TopN struct 
  events  []Event
  counts  []int
  current int
  mapped  map[Event]int

func makeTopN(N int) *TopN 
  return &TopN
    counts: make([]int, N),
    events: make([]Event, N),
    current: 0,
    mapped: make(map[Event]int, N),
  


func (t *TopN) RegisterEvent(e Event) 
  if index, ok := t.mapped[e]; ok 
    t.counts[index]++
   else 
    if t.counts[t.current] == 0 
      t.counts[t.current] = 1
      t.events[t.current] = e
      t.mapped[e] = t.current
     else 
      t.counts[t.current]--
      if t.counts[t.current] == 0 
        delete(t.mapped, t.events[t.current])
      
    
  
  t.current = (t.current + 1) % len(t.counts)

【讨论】:

【参考方案5】:

给定一个每行包含一个单词的文件,计算最常见的 n 个数字。 ... 我在互联网上搜索了它并阅读了该哈希表和优先级队列可能会提供具有 O(n)

的算法

如果您的意思是 *n*s 是相同的,那么不,这是不可能的。但是,如果您只是指时间与输入文件的大小呈线性关系,那么使用哈希表的简单实现就可以满足您的需求。

可能存在具有亚线性记忆的概率近似算法。

【讨论】:

以上是关于算法:计算单词列表频率的更好方法的主要内容,如果未能解决你的问题,请参考以下文章

计算单词的频率和java8中列表的返回列表[重复]

使用熊猫将列表中的单词与频率列表中的单词进行比较

从频率词典中获取字数和平均长度

使用 R 进行文本挖掘来计算单词的频率

如何计算文本文件中重复单词的频率?

Python中的项目频率计数