假设您对一堆独立的时变值感兴趣,每个值都代表某事物的当前状态。这些值不会在任何固定的时间表上发生变化,并且无法从旧值中预测新值。为了一个具体的例子,假设您有一堆股票,并且您有兴趣跟踪它们的价值,并且只要对该股票进行交易,您就会获得有关个别股票的更新。 (我的实际问题不是关于股票,而是希望他们能让我更容易理解。)
除了了解每只股票的当前价格之外,您还希望能够选择过去的任意点并获得一个“快照”,告诉您每只股票的最新交易价格是多少那时候。因此,举例来说,你应该可以说“我上周二下午4点53分跟踪的每股股票的最近价值是多少?”并有效地得到准确的答案。
我可以想到三种方法,但我对它们中的任何一种都不满意。
1。保留期刊。按时间顺序维护所有交易的清单。更新只是添加到列表中,并且查询是向后的线性扫描,从时间戳开启或早于指定时间戳的第一个条目开始。这将使更新成为一个恒定时间操作,但您可能必须扫描整个日志以查找所有交易的值,因此Update为O(1),Snapshot为O(u),其中u是更新总数。由于显而易见的原因,所需的内存是O(u)。
2。写检查点。 像以前一样维护单个日记帐,但更新包含每个股票的当前价格(截至该更新),而不是仅包含新股票价格的每个条目。计算起来很便宜:由于上次更新也包含所有这些信息,因此除了价格实际发生变化的股票外,您只需复制它。现在可以使用O(log u)操作完成快照(使用日志上的二进制搜索来查找指定时间戳之前或之后的最后一个条目)。然而,更新变为O(s),其中s是系统中的股票数量,此外,所需的总内存从第一个策略中的O(u)变为O(s * u)---两者都是问题,如果s和u都很大。
第3。分开的期刊。 为每个股票维护一个单独的期刊,并按时间顺序将每个股票的更新写入其自己的期刊。要快照,请检查每个日记并使用二进制搜索来查找正确的更新。它需要O(u)内存,Update是O(1)操作,快照可以在O(s * log u)时间内完成。这是我最喜欢的三种方法,但我觉得它可能会有所改进,因为它忽略了不同股票更新时间之间的任何关系。
我错过了更好的方法吗?这是一个已经研究过并且有一个普遍接受的解决方案的问题吗?
答案 0 :(得分:4)
查看有关持久性数据结构的文献。特别地,this early paper描述了维持对数运算的持久二进制搜索树的构造,但是可以在任何版本(例如,时间点)访问。对某些特定版本中未更新的结构部分的访问自然会查看最后一个版本。因此,您可以在O(log)时间内进行自然操作,如果您事先知道所有密钥并且永远不必重新平衡,则结构可能占用O(u)空间,如果您知道O(u * log s)空间,则每次更新都修改了O(log s)指针。
These class notes似乎用相当简单的术语描述了你需要实现的内容。
答案 1 :(得分:2)
我怀疑你会找到一个在所有措施中都很出色的解决方案。你选择什么在很大程度上取决于你愿意做出的权衡。如果快照很少,#3很棒;如果它们很频繁,可能不是:例如,O( S log U )可能是源控制存储库的杀手。
以下是我的一些其他想法:
<强> 4。定期检查点。在指定的时间间隔内(每 x 小时,每次 y 更新,无论如何)都会生成一个检查点,其中包含每个库存的当前价格。在过去的时间点确定数据意味着在该时间之前找到最新的快照,然后在之后添加各个更新。这将具有与#2相同的渐近性能,但更新和内存使用的乘法常数会低很多,因为你拍摄的快照要少得多。
<强> 5。仅限Delta检查点。与#4相同,但不拍摄整个系统的快照。而是仅存储自上一个检查点以来已更改的项目。未更改的条目将在先前的检查点中查找。这样可以节省大量编写检查点的时间并大大减少内存使用量。如果Δ U 是检查点之间的平均更新次数,则这两个现在都是O(Δ U )。这实际上是一个固定的数额;数据库会随着时间的推移而增长,但不会增加每个检查点的平均更新次数。您可以将更新时间视为已摊销的O(1),将内存使用量视为O( U ),然后。
几年前,我写了一个wiki克隆。我遇到的一个问题是如何存储页面增量。我只存储差异,还是每次更新都存储整页文本?如何平衡速度与内存使用情况?连续应用数十个或数百个差异来重建页面可能会过于缓慢,但如果有人只更改一个句子则存储整个页面将非常浪费。
我想要的东西即使对于经常更新的大页面也能很好地扩展。
我最终采用类似于#5的混合方法。我存储带有定期整页快照的差异。为了确定何时拍摄快照,我将新页面文本与最新快照的文本进行比较。如果diff大小超过整页文本的一半,我会存储整页文本而不是diff。这样,如果人们进行小型更新,那么我可以存储差异,但最终一旦页面发生了足够的变化,我就会拍摄新的快照。
答案 2 :(得分:2)
Novelocrat提出的持久数据结构的想法似乎是一般案例的最佳解决方案。我想在你的情况下它会正常工作。
我只是想到了(2)的变体。管理按修改时间戳排序的动态数组。每个条目对应一个版本,它由一个s项目数组组成。而不是每个版本存储所有库存记录,而是lazilly;创建版本时,只有一个其值已更改的库存项目将被分配一个新记录。其他s-1项指向null。
当对时间T和库存S执行搜索时,您应该向后线性扫描版本,从时间T之前的最新版本开始。扫描继续,直到您找到S的非空值。然后您继续修复您在路上找到的所有S的空指针,以便对它们进行下一次查询是有效的。
此解决方案提供O(1)添加时间,以及摊销查询时间O(log u)。完整的快照查询需要O(s + log u),这比实现(4)更好。空间仍然是O(u * s)。
查询的分摊成本来自于每当我们查询版本V的项目S时,版本&lt; = V的所有S值都是固定的。因此,一系列u唯一查询会对阵列执行2 * u访问(无论其顺序如何!),每次查询平均会有2次访问。因此,我们保持O(log u)的初始查找时间。