JavaのTrieデータ構造
1. 概要
データ構造はコンピュータープログラミングの重要な資産であり、データ構造を使用するタイミングと理由を知ることは非常に重要です。
この記事は、トライ(「トライ」と発音)データ構造、その実装、および複雑性分析の簡単な紹介です。
2. Trie
トライは離散データ構造であり、一般的なアルゴリズムコースではあまり知られていないか、広く言及されていませんが、それでも重要なものです。
トライ(デジタルツリーとも呼ばれます)および場合によっては基数ツリーまたはプレフィックスツリー(プレフィックスで検索できるため)は、順序付けられたツリー構造であり、格納するキー(通常は文字列)を利用します。
ツリー内のノードの位置は、そのノードが関連付けられているキーを定義します。これにより、ノードがそのノードにのみ対応するキーを格納するバイナリ検索ツリーとは異なり、試行が異なります。
ノードのすべての子孫には、そのノードに関連付けられたStringの共通プレフィックスがありますが、ルートは空のString.に関連付けられています
ここに、Trie:の実装で使用するTrieNodeのプレビューがあります。
public class TrieNode {
private HashMap children;
private String content;
private boolean isWord;
// ...
}
トライがバイナリ検索ツリーである場合もありますが、一般的にはこれらは異なります。 バイナリ検索ツリーとトライの両方がツリーですが、バイナリ検索ツリーの各ノードには常に2つの子がありますが、トライのノードにはさらに多くの子があります。
トライでは、すべてのノード(ルートノードを除く)に1つの文字または数字が格納されます。 ルートノードから特定のノードnまでトライをトラバースすることにより、文字または数字の共通のプレフィックスを形成できます。これは、トライの他のブランチでも共有されます。
リーフノードからルートノードまでトライをトラバースすることにより、Stringまたは数字のシーケンスを形成できます。
これは、trieデータ構造の実装を表すTrieクラスです。
public class Trie {
private TrieNode root;
//...
}
3. 一般的な操作
それでは、基本的な操作を実装する方法を見てみましょう。
3.1. 要素を挿入する
ここで説明する最初の操作は、新しいノードの挿入です。
実装を開始する前に、アルゴリズムを理解することが重要です。
-
現在のノードをルートノードとして設定する
-
現在の文字を単語の最初の文字として設定します
-
現在のノードに現在の文字への既存の参照が既にある場合(「子」フィールドの要素の1つを使用して)、現在のノードをその参照ノードに設定します。 そうでない場合は、新しいノードを作成し、文字を現在の文字に設定し、現在のノードをこの新しいノードに初期化します
-
キーがトラバースされるまで手順3を繰り返します
この操作の複雑さはO(n)です。ここで、nはキーサイズを表します。
このアルゴリズムの実装は次のとおりです。
public void insert(String word) {
TrieNode current = root;
for (int i = 0; i < word.length(); i++) {
current = current.getChildren()
.computeIfAbsent(word.charAt(i), c -> new TrieNode());
}
current.setEndOfWord(true);
}
次に、このメソッドを使用してトライに新しい要素を挿入する方法を見てみましょう。
private Trie createExampleTrie() {
Trie trie = new Trie();
trie.insert("Programming");
trie.insert("is");
trie.insert("a");
trie.insert("way");
trie.insert("of");
trie.insert("life");
return trie;
}
次のテストから、トライに新しいノードがすでに入力されていることをテストできます。
@Test
public void givenATrie_WhenAddingElements_ThenTrieNotEmpty() {
Trie trie = createTrie();
assertFalse(trie.isEmpty());
}
3.2. 要素を見つける
次に、特定の要素がトライにすでに存在するかどうかを確認するメソッドを追加しましょう。
-
ルートの子を取得する
-
Stringの各文字を反復処理します
-
そのキャラクターがすでにサブトライの一部であるかどうかを確認してください。 トライのどこにも存在しない場合は、検索を停止してfalseを返します。
-
String.に文字がなくなるまで、2番目と3番目の手順を繰り返します。Stringの終わりに達した場合は、trueを返します。
このアルゴリズムの複雑さはO(n)です。ここで、nはキーの長さを表します。
Java実装は次のようになります。
public boolean find(String word) {
TrieNode current = root;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
TrieNode node = current.getChildren().get(ch);
if (node == null) {
return false;
}
current = node;
}
return current.isEndOfWord();
}
そして実際には:
@Test
public void givenATrie_WhenAddingElements_ThenTrieContainsThoseElements() {
Trie trie = createExampleTrie();
assertFalse(trie.containsNode("3"));
assertFalse(trie.containsNode("vida"));
assertTrue(trie.containsNode("life"));
}
3.3. 要素の削除
要素を挿入して見つけるだけでなく、要素を削除できる必要があることは明らかです。
削除プロセスでは、次の手順に従う必要があります。
-
この要素が既にトライの一部であるかどうかを確認します
-
要素が見つかったら、トライから削除します
このアルゴリズムの複雑さはO(n)です。ここで、nはキーの長さを表します。
実装を簡単に見てみましょう。
public void delete(String word) {
delete(root, word, 0);
}
private boolean delete(TrieNode current, String word, int index) {
if (index == word.length()) {
if (!current.isEndOfWord()) {
return false;
}
current.setEndOfWord(false);
return current.getChildren().isEmpty();
}
char ch = word.charAt(index);
TrieNode node = current.getChildren().get(ch);
if (node == null) {
return false;
}
boolean shouldDeleteCurrentNode = delete(node, word, index + 1) && !node.isEndOfWord();
if (shouldDeleteCurrentNode) {
current.getChildren().remove(ch);
return current.getChildren().isEmpty();
}
return false;
}
そして実際には:
@Test
void whenDeletingElements_ThenTreeDoesNotContainThoseElements() {
Trie trie = createTrie();
assertTrue(trie.containsNode("Programming"));
trie.delete("Programming");
assertFalse(trie.containsNode("Programming"));
}
4. 結論
この記事では、トライデータ構造とその最も一般的な操作とその実装について簡単に紹介しました。
この記事に示されている例の完全なソースコードは、over on GitHubにあります。