別のオプションは、かなりの量のメモリを必要としますが、接尾辞配列(文字列内の位置のリスト、それらが指す文字列でソートされたもの)のようなものを事前計算することです。
http://en.wikipedia.org/wiki/Suffix_array
これは、検索する文字列のリストが比較的静的である場合に最も実行可能です。文字列インデックスのリスト全体をタプルの単一配列(indexOfString、positionInString)に格納し、その上でを使用してバイナリ検索を実行できますString.Compare(keyword, 0, target, targetPos, keyword.Length)
。
したがって、平均20の長さの100,000の文字列がある場合、構造体には100,000 * 20 * 2 * sizeof(int)のメモリが必要になります。indexOfStringとpositionInStringの両方を単一の32ビットintにパックすることで、これを半分に減らすことができます。たとえば、下位12ビットにpositionInStringを、残りの上位ビットにindexOfStringを配置します。2つの値を元に戻すには、少し手を加える必要があります。構造自体には文字列や部分文字列が含まれていないことに注意することが重要です。検索する文字列は1回だけ存在します。
これにより、基本的に完全なインデックスが得られ、実際の文字列の比較を最小限に抑えて、サブ文字列を非常にすばやく見つけることができます(接尾辞配列が表すインデックスの二分探索)。
メモリが重要な場合、元のブルートフォースアルゴリズムの単純な最適化は、一意の文字の辞書を事前に計算し、それぞれを表す序数を割り当てることです。次に、文字列内に含まれる一意の文字ごとに設定されたビットを使用して、各文字列のビット配列を事前計算します。文字列は比較的短いため、結果として得られるBitArrayにはかなりのばらつきがあるはずです(文字列が非常に長い場合はうまく機能しません)。次に、BitArrayまたは検索キーワードを計算し、これらの文字列内のキーワードのみを検索します。keywordBits & targetBits == keywordBits
。文字列が小文字に事前変換されていて、英語のアルファベットだけである場合、BitArrayは単一のint内に収まる可能性があります。したがって、これには最小限の追加メモリが必要であり、実装が簡単であり、キーワードが確実に見つからない文字列をすばやく除外できます。文字列検索は高速であるため、これは便利な最適化になる可能性がありますが、ブルートフォース検索を使用して行うことが非常に多くあります。
編集興味のある方のために、これが私が提案した最初のソリューションの基本的な実装です。OPによって記述された長さのランダムに生成された100,000の文字列を使用してテストを実行しました。インデックスの作成と並べ替えには約30秒かかりましたが、一度作成すると、キーワードの検索速度はブルートフォースで49,805ミリ秒、インデックス検索で18ミリ秒と、数千倍速くなりました。リストを作成することがめったにない場合は、最初に接尾辞配列を作成するという、単純ですが比較的遅い方法で十分です。より高速に構築するためのよりスマートな方法がありますが、以下の基本的な実装よりも多くのコーディングが必要になります。
// little test console app
static void Main(string[] args) {
var list = new SearchStringList(true);
list.Add("Now is the time");
list.Add("for all good men");
list.Add("Time now for something");
list.Add("something completely different");
while (true) {
string keyword = Console.ReadLine();
if (keyword.Length == 0) break;
foreach (var pos in list.FindAll(keyword)) {
Console.WriteLine(pos.ToString() + " =>" + list[pos.ListIndex]);
}
}
}
~~~~~~~~~~~~~~~~~~
// file for the class that implements a simple suffix array
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
namespace ConsoleApplication1 {
public class SearchStringList {
private List<string> strings = new List<string>();
private List<StringPosition> positions = new List<StringPosition>();
private bool dirty = false;
private readonly bool ignoreCase = true;
public SearchStringList(bool ignoreCase) {
this.ignoreCase = ignoreCase;
}
public void Add(string s) {
if (s.Length > 255) throw new ArgumentOutOfRangeException("string too big.");
this.strings.Add(s);
this.dirty = true;
for (byte i = 0; i < s.Length; i++) this.positions.Add(new StringPosition(strings.Count-1, i));
}
public string this[int index] { get { return this.strings[index]; } }
public void EnsureSorted() {
if (dirty) {
this.positions.Sort(Compare);
this.dirty = false;
}
}
public IEnumerable<StringPosition> FindAll(string keyword) {
var idx = IndexOf(keyword);
while ((idx >= 0) && (idx < this.positions.Count)
&& (Compare(keyword, this.positions[idx]) == 0)) {
yield return this.positions[idx];
idx++;
}
}
private int IndexOf(string keyword) {
EnsureSorted();
// binary search
// When the keyword appears multiple times, this should
// point to the first match in positions. The following
// positions could be examined for additional matches
int minP = 0;
int maxP = this.positions.Count - 1;
while (maxP > minP) {
int midP = minP + ((maxP - minP) / 2);
if (Compare(keyword, this.positions[midP]) > 0) {
minP = midP + 1;
} else {
maxP = midP;
}
}
if ((maxP == minP) && (Compare(keyword, this.positions[minP]) == 0)) {
return minP;
} else {
return -1;
}
}
private int Compare(StringPosition pos1, StringPosition pos2) {
int len = Math.Max(this.strings[pos1.ListIndex].Length - pos1.StringIndex, this.strings[pos2.ListIndex].Length - pos2.StringIndex);
return String.Compare(strings[pos1.ListIndex], pos1.StringIndex, this.strings[pos2.ListIndex], pos2.StringIndex, len, ignoreCase);
}
private int Compare(string keyword, StringPosition pos2) {
return String.Compare(keyword, 0, this.strings[pos2.ListIndex], pos2.StringIndex, keyword.Length, this.ignoreCase);
}
// Packs index of string, and position within string into a single int. This is
// set up for strings no greater than 255 bytes. If longer strings are desired,
// the code for the constructor, and extracting ListIndex and StringIndex would
// need to be modified accordingly, taking bits from ListIndex and using them
// for StringIndex.
public struct StringPosition {
public static StringPosition NotFound = new StringPosition(-1, 0);
private readonly int position;
public StringPosition(int listIndex, byte stringIndex) {
this.position = (listIndex < 0) ? -1 : this.position = (listIndex << 8) | stringIndex;
}
public int ListIndex { get { return (this.position >= 0) ? (this.position >> 8) : -1; } }
public byte StringIndex { get { return (byte) (this.position & 0xFF); } }
public override string ToString() {
return ListIndex.ToString() + ":" + StringIndex;
}
}
}
}