PriorityQueue,带有用于保持计数排序的索引

时间:2015-01-01 20:13:17

标签: java sorting heap counting

我经常在Java中遇到的问题(通常在编写计算语言学代码时)是需要计算数据集中某些项目的出现次数,然后按项目的计数对项目进行排序。最简单的具体例子是字数统计:我需要计算文本文件中每个单词的出现次数,然后按照计数对单词进行排序,以找到最常用的单词。

不幸的是,Java似乎没有适合此任务的良好数据结构。我需要在计算时使用单词作为集合的索引,这样我每次读取单词时都能有效地查找正确的计数器,但我想要排序的值是计数,而不是词语的

Map<String, Integer>提供了查找与单词关联的计数所需的界面,但地图只能按其键排序(即TreeMap)。 PriorityQueue是一个很好的堆实现,可以对你给它的任何比较器进行排序,但是它无法通过某种索引访问元素,也无法更新和重新堆积元素(除了通过删除并添加它)。它的单一类型参数也意味着我需要将单词和它们的计数一起粘贴到一个对象中才能使用它。

我当前的“解决方案”是将计数存储在地图中,然后将它们全部复制到PriorityQueue进行排序:

Map<String, Integer> wordCounts = countStuff();
PriorityQueue<NamedCount> sortedCounts = new PriorityQueue<>(wordCounts.size(),
                                             Collections.reverseOrder());
for(Entry<String, Integer> count : wordCounts.entrySet()) {
    sortedCounts.add(new NamedCount(count.getKey(), count.getValue()));
}

(注意NamedCount只是一个简单的pair<string, int>,它实现Comparable来比较整数。但这是低效的,特别是因为数据集可能非常大,并且在内存中保留两份计数集是浪费。

有没有什么方法可以随机访问PriorityQueue内的对象,这样我就可以在PriorityQueue中存储一个计数副本,并在更新它们时重新堆积?使用Map<String, NamedCount>来保持PriorityQueue<NamedCount>中对象的“指针”是否有意义?

2 个答案:

答案 0 :(得分:2)

首先,对于基础数据结构,通常Guava的Multiset<String>优于Map<String, Integer>,与Set<String>优于Map<String, Boolean>的方式相同。它是一个更干净的API,并封装了递增。

现在,如果这是我,我会实现一个自定义Multiset,它会添加一些额外的逻辑来索引计数,并返回它们。像这样:

class IndexedMultiset<T extends Comparable<T>> extends ForwardingMultiset<T> {

    private final Multiset<T> delegate = HashMultiset.create();
    private final TreeMultimap<Integer, T> countIndex = TreeMultimap.create();

    @Override
    protected Multiset<T> delegate() {
        return delegate;
    }


    @Override
    public int add(T element, int occurrences) {
        int prev = super.add(element, occurrences);
        countIndex.remove(prev, element);
        countIndex.put(count(element), element);
        return prev;
    }

    @Override
    public boolean add(T element) {
        return super.standardAdd(element);
    }

    //similar for remove, setCount, etc


}

然后我根据计数添加您需要的任何查询功能。例如,以降序检索可迭代的单词/计数对可能如下所示:

public Iterable<CountEntry<T>> descendingCounts() {
    return countIndex.keySet().descendingSet().stream()
            .flatMap((count) -> countIndex.get(count).stream())
            .map((element) -> new CountEntry<>(element, count(element)))
            .collect(Collectors.toList());
}

public static class CountEntry<T> {
    private final T element;
    private final int count;

    public CountEntry(T element, int count) {
        this.element = element;
        this.count = count;
    }

    public T element() {
        return element;
    }

    public int count() {
        return count;
    }

    @Override
    public String toString() {
        return element + ": " + count;
    }
}

它会像这样使用:

public static void main(String... args) {
    IndexedMultiset<String> wordCounts = new IndexedMultiset<>();

    wordCounts.add("foo");
    wordCounts.add("bar");
    wordCounts.add("baz");
    wordCounts.add("baz");

    System.out.println(wordCounts.descendingCounts()); //[baz: 2, bar: 1, foo: 1]


    wordCounts.add("foo");
    wordCounts.add("foo");
    wordCounts.add("foo");

    System.out.println(wordCounts.descendingCounts()); //[foo: 4, baz: 2, bar: 1]
}

答案 1 :(得分:1)

如果您可以使用像Guava这样的第三方库,Multiset非常专门用作解决此问题的方法:

Multiset<String> multiset = HashMultiset.create();
for (String word : words) {
  multiset.add(word);
}
System.out.println(Multisets.copyHighestCountFirst(multiset));