TRIE木
トライ木(英: trie)やプレフィックス木(英: prefix tree)とは、順序付き木の一種。あるノードの配下の全ノードは、自身に対応する文字列に共通するプレフィックス(接頭部)があり、ルート(根)には空の文字列が対応している。値は一般に全ノードに対応して存在するわけではなく、末端ノードや一部の中間ノードだけがキーに対応した値を格納している。2分探索木と異なり、各ノードに個々のキーが格納されるのではなく、木構造上のノードの位置とキーが対応している。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
「文字列の検索」のために使われる木構造で、2分探索木やハッシュテーブルの代替として使うことができます。
トライ木では、各ノードを文字列のそこまでの “prefix” に対応させることで文字列を表現します。
2分探索木より全般的に高速に動作します。
ハッシュテーブルと比較すると検索が速く、またキー順によるソートを行うことができます。
しかし、単純な実装ではノードに使う文字全てを保持するので、メモリ効率がとても悪くなります。
TRIE木の単純な実装
Python で単純な TRIE木を実装してみます。
TRIE木の単純な実装では、ルートを空ノードにしてそれぞれのノードから必要なだけエッジを伸ばした木構造(例えばアルファベットをキーに使う場合は26のエッジを伸ばす)を、配列を使って構成するだけです。
より効率的にするためには、配列ではなく、ダブルアレイ、LOUDS、三分探索木といったデータ構造を使うそうです。
【前編】ダブルアレイで高速でコンパクトな辞書を実装する〜準備〜
簡潔データ構造 LOUDS の解説(全12回、練習問題付き)
エッジ数
エッジの数を定数として保持します。
ここでは、小文字のアルファベットのみがキーになると想定して、26文字とします。
ALPHABET_SIZE = 26
ノード
それぞれのノードは、エッジ数分の子を配列で管理し、-1で初期化します。
また、そこで単語が終わったのかどうかを管理する変数、そこまでの文字列でprefix として該当する総数を保持する変数を持ちます。
ALPHABET_SIZE = 26 class Node(object): def __init__(self, char): self.char = char self.children = [-1] * ALPHABET_SIZE self.word_finished = False self.matched_prefix_cnt = 0
Trie クラス
ルートは空文字で初期化します。
また、小文字アルファベットを配列で管理するためのヘルパー関数 _convert_char_to_num
を定義します。
ALPHABET_SIZE = 26 class Node(object): def __init__(self, char): self.char = char self.children = [-1] * ALPHABET_SIZE self.word_finished = False self.matched_prefix_cnt = 0 class Trie(object): def __init__(self): self.root = Node(' ') def _convert_char_to_num(self, char): return ord(char) - ord('a')
単語の追加
木構造にそこまでの prefix が既に含まれるかどうか、ループで単語を一文字ずつ確認し、含まれない場合は文字を追加します。
単語が終わったら、そのノードの終了フラグをTrue
にして、その単語の存在を記憶します。
ALPHABET_SIZE = 26 class Node(object): def __init__(self, char): self.char = char self.children = [-1] * ALPHABET_SIZE self.word_finished = False self.matched_prefix_cnt = 0 class Trie(object): def __init__(self): self.root = Node(' ') def _convert_char_to_num(self, char): return ord(char) - ord('a') def insert(self, word): current = self.root for char in word: char_index = self._convert_char_to_num(char) if current.children[char_index] != -1: current = current.children[char_index] current.matched_prefix_cnt += 1 else: new_node = Node(char) current.children[char_index] = new_node current = new_node current.matched_prefix_cnt += 1 current.word_finished = True
単語の検索
ループを使って、一文字ずつ単語が木構造に当てはまるかどうかを確認します。
ALPHABET_SIZE = 26 class Node(object): def __init__(self, char): self.char = char self.children = [-1] * ALPHABET_SIZE self.word_finished = False self.matched_prefix_cnt = 0 class Trie(object): def __init__(self): self.root = Node(' ') def _convert_char_to_num(self, char): return ord(char) - ord('a') def insert(self, word): current = self.root for char in word: char_index = self._convert_char_to_num(char) if current.children[char_index] != -1: current = current.children[char_index] current.matched_prefix_cnt += 1 else: new_node = Node(char) current.children[char_index] = new_node current = new_node current.matched_prefix_cnt += 1 current.word_finished = True def search(self, word): if not self.root.children: return False, 0 current = self.root for char in word: char_index = self._convert_char_to_num(char) if current.children[char_index] != -1: current = current.children[char_index] else: return False, 0 if current.word_finished: return True, current.matched_prefix_cnt return False, current.matched_prefix_cnt
テスト
想定通りに動くか、下のような単語を入力して確認します。
‘A’ は ‘a’ にします。
ALPHABET_SIZE = 26 class Node(object): def __init__(self, char): self.char = char self.children = [-1] * ALPHABET_SIZE self.word_finished = False self.matched_prefix_cnt = 0 class Trie(object): def __init__(self): self.root = Node(' ') def _convert_char_to_num(self, char): return ord(char) - ord('a') def insert(self, word): current = self.root for char in word: char_index = self._convert_char_to_num(char) if current.children[char_index] != -1: current = current.children[char_index] current.matched_prefix_cnt += 1 else: new_node = Node(char) current.children[char_index] = new_node current = new_node current.matched_prefix_cnt += 1 current.word_finished = True def search(self, word): if not self.root.children: return False, 0 current = self.root for char in word: char_index = self._convert_char_to_num(char) if current.children[char_index] != -1: current = current.children[char_index] else: return False, 0 if current.word_finished: return True, current.matched_prefix_cnt return False, current.matched_prefix_cnt if __name__ == '__main__': trie = Trie() trie.insert('a') trie.insert('to') trie.insert('tea') trie.insert('ted') trie.insert('ten') trie.insert('i') trie.insert('in') trie.insert('inn') # (True, 1) print(trie.search('a')) # (False, 4) print(trie.search('t')) # (True, 1) print(trie.search('ted')) # (False, 0) print(trie.search('tel')) # (True, 2) print(trie.search('in')) # (True, 1) print(trie.search('inn'))