-
Notifications
You must be signed in to change notification settings - Fork 0
98 validate binary search tree medium #29
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
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,173 @@ | ||
| # 問題へのリンク | ||
| [98. Validate Binary Search Tree](https://leetcode.com/problems/validate-binary-search-tree/) | ||
|
|
||
| # 言語 | ||
| Python | ||
|
|
||
|
|
||
| # 自分の解法 | ||
|
|
||
| ## step1 | ||
|
|
||
| ```python | ||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| if root is None: | ||
| return True | ||
| def find_validity_and_minmax(root: TreeNode) -> tuple[bool, int, int]: | ||
| min_val = max_val = root.val | ||
| if root.left is not None: | ||
| is_left_valid, min_val, left_max = find_validity_and_minmax(root.left) | ||
| if not is_left_valid or left_max >= root.val: | ||
| return False, 0, 0 | ||
| if root.right is not None: | ||
| is_right_valid, right_min, max_val = find_validity_and_minmax(root.right) | ||
| if not is_right_valid or root.val >= right_min: | ||
| return False, 0, 0 | ||
| return True, min_val, max_val | ||
| is_valid, _, _ = find_validity_and_minmax(root) | ||
| return is_valid | ||
| ``` | ||
|
|
||
| `n`をノード数とすると、 | ||
| - 時間計算量:`O(n)` | ||
| - 空間計算量:`O(n)` | ||
| - もっと厳密には木の高さを`h`とすると`O(h)`。ただし、最悪の場合`h = n`。 | ||
| - 再帰呼び出しのスタックが`h`深くなるため。 | ||
| - 平衡木の場合は`O(log n)`。 | ||
|
|
||
| テストケース | ||
| - 標準的なBSTケース:`root = [2,1,3]` -> `True` | ||
| - 標準的なBSTでないケース:`root = [5,1,4,null,null,3,6]` -> `False` | ||
| - 空の木:`root = []` -> `True` | ||
| - 単一ノード:`root = [1]` -> `True` | ||
| - 右に偏った木:`root = [2, null, 3]` -> `True` | ||
| - 左に偏った木:`root = [2, 1, null]` -> `True` | ||
| - 境界ケース:`root = [1, 1, 2]` -> `False`、`root = [2, 1, 2]` -> `False` | ||
|
|
||
| 木をテキストとして書く方法 | ||
| ``` | ||
| 1 | ||
| / \ | ||
| 2 3 | ||
| \ | ||
| 4 | ||
| ``` | ||
|
|
||
|
|
||
| - 「各subtreeがBSTか」、「subtreeの最小値・最大値」を再帰的に取得し、rootの値と比較することでBSTかどうかを判定している。 | ||
| - が、関数として自然でない上に、コードが冗長になってしまっている。 | ||
| - これは考え方としては、左右のsubtreeをgivenとして、中央にrootを置いたらそれはBSTか、という形で考えている。 | ||
| - 別の考え方は、rootから順にノードを置いていく。rootをgivenとして、左(右)のsubtreeのノードを配置していく時には、「root.valより小さい(大きい)」という制約が課されるという考え方。 | ||
| - step2以降の`lower`/`upper`を用いた方法の方がシンプルに実装できる。 | ||
|
|
||
|
|
||
| 二分木の走査方法としては、以下の3つがある。 | ||
| - Inorder (left -> root -> right) | ||
| - Preorder (root -> left -> right) | ||
| - Postorder (left -> right -> root) | ||
|
|
||
| ## step2 | ||
|
|
||
| ```python | ||
| import math | ||
|
|
||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| def is_valid(root, lower, upper): | ||
| if not root: | ||
| return True | ||
| if root.val <= lower or root.val >= upper: | ||
| return False | ||
| return is_valid(root.left, lower, root.val) and is_valid(root.right, root.val, upper) | ||
|
|
||
| return is_valid(root, -math.inf, math.inf) | ||
| ``` | ||
|
|
||
| - 「rootが、左のsubtreeの最大値より大きく、右のsubtreeの最小値より小さい」という条件を、「左のsubtreeのノードが全てroot.valより小さい、右のsubtreeのノードが全てroot.valより大きい」と言い換えれば、よりシンプルに実装できる。 | ||
|
|
||
| ## step3 | ||
|
|
||
| ```python | ||
| import math | ||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| def is_valid(root, lower_bound, upper_bound) -> bool: | ||
| if root is None: | ||
| return True | ||
| if not (lower_bound < root.val < upper_bound): | ||
| return False | ||
| left_validity = is_valid(root.left, lower_bound, root.val) | ||
| right_validity = is_valid(root.right, root.val, upper_bound) | ||
| return left_validity and right_validity | ||
|
|
||
| return is_valid(root, -math.inf, math.inf) | ||
| ``` | ||
|
|
||
| - `lower`/`upper`を`lower_bound`/`upper_bound`に変えた | ||
| - 条件式を`not (lower < root.val < upper)`に変えた。この方が読みやすいと思ったため。 | ||
|
|
||
|
|
||
| ## step4 (FB) | ||
|
|
||
|
|
||
|
|
||
| # 別解・模範解答 | ||
| ## 反復的な解法 | ||
|
|
||
| rootから始めて、左右に進む際に、`lower`/`upper`の制約を更新していく。 | ||
| ノードをpushした時点で、そのノードに対する制約(`lower`/`upper`)は確定するので、ノードを取り出す順序はなんでも良い(スタックでもキューでもなんでも良い)。 | ||
|
|
||
| `iterative_lifo.py` | ||
|
|
||
| ```python | ||
| import math | ||
|
|
||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| frontiers = [(root, -math.inf, math.inf)] | ||
| while frontiers: | ||
| node, lower, upper = frontiers.pop() | ||
| if node is None: | ||
| continue | ||
| if lower >= node.val or upper <= node.val: | ||
| return False | ||
| frontiers.append((node.left, lower, node.val)) | ||
| frontiers.append((node.right, node.val, upper)) | ||
| return True | ||
| ``` | ||
|
|
||
|
|
||
| `iterative_fifo.py` | ||
| ```python | ||
| import math | ||
| from collections import deque | ||
|
|
||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| frontiers = deque([(root, -math.inf, math.inf)]) | ||
| while frontiers: | ||
| node, lower, upper = frontiers.popleft() | ||
| if node is None: | ||
| continue | ||
| if not (lower < node.val < upper): | ||
| return False | ||
| frontiers.append((node.left, lower, node.val)) | ||
| frontiers.append((node.right, node.val, upper)) | ||
| return True | ||
| ``` | ||
|
|
||
| - 時間計算量:`O(n)` | ||
| - 空間計算量:`O(n)` | ||
| - 今度は、木が非常に偏っている場合は`O(1)`になるが、平衡木の場合は`O(n)`になる。 | ||
| - スタック/キューにノードが最大で`n/2`個入る可能性があるため。 | ||
|
|
||
|
|
||
| # 想定されるフォローアップ質問 | ||
|
|
||
| - Q. 再帰的な解法と反復的な解法の違い、使い分けは? | ||
| - A. 再帰的な解法はコードがシンプルになる一方で、再帰の深さの分だけメモリを消費する。この問題では、木が偏っている場合には、再帰的な解法はスタックオーバーフローのリスクがあるため、反復的な解法が好まれる場合がある。一方、木が平衡に保たれている場合、反復的な解法よりも再帰的な解法の方が空間計算量が小さくなる。 | ||
|
|
||
| # 次に解く問題の予告 | ||
| - [Word Ladder - LeetCode](https://leetcode.com/problems/word-ladder/description/) | ||
| - [Top K Frequent Elements - LeetCode](https://leetcode.com/problems/top-k-frequent-elements/description/) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # | ||
| # @lc app=leetcode id=98 lang=python3 | ||
| # | ||
| # [98] Validate Binary Search Tree | ||
| # | ||
|
|
||
| # @lc code=start | ||
| # Definition for a binary tree node. | ||
| class TreeNode: | ||
| def __init__(self, val=0, left=None, right=None): | ||
| self.val = val | ||
| self.left = left | ||
| self.right = right | ||
|
|
||
| import math | ||
| from collections import deque | ||
|
|
||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| frontiers = deque([(root, -math.inf, math.inf)]) | ||
| while frontiers: | ||
| node, lower, upper = frontiers.popleft() | ||
| if node is None: | ||
| continue | ||
|
Comment on lines
+23
to
+24
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. Noneチェックはpop後にやる他、append前にやる方法もあります。 一応速度の面ではappend前にやった方がわずか速いことになりますが、趣味の範囲だと思います。
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. コメントありがとうございます。そうですね、自分もpop後にやる方がシンプルになると考えて、今回はこちらを採用しています。普段も基本的にはpop後にやる方を採用している気がします。
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. totally agreeです! 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. pop 後、append 前の比較ですが、基本的にはどちらでもいいと思います。 |
||
| if not (lower < node.val < upper): | ||
| return False | ||
| frontiers.append((node.left, lower, node.val)) | ||
| frontiers.append((node.right, node.val, upper)) | ||
| return True | ||
|
|
||
| # @lc code=end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # | ||
| # @lc app=leetcode id=98 lang=python3 | ||
| # | ||
| # [98] Validate Binary Search Tree | ||
| # | ||
|
|
||
| # @lc code=start | ||
| # Definition for a binary tree node. | ||
| class TreeNode: | ||
| def __init__(self, val=0, left=None, right=None): | ||
| self.val = val | ||
| self.left = left | ||
| self.right = right | ||
|
|
||
| import math | ||
|
|
||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| frontiers = [(root, -math.inf, math.inf)] | ||
| while frontiers: | ||
| node, lower, upper = frontiers.pop() | ||
| if node is None: | ||
| continue | ||
| if lower >= node.val or upper <= node.val: | ||
|
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. iterative_fifo.py のように if not (lower < node.val node.val):と数直線上に一直線になるように書いたほうが、読み手にとって読みやすくなると思います。
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. レビューありがとうございます。その通りですね。 |
||
| return False | ||
| frontiers.append((node.left, lower, node.val)) | ||
| frontiers.append((node.right, node.val, upper)) | ||
| return True | ||
|
|
||
| # @lc code=end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # | ||
| # @lc app=leetcode id=98 lang=python3 | ||
| # | ||
| # [98] Validate Binary Search Tree | ||
| # | ||
|
|
||
| # @lc code=start | ||
| # Definition for a binary tree node. | ||
| class TreeNode: | ||
| def __init__(self, val=0, left=None, right=None): | ||
| self.val = val | ||
| self.left = left | ||
| self.right = right | ||
|
|
||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| if root is None: | ||
| return True | ||
| def find_validity_and_minmax(root: TreeNode) -> tuple[bool, int, int]: | ||
| min_val = max_val = root.val | ||
| if root.left is not None: | ||
| is_left_valid, min_val, left_max = find_validity_and_minmax(root.left) | ||
| if not is_left_valid or left_max >= root.val: | ||
| return False, 0, 0 | ||
| if root.right is not None: | ||
| is_right_valid, right_min, max_val = find_validity_and_minmax(root.right) | ||
| if not is_right_valid or root.val >= right_min: | ||
| return False, 0, 0 | ||
| return True, min_val, max_val | ||
| is_valid, _, _ = find_validity_and_minmax(root) | ||
| return is_valid | ||
|
Comment on lines
+30
to
+31
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 find_validity_and_minmax(root)[0] と書いてもいいですね。 |
||
|
|
||
| # @lc code=end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # | ||
| # @lc app=leetcode id=98 lang=python3 | ||
| # | ||
| # [98] Validate Binary Search Tree | ||
| # | ||
|
|
||
| # @lc code=start | ||
| # Definition for a binary tree node. | ||
| class TreeNode: | ||
| def __init__(self, val=0, left=None, right=None): | ||
| self.val = val | ||
| self.left = left | ||
| self.right = right | ||
|
|
||
| import math | ||
|
|
||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| def is_valid(root, lower, upper): | ||
| if not root: | ||
| return True | ||
| if root.val <= lower or root.val >= upper: | ||
| return False | ||
| return is_valid(root.left, lower, root.val) and is_valid(root.right, root.val, upper) | ||
|
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. この行ですが、インデント込みで97文字あるようです。
https://peps.python.org/pep-0008/#maximum-line-length step3のように返り値を変数でおいてもいいですし、一応素朴には下記のように書くこともできます。 return (
is_valid(root.left, lower, root.val)
and is_valid(root.right, root.val, upper)
)
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. レビューとご指摘ありがとうございます。 |
||
|
|
||
| return is_valid(root, -math.inf, math.inf) | ||
|
|
||
| # @lc code=end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # | ||
| # @lc app=leetcode id=98 lang=python3 | ||
| # | ||
| # [98] Validate Binary Search Tree | ||
| # | ||
|
|
||
| # @lc code=start | ||
| # Definition for a binary tree node. | ||
| class TreeNode: | ||
| def __init__(self, val=0, left=None, right=None): | ||
| self.val = val | ||
| self.left = left | ||
| self.right = right | ||
|
|
||
| import math | ||
| class Solution: | ||
| def isValidBST(self, root: TreeNode|None) -> bool: | ||
| def is_valid(root, lower_bound, upper_bound) -> bool: | ||
| if root is None: | ||
| return True | ||
| if not (lower_bound < root.val < upper_bound): | ||
| return False | ||
| left_validity = is_valid(root.left, lower_bound, root.val) | ||
| right_validity = is_valid(root.right, root.val, upper_bound) | ||
| return left_validity and right_validity | ||
|
|
||
| return is_valid(root, -math.inf, math.inf) | ||
|
|
||
| # @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.
先頭が be 同士だと関数名のように感じる人がいるかもしれません。 left_valid で十分だと思います。
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.
確かにその通りですね。名詞形の
left_validityにまではしなくても良いのでしょうか。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.
他の平均的なソフトウェアエンジニアが最短で正確に認知負荷少なく、違和感忌避感嫌悪感少なく理解できるのであれば良いと思います。他の方から
left_validityのほうが良いというコメントをいただいたら、left_validityにすれば十分だと思います。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.
ありがとうございます!