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

时间:2012-04-17 23:48:33

标签: c++ performance algorithm data-structures

这个问题实际上非常简单,但我希望在进入编码之前听到一些想法。给定每行中包含单词的文件,计算最多n个频繁的数字。

第一个也是唯一一个在我脑海中突然出现的事情就是使用std::map。我知道C ++的同事会说unordered_map会非常合理。

我想知道是否有任何东西可以添加到算法方面,或者这基本上是“谁选择了最好的数据结构获胜”类型的问题。我在互联网上搜索过它并读取哈希表和优先级队列可能会提供一个运行时间为 O(n)的算法,但我认为这将是复杂的实现

有什么想法吗?

5 个答案:

答案 0 :(得分:5)

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

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

它将胜过用于计算字符串的哈希表。

答案 1 :(得分:2)

如果您只对前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个元素的空间。根据您的使用情况,这里节省空间可能非常有用。它也很快。

以下是我使用的topN的实现,用Go:

编写
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)
}

答案 2 :(得分:1)

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

  1. 如果您知道唯一字数非常有限,则可以使用TreeMapstd::map
  2. 如果单词数量非常大,那么您可以构建trie并在另一个数据结构中保留各种单词的计数。这可能是一个大小为n的堆(最小值/最大值取决于您想要做什么)。所以你不需要存储所有的单词,只需要存储必要的单词。

答案 3 :(得分:1)

如果我有很多选择,我会std::map(或unordered_map)开头(虽然我不知道其他约束可能适用)。

这里有两个数据项,你使用一个作为时间的关键部分,而另一个作为关键时间的另一部分。为此,您可能需要类似Boost BimapBoost 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最常见的数字非常简单。

答案 4 :(得分:0)

  

给定每行中包含单词的文件,计算大多数 n 频繁数字。   ...   我在互联网上搜索过它并读取哈希表和优先级队列可能会提供 O(n)

的算法

如果你的意思是* n * s相同然后没有,这是不可能的。但是,如果您只是根据输入文件的大小来表示时间线性,那么使用哈希表的简单实现将完成您想要的操作。

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