后缀数组和q-gram索引
如果你的字符串长度有严格的上限,考虑使用后缀数组:将所有字符串都用一个特殊字符(例如空字符)填充到相同的最大长度。然后将所有字符串连接起来,并构建一个后缀数组索引。
这样可以使查询运行时间为m*logn,其中m是查询字符串的长度,n是所有字符串的总长度。如果仍然不够好,并且m具有固定的小长度,并且字母表Σ也很小(比如,Σ<128个不同字符),你还可以构建一个q-gram索引,这将允许在常数时间内检索。但是,q-gram表需要存储Σm个条目(对于只有3个字符的情况,占用8 MiB,对于4个字符则需要1 GiB!)。
缩小索引的大小
通过调整哈希函数,可能可以指数级地减小q-gram表的大小(在最佳情况下)。不是为每个可能的q-gram分配唯一的数字,而是使用有损哈希函数。然后表将必须存储后缀数组索引的可能列表,而不是仅对应于精确匹配的一个后缀数组条目。但这意味着查找不再是常数时间,因为必须考虑列表中的所有条目。
顺便说一下,我不确定你是否熟悉 q-gram索引 的工作原理,因为互联网上对这个主题的信息并不有用。我之前在另一个话题中提到过。因此,在我的学士论文中,我包括了构建的描述和算法。
概念证明
我写了一个非常小的C#概念验证(因为你表示你也用C#)。它可以工作,但是非常缓慢,原因有两个。首先,后缀数组的创建只是简单地对后缀进行排序。单单这一步就有一个运行时复杂度为n2 log n,而实际上有更好的方法。更糟糕的是,我使用SubString
来获取后缀。不幸的是,.NET会为此创建整个后缀的副本。要在实践中使用此代码,请确保使用不会不必要地复制任何数据的原地方法。从字符串中检索q-gram时同样如此。
可能甚至最好不要构造我示例中使用的m_Data
字符串。相反,您可以保存对原始数组的引用,并模拟我所有SubString
访问,从而在该数组上工作。
尽管如此,很容易看出该实现基本上具有期望恒定时间检索(如果字典表现良好)!这是相当值得称赞的成就,搜索树/trie不可能超越它!
class QGramIndex {
private readonly int m_Maxlen;
private readonly string m_Data;
private readonly int m_Q;
private int[] m_SA;
private Dictionary<string, int> m_Dir = new Dictionary<string, int>();
private struct StrCmp : IComparer<int> {
public readonly String Data;
public StrCmp(string data) { Data = data; }
public int Compare(int x, int y) {
return string.CompareOrdinal(Data.Substring(x), Data.Substring(y));
}
}
private readonly StrCmp cmp;
public QGramIndex(IList<string> strings, int maxlen, int q) {
m_Maxlen = maxlen;
m_Q = q;
var sb = new StringBuilder(strings.Count * maxlen);
foreach (string str in strings)
sb.AppendFormat(str.PadRight(maxlen, '\u0000'));
m_Data = sb.ToString();
cmp = new StrCmp(m_Data);
MakeSuffixArray();
MakeIndex();
}
public int this[string s] { get { return FindInIndex(s); } }
private void MakeSuffixArray() {
m_SA = Enumerable.Range(0, m_Data.Length).ToArray();
Array.Sort(m_SA, cmp);
}
private int FindInArray(int ith) {
return Array.BinarySearch(m_SA, ith, cmp);
}
private int FindInIndex(string s) {
int idx;
if (!m_Dir.TryGetValue(s, out idx))
return -1;
return m_SA[idx] / m_Maxlen;
}
private string QGram(int i) {
return i > m_Data.Length - m_Q ?
m_Data.Substring(i) :
m_Data.Substring(i, m_Q);
}
private void MakeIndex() {
for (int i = 0; i < m_Data.Length; ++i) {
int pos = FindInArray(i);
if (pos < 0) continue;
m_Dir[QGram(i)] = pos;
}
}
}
使用示例:
static void Main(string[] args) {
var strings = new [] { "hello", "world", "this", "is", "a",
"funny", "test", "which", "i", "have",
"taken", "much", "too", "far", "already" };
var index = new QGramIndex(strings, 10, 3);
var tests = new [] { "xyz", "aki", "ake", "muc", "uch", "too", "fun", "est",
"hic", "ell", "llo", "his" };
foreach (var str in tests) {
int pos = index[str];
if (pos > -1)
Console.WriteLine("\"{0}\" found in \"{1}\".", str, strings[pos]);
else
Console.WriteLine("\"{0}\" not found.", str);
}
}