支持范围查询的可扩展多维数据结构

时间:2016-12-15 08:14:50

标签: java data-structures

让我先提出问题:考虑到我将进一步描述的情况和要求,哪些数据结构有意义/有助于实现非功能性要求?

我试图查找几个结构但到目前为止还不是很成功,这可能是因为我错过了一些术语。

由于我们将在Java中实现这一点,因此任何答案都应考虑到这一点(例如,没有指针魔术,假设8字节引用等)。

情况

我们有一些大的值通过一个4维键映射(让我们调用那些维A,B,C和D)。每个维度可以有不同的大小,因此我们假设以下内容:

  • A:100
  • B:5
  • C:10000
  • D:2

这意味着完全填充的结构将包含1000万个元素。不考虑它们的大小,单独保存引用所需的空间将是80兆字节,因此这将被视为内存消耗的下限。

我们进一步可以假设结构不会被完全填满,而是非常密集。

要求

由于我们经常构建和查询该结构,因此我们有以下要求:

  • 构建结构应该快速
  • 对单个元素和范围的查询(例如[A1-A5,B3,任何C,D0])应该是有效的
  • 不需要快速删除元素(不会经常发生)
  • 内存占用率应该低

我们已经考虑过的事情

KD树

构建这样一棵树需要一些时间,因为它可以变得非常深,我们要么接受较慢的查询,要么采取重新平衡措施。另外,由于我们需要在每个节点中保存完整的密钥,因此内存占用量非常高(尽管可能有减少这种方法的方法)。

嵌套地图/地图树

使用嵌套地图,我们只能存储每个维度的关键字以及对下一个维度图或值的引用 - 从这些地图中有效地构建树。为了支持范围查询,我们保留可能密钥的有序集合,并在遍历树时访问这些密钥。

构建和查询比使用kd-tree更快,但内存占用率更高(正如预期的那样)。

单个大型地图

另一种方法是保留各个可用密钥的集合,改为使用单个大型地图。

构造和查询速度也很快但由于每个地图节点都较大(因此需要保存密钥的所有维度),因此内存消耗更高。

我们目前正在考虑的是

为维度键构建插入顺序索引映射,即我们将每个传入键映射到新的整数索引。因此,我们可以确保这些索引一步一步增长而没有任何间隙(不考虑缺失)。

使用这些索引,我们然后访问一个n维数组的树(当然,扁平化为1-d数组) - 也就是n-ary树。那棵树会根据需要增长,即如果我们需要一个新阵列,那么不是创建一个更大的阵列而是复制所有数据,我们只需创建新块。将根据需要创建任何所需的非叶节点,并在需要时替换根。

让我用2维A和B的例子来说明。我们将为每个维度分配2个元素,得到一个2x2矩阵(长度为4的数组)。

添加第一个元素A1 / B1,我们得到这样的结果:

[A1/B1,null,null,null]

现在我们添加元素A2 / B2:

[A1/B1,null,A2/B2,null]

现在我们添加元素A3 / B3。由于我们无法将新元素映射到现有数组,因此我们将创建一个新元素以及一个公共根:

                [x,null,x,null]  
                /        \
[A1/B1,null,A2/B2,null]  [A3/B3,null,null,null]

密集填充矩阵的内存消耗应该相当低,具体取决于每个数组的大小(在数组中每个维度有4个维度和4个值,我们有长度为256的数组,因此获得最大树深度在大多数情况下2-4。)

这有意义吗?

2 个答案:

答案 0 :(得分:1)

如果结构“非常密集”,那么我认为假设它将是满的是有意义的。这简化了事情。并且你不会使用密集填充矩阵的稀疏矩阵表示来节省很多(或任何东西)。

我先尝试最简单的结构。它可能不是最有效的内存,但它应该合理且易于使用。

首先,一个包含10,000,000个引用的简单数组。那是(请原谅C#,因为我不是真正的Java程序员):

MyStructure[] theArray = new MyStructure[](10000000);

正如你所说,这将消耗80兆字节。

接下来是四个不同的词典(地图,我认为,在Java中),每个键类型一个:

Dictionary<KeyAType, int> ADict;
Dictionary<KeyBType, int> BDict;
Dictionary<KeyCType, int> CDict;
Dictionary<KeyDType, int> DDict;

当您在{A,B,C,D}添加元素时,您在字典中查找相应的键以获取其索引(或者如果该键不存在则添加新索引),并执行math用于计算数组的索引。数学是,我想:

DIndex + 2*(CIndex + 10000*(BIndex + 5*AIndex));

在.NET中,字典开销类似于每个键24个字节。但是你只有11,007个密钥,所以字典会消耗250千字节。

直接查询应该非常快,范围查询应该与单个查找一样快,然后进行一些数组操作。

我不清楚的一件事是,如果你想要一个密钥,要在每次构建时解析为相同的索引。也就是说,如果“foo”映射到一个构建中的索引1,它是否总是映射到索引1?

如果是这样,您可能应该静态构建字典。我想这取决于您的范围查询是否总是期望按相同的键顺序排列。

无论如何,这是一个非常简单且非常有效的数据结构。如果您能够承受81兆字节作为结构的最大大小(减去实际数据),它似乎是一个好的起点。你可能会在几个小时内完成它。

充其量只是你必须要做的。如果您最终必须更换它,至少您有一个可用的实现,可用于验证您提出的任何新结构的正确性。

答案 1 :(得分:1)

还有其他多维树通常比kd树更好:quadtreesR*Trees(如R-Tree,但更新速度更快)或PH-Tree。 PH-Tree就像一个四叉树,但空间效率更高,尺寸和深度更好地扩展受到最大值的位宽限制,即最大值10000&#39;需要14位,因此深度不会超过14。

所有树的Java实现都可以在我的仓库中找到,here(四叉树可能有点儿错误)或here

修改 可以忽略以下优化。当然,所描述的查询将导致完整扫描,但这可能没有听起来那么糟糕,因为它平均无论如何都会返回整棵树的33%-50%。

可能的优化(未经过测试,但可能适用于PH-Tree):

范围查询的一​​个问题是维度的不同选择性,这可能会导致对树的完整扫描。例如,查询[0..100] [0..5] [0..10000] [1..1]时,即仅约束最后一个维度(选择性最小)。

为避免这种情况,特别是对于PH树,我会尝试将您的值乘以固定常数。例如,将A乘以100,B乘以2000,C乘以1,D乘以5000.这允许所有值的范围为0到10000,当仅约束具有低选择性的维度时,可以提高查询性能(第二或第四)。