【问题标题】:Data structure for partial multi-keys mapping?部分多键映射的数据结构?
【发布时间】:2013-09-01 07:55:54
【问题描述】:

我的数据由映射到值的键组成,如下所示:

---------------------
Key          | Value
---------------------
(0, 0, 0, 0) | a
(0, 0, 0, 1) | b
(0, 1, 0, 1) | c
(0, 1, 1, 0) | d
....

我正在寻找一种可以有效地对键执行搜索查询的数据结构,其中查询可以是完整或部分指定键。例如:

(0, 0, 0, 1) -> a
(0, *, *, *) -> [a, b, c, d]
(0, 1, *, *) -> [c, d]

我现在的想法是使用常规树来实现这一点,类似于: 叶节点表示值,非叶节点是键的一部分(即 w、x、y 和 z 节点分别是键的第一、第二、第三和第四部分)。一个简单的 BFS 算法可以用来回答任何查询。但问题是这棵树随着密钥的每个新部分呈指数增长。

什么数据结构/算法更适合解决这个问题?请注意,关键部分可以是数字或字符串。

【问题讨论】:

  • 我认为这两个答案都有一些优点,但我选择了@zoltan-nagy 答案,因为它比另一个更通用。谢谢哈罗德和佐尔坦纳吉。

标签: algorithm data-structures


【解决方案1】:

一个数组。对真的!您将没有空间开销,没有“指针追逐”开销,计算索引只需要一点点数学,处理器真的很擅长。

假设你得到一个作为maskbits 的部分密钥,其中mask 的位为0,表示通配符,其他位置为1,bits 为0,表示通配符和任何你想要的对于非通配符。

收集具有与该模式匹配的键的所有项目的算法是:

int key = bits;
do {
    yield items[key];
    key = (key | mask) + 1 & ~mask | bits;
} while (key != bits);

key = (key | mask) + 1 & ~mask | bits 部分看起来很有趣,下面是它的工作原理。

|(按位或)使所有非通配符为 1。这确保了增量继续通过非通配符的位。添加之后,应该“固定”的位被破坏(如果进位通过它们,则为 0,否则为 1),因此必须将它们屏蔽掉(& ~mask),然后设置回正确的值(| bits)。运算符的优先级使得它可以在很大程度上不用括号来编写。你也可以写成

key = (((key | mask) + 1) & (~mask)) | bits;

这适用于任何类型的模式。如果您只需要“最后 x 位是可变的”,您可以优化一下:

int wildcards = 0;
int invmask = ~mask;
do {
    yield items[wildcards++ | bits];
} while (wildcards & invmask);

这只是从 0 到 2number-of-wildcards,然后将固定位放入顶部。

非二进制键

在最简单的非二进制情况下,密钥的部分仍然是整数位,即它们的范围从 0 到 2n-1。在这种情况下,您可以完全使用相同的代码,但对掩码的解释是不同的:通配符不是使用单个 0 位,也不是对非通配符使用单个 1 位,而是有一些其他位数(对应于关键部分的位宽度)。

对于非二次方,这需要更多技巧。问题是必须尽快生成进位,以满足关键部分小于某个值的约束。

例如,如果所有关键部分都可以是 0、1 或 2(但不是 3),您可以这样做(未测试):

int key = bits;
int increment = (0x55555555 & ~mask) + 1;
do {
    yield items[key];
    int temp = (key | mask) + increment & ~mask;
    int fix = (temp | (temp >> 1)) & 0x55555555;
    key = temp - fix | bits;
} while (key != bits);

额外的increment 是 1 加上“最接近的 2 次幂与键部分的最大值之差”的掩码,在这种情况下,每个键部分都为 1,所以有一个 1在每个“槽”中(槽是 2 位宽,这是在这种情况下它们可以是最窄的)。它仅在通配符的位置具有那些“偏移量”。

偏移关键部分,以便它们的最高允许值映射到“全1”,确保进位通过它们传播。但是,这意味着它们通常处于无效状态(除非它接收到进位并变为零)。那么烦人的部分就来了:对于没有归零的关键部分必须撤消偏移量。

所以fix 进来了。它计算一个不为零的关键部分的掩码。如果关键部分更宽,那就更烦人了,如果它们的关键部分大小不一样,那就太糟糕了。

最后一部分,key = temp - fix | bits,撤消偏移并将非通配符放回原处。那个减法永远不会破坏任何东西,因为只有 1 只从 2 位的组中减去,而这组 2 位一起至少为 1,所以随身携带永远不会离开关键部分。

当然,这种索引方式确实会浪费一些空间,这与二次幂的情况不同,因为数组中存在永远无法索引的“洞”。

【讨论】:

  • 这是否容易推广到关键部分不是二进制的情况?
  • @TinyProton 取决于,如果关键部分在其他一些二的幂的基础上,它仍然很容易,但是对于非二的幂的基础来说有点毛茸茸
【解决方案2】:

如果键的每个部分都存在一个最大值 (M),您可以通过将键解释为以 M(或混合基数)编写的数字来创建单个键控树

  • 我假设通配符只出现在一个索引中,并且所有进一步都是通配符,这样(x,*,*,*) 将成为(x*M^3,(x+1)*M^3-1) 的查询

对于字符串:

  • 您可以使用分隔符和标记粘贴密钥(使用|:

('ax','bc','a','x') -> 'ax|bc|a|x'

分隔符不应出现在输入字符串中(它可能会出现,但在这种情况下它可能会干扰访问询问的结果)

但是...如果您的情况是difficult,您可以将对象用作keys,在java中我将为键创建一个class,并在它们之间定义一个比较操作符

例如,我会引用: How to compare objects by multiple fields

【讨论】:

  • 这实际上是一个非常好的解决方案,但问题是键的部分可以是字符串或数字。很抱歉,我没有在我的问题中澄清这一点。
  • 如何查询(*, x, *, *)
【解决方案3】:

将每个关系编码为一行文本,然后使用正则表达式 where '.'可以匹配键中该位置的任何单个字符。

这将消除对无关位置的任何限制。

这里有一些 Python:

>>> import re
>>> 
>>> map = '''
... 0000 a
... 0001 b
... 0101 c
... 0110 d
... '''
>>> 
>>> def search(query='0001'):
...     matches = re.findall(query + r' .', map)
...     return [match.split()[-1] for match in matches]
... 
>>> search('0001')
['b']
>>> search('0...')
['a', 'b', 'c', 'd']
>>> search('01..')
['c', 'd']
>>> 

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-01
    • 1970-01-01
    • 2013-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-09
    相关资源
    最近更新 更多