考虑一个具有以下签名的查找函数,它需要为给定的字符串键返回一个整数:
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;
但是,此类调用的顺序未知。例如,上面可以描述运行时的以下调用序列:
GetValue("foo");
GetValue("bazz");
GetValue("bar");
GetValue("bazz");
或任何其他序列,前提是呼叫计数匹配。
还有一个限制 M,以最方便的任何单位指定,定义任何查找表和其他辅助结构可以使用的内存上限GetValue
(这些结构是预先初始化的;该初始化不计入复杂性函数)。例如,M=100 个字符,或 M=256 sizeof(object reference)。
问题是,如何编写GetValue
尽可能快的主体 - 换句话说,所有GetValue
调用的总时间(请注意,我们知道总计数,以上所有内容)是最小的,对于给定的 N,C和M?
该算法可能需要 M 的合理最小值,例如 M >= char.MaxValue
。它还可能要求 M 与某个合理的边界对齐——例如,它可能只是 2 的幂。它还可能要求 M 必须是某种 N 的函数(例如,它可能允许有效的 M=N,或 M=2N,...;或有效的 M=N,或 M=N^2, ...; ETC)。
该算法可以用任何合适的语言或其他形式来表达。对于生成代码的运行时性能约束,假设生成的代码GetValue
将使用 C#、VB 或 Java(实际上,任何语言都可以,只要字符串被视为不可变的字符数组 - 即 O(1) 长度和 O (1) 索引,并且没有预先为它们计算的其他数据)。此外,为了简化这一点,假设所有键的 C=1 的答案被认为是有效的,尽管那些涵盖更一般情况的答案是首选。
关于可能方法的一些思考
对上述问题的第一个显而易见的答案是使用完美的哈希,但是寻找一个的通用方法似乎并不完美。例如,对于上面的示例数据,可以使用 Pearson 散列轻松生成一个最小完美散列表,但是每次调用 时都必须对输入键进行散列GetValue
,并且 Pearson 散列必须扫描整个输入字符串。但实际上所有示例键的第三个字符都不同,因此只能将其用作散列的输入,而不是整个字符串。此外,如果要求 M 至少为char.MaxValue
,则第三个字符本身将成为完美哈希。
对于一组不同的键,这可能不再适用,但仍有可能减少在给出精确答案之前考虑的字符数量。此外,在某些情况下,最小完美散列需要检查整个字符串,可以减少对子集的查找,或者通过使散列非最小来使其更快(例如不太复杂的散列函数?) (即 M > N) - 为了速度而有效地牺牲空间。
也可能是传统的散列一开始就不是一个好主意,而且更容易将主体构造GetValue
为一系列条件,排列成第一个检查“最可变”的字符(那个在大多数键),并根据需要进行进一步的嵌套检查以确定正确答案。请注意,此处的“方差”可能会受到每个键将被查找的次数的影响 (C)。此外,最好的分支结构应该是什么并不总是很明显 - 例如,可能是“最可变”字符只能让您区分 100 个键中的 10 个,但对于剩余的 90 个键,需要额外检查无需区分它们,不以“最易变”的字符开头。然后,目标是确定完美的检查顺序。