トライ木:接頭辞をたどって学ぶ

16語を木に収め、各ノードに一文字ずつ持たせています。語頭が同じ単語は、その部分を共有します。たとえばcar、card、cartは、同じc、a、rの経路をたどります。二重丸は、そこで単語が終わる印です。単語を入力し、操作を選んで、一段階ずつ進めてみましょう。

インタラクティブシミュレーションを読み込んでいます...

レッスン

理論 — トライ木:接頭辞をたどって学ぶ

トライ木は接頭辞木とも呼ばれ、単語の集合を、異なる接頭辞ごとにノードを持つ木で表します。根は空の接頭辞を表し、各辺に一文字が対応します。根からノードまでの経路に沿って文字を読むと、そのノードが表す接頭辞になります。その接頭辞自体が格納された単語なら、ノードに終端の印を付けます。語頭が共通する単語は、その部分の経路を共有します。検索では、根から単語の文字を順にたどります。前方一致検索では、指定した接頭辞のノードまで進み、そこを根とする部分木から、印の付いたノードをすべて拾います。このデータ構造をtrieと名付けたのは、1960年のエドワード・フレドキンです。retrievalから取った名前で、treeと同じ発音を意図していました。

各記号の意味

W
リストにある単語の総数
L
全単語の文字数の合計
N
異なる接頭辞ごとに置くノードの総数(根を除く)
前提
文字は小文字のa〜zで、各文字は1バイト、ポインタは64ビットとします。どちらのデータ構造でも、パディングやメモリ割り当ての管理領域は数えません。二分探索では、比較した文字の組を一組ずつ数え、異なる文字が現れたところで比較を止めます。単語の末尾に達したことの確認には、追加のコストを数えません。ハッシュ表は、最も都合のよい、衝突がまったくない場合を扱います。18バイトのノードでは子をアルファベット順に並べているので、順に探して目当ての文字を通り過ぎたら、そこで探索を止めます。

二分探索はc、a、rを4回読む。トライ木なら一度 🖖

carを検索を押してみましょう。トライ木は各段階で一文字ずつたどり、carの終端の印を見つけます。読むのは3文字です。同じ16語を辞書順に並べて二分探索すると、catch、careful、card、carと照合します。どれもcaで始まるため、毎回3文字を読んでから次に進む方向を決めます。合計12文字です。ハッシュ表では、格納先の計算に3文字、そこにある単語の確認にさらに3文字を読み、計6文字になります。格納した16語をすべて検索すると、トライ木が読むのは70文字、ハッシュ表は140文字、二分探索は169文字です。トライ木ではノードの節約につながる共通の語頭が、二分探索では比較を長くしています。aaaからzzzまで、3文字の文字列を全17,576通り格納した場合、二分探索で一つを見つけるには平均26.4文字を調べます。トライ木で読むのは、やはり3文字です。

辞書順リストでも自動補完はできる。ハッシュ表は全単語を調べる 🖖

carで始まる単語を押してみましょう。トライ木はc、a、rをたどり、carより下の8ノードを訪ねます。合計11ノードのうち、6ノードが単語の終端です。dogやplanなどは調べません。辞書順リストでも、carで始まる単語はひとまとまりに並びます。二分探索で先頭の単語を見つけるまでに5回照合し、そこから順に6語を集め、最初に条件から外れるcatで止まります。読んだ単語は計12語です。ハッシュ表では、car、card、cargoの格納先が互いに無関係になるように作られているため、16語すべてを読んで一つずつ条件を確かめます。単語の追加では、トライ木の利点がはっきり現れます。carpetを挿入を押すと、トライ木は3ノードを追加し、既存のノードは動かしません。辞書順リストは空きを作るため、11語分のポインタを一つずつ後ろにずらします。

70文字のうち40文字を共有しても、トライ木はリストの2.6倍 🖖

16語の文字数は合計70文字です。トライ木ではcar、card、careなどがc、a、rを共有するため、30ノードに収まります。ただし、ノードには文字以外の情報も必要です。省メモリな構成では、文字に1バイト、終端の印に1バイト、先頭の子と次の兄弟を指す8バイトのポインタにそれぞれ8バイトを使い、計18バイトになります。根を含む31ノードで558バイトです。単純なリストは各文字に1バイト、各単語に終端の1バイトと8バイトのポインタを使い、計214バイト。各ノードにアルファベットの各文字に対応するポインタを持たせ、配列の添字一つで次のノードを選べるようにすると、トライ木は6,479バイトとなり、リストの30倍に膨らみます。このページのように単語が最大14文字の場合、新しい単語の追加でトライ木のほうが少ないバイト数で済むのは、ノードをまったく増やさないときか、増やすノードが1個で単語が10文字以上あるときだけです。前者の例は、すでにplanetがある木にplanを加える場合です。省メモリな構成では、検索時間も増えます。目当ての文字を兄弟ノードから順に探すので、doorbellの検索には10回の比較が必要となり、二分探索より1回多くなります。

全プロセスの詳細解説

  1. どこまで共有できれば、トライ木のほうが小さくなる? 4 ステップ

    長い単語を押してみましょう。85文字が21ノードに収まり、文字の4分の3を共有していても、18バイトのノードを使うトライ木は396バイト、リストは157バイトです。すべての単語がk文字のリストを考えると、トライ木にとって最も有利なのは、可能な文字列をすべて格納する場合です。このページのバイト数の数え方では、トライ木が単純なリストより小さくなる最小のkはいくつでしょうか。

    この問題をツールで開く

    1. 画面の例では、21ノードに根を加えて22 × 18 = 396バイト。リストは85文字に加え、8語のそれぞれに終端の1バイトと8バイトのポインタを使うので、85 + 9 × 8 = 157バイトです。

    2. k文字の文字列は全部で26k通りあります。それぞれk文字なので、リストに必要なのは(k + 9) × 26kバイトです。

    3. トライ木では、1文字からk文字までのすべての文字列に対応するノードが必要です。その数は26 + 26² + … + 26kで、26/25 × 26kを少し下回ります。根を加えて18を掛けると、必要な容量は18.72 × 26kバイト弱です。

    4. したがって、k + 9が18.72を超えるとリストのほうが大きくなり、最初にそうなるのはk = 10です。k = 9ではトライ木のほうが4.0%大きく、k = 10では1.5%小さくなります。

    解答

    答えは10文字。ただし、2610通りすべて、約1.4 × 1014語をリストに格納する場合です。209バイトのノードなら、同じ計算でk + 9が約217を超える必要があり、単語の長さは209文字になります。

学習の道すじ

データ構造が得意なこと

参考文献 (3)