-
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?
Conversation
thonda28
left a comment
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.
Group anagrams も PR に含まれてしまっています
| 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 comment
The reason will be displayed to describe this comment to others. Learn more.
is_final_char は「単語の最後の文字」ということかと思いますが、単語というニュアンスがないので伝えたい意味に伝わらない可能性もあるかなと思いました。Step1 の is_terminal や他の例として is_word あたりのが簡潔かつわかりやすいかなと個人的には思いました。
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.
レビューありがとうございます。確かにtreeの葉なのかな、とも思ってしまいますよね。
is_terminal は一般的なprefix treeではわかりやすいように感じましたが、今回の文脈の情報がないのが微妙かなと感じて却下しました。
is_wordがいちばんわかりやすいかもしれません。
今思いついたもので言うと、 word_ends_hereもありかなと思いました。
| if not word: | ||
| return |
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.
問題の制約として empty ("") は入りませんが、ここをどうするかは仕様次第かなと思いました。
今回の解法のように early return を入れることでスキップするパターンの他にも、early return を削って self.root.is_final_char = True とする(empty を単語とみなす)パターンや、不適切な入力なので例外を返すといったパターンも考えられそうです。
今回 early return したこと自体に問題はまったくないですが、もしこのあたり検討していたのであれば README とかに書いておいてもよさそうです
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.
有用なアドバイス、ありがとうございます。
確かにほかの可能性まではきちんと考えられていませんでした。
| 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 comment
The reason will be displayed to describe this comment to others. Learn more.
個人的には不要な return は省略したいです
| final_node = self._find_node_with(prefix) | ||
| return final_node is not None | ||
|
|
||
| def _find_node_with(self, prefix: str) -> TrieNode: |
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.
_find_node_with() の共通化によってむしろ複雑になっている気がしました。search() および startsWith() で無理に共通化せず素直に書くほうが可読性が高そうです。また返り値の型ヒントとして TrieNode としていますが実際には None も返すので正しくなさそうです。
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.
確かにこのレベルのロジックならどちらも素直に書いてしまって良い気がしますね。
あまりに同じ処理だったので、まとめたくなってしまったのですが、可読性とのトレードオフが迷わしいところですね。
あと、型ヒントが間違っているトの指摘もありがとうございます。その通りです。
| child_node = TrieNode() | ||
| child_node.char = char | ||
| node.children[char] = child_node |
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.
この処理はまとめたほうがシンプルに思いました
| child_node = TrieNode() | |
| child_node.char = char | |
| node.children[char] = child_node | |
| node.children[char] = TrieNode(char) |
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.
TrieNode のコンストラクタでcharを設定できるようにするということですね。確かにその通りだと思います。
Kaichi-Irie
left a comment
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.
レビューありがとうございます。
| final_node = self._find_node_with(prefix) | ||
| return final_node is not None | ||
|
|
||
| def _find_node_with(self, prefix: str) -> TrieNode: |
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.
確かにこのレベルのロジックならどちらも素直に書いてしまって良い気がしますね。
あまりに同じ処理だったので、まとめたくなってしまったのですが、可読性とのトレードオフが迷わしいところですね。
あと、型ヒントが間違っているトの指摘もありがとうございます。その通りです。
| child_node = TrieNode() | ||
| child_node.char = char | ||
| node.children[char] = child_node |
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.
TrieNode のコンストラクタでcharを設定できるようにするということですね。確かにその通りだと思います。
|
|
||
|
|
||
| ## step3 | ||
| - v1. 16minかかってしまったのでやり直し |
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つのノードにあてるという仕様も、実用的だと感じました。
t = pygtrie.StringTrie()
t['foo/bar'] = 'Bar'
問題へのリンク
Implement Trie (Prefix Tree) - LeetCode
言語
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
別解・模範解答
再帰関数を使わずに、
childrenを辿るループで実装する。O(n)O(1)childrenを辿るループで実装する。TrieNodeクラスを作成し、Trieのノードを表現する。char: str,children: dict[str, TrieNode],is_final_char: boolのフィールドを持つ。次に解く問題の予告