接尾辞配列とq -gram インデックス
文字列のサイズに厳密な上限がある場合は、接尾辞配列の使用を検討できます。特殊文字 (null char など) を使用して、すべての文字列を同じ最大長になるまで単純にパディングします。次に、すべての文字列を連結し、それらに接尾辞配列インデックスを作成します。
これにより、 m * log nのルックアップ ランタイムが得られます。ここで、mはクエリ文字列の長さであり、nは結合された文字列の全体の長さです。これでも十分ではなく、mの長さが固定で短く、アルファベット Σ のサイズが制限されている場合 (たとえば、Σ < 128 文字)、追加でq -gram indexを作成できます。これにより、一定時間で取得できます。ただし、q -gram テーブルには Σ m 個のエントリが必要です (= 3 文字だけの場合は 8 MiB、4 文字の場合は 1 GiB!)。
インデックスを小さくする
ハッシュ関数を調整することで、 q -gram テーブルのサイズを (最良の場合、指数関数的に) 削減できる可能性があります。可能なすべてのqグラムに一意の番号を割り当てる代わりに、非可逆ハッシュ関数を使用できます。その場合、テーブルは、完全一致に対応する 1 つの接尾辞配列エントリではなく、可能な接尾辞配列インデックスのリストを格納する必要があります。ただし、リスト内のすべてのエントリを考慮する必要があるため、ルックアップは一定ではなくなります。
ところで、q -gram インデックスがどのように機能するかについて、インターネットは役に立たないので、あなたがこのトピックに精通しているかどうかはわかりません。これについては、以前別のトピックで言及しました。したがって、私は学士論文に構築の説明とアルゴリズムを含めました。
コンセプトの証明
私は非常に小さな C# の概念実証を作成しました (C# で作業したと別の方法で述べたため)。動作しますが、2 つの理由で非常に遅いです。まず、接尾辞配列の作成では、単に接尾辞をソートします。これだけでも実行時間はn 2 log nです。はるかに優れた方法があります。SubString
しかし、もっと悪いのは、接尾辞を取得するために使用するという事実です。残念ながら、.NET はこのためにサフィックス全体のコピーを作成します。このコードを実際に使用するには、データを不必要にコピーしないインプレース メソッドを使用するようにしてください。文字列からqグラムを取得する場合も同様です。
m_Data
私の例で使用されている文字列を構築しない方が良いかもしれません。SubString
代わりに、元の配列への参照を保存し、この配列で作業することですべてのアクセスをシミュレートできます。
それでも、この実装が本質的に一定時間の取得を期待していることは簡単にわかります (ディクショナリが適切に動作している場合)。これは、検索ツリー/トライに勝るものはありません。
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() {
// Approx. runtime: n^3 * log n!!!
// But I claim the shortest ever implementation of a suffix array!
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);
}
}