Skip to content

Conversation

@Kaichi-Irie
Copy link
Owner

問題へのリンク

Implement Trie (Prefix Tree) - LeetCode

言語

Python

問題の概要

prefix tree(トライ木)を実装する問題。
3つのメソッドを実装する。

  • insert(word: str): 単語を挿入する。
  • search(prefix: str): 単語が存在するかを確認する。
  • startsWith(prefix: str): 単語が指定の接頭辞で始まるかを確認する。

自分の解法

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

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

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

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

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

step2

  • charフィールドを削除

    • 子ノードのキーとして文字を使用するため、charフィールドは不要。
  • startsWithsearchの実装で大きく重複していた処理を_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のフィールドを持つ。

次に解く問題の予告

Copy link

@thonda28 thonda28 left a 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

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もありかなと思いました。

Comment on lines +26 to +27
if not word:
return

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.

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

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 は省略したいです

final_node = self._find_node_with(prefix)
return final_node is not None

def _find_node_with(self, prefix: str) -> TrieNode:

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 も返すので正しくなさそうです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにこのレベルのロジックならどちらも素直に書いてしまって良い気がしますね。
あまりに同じ処理だったので、まとめたくなってしまったのですが、可読性とのトレードオフが迷わしいところですね。
あと、型ヒントが間違っているトの指摘もありがとうございます。その通りです。

Comment on lines 30 to 32
child_node = TrieNode()
child_node.char = char
node.children[char] = child_node

Choose a reason for hiding this comment

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

この処理はまとめたほうがシンプルに思いました

Suggested change
child_node = TrieNode()
child_node.char = char
node.children[char] = child_node
node.children[char] = TrieNode(char)

Copy link
Owner Author

Choose a reason for hiding this comment

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

TrieNode のコンストラクタでcharを設定できるようにするということですね。確かにその通りだと思います。

Copy link
Owner Author

@Kaichi-Irie Kaichi-Irie left a 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:
Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにこのレベルのロジックならどちらも素直に書いてしまって良い気がしますね。
あまりに同じ処理だったので、まとめたくなってしまったのですが、可読性とのトレードオフが迷わしいところですね。
あと、型ヒントが間違っているトの指摘もありがとうございます。その通りです。

Comment on lines 30 to 32
child_node = TrieNode()
child_node.char = char
node.children[char] = child_node
Copy link
Owner Author

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かかってしまったのでやり直し
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'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants