-
Notifications
You must be signed in to change notification settings - Fork 0
208 implement trie prefix tree medium #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e3b7a1a
8ae64b4
4c9c026
ea5300e
0fe8c42
b828354
a4cc78a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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かかってしまったのでやり直し | ||
|
|
||
| # 別解・模範解答 | ||
| 再帰関数を使わずに、`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/) | ||
| 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 |
| 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 |
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. レビューありがとうございます。確かにtreeの葉なのかな、とも思ってしまいますよね。 |
||
|
|
||
|
|
||
| # 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 問題の制約として empty ( 今回 early return したこと自体に問題はまったくないですが、もしこのあたり検討していたのであれば README とかに書いておいてもよさそうです
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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つのノードにあてるという仕様も、実用的だと感じました。