web-dev-qa-db-ja.com

トライをより速く構築する

何千もの高速な文字列ルックアップとプレフィックスチェックを必要とするモバイルアプリを作成しています。これをスピードアップするために、約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

22
Bruce

Double-Array試行 すべてのデータが線形配列に格納されるため、保存/読み込みが非常に高速です。また、検索は非常に高速ですが、挿入にはコストがかかる可能性があります。どこかにJava実装があるに違いない。

また、データが静的である場合(つまり、電話でデータを更新しない場合)は、タスクに [〜#〜] dafsa [〜#〜] を検討してください。これは、単語を格納するための最も効率的なデータ構造の1つです(サイズと速度の両方で「標準」試行と基数試行よりも優れている必要があります。速度については簡潔な試行よりも優れており、サイズについては簡潔な試行よりも優れていることがよくあります)。優れたC++実装があります: dawgdic -これを使用してコマンドラインからDAFSAを構築し、結果のデータ構造にJavaリーダーを使用できます(実装例は ここ )。

25
Mikhail Korobov

子ノードへの参照を配列インデックスに置き換えて、トライをノードの配列として格納できます。ルートノードが最初の要素になります。そうすれば、単純なバイナリ形式またはテキスト形式からトライを簡単に保存/ロードできます。

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;
    }
}
3
el.pescado

大きな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;
}
3
Justin

すべての可能な子(256)にスペースを事前割り当てしようとすると、大量の無駄なスペースがあります。あなたはあなたのキャッシュを泣かせています。子へのこれらのポインタをサイズ変更可能なデータ構造に格納します。

一部の試行では、1つのノードで長い文字列を表すことで最適化し、必要な場合にのみその文字列を分割します。

1
DanielV

これは特効薬ではありませんが、小さなものの束の代わりに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
Nate Kohl

これは、トライをディスクに保存するための適度にコンパクトなフォーマットです。その(効率的な)逆シリアル化アルゴリズムによって指定します。初期内容がトライのルートノードであるスタックを初期化します。文字を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')
1
David Eisenstat

単純なファイルの代わりに、sqliteのようなデータベースとネストされたセットまたはセルコツリーを使用してトライを格納できます。また、三分探索トライを使用して、より高速で短い(ノードが少ない)トライを構築できます。

0
Gigamegs

それはスペース効率が悪いのですか、それとも時間効率が悪いのですか?プレーントライを転がしている場合は、モバイルデバイスを扱うときにスペースが問題の一部になる可能性があります。特にプレフィックス検索ツールとして使用している場合は、patricia/radixの試行を確認してください。

トライ: http://en.wikipedia.org/wiki/Trie

パトリシア/基数トライ: http://en.wikipedia.org/wiki/Radix_tree

言語については言及していませんが、Javaでのプレフィックス試行の2つの実装があります。

通常のトライ: http://github.com/phishman3579/Java-algorithms-implementation/blob/master/src/com/jwetherell/algorithms/data_structures/Trie.Java

Patricia/Radix(space-effecient)trie: http://github.com/phishman3579/Java-algorithms-implementation/blob/master/src/com/jwetherell/algorithms/data_structures/PatriciaTrie.Java

0
Justin

配列内のインデックスでノードをアドレス指定するというアイデアは好きではありませんが、もう1つ追加(ポインタへのインデックス)が必要なためです。ただし、事前に割り当てられたノードの配列を使用すると、割り当てと初期化にかかる時間を節約できる可能性があります。また、リーフノードの最初の26個のインデックスを予約することで、多くのスペースを節約することもできます。したがって、180000リーフノードを割り当てて初期化する必要はありません。

また、インデックスを使用すると、準備されたノード配列をディスクからバイナリ形式で読み取ることができます。これは数倍速くなければなりません。しかし、あなたの言語でこれを行う方法がわかりません。これはJavaですか?

ソースボキャブラリがソートされていることを確認した場合は、現在の文字列のプレフィックスを前の文字列と比較することで、時間を節約することもできます。例えば。最初の4文字。それらが等しい場合、あなたはあなたを始めることができます

for(int level = 0; level <key.length(); level ++){

5レベルからループします。

0
Mikhail M