Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions 208_implement_trie_prefix_tree_medium/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 問題へのリンク

[Implement Trie (Prefix Tree) - LeetCode](https://leetcode.com/problems/implement-trie-prefix-tree/description/)

# 言語
Python

# 問題の概要
prefix tree(トライ木)を実装する問題。
3つのメソッドを実装する。
- `insert(word: str)`: 単語を挿入する。
- `search(prefix: str)`: 単語が存在するかを確認する。
- `startsWith(prefix: str)`: 単語が指定の接頭辞で始まるかを確認する。

# 自分の解法

`Trie`クラスには`char: str`と`children: dict[str, Trie]`というフィールドを持たせる。また、`is_final_char: bool`を持たせることで、単語の終端を示す。これにより、`apple`と`app`などの単語を区別できる。

走査は、`children`を辿っていく。再帰関数を使うことで、単語の各文字を順に確認していく。こうして`word`や`prefix`の最後の文字まで到達できれば、単語や接頭辞が存在することが確認できる。

`Trie`クラス自体が保持するデータは、各入力文字列の長さを`n_1, n_2, ...`とすると、最悪ケースでは`O(n_1+n_2+...)`の空間を使用する。(重複が多ければ、より少なくなる。)

各メソッドについて、`prefix`や`word`の長さを`n`とすると、以下のような時間計算量と空間計算量になる。

- 時間計算量:`O(n)`
- 空間計算量:`O(n)`
- 再帰関数による実装のせいで各メソッドの呼び出しのたびにサイズ`n`のスタックを使用する。

## step2
- `char`フィールドを削除
- 子ノードのキーとして文字を使用するため、`char`フィールドは不要。
- `startsWith`と`search`の実装で大きく重複していた処理を`_find_node_with`メソッドに切り出す。
- `prefix:str`を引数に取り、`Trie`のノードを返す。
- `prefix`の文字列が存在しない場合は`None`を返す。
- `search`メソッドは、`_find_node_with`を使って、単語の終端であるかを確認する。
- `startsWith`メソッドは、`_find_node_with`を使って、接頭辞が存在するかを確認する。
- `children = defaultdict(Trie)`を使って、`children`の初期化やキーの存在確認を簡潔にする。

- `head`, `tail`の変数名を`first_char`, `trailing_chars`に変更。


## step3
- v1. 16minかかってしまったのでやり直し
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

問題ないと思います。
気が向いたら、サードパーティーライブラリーにどのような API があるのか、longest_prefix とか shortest_prefix とかを眺めておきましょう。
https://pygtrie.readthedocs.io/en/latest/index.html

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます。
早速ざっとながめてみました。Node_NoChild_OneChild_Childrenなど細かく何層にも分かれていて、なぜこんなに細かく分けているのか、等を考えてみることはとても勉強になると感じました。

また、Web APIのルーティングへの用途を意識してか、1文字1ノードではなくて、 separator で区切られた文字列を1つのノードにあてるという仕様も、実用的だと感じました。

t = pygtrie.StringTrie()
t['foo/bar'] = 'Bar'


# 別解・模範解答
再帰関数を使わずに、`children`を辿るループで実装する。
- 時間計算量:`O(n)`
- 空間計算量:`O(1)`
- 再帰関数を使わずに、`children`を辿るループで実装する。
- `TrieNode`クラスを作成し、`Trie`のノードを表現する。
- `char: str`, `children: dict[str, TrieNode]`, `is_final_char: bool`のフィールドを持つ。
# 次に解く問題の予告
- [Subsets - LeetCode](https://leetcode.com/problems/subsets/)
- [Is Subsequence - LeetCode](https://leetcode.com/problems/is-subsequence/description/)
55 changes: 55 additions & 0 deletions 208_implement_trie_prefix_tree_medium/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#
# @lc app=leetcode id=208 lang=python3
#
# [208] Implement Trie (Prefix Tree)
#


# %%
# @lc code=start
class Trie:
def __init__(self, root=""):
self.root = root
self.children: dict[str, Trie] = {} # character -> Trie Tree
self.is_terminal = False

def insert(self, word: str) -> None:
if not word:
self.is_terminal = True
return

head, tail = word[0], word[1:]
if head not in self.children:
self.children[head] = Trie(root=head)
child: Trie = self.children[head]
child.insert(tail)

def search(self, word: str) -> bool:
if not word:
return self.is_terminal
head, tail = word[0], word[1:]
if head not in self.children:
return False
else:
child: Trie = self.children[head]
return child.search(tail)

def startsWith(self, prefix: str) -> bool:
if not prefix:
return True
head, tail = prefix[0], prefix[1:]
if head not in self.children:
return False
child: Trie = self.children[head]
return child.startsWith(tail)


# %%

# %%
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
# @lc code=end
62 changes: 62 additions & 0 deletions 208_implement_trie_prefix_tree_medium/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#
# @lc app=leetcode id=208 lang=python3
#
# [208] Implement Trie (Prefix Tree)
#


# @lc code=start
from collections import defaultdict


class Trie:
def __init__(self):
self.children = defaultdict(Trie)
self.is_final_char = False

# TC: O(len(word))
# SC: O(len(word))
def insert(self, word: str) -> None:
if not word:
self.is_final_char = True
return
first_char, trailing_chars = word[0], word[1:]
child_node: Trie = self.children[first_char]
child_node.insert(trailing_chars)

def search(self, word: str) -> bool:
final_node: Trie | None = self._find_prefix_node(word)
# word is not found
if final_node is None:
return False
# no word end at the final node
elif not final_node.is_final_char:
return False
# word end at the final node
else:
return True

def startsWith(self, prefix: str) -> bool:
final_node: Trie | None = self._find_prefix_node(prefix)
return final_node is not None

# TC: O(len(prefix))
# SC: O(len(prefix))
from typing import Optional

def _find_prefix_node(self, prefix: str) -> Optional["Trie"]:
if not prefix:
return self
first_char, trailing_chars = prefix[0], prefix[1:]
if first_char not in self.children:
return None
child_node: Trie = self.children[first_char]
return child_node._find_prefix_node(trailing_chars)


# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
# @lc code=end
68 changes: 68 additions & 0 deletions 208_implement_trie_prefix_tree_medium/step2_iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#
# @lc app=leetcode id=208 lang=python3
#
# [208] Implement Trie (Prefix Tree)
#


# %%
# @lc code=start
from collections import defaultdict


# TrieNode has char:str, children: dict[str, TrieNode], and is_final_char
class TrieNode:
def __init__(self):
self.children = defaultdict(TrieNode)
self.is_final_char = False

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_final_char は「単語の最後の文字」ということかと思いますが、単語というニュアンスがないので伝えたい意味に伝わらない可能性もあるかなと思いました。Step1 の is_terminal や他の例として is_word あたりのが簡潔かつわかりやすいかなと個人的には思いました。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます。確かにtreeの葉なのかな、とも思ってしまいますよね。
is_terminal は一般的なprefix treeではわかりやすいように感じましたが、今回の文脈の情報がないのが微妙かなと感じて却下しました。
is_wordがいちばんわかりやすいかもしれません。
今思いついたもので言うと、 word_ends_hereもありかなと思いました。



# Trie has root TrieNode
class Trie:
def __init__(self):
self.root = TrieNode()

def insert(self, word: str) -> None:
if not word:
return
Comment on lines +26 to +27

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

問題の制約として empty ("") は入りませんが、ここをどうするかは仕様次第かなと思いました。
今回の解法のように early return を入れることでスキップするパターンの他にも、early return を削って self.root.is_final_char = True とする(empty を単語とみなす)パターンや、不適切な入力なので例外を返すといったパターンも考えられそうです。

今回 early return したこと自体に問題はまったくないですが、もしこのあたり検討していたのであれば README とかに書いておいてもよさそうです

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有用なアドバイス、ありがとうございます。
確かにほかの可能性まではきちんと考えられていませんでした。


node = self.root
for char in word:
node = node.children[char]
node.is_final_char = True
return

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的には不要な return は省略したいです


def search(self, word: str) -> bool:
final_node: TrieNode | None = self._find_node_with(word)
if final_node is None:
return False
elif not final_node.is_final_char:
return False
else:
return True

def startsWith(self, prefix: str) -> bool:
final_node = self._find_node_with(prefix)
return final_node is not None

def _find_node_with(self, prefix: str) -> TrieNode | None:
if not prefix:
return self.root

node = self.root
for char in prefix:
if char not in node.children:
return None
node = node.children[char]
return node


# %%

# %%
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
# @lc code=end
56 changes: 56 additions & 0 deletions 208_implement_trie_prefix_tree_medium/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#
# @lc app=leetcode id=208 lang=python3
#
# [208] Implement Trie (Prefix Tree)
#


# %%
# @lc code=start
# TrieNode has char:str, children: dict[str, TrieNode], and is_final_char
from collections import defaultdict


class TrieNode:
def __init__(self):
self.children: dict[str, TrieNode] = defaultdict(TrieNode)
self.word_ends_here = False


# Trie has root TrieNode
class Trie:
def __init__(self):
self.root = TrieNode()

def insert(self, word: str) -> None:
node: TrieNode = self.root
for char in word:
node = node.children[char]
node.word_ends_here = True

def search(self, word: str) -> bool:
node: TrieNode = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.word_ends_here

def startsWith(self, prefix: str) -> bool:
node: TrieNode = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True


# %%

# %%
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
# @lc code=end