4

我正在寻找一种有效的数据结构,它可以让我提示事件......也就是说,我将拥有一个应用程序,在执行的任何时候,都有可能为未来引发一个事件执行点...类似于:

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

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

我的第一个想法是,也许要做某种树,但我想删除到期事件部分需要大量的重新平衡......

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

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


编辑:

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

谢谢

back2dos

4

4 回答 4

10

我相信您正在寻找一个优先级队列,其中事件发生时的时间戳是优先级(嗯,较低的时间戳将是较高的优先级)

只需对您的用例进行一些说明:

...我可以在未来任何时间参加任何活动...

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

...以及我可以在哪里获得并(通过这样做)删除所有到期事件...

您将重复调用 getTop(获取具有最低时间戳的事件)收集所有元素直到感兴趣的时间。

...另外,如果我能够从数据结构中删除一个事件(因为它已被取消),那将是一个加分项......虽然不是太重要,因为我可以简单地将其标记为已取消..

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

于 2009-10-01T13:30:45.253 回答
3

N有多大?与其他所有事情相比,您需要多久插入和移除一次项目?如果这超过总执行时间的 10%,并且如果 N 通常超过 100(比如说),也许是时候关注 big-O 了。我见过使用花哨的容器算法实现优先级队列的程序,分配迭代器、哈希映射、堆等,并将所有时间都花在创建和释放抽象对象上,其中队列长度的中值约为3

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

于 2009-10-01T21:12:46.067 回答
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) 的处理成本,尽管这对于散列来说并不是太微不足道。听起来很酷,实际上。还是我错过了什么?当然,要让一个hash表现良好并不是那么容易,但除此之外,还有什么问题吗?还是哈希有问题?Wikipedia 指出:
      “在尺寸良好的哈希表中,每次查找的平均成本(指令数)与存储在表中的元素数量无关。许多哈希表设计还允许任意插入和删除键值对,以每项操作的恒定平均(实际上是摊销)成本计算。”
      一个快速的基准测试表明,我的平台的标准实现似乎与此相匹配。
    • DVK 提供的事件列表数组方法。这有 O(1) 插入。现在这很好。但如果我理解正确,它有 O(M+T) 处理成本,其中 T 是数组的大小(如果你愿意的话,是时隙的数量),因为从数组中移除是以线性成本为代价的。此外,这仅在存在最大时间偏移时才有效。

实际上,我想讨论数组方法。O(M+T) 不好。一点也不。但是我花了一些脑筋,这就是我想出的:

第一个想法:懒惰

O(T) 可以被任意因素压缩,引入一点惰性,但最终它会保持 O(T)。但这有多糟糕?假设 T=2419200,即 28 天。然后,我会每天清理一次(最好在预计负载较低的情况下)。这将浪费不到 5% 的阵列。在我的目标平台上,复制操作在相当旧的 2GHz 内核上需要 31 毫秒,所以这似乎不是一个坏主意。

第二个想法:块

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

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

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


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

我会非常感谢您对此的任何意见。

于 2009-10-05T15:09:13.840 回答
1

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

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

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

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

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

于 2009-10-01T13:32:52.613 回答