用于处理未来事件的查找结构(基于时间)

时间:2009-10-01 13:26:01

标签: performance language-agnostic data-structures advanced-queuing

我正在寻找一种有效的数据结构,这将允许我提示事件......也就是说,我将拥有一个应用程序,在执行的任何时候,有可能会引发一个事件对于未来的执行点......如:

  • t = 20:在420秒内,A出现
  • t = 25:在13秒内,B出现
  • t = 27:在735秒内,C发生
  • ...

所以我想拥有一个数据结构,我可以在任何时间放入任何时间以及我可以获得的地方(通过这样做)删除所有应有的事件......还有,是的,如果我能够从数据结构中删除一个事件(因为它已被取消)...虽然不太重要,因为我可以简单地将其标记为已取消......

我的第一个想法是,可能要做某种树,但我想删除 - 因事件部分需要大量的再平衡......

我正在考虑简单地使用一个int哈希,将时间戳映射到null或在那个时间点发生的事件堆栈......我认为在场景中,有很多事件(可能每秒多次 - 哪个是我打算合作的),毕竟这实际上并不是一个坏主意......

所以我很想听听你的意见......:)


修改

  • 更具体一点:我认为这里约为100K-1M,我想我可能会有大约1-100个事件/秒......
  • t并不特别重要......它只是为了说明未来的事件可以随时“排队”......

感谢

back2dos

4 个答案:

答案 0 :(得分:10)

我相信你正在寻找一个Priority Queue,其中包含事件发生时的时间戳作为优先级(好的,更低的时间戳将是更高的优先级)

对您的使用案例稍作澄清:

  

...我可以把任何事件放进去   将来的任何时候......

使用insertWithPriority插入优先级队列,使用事件发生时的时间戳。这将是O(lgN)

  

......我可以得到的地方(通过这样做   所以)删除所有到期事件......

你反复调用getTop(获取时间戳最短的事件)收集所有感兴趣的元素。

  

......如果我是的话,还会加分   能够从中移除一个事件   数据结构(因为它是   取消)......不太重要   但是,因为我可以简单地将其标记为   取消..

这是可能的,但由于重新平衡将是O(lgN)。

答案 1 :(得分:3)

N有多大?您多久需要插入一次&删除项目,与其他一切相比?如果这超过总执行时间的10%,并且如果N通常超过100(比如说),那么可能是时候关注big-O了。我已经看到使用花哨的容器算法实现优先级队列的程序,分配迭代器,哈希映射,堆等,并花费所有时间来创建和释放抽象对象,其中中间队列长度就像

ADDED:好的,因为N~10 ^ 6,频率是~100hz,你可能想要某种二进制树或堆的O(log(N))插入/移除时间。如果你愿意将1%的CPU时间用于此,那就是10 ^ 6微秒* 1%/ 100 = 10 ^ 2微秒/操作。这应该不是很困难,因为如果典型的搜索深度是20,每次比较大约50ns,那么进行搜索大约需要1微秒。只要确保保持简单,不要将所有内容都包含在抽象数据类型中。您不必担心分配/释放树节点所花费的时间,因为每个操作只分配/释放一个节点。不需要经常进行重新平衡,例如可能仅在每1000次操作之后。如果您可以批量收集插入,然后以随机顺序插入它们,这可能会阻止树太不平衡。如果您的许多事件是同步的,您可以在时间码中添加少量噪音,以防止树的某些部分变得更像线性列表。

答案 2 :(得分:2)

好的,我要感谢大家的答案 - 非常有趣和乐于助人。 :)

PriorityQueue绝对是我正在寻找的合适名词 - 谢谢你。 现在一切都是关于实施的。

以下是我的想法:

设N是队列的大小,M是处理时每个时间戳的平均事件量(可以说是“并发”事件)(事件密度不均匀分布,“远期未来” “变得更加稀疏,但随着时间的推移,这个时间区域变得更加密集(实际上,我认为最大密度将在未来的4到12小时内某处))。我正在寻找一个可扩展的解决方案,对于相当大的M表现良好。目标是在一秒钟内真正处理那些M到期事件,所以我想花最少的时间来寻找它们。

  1. 按照建议的简单方法,我会有O(log N)插入,这是非常好的,我想。处理一个时间戳的成本是O(M * log N),如果我是对的,那就不再那么好了。
    • 另一种方法是,使用包含事件列表的而不是单个事件。如果没有列表存在,那么实现一些getlistForGivenStampAndCreateIfNoneExists操作应该是可行的,这比在树上下两次要快一些。但无论如何,随着M的增长,这甚至不应该太重要。因此插入将是O(log N),如前所述,并且处理将在O(M + log N),我认为这也是好的。
    • 事件列表哈希方法,我制定了。这也应该具有O(1)插入和O(M)处理成本,尽管这对于哈希来说并不是太微不足道。听起来很酷,实际上。或者我错过了什么?当然,使哈希表现不是那么容易,但除此之外,还有什么问题吗?哈希问题是哈哈?维基百科指出:“在一个尺寸合适的哈希表中,每个查找的平均成本(指令数)与表中存储的元素数量无关。许多哈希表设计也允许任意插入以及每次操作的固定平均(实际上是摊销)成本的键值对的删除和删除。“
      快速基准测试表明,我的平台的标准实现似乎与此相符。
    • DVK提供的事件列表数组方法。这有O(1)插入。现在这很好。但是,如果我理解正确,它有O(M + T)处理成本,T是数组的大小(如果你愿意的话,时隙数),因为从数组中删除是线性成本。此外,这仅在存在最大时间偏移时才有效。
  2. 实际上,我想讨论阵列方法。 O(M + T)不好。一点也不。但是我把它放在脑子里,这就是我想出的:

    第一个想法:懒惰

    O(T)可能会受到任意因素的影响,引入一些懒惰,但最终它会保持O(T)。但那有多糟糕?我们有T = 2419200,这是28天。然后,每天我会清理一次(最好是在预期低负荷时)。那浪费不到阵列的5%。在我的目标平台上,复制操作在一个相当老的2GHz核心上需要31毫秒,所以它毕竟不是一个坏主意。

    第二个想法:大块

    在思考了一下后,我想到了这个解决方案:间隔散列,间隔(即给定时间范围)又是一个事件列表数组。间隔都是相同的大小,最好是简单的,如几天或几小时。

    对于插入,我通过哈希查找正确的间隔(如果不存在则创建),并在间隔中查找正确的事件列表(如果不存在则再次创建)然后只插入它,即O( 1)。

    为了处理,我只需要处理当前到期事件列表,然后处理它,只需获取当前间隔并处理到期事件。数组保持恒定长度,因此我们处于O(M)(这是处理M元素时最好的)。一旦当前间隔被完全处理(因此如果间隔现在代表“过去”),我只需将其处理为O(1)。我可以保留对当前间隔的额外参考,无需查找,但我想这并没有提供任何显着的改进。


    在我看来,第二次优化确实是最好的解决方案,因为它快速且无约束。为间隔选择合适的大小允许优化内存开销与散列查找开销。我不知道,我是否应该担心哈希查找时间。对于高M,它应该不重要,是吗?因此,我选择间隔大小为1,这使我回到接近3号。

    我真的非常感谢你的任何投入。

答案 3 :(得分:1)

如果您的事件有一个明确定义的上限(例如,将来没有事件发生在2天以后),您可以简单地将一个数组从“开始时间”开始的秒数索引。 数组的值是该偏移量处的事件列表。

列表或删除非常有效 - 只需找到您希望列出或切断的时间的偏移量,然后获取或重新初始化该偏移量后索引所指向的数组。

如果您的事件可以无限延伸到未来,那么您自己想要使用从偏移到事件列表的散列图是最好的,有一个扭曲 - 有一个排序列表(但是你希望实现它)已知的偏移量,这样你就可以获得非常有效的查找(例如,你不必遍历地图上的每个关键点)。

您不需要从已知偏移列表中删除任何内容,因此不需要重新平衡 - 只需从hashmap指向的数组中删除。

此外,您的问题似乎还不清楚是否需要知道“t” - 事件发生的时间。如果您需要了解它,请将其存储为活动的一部分。但是事件发生时的引用应该都是绝对的一些起点(如果它是一个无限范围的散列图,你可以使用epoch秒,如果事件有像我列出的第一个数组解决方案那样的边界,你应该而是使用“自范围开始以来的秒数” - 例如从昨天开始。