如何实施随时间衰减的计数器?

时间:2017-02-17 10:07:12

标签: algorithm data-structures

特殊计数器的要求

我想实现一个特殊的计数器:所有增量操作在一段固定的时间(比如30天)后超时。

一个例子:

  • 第0天:计数器= 0. TTL = 30天
  • 第1天:增加计数器(+1)
  • 第2天:递增计数器(+1)
  • 第3天:计数器的值== 2
  • 第31天:计数器的值== 1
  • 第32天:计数器的值== 0

天真的解决方案

天真的实现是维护一组时间戳,其中每个时间戳等于增量的时间。减去所有已超时的时间戳后,计数器的值等于该组的大小。

这个天真的计数器有O(n)空间(集合的大小),有O(n)查找和O(1)插入。这些值是准确的。

更好的解决方案(对我来说)

准确性的交易速度和记忆。

我想要一个带O(1)查找和插入O(1)空间的计数器。精度<精确。

或者,我会接受O(log n)空格并查找。

计数器表示应适合存储在数据库字段中,即,我应该能够快速更新和轮询计数器,而不会产生太多(反)序列化开销。

我实际上是在寻找一个类似于HyperLogLog计数器的计数器,但是对于不同类型的近似计数:衰减增量与不同元素的数量

我怎么能实现这样的计数器?

3 个答案:

答案 0 :(得分:4)

如果您可以使用24小时粒度,那么您可以将计数器装入k桶中,其中k是最长TTL中的天数。

递增是一个O(1)操作 - 只需使用索引(k-TTL)增加存储桶中的值,以及当前的总和。

读取是另一个O(1)操作,因为您只需读取当前总和。

cronjob每晚都会从现在过期的桶中弹出(并在另一端添加一个值为0的桶)并将该计数器减少该桶中的总和(这是一个后台任务,因此它不会影响您的插入或阅读操作)

答案 1 :(得分:3)

基于退火的衰减计数器

这是一个基于退火的计数器(在Python中实现)。

  • 计数器随着时间的推移呈指数衰减;由费率class AnnealingCounter(): def __init__(self, alpha=0.9): self.alpha = alpha # rate of decay self.last_t = .0 # time of last increment self.heat = .0 # value of counter at last_t def increment(self, t=None, amount=1.0): """ t is a floating point temporal index. If t is not provided, the value of last_t is used """ if t is None: t = self.last_t elapsed = t - self.last_t if elapsed < .0 : raise ValueError('Cannot increment the counter in the past, i.e. before the last increment') self.heat = amount + self.heat * (self.alpha ** elapsed) self.last_t = t def get_value(self, t=None): """ t is a floating point temporal index. If t is not provided, the value of last_t is used """ if t is None: t = self.last_t elapsed = t - self.last_t if elapsed < .0 : raise ValueError('Cannot increment the counter in the past, i.e. before the last increment') return self.heat * (self.alpha ** elapsed) def __str__(self): return str('Counter value at time {}: {}'.format(self.last_t, self.heat)) def __repr__(self): return self.__str__()
  • 控制
  • 当您读写计数器时,您提供时间索引(在时间t递增或读取计数器)
  • 您可以在当前和未来阅读计数器(w.r.t.最后一次增量的索引),但不是过去
  • 顺序增量的时间指数必须微弱地单调增加

算法准确w.r.t。替代配方(退火与TTL)。它有O(1)增量和读数。它消耗O(1)空间,实际上只有三个浮点字段。

>>> c = AnnealingCounter(alpha=0.9)
Counter has value 0.0 at time 0.0

>>> c.increment()  # increment by 1.0, but don't move time forward
Counter has value 1.0 at time 0.0
>>> c.increment(amount=3.2, t=0.5)  # increment by 3.2 and move time forward (t=0.5)
Counter has value 4.14868329805 at time 0.5
>>> c.increment()  # increment by 1.0, but don't move time forward
Counter has value 5.14868329805 at time 0.5


>>> c.get_value()  # get value as after last increment (t=0.5)
5.148683298050514
>>> c.get_value(t=2.0)
4.396022866630942  # get future value (t=2.0)

以下是如何使用它:

declare List<String> intrestedIn= new ArrayList<String>();

答案 2 :(得分:1)

由于增量按照它们发生的顺序到期,因此时间戳形成一个简单的队列。

计数器的当前值可以单独存储在 O(1)附加存储器中。在每个操作开始时(插入或查询),当队列的前面过期时,它会从队列中弹出,并且计数器会减少。

请注意,每个 n 时间戳都会创建并弹出一次。因此,您有 O(1)分配时间来访问当前值, O(n)内存来存储未过期的时间戳。实际最高内存使用量也受新时间戳插入的TTL /频率比限制。