Menu
Coddy logo textTech

トライ木(プレフィックスツリー)

最終更新

トライ木(「トライ」と発音)、またはプレフィックスツリーは、文字列の集合をその文字ごとに格納します。各辺には1文字がラベル付けされ、ルートからのパスがプレフィックスを綴ります。プレフィックスを共有する単語は同じノードを共有するため、「car」「card」「care」はすべて c-a-r のパスを再利用します。上の再生ボタンを押すと、単語が1文字ずつ挿入され、異なる箇所でのみ分岐する様子が見られます。

検索は1文字あたり1ノードをたどるため、長さ m の単語の検索は、トライ木が保持する単語数に関わらず O(m) で済みます。これにより、トライ木はオートコンプリート、スペルチェック、プレフィックス検索に最適です。

時間計算量と空間計算量

操作計算量備考
挿入O(m)m = 単語の長さ
検索O(m)1文字あたり1ステップ
プレフィックス検索O(m)プレフィックスのノードまでたどる
空間O(total chars)共有プレフィックスは一度だけ格納される

ステップ・バイ・ステップ(挿入)

ステップ何が起こるか
1ルートノードから始める。
2単語の各文字について、一致する子の辺を探す。
3存在すればそれをたどる(共有プレフィックスを再利用する)。
4なければ、その文字用の新しい子ノードを作成する。
5最後の文字の後、そのノードを単語の終端として印を付ける。

具体例で追う

空のトライ木に ["car", "card", "care"] を挿入する場合:

ステップ構造アクション
car を挿入root → c → a → r✓一致する子が存在しないので、car を作成し、r を単語の終端として印を付ける。
card を挿入root → c → a → r✓ → d✓既存の c-a-r パスを再利用し、新しい子 d を1つ作成して単語の終端として印を付ける。
care を挿入root → c → a → r✓ → {d✓, e✓}c-a-r を再利用し、r から新しい子 e で分岐して、e を単語の終端として印を付ける。
care を検索root → c → a → r → e✓care をたどる。最後のノードが単語の終端として印付けされているので、care は存在する。
ca を検索root → c → aパスは存在するが a は単語の終端として印付けされていないので、ca はプレフィックスであって格納された単語ではない。

トライ木を使うべきとき

使うべき場合避けるべき場合
文字列の集合に対してプレフィックス検索やオートコンプリートが必要な場合。完全なキーの検索だけが必要な場合 — ハッシュテーブルの方が速く軽量。
格納する多くの単語が共通のプレフィックスを共有し、ノードが再利用される場合。キーが長くめったに重ならず、1文字ごとにノードを無駄にする場合。
走査によってキーをソート順で返したい場合。メモリが逼迫している場合 — ノードごとの子ポインタが大きなオーバーヘッドを加える。
検索コストがデータセットのサイズではなくキーの長さに依存すべき場合。アルファベットが巨大(例:Unicode全体)で子を密に格納する場合。

Trie (Prefix Tree)のコード

Python, JavaScript, Java, C++, Cによるクリーンで実行可能なTrie (Prefix Tree)の実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。

PythonでのTrie (Prefix Tree)のコード

Python
1class TrieNode:2    def __init__(self):3        self.children = {}4        self.is_word = False5
6
7class Trie:8    def __init__(self):9        self.root = TrieNode()10
11    def insert(self, word):12        node = self.root13        for ch in word:14            node = node.children.setdefault(ch, TrieNode())15        node.is_word = True16
17    def search(self, word):18        node = self._walk(word)19        return node is not None and node.is_word20
21    def starts_with(self, prefix):22        return self._walk(prefix) is not None23
24    def _walk(self, s):25        node = self.root26        for ch in s:27            if ch not in node.children:28                return None29            node = node.children[ch]30        return node31
32
33trie = Trie()34for word in ["car", "card", "care", "dog"]:35    trie.insert(word)36
37print("search(card):     ", trie.search("card"))38print("search(ca):       ", trie.search("ca"))39print("starts_with(ca):  ", trie.starts_with("ca"))40print("starts_with(do):  ", trie.starts_with("do"))41print("starts_with(cat): ", trie.starts_with("cat"))
このコードをPythonプレイグラウンドで実行

トライ木のよくある質問

トライ木は何に使われますか?
トライ木はプレフィックスベースの機能を支えます:オートコンプリートと検索サジェスト、スペルチェッカー、IPルーティングテーブル、辞書検索などです。「格納されたいずれかの単語がこのプレフィックスで始まるか?」という問い合わせに素早く答える必要がある場面で、トライ木は真価を発揮します。
トライ木の時間計算量はどれくらいですか?
挿入・検索・プレフィックス検索はすべて O(m) で実行されます。m は単語またはプレフィックスの長さで、格納されている単語数には依存しません。トレードオフはメモリです:共有プレフィックスは一度だけ格納されますが、トライ木は多くの空間を使うことがあります。
トライ木とハッシュテーブルの違いは何ですか?
ハッシュテーブルは完全なキーを平均 O(1) で検索できますが、プレフィックス検索には答えられません。トライ木は1回の検索がわずかに遅いものの、プレフィックス検索、順序付き走査、オートコンプリートを自然にサポートします — それがこれらの用途で好まれる理由です。
二分探索木ではなくトライ木を使うべきなのはどんなときですか?
キーが文字列でプレフィックス検索が必要なときはトライ木を使いましょう:キーの長さに対して1操作あたり O(m) で動作しますが、平衡二分探索木は各比較で文字列を走査し、その比較が log n 回あるため O(m log n) かかります。キーが文字列的でない場合や、メモリのオーバーヘッドがプレフィックス速度より重要な場合は、二分探索木の方が良い選択です。
トライ木では単語の終端をどう扱いますか?
各ノードは単語終端フラグを持ち、挿入された完全な単語がそこで終わるときにのみ立てられます。これがないと、格納された単語と単なるプレフィックスを区別できません — 例えば card を挿入した後、car のノードは存在しますが、car も挿入された場合にのみ単語として報告されるべきです。
トライ木はプレフィックスを共有することで常にメモリを節約しますか?
常にではありません。共有が役立つのは多くのキーが重なる場合だけです。長く互いに異なるキーでは、各文字が子ポインタを持つ独自のノードになるため、トライ木はハッシュ集合よりはるかに多くのメモリを使うことがあります。空間が問題なら、圧縮された基数木(radix tree)が単一子のチェーンを統合してそのオーバーヘッドを削減します。
Coddy programming languages illustration

Coddy でアルゴリズムをマスターしよう

始める