Hash 已知密钥集的最快字符串密钥查找

Hash 已知密钥集的最快字符串密钥查找,hash,lookup-tables,perfect-hash,Hash,Lookup Tables,Perfect Hash,考虑一个具有以下签名的查找函数,该函数需要为给定的字符串键返回一个整数: int GetValue(string key) { ... } 进一步考虑,在编写函数的源代码时,键值映射(编号N)是预先知道的,例如: // N=3 { "foo", 1 }, { "bar", 42 }, { "bazz", 314159 } 因此,上述输入函数的有效(但不是完美)实现是: int GetValue(string key) { switch (key) { cas

考虑一个具有以下签名的查找函数,该函数需要为给定的字符串键返回一个整数:

int GetValue(string key) { ... }
进一步考虑,在编写函数的源代码时,键值映射(编号N)是预先知道的,例如:

// N=3
{ "foo", 1 },
{ "bar", 42 },
{ "bazz", 314159 }
因此,上述输入函数的有效(但不是完美)实现是:

int GetValue(string key)
{
    switch (key)
    {
         case "foo": return 1;
         case "bar": return 42;
         case "bazz": return 314159;
    }

    // Doesn't matter what we do here, control will never come to this point
    throw new Exception();
}
对于每个给定的键,在运行时调用函数的准确次数(C>=1)也是预先知道的。例如:

C["foo"] = 1;
C["bar"] = 1;
C["bazz"] = 2;
char *table = "a\0bi\0bo\0j\0"; // last 0 is really redundant..but
char *keys[4];
keys[0] = table;
keys[1] = table + 2;
keys[2] = table + 5;
keys[3] = table + 8;
然而,这些电话的顺序不得而知。例如,上面可以描述运行时的以下调用序列:

GetValue("foo");
GetValue("bazz");
GetValue("bar");
GetValue("bazz");
或任何其他序列,前提是呼叫计数匹配

还有一个限制M,以最方便的单位指定,它定义了
GetValue
可以使用的任何查找表和其他辅助结构的内存上限(这些结构是预先初始化的;初始化不计入函数的复杂性)。例如,M=100个字符,或M=256个sizeof(对象引用)

问题是,对于给定的N、C和M,如何编写
GetValue
的主体,使其尽可能快-换句话说,所有
GetValue
调用的聚合时间(请注意,我们知道上述所有调用的总计数)是最小的

该算法可能需要M的合理最小值,例如M>=
char.MaxValue
。它还可能要求M与某个合理的边界对齐-例如,它可能只是2的幂。它还可能要求M必须是某种类型的N的函数(例如,它可能允许有效的M=N,或M=2N,…;或有效的M=N,或M=N^2,…;等等)

该算法可以用任何合适的语言或其他形式表示。对于生成代码的运行时性能约束,假设生成的
GetValue
代码将使用C#、VB或Java(实际上,任何语言都可以,只要字符串被视为不可变的字符数组,即O(1)长度和O(1)索引,并且事先没有为它们计算其他数据)。此外,为了简化这一点,假设所有键的C=1的答案被认为是有效的,尽管那些涵盖更一般情况的答案是首选的

关于可能途径的一些思考 显然,上述问题的第一个答案是使用一个完美的散列,但寻找散列的通用方法似乎并不完美。例如,可以很容易地为上面的示例数据使用Pearson哈希生成一个最小完美哈希表,但是每次调用
GetValue
,都必须对输入键进行哈希,Pearson哈希必须扫描整个输入字符串。但所有示例键实际上在第三个字符上都不同,因此只有第三个字符可以用作哈希的输入,而不是整个字符串。此外,如果要求M至少为
char.MaxValue
,则第三个字符本身将成为一个完美的散列

对于不同的一组键,这可能不再正确,但在给出精确答案之前,可能仍然可以减少考虑的字符数。此外,在一些最小完美散列需要检查整个字符串的情况下,可能会将查找减少到一个子集,或者通过使散列非最小(即M>N)而使其更快(例如,更复杂的散列函数?)——为了速度而有效地牺牲空间


也可能是传统的散列一开始就不是一个好主意,它更容易将
GetValue
的主体结构为一系列条件,其排列方式使得第一个检查“最可变”字符(在大多数键中变化的字符),根据需要进行进一步的嵌套检查以确定正确答案。请注意,此处的“差异”可能会受到每个键的查找次数(C)的影响。此外,分支的最佳结构应该是什么并不总是显而易见的——例如,“最可变”字符可能只允许您区分100个键中的10个键,但对于剩余的90个键,不需要额外检查来区分它们,平均而言(考虑C)与不以“最大变量”字符开头的不同解决方案相比,每个键的检查次数更多。然后,目标是确定检查的完美顺序。

您可以使用搜索,但我认为这将是一种更有效的方法。您可以修改Trie以折叠单词,同时将关键字的命中计数设为零,从而减少搜索次数。您将获得的最大好处是,您正在对索引进行数组查找,这比比较快得多。

以下是一种可行的方法,可以确定哈希例程的目标字符的最小子集:

let:
k是所有关键字中不同字符的数量
c是最大关键字长度
n是关键字的数目
在您的示例中(带空格的填充较短关键字):

k=7(f,o,b,a,r,z,),c=4,n=3

我们可以用它来计算搜索的下限。我们至少需要log_k(n)个字符来唯一标识关键字,如果log_k(n)>=c,则需要使用整个关键字,并且没有理由继续

接下来,一次删除一列,并检查是否还有n个不同的值。使用每列中不同的字符作为启发式优化搜索:

2 2 3 2
f o o .
b a r .
b a z z

首先消除具有最低不同字符的列。如果你有,对表进行二进制搜索真的那么糟糕吗?我将获取潜在字符串的列表并“最小化”它们,对它们进行排序,最后对它们的块进行二进制搜索

我所说的最小化是指将它们减少到最小值
char *table = "a\0bi\0bo\0j\0"; // last 0 is really redundant..but
char *keys[4];
keys[0] = table;
keys[1] = table + 2;
keys[2] = table + 5;
keys[3] = table + 8;
keys[0] = "a";
keys[1] = "bi";
keys[2] = "bo";
keys[3] = "j";
inline int GetValue(char *key) {
    return 1234;
}
const int POSSIBLE_CHARCODES = 256; //256 for ascii //65536 for unicode 16bit
struct LutMap {
    int value;
    LutMap[POSSIBLE_CHARCODES] next;
}
int GetValue(string key) {
    LutMap root = Global.AlreadyCreatedLutMap;
    for(int x=0; x<key.length; x++) {
        int c = key.charCodeAt(x);
        if(root.next[c] == null) {
            return root.value;
        }
        root = root.next[c];
    }
}
foo  = 0x666F6F (hex value)
bar  = 0x626172
bazz = 0x62617A7A
foo  = 0xF = 1111
bar  = 0x2 = 0010
bazz = 0xA = 1010
foo  = 0011
bar  = 0000
bazz = 0010
String string = "{foo:1}{bar:42}{bazz:314159}";
int length = string.length();