トライ木(trie)は有向木の一種で主に文字列を記録するためのデータ構造です。trie という名称は “retrieval”(リトゥリーヴァル。探索、検索)が語源であるため、”tree” と同じ発音を用いますが、ツリー構造との混同を避けるため日本語では「トライ木」という呼ばれます。

トライ木の特徴として頂点とラベル付き有向辺に文字を割り当てて文字列を記録する方法がとられます。これによってトライ木の根からラベル付き有向辺をたどることで記録された文字列を取り出すことができます。文字列の挿入・検索・削除がすべてO(M)(Mは文字列の長さ)でおこなうことができるのです。そのため辞書のアルゴリズムでも用いられます。

辞書ならC#にはDictionary<TKey,TValue>クラスがあるじゃないか、自作しなくても既存のクラスを使えばよいではないかと言われたらそれまでなのですが、アルゴリズムとしては重要なものだと思うので、ここはあえて車輪の再発明をすることにします。

Nodeクラスの定義

まずNodeクラスを定義します。

IsEndは格納されている文字列がこのノードで終わっていることを示すものです。上の図の黄色いノードに相当する部分です。 “inn”だけでなく”i”や”in”も格納する場合、IsEndは重要な役割をします。IsEnd == trueのときValueも同時に設定することで辞書と同じ機能を持たせることもできます。

UseCountはこのノードを使用している格納されている文字列の数です。文字列を追加したら該当するノードのUseCountをインクリメントし、削除したたデクリメントします。0になったらこのノードは不要ということになります。

Trieクラスの定義

次にTrieクラスを定義します。

文字列は存在するか?

まず文字列がTrie木に存在するかを調べるメソッドを定義します。

_rootから順番にノードをたどっていき、最後の文字までたどることができるかを調べます。最後までたどることができない場合は文字列は存在しないことになります。また最後までたどることができても最後のノードのIsEndがtrueでなければやはり探している文字列は存在しないことになります(”ab”を探しているとき”abc”はあるが”ab”はないとか・・)。

文字列の追加

文字列を追加する処理を示します。

まず文字列がTrie木のなかに存在するか調べます。すでに存在する場合はなにもしません。存在しない場合は_rootから順番にノードをたどっていき、その文字が格納されているノードがあればそのまま使い、ない場合は新しくつくります。どちらの場合も該当するノードのUseCountをインクリメントします。そして最後のノードのIsEndをtrueにします。

このInsertメソッドは文字列を追加するだけでなく値の設定もおこないます。これによって辞書と同じ機能を持たせることができます。

文字列を削除する

文字列を削除する場合も最初に文字列がTrie木のなかに存在するか調べます。存在しない場合はなにもしません。

存在する場合は_rootから順番にノードをたどっていき、使用されているノードのUseCountをデクリメントします。もしデクリメントすることで0になったらこのノードとその配下のノードは使われないことを意味するのでそのまま削除してしまいます。

ノードが削除されなかった場合は削除しようとしている文字列を含む別の文字列が存在するということなのでIsEndをfalseにする(Valueもクリアする)だけにします。

文字列のソート

Trie木に格納された単語を辞書順に取り出します。ここでは深さ優先探索で文字列を取得しています。

その他

これだと既存のDictionary<TKey,TValue>クラスを使ったほうがよいです。そこで自由研究として与えられた文字列 T をプレフィックスとして持つ文字列の数を返すメソッドを実装してみることにします。

Trie木に格納された文字列と引数strとの共通プレフィックスのうち最長のものを返すメソッドを実装します。

Trie木に格納された文字列のなかでプレフィックスに引数prefixを含むものを辞書順ですべて返すメソッドを実装します。

実装したCountPrefixメソッドとGetLCPメソッドとGetWordsメソッドを使ってみます。