1. HOME
  2. ブログ
  3. IT技術
  4. トライ木で高速な辞書を作ってみた!

トライ木で高速な辞書を作ってみた!

トライ木で高速な辞書を作りたい!

トライ木(英: trie)というデータ構造を知ってますか?

トライ木はテキストを扱う際に良く用いられているデータ構造で、文字列を非常に高速に検索することができるため、辞書の実装などに利用されています。

本記事では、トライ木の特徴の紹介構築方法の解説・実装を行っていきます。

最終的に単語とその読み方を入力としたトライ木を構築し、単語を入力することでその読み方を出力する辞書システムを作成します。

こちらの記事もオススメ!


トライ木とは

トライ木とは、上の図のようなデータ構造で、順序付き木の一つです。

この図は、例として「a」「ab」「abc」「ac」「bc」という5単語が入ったトライ木です。

ノードの左上の数字は、ノードidを表します。

id0のノードの <s> というのは先頭文字を表していて、検索の際はここからスタートします。

丸が二重になっているノードは、そのノードが単語の終端文字を表していることを示します。

検索の際は、先頭ノードから入力文字に対応するエッジをたどります。

手順

例えば、「ab」という単語を検索する際には、以下のような手順をたどります。

1先頭ノードからでているエッジに「ab」の1文字めである「a」があるかを探す
2ノード1に伸びるエッジが「a」なので、ノード1に遷移
3ノード1のエッジに「ab」の2文字目である「b」があるかを探す
4ノード3に伸びるエッジが「b」なので、ノード2に遷移
5入力文字が最後まで行ったので現在のノードが終端文字を表すノードか確認
6ノード3は終端文字を表すノードなので辞書に「ab」が存在することがわかる

トライ木の特徴

トライ木には、以下のような特徴があります。

検索が高速[計算量O(M)]で、構築が高速[単語1つにつきO(L)]

検索が高速という点は、トライ木を用いる際の最も大きな利点といえるでしょう。

検索単語の長さがMのとき、計算量はO(M)となります。

これは辞書に格納されている単語数によらず、検索単語の長さにのみ比例した時間で検索できることを表しています。

つまり、数百万単語(あるいは数億、数兆)の中から「トライ木」という単語を抜き出してくる処理を、2単語の中から「トライ木」という単語を抜き出す処理と同じくらい高速にできるということです。

同様に、構築の際も長さLの1単語を挿入するのにかかる計算量はO(L)となり、辞書の大きさによらず、挿入単語の長さに比例した時間で挿入できます。

一度の探索でキーのprefixの検索もできる

一度の探索でキーのprefixの検索もできるというのもトライ木特有の優れた点です。

例えば、「情報処理技術者」という単語を検索することを考えます。

このとき、辞書には「情報」「情報処理」「情報処理技術」という3つの単語も登録されていたとします。

この辞書がトライ木で実装されていたとしたら、「情報処理技術者」という単語を検索する1回の処理で、他の3単語の情報も得ることができます。

この特徴は、日本語の形態素解析をする際に非常によく利用されています。

また、オートコンプリートシステムの構築にも用いることができます。

サイズが大きくなることがある

次は、欠点です。

トライ木においては、登録されている単語によっては、サイズが大きくなることがあります。

そのため、なるべくサイズが小さくなるような実装方法や工夫が研究されています。

トライ木の実装方法

トライ木には、LOUDSダブルアレイなどの様々な実装方法があります。

しかし、今回は(おそらく)最も素直で単純な実装である配列を用いた実装を行います。

この実装では、1つのノードを1つの配列として表現します。

図で表すと

次の図は、先ほど例として示した図を配列表現したものです。

各行各ノードを表す
WORDの列格納される単語を表す
abc…zのどの文字でどのノードに遷移するか、つまり木の表現におけるエッジを表す
ENDの列そのノードが単語の終端文字であるかどうか、木の表現における二重丸かどうかを表す

検索を行う際のノードの巡り方

実際に検索を行う際のノードの巡り方の例は以下の図のようになります。


図の、木の表現と列の表現の赤い矢印は、同じ「ab」という単語を検索した際のノードの巡り方を示しています。

トライ木の実装(準備)

それでは、いよいよ実際にトライ木を実装し、辞書システムを作っていきましょう!

今回の言語は、Python(パイソン)を用いたいと思います。

要件

要件は以下のようなものです。

  1. 単語とその読み方のペアを登録する
  2. 単語を入力することで読み方を出力する
  3. 単語に使える文字はasciiのみ

単語とその読み方のペアとして、今回は以下のデータを用います。

単語読み方
aエー
abエービー
acエーシー
abcエービーシー
bcビーシー

さて、まずは準備段階として、トライ木実装の際に用いる構造体(クラス)を作っていきます。

今回は、TrieNodeクラスTrieクラスという2種類のクラスを用います。

TrieNodeクラス

TrieNodeクラスは、トライ木の1つのノードを表すクラスで、itemchildrenという2つの変数を持ちます。

itemは、辞書引きを行った際に取得したい情報です。

今回は単語を入力し、その読み方を得るシステムなので、itemには単語の読み方が入ります。

単語の終端ノードでない場合には、Noneが入ります。

今回は、このitemを単語の終端ノードかどうかの判定にも利用します。

childrenは、どの文字でどのノードに遷移するかのリストです。

先のトライ木の配列の例の図におけるabc…zの列を表します。

Trieクラス

Trieクラスは、トライ木の実態を表すクラスで、nodesという変数を持ちます。

nodesは、TrieNodeのリストになります。

コード

トライ木の実装(構築部分)

前章で作ったTrieクラスに単語を登録し、トライ木を構築するためのinsertメソッドを実装します。

insertを実装するために __add_node()__get_char_num() というメソッドを作りました。

それぞれ、nodesにnodeを追加し、追加したノードのインデックスを返すメソッドと、文字のidを返すメソッドになります。

詳しくは実装とコメントを参考にしてください。

insertメソッド

insertメソッドを見ていきます。

insertメソッドは、再帰的に呼び出され、すでにあるトライ木を辿っていき、その過程で対応するノードがなかった場合に追加していくことで、トライ木を構築するメソッドです。

一度、insertメソッドが呼び出されるたびに登録単語の1文字分のノードをたどります。

例えば、「ab」という単語を登録する際には、「a」の分と「b」の分の2回のinsertメソッドが呼び出されます。

まず、1,2行目で、現在の文字で遷移すべきノード(next_node_index)を取得します。

このときに遷移すべきノードがまだなかった場合、3行目からの分岐で新たなノードを作成します。

9行目の if char_index == len(word) - 1: の分岐は、現在見ている文字が単語の最後の文字かどうかを判定する分岐です。

最後の文字だった場合、対応するノードに item (今回は読み方)を保存します。

そうでなかった場合は、文字を1つ進めて、もう一度 insert を呼び出します。

トライ木の実装(検索部分)

続いて、検索を行うqueryメソッドを実装します。

queryメソッドは、入力された単語を一文字ずつ見て、トライ木を辿っていきます。

最終的に、単語の終端となるノードのインデックスを取得し、そのノードに保存された item を返します。

コード解説

まず、1行目で現在着目しているノードを表すnode_indexを先頭文字のノードである0に初期化します。

次に、3行目から始まるループで、単語を1文字ずつ見ていきます。

5行目では、現在着目している文字に対応するノードである tmp_node を取得しています。

7行目で、次に遷移するべきノードの index を取得します。

このとき、遷移すべきノードがなかった場合は、その単語が辞書に登録されていないことになるので、Noneを返します。(8行目の分岐)

トライ木の実装(全体)

ここまでで、トライ木を用いた辞書の実装が終わりました

構築部分と検索部分をまとめたコードは以下のようになります。

登録・検索

最後に、Trieクラスを利用して単語の登録、検索を行います。

実行

このコードを実行すると、以下のような結果になります。

エー
エービーシー
ビーシー
None

辞書に登録した3単語はきちんと読み方が返ってきて、登録していない「abcd」という単語はNoneが返ることがわかります。

さいごに

トライ木は一見難しそうですが、一度理解してしまえば実装は、さほど難しくなかったと思います。

トライ木を用いると今回のような辞書だけでなく、「形態素解析機」「オートコンプリート」など、様々な分野に応用がきく便利なデータ構造です。

この期にぜひマスターしてみてください。

また、今回の実装では配列を用いた実装ということで、サイズがとても大きくなっています。

実際に使う際には「パトリシア木」「LOUDS」「ダブルアレイ」といった、サイズを小さくする工夫、実装を行います。

これについても、近いうちに記事にできたらと思います!

この記事の続き

こちらの記事もオススメ!


関連記事

ライトコードよりお知らせ

にゃんこ師匠にゃんこ師匠
システム開発のご相談やご依頼はこちら
ミツオカミツオカ
ライトコードの採用募集はこちら
にゃんこ師匠にゃんこ師匠
社長と一杯飲みながらお話してみたい方はこちら
ミツオカミツオカ
フリーランスエンジニア様の募集はこちら
にゃんこ師匠にゃんこ師匠
その他、お問い合わせはこちら
ミツオカミツオカ
   
お気軽にお問い合わせください!せっかくなので、別の記事もぜひ読んでいって下さいね!

一緒に働いてくれる仲間を募集しております!

ライトコードでは、仲間を募集しております!

当社のモットーは「好きなことを仕事にするエンジニア集団」「エンジニアによるエンジニアのための会社」。エンジニアであるあなたの「やってみたいこと」を全力で応援する会社です。

また、ライトコードは現在、急成長中!だからこそ、あなたにお任せしたいやりがいのあるお仕事は沢山あります。「コアメンバー」として活躍してくれる、あなたからのご応募をお待ちしております!

なお、ご応募の前に、「話しだけ聞いてみたい」「社内の雰囲気を知りたい」という方はこちらをご覧ください。

ライトコードでは一緒に働いていただける方を募集しております!

採用情報はこちら

書いた人はこんな人

ライトコードメディア編集部
ライトコードメディア編集部
「好きなことを仕事にするエンジニア集団」の(株)ライトコードのメディア編集部が書いている記事です。

関連記事

採用情報

\ あの有名サービスに参画!? /

バックエンドエンジニア

\ クリエイティブの最前線 /

フロントエンドエンジニア

\ 世界はお前の手の中に・・・ /

モバイルエンジニア

\ サービスの守り神! /

インフラエンジニア