何千もの高速な文字列ルックアップとプレフィックスチェックを必要とするモバイルアプリを作成しています。これをスピードアップするために、約18万語の単語リストからTrieを作成しました。
すべてが素晴らしいですが、唯一の問題は、この巨大なトライ(約400,000ノード)を構築するのに、現在私の電話で約10秒かかることです。スロー。
これがトライを構築するコードです。
_public SimpleTrie makeTrie(String file) throws Exception {
String line;
SimpleTrie trie = new SimpleTrie();
BufferedReader br = new BufferedReader(new FileReader(file));
while( (line = br.readLine()) != null) {
trie.insert(line);
}
br.close();
return trie;
}
_
O(length of key)
で実行されるinsert
メソッド
_public void insert(String key) {
TrieNode crawler = root;
for(int level=0 ; level < key.length() ; level++) {
int index = key.charAt(level) - 'A';
if(crawler.children[index] == null) {
crawler.children[index] = getNode();
}
crawler = crawler.children[index];
}
crawler.valid = true;
}
_
トライをより速く構築するための直感的な方法を探しています。たぶん私は自分のラップトップでトライを一度だけ構築し、それを何らかの方法でディスクに保存し、電話のファイルからロードしますか?しかし、これを実装する方法がわかりません。
または、構築に時間がかからないが、ルックアップ時間の複雑さが似ている他のプレフィックスデータ構造はありますか?
任意の提案をいただければ幸いです。前もって感謝します。
[〜#〜]編集[〜#〜]
誰かがJavaシリアル化を使用することを提案しました。私はそれを試しましたが、このコードでは非常に遅いです:
_public void serializeTrie(SimpleTrie trie, String file) {
try {
ObjectOutput out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
out.writeObject(trie);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public SimpleTrie deserializeTrie(String file) {
try {
ObjectInput in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
SimpleTrie trie = (SimpleTrie)in.readObject();
in.close();
return trie;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
_
上記のコードを高速化できますか?
私のトライ: http://Pastebin.com/QkFisi09
単語リスト: http://www.isc.ro/lists/twl06.Zip
Android IDEコードの実行に使用: http://play.google.com/store/apps/details?id=com.jimmychen.app.sand
Double-Array試行 すべてのデータが線形配列に格納されるため、保存/読み込みが非常に高速です。また、検索は非常に高速ですが、挿入にはコストがかかる可能性があります。どこかにJava実装があるに違いない。
また、データが静的である場合(つまり、電話でデータを更新しない場合)は、タスクに [〜#〜] dafsa [〜#〜] を検討してください。これは、単語を格納するための最も効率的なデータ構造の1つです(サイズと速度の両方で「標準」試行と基数試行よりも優れている必要があります。速度については簡潔な試行よりも優れており、サイズについては簡潔な試行よりも優れていることがよくあります)。優れたC++実装があります: dawgdic -これを使用してコマンドラインからDAFSAを構築し、結果のデータ構造にJavaリーダーを使用できます(実装例は ここ )。
子ノードへの参照を配列インデックスに置き換えて、トライをノードの配列として格納できます。ルートノードが最初の要素になります。そうすれば、単純なバイナリ形式またはテキスト形式からトライを簡単に保存/ロードできます。
public class SimpleTrie {
public class TrieNode {
boolean valid;
int[] children;
}
private TrieNode[] nodes;
private int numberOfNodes;
private TrieNode getNode() {
TrieNode t = nodes[++numberOnNodes];
return t;
}
}
大きなString []を作成してソートするだけです。次に、バイナリ検索を使用して文字列の場所を見つけることができます。あまり手間をかけずに、プレフィックスに基づいてクエリを実行することもできます。
プレフィックスルックアップの例:
比較方法:
private static int compare(String string, String prefix) {
if (prefix.length()>string.length()) return Integer.MIN_VALUE;
for (int i=0; i<prefix.length(); i++) {
char s = string.charAt(i);
char p = prefix.charAt(i);
if (s!=p) {
if (p<s) {
// prefix is before string
return -1;
}
// prefix is after string
return 1;
}
}
return 0;
}
配列内のプレフィックスの出現を検索し、その場所を返します(MINまたはMAXは平均が見つからないことを意味します)
private static int recursiveFind(String[] strings, String prefix, int start, int end) {
if (start == end) {
String lastValue = strings[start]; // start==end
if (compare(lastValue,prefix)==0)
return start; // start==end
return Integer.MAX_VALUE;
}
int low = start;
int high = end + 1; // zero indexed, so add one.
int middle = low + ((high - low) / 2);
String middleValue = strings[middle];
int comp = compare(middleValue,prefix);
if (comp == Integer.MIN_VALUE) return comp;
if (comp==0)
return middle;
if (comp>0)
return recursiveFind(strings, prefix, middle + 1, end);
return recursiveFind(strings, prefix, start, middle - 1);
}
文字列配列とプレフィックスを取得し、配列内のプレフィックスの出現を出力します
private static boolean testPrefix(String[] strings, String prefix) {
int i = recursiveFind(strings, prefix, 0, strings.length-1);
if (i==Integer.MAX_VALUE || i==Integer.MIN_VALUE) {
// not found
return false;
}
// Found an occurrence, now search up and down for other occurrences
int up = i+1;
int down = i;
while (down>=0) {
String string = strings[down];
if (compare(string,prefix)==0) {
System.out.println(string);
} else {
break;
}
down--;
}
while (up<strings.length) {
String string = strings[up];
if (compare(string,prefix)==0) {
System.out.println(string);
} else {
break;
}
up++;
}
return true;
}
すべての可能な子(256)にスペースを事前割り当てしようとすると、大量の無駄なスペースがあります。あなたはあなたのキャッシュを泣かせています。子へのこれらのポインタをサイズ変更可能なデータ構造に格納します。
一部の試行では、1つのノードで長い文字列を表すことで最適化し、必要な場合にのみその文字列を分割します。
これは特効薬ではありませんが、小さなものの束の代わりに1つの大きなメモリ割り当てを実行することで、ランタイムをわずかに短縮できる可能性があります。
個々の割り当てに依存する代わりに「ノードプール」を使用すると、以下のテストコード(JavaではなくC++、申し訳ありません)で最大10%のスピードアップが見られました。
#include <string>
#include <fstream>
#define USE_NODE_POOL
#ifdef USE_NODE_POOL
struct Node;
Node *node_pool;
int node_pool_idx = 0;
#endif
struct Node {
void insert(const std::string &s) { insert_helper(s, 0); }
void insert_helper(const std::string &s, int idx) {
if (idx >= s.length()) return;
int char_idx = s[idx] - 'A';
if (children[char_idx] == nullptr) {
#ifdef USE_NODE_POOL
children[char_idx] = &node_pool[node_pool_idx++];
#else
children[char_idx] = new Node();
#endif
}
children[char_idx]->insert_helper(s, idx + 1);
}
Node *children[26] = {};
};
int main() {
#ifdef USE_NODE_POOL
node_pool = new Node[400000];
#endif
Node n;
std::ifstream fin("TWL06.txt");
std::string Word;
while (fin >> Word) n.insert(Word);
}
これは、トライをディスクに保存するための適度にコンパクトなフォーマットです。その(効率的な)逆シリアル化アルゴリズムによって指定します。初期内容がトライのルートノードであるスタックを初期化します。文字を1つずつ読み、次のように解釈します。文字A〜Zの意味は、「新しいノードを割り当て、それを現在のスタックの最上位の子にし、新しく割り当てられたノードをスタックにプッシュする」ということです。この文字は、子がどの位置にあるかを示します。スペースの意味は、「スタックの最上位のノードの有効なフラグをtrueに設定する」ことです。バックスペース(\ b)の意味は、「スタックをポップする」です。
たとえば、入力
TREE \b\bIE \b\b\bOO \b\b\b
単語リストを与える
TREE
TRIE
TOO
。デスクトップで、いずれかの方法を使用してトライを作成し、次の再帰的アルゴリズム(擬似コード)でシリアル化します。
serialize(node):
if node is valid: put(' ')
for letter in A-Z:
if node has a child under letter:
put(letter)
serialize(child)
put('\b')
単純なファイルの代わりに、sqliteのようなデータベースとネストされたセットまたはセルコツリーを使用してトライを格納できます。また、三分探索トライを使用して、より高速で短い(ノードが少ない)トライを構築できます。
それはスペース効率が悪いのですか、それとも時間効率が悪いのですか?プレーントライを転がしている場合は、モバイルデバイスを扱うときにスペースが問題の一部になる可能性があります。特にプレフィックス検索ツールとして使用している場合は、patricia/radixの試行を確認してください。
トライ: http://en.wikipedia.org/wiki/Trie
パトリシア/基数トライ: http://en.wikipedia.org/wiki/Radix_tree
言語については言及していませんが、Javaでのプレフィックス試行の2つの実装があります。
Patricia/Radix(space-effecient)trie: http://github.com/phishman3579/Java-algorithms-implementation/blob/master/src/com/jwetherell/algorithms/data_structures/PatriciaTrie.Java
配列内のインデックスでノードをアドレス指定するというアイデアは好きではありませんが、もう1つ追加(ポインタへのインデックス)が必要なためです。ただし、事前に割り当てられたノードの配列を使用すると、割り当てと初期化にかかる時間を節約できる可能性があります。また、リーフノードの最初の26個のインデックスを予約することで、多くのスペースを節約することもできます。したがって、180000リーフノードを割り当てて初期化する必要はありません。
また、インデックスを使用すると、準備されたノード配列をディスクからバイナリ形式で読み取ることができます。これは数倍速くなければなりません。しかし、あなたの言語でこれを行う方法がわかりません。これはJavaですか?
ソースボキャブラリがソートされていることを確認した場合は、現在の文字列のプレフィックスを前の文字列と比較することで、時間を節約することもできます。例えば。最初の4文字。それらが等しい場合、あなたはあなたを始めることができます
for(int level = 0; level <key.length(); level ++){
5レベルからループします。