让我先提出问题:考虑到我将进一步描述的情况和要求,哪些数据结构有意义/有助于实现非功能性要求?
我试图查找几个结构但到目前为止还不是很成功,这可能是因为我错过了一些术语。
由于我们将在Java中实现这一点,因此任何答案都应考虑到这一点(例如,没有指针魔术,假设8字节引用等)。
情况
我们有一些大的值通过一个4维键映射(让我们调用那些维A,B,C和D)。每个维度可以有不同的大小,因此我们假设以下内容:
这意味着完全填充的结构将包含1000万个元素。不考虑它们的大小,单独保存引用所需的空间将是80兆字节,因此这将被视为内存消耗的下限。
我们进一步可以假设结构不会被完全填满,而是非常密集。
要求
由于我们经常构建和查询该结构,因此我们有以下要求:
我们已经考虑过的事情
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。)
这有意义吗?
答案 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树更好:quadtrees,R*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,当仅约束具有低选择性的维度时,可以提高查询性能(第二或第四)。