4

当字典更新其键时,如何有效地跟踪具有最大值的字典的前 k 个键?

我尝试过在每次更新后从字典中创建排序列表的天真方法(如获取字典中具有最大值的键中所述?),但是这种方法非常昂贵且无法扩展

现实世界的例子:

计算来自无限数据流的词频。在任何给定时刻,程序可能会被要求报告一个词是否在当前的前 k 个最常见的值中。我们如何有效地做到这一点?

collections.Counter 太慢了

>>> from itertools import permutations
>>> from collections import Counter
>>> from timeit import timeit
>>> c = Counter()
>>> for x in permutations(xrange(10), 10):
    c[x] += 1


>>> timeit('c.most_common(1)', 'from __main__ import c', number=1)
0.7442058258093311
>>> sum(c.values())
3628800

计算这个值需要将近一秒钟!

我正在寻找一个 most_common()函数的 O(1) 时间。这应该可以通过使用另一个数据结构来实现,该结构仅在内部存储当前的前 k 个项目,并跟踪当前的最小值。

4

3 回答 3

2

collections.Counter.most_common 对所有值进行传递,通过将它们放入堆中找到第 N 个最大的值(我认为,在 O(M log N) 时间内,其中 M 是字典元素的总数)。

heapq,正如 Wei Yen 在评论中所建议的那样,可能工作正常:与字典平行,维护heapqN 个最大值中的一个,当您修改 dict 时检查该值是否在其中或现在应该在其中。问题是,正如您所指出的,接口实际上没有任何方法来修改已经 -现有元素。

您可以就地修改相关项目,然后运行heapq.heapify以恢复 heapiness。这需要对堆 (N) 的大小进行线性传递以找到相关项目(除非您正在做额外的簿记以将元素与位置相关联;可能不值得),以及另一个线性传递来重新堆化。如果某个元素不在列表中而现在在列表中,则需要通过替换最小元素将其添加到堆中(在线性时间内,除非有一些额外的结构)。

但是,heapq 私有接口包含一个_siftdown具有以下注释的函数:

# 'heap' is a heap at all indices >= startpos, except possibly for pos.  pos
# is the index of a leaf with a possibly out-of-order value.  Restore the
# heap invariant.

听起来不错!调用heapq._siftdown(heap, 0, pos_of_relevant_idx)将在日志 N 时间内修复堆。当然,您必须首先找到要递增的索引的位置,这需要线性时间。您可能会维护一个索引元素字典以避免这种情况(还保留一个指向最小元素位置的指针),但是您要么必须复制源_siftdown并修改它以在字典交换时更新字典事情,或者做一个线性时间传递之后重建字典(但你只是试图避免线性传递......)。

小心,这应该可以解决 O(log N) 时间。但事实证明,有一种称为斐波那契堆的东西确实支持您需要的所有操作,在(摊销的)恒定时间内。不幸的是,这是大 O 不是全部的情况之一。斐波那契堆的复杂性意味着在实践中,除了可能非常大的堆之外,它们实际上并不比二叉堆快。此外(也许是“因此”),虽然 Boost C++ 库确实包含一个,但我在快速谷歌搜索中没有找到标准的 Python 实现。

我首先尝试使用heapq,对您正在更改的元素进行线性搜索,然后调用_siftdown; 与该方法的 O(M log N) 相比,这是 O(N) 时间Counter。如果结果太慢,您可以维护额外的索引字典并制作您自己的版本_siftdown来更新字典,这应该会花费 O(log N) 时间。如果这仍然太慢(我怀疑),您可以寻找 Boost 的 Fibonacci 堆(或其他实现)的 Python 包装器,但我真的怀疑这是否值得麻烦。

于 2013-03-15T08:14:31.900 回答
1

使用collections.Counter它已经为那个真实世界的例子做到了。你还有其他用例吗?

于 2013-03-15T06:52:28.500 回答
0

我们可以实现一个跟踪 top-k 值的类,因为我不相信标准库有这个内置的。这将与主字典对象(可能是 a )并行保持最新。Counter您还可以将其用作主字典对象的子类的属性。

执行

class MostCommon(object):
    """Keep track the top-k key-value pairs.

    Attributes:
        top: Integer representing the top-k items to keep track of.
        store: Dictionary of the top-k items.
        min: The current minimum of any top-k item.
        min_set: Set where keys are counts, and values are the set of
            keys with that count.
    """
    def __init__(self, top):
        """Create a new MostCommon object to track key-value paris.

        Args:
            top: Integer representing the top-k values to keep track of.
        """
        self.top = top
        self.store = dict()
        self.min = None
        self.min_set = defaultdict(set)

    def _update_existing(self, key, value):
        """Update an item that is already one of the top-k values."""
        # Currently handle values that are non-decreasing.
        assert value > self.store[key]
        self.min_set[self.store[key]].remove(key)
        if self.store[key] == self.min:  # Previously was the minimum.
            if not self.min_set[self.store[key]]:  # No more minimums.
                del self.min_set[self.store[key]]
                self.min_set[value].add(key)
                self.min = min(self.min_set.keys())
        self.min_set[value].add(key)
        self.store[key] = value

    def __contains__(self, key):
        """Boolean if the key is one of the top-k items."""
        return key in self.store

    def __setitem__(self, key, value):
        """Assign a value to a key.

        The item won't be stored if it is less than the minimum (and
        the store is already full). If the item is already in the store,
        the value will be updated along with the `min` if necessary.
        """
        # Store it if we aren't full yet.
        if len(self.store) < self.top:
            if key in self.store:  # We already have this item.
                self._update_existing(key, value)
            else:  # Brand new item.
                self.store[key] = value
                self.min_set[value].add(key)
                if value < self.min or self.min is None:
                    self.min = value
        else:  # We're full. The value must be greater minimum to be added.
            if value > self.min:  # New item must be larger than current min.
                if key in self.store:  # We already have this item.
                    self._update_existing(key, value)
                else:  # Brand new item.
                    # Make room by removing one of the current minimums.
                    old = self.min_set[self.min].pop()
                    del self.store[old]
                    # Delete the set if there are no old minimums left.
                    if not self.min_set[self.min]:
                        del self.min_set[self.min]
                    # Add the new item.
                    self.min_set[value].add(key)
                    self.store[key] = value
                    self.min = min(self.min_set.keys())

    def __repr__(self):
        if len(self.store) < 10:
            store = repr(self.store)
        else:
            length = len(self.store)
            largest = max(self.store.itervalues())
            store = '<len={length}, max={largest}>'.format(length=length,
                                                           largest=largest)
        return ('{self.__class__.__name__}(top={self.top}, min={self.min}, '
                'store={store})'.format(self=self, store=store))

示例用法

>>> common = MostCommon(2)
>>> common
MostCommon(top=2, min=None, store={})
>>> common['a'] = 1
>>> common
MostCommon(top=2, min=1, store={'a': 1})
>>> 'a' in common
True
>>> common['b'] = 2
>>> common
MostCommon(top=2, min=1, store={'a': 1, 'b': 2})
>>> common['c'] = 3
>>> common
MostCommon(top=2, min=2, store={'c': 3, 'b': 2})
>>> 'a' in common
False
>>> common['b'] = 4
>>> common
MostCommon(top=2, min=3, store={'c': 3, 'b': 4})

更新值后的访问确实是 O(1)

>>> counter = Counter()
>>> for x in permutations(xrange(10), 10):
        counter[x] += 1

>>> common = MostCommon(1)
>>> for key, value in counter.iteritems():
    common[key] = value

>>> common
MostCommon(top=1, min=1, store={(9, 7, 8, 0, 2, 6, 5, 4, 3, 1): 1})
>>> timeit('repr(common)', 'from __main__ import common', number=1)
1.3251570635475218e-05

访问是 O(1),但是当在 O(n) 操作的 set-item 调用期间最小值发生变化时,其中n是最高值的数量。这仍然优于Counter,在每次访问期间都是 O(n),其中n是整个词汇表的大小!

于 2013-03-15T09:23:36.490 回答