Skip to content

Conversation

@Kaichi-Irie
Copy link
Owner

問題へのリンク

98. Validate Binary Search Tree

言語

Python

自分の解法

step1

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] -> Falseroot = [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

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

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/upperlower_bound/upper_boundに変えた
  • 条件式をnot (lower < root.val < upper)に変えた。この方が読みやすいと思ったため。

step4 (FB)

別解・模範解答

反復的な解法

rootから始めて、左右に進む際に、lower/upperの制約を更新していく。
ノードをpushした時点で、そのノードに対する制約(lower/upper)は確定するので、ノードを取り出す順序はなんでも良い(スタックでもキューでもなんでも良い)。

iterative_lifo.py

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

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. 再帰的な解法はコードがシンプルになる一方で、再帰の深さの分だけメモリを消費する。この問題では、木が偏っている場合には、再帰的な解法はスタックオーバーフローのリスクがあるため、反復的な解法が好まれる場合がある。一方、木が平衡に保たれている場合、反復的な解法よりも再帰的な解法の方が空間計算量が小さくなる。

次に解く問題の予告

Copy link

@docto-rin docto-rin left a comment

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 +31
is_valid, _, _ = find_validity_and_minmax(root)
return is_valid

Choose a reason for hiding this comment

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

return find_validity_and_minmax(root)[0] と書いてもいいですね。

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)

Choose a reason for hiding this comment

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

この行ですが、インデント込みで97文字あるようです。
PEP8では一行あたりの文字数が79文字に制限されています。

Limit all lines to a maximum of 79 characters.

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)
            )

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューとご指摘ありがとうございます。
確かに長すぎますね、Formatter/Linterをオフにしても長すぎると違和感を抱けるように気をつけます。

Comment on lines +23 to +24
if node is None:
continue

Choose a reason for hiding this comment

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

Noneチェックはpop後にやる他、append前にやる方法もあります。

一応速度の面ではappend前にやった方がわずか速いことになりますが、趣味の範囲だと思います。
pop後にやる方が大抵コードはシンプルになる気がします。

Copy link
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます。そうですね、自分もpop後にやる方がシンプルになると考えて、今回はこちらを採用しています。普段も基本的にはpop後にやる方を採用している気がします。
もしappend前にするなら、以下の場合にNoneのチェックが入ることになると思います。

  • left, rightの両方をappendする前
  • rootNoneの場合
    多少コードが長くなっても、append前にNoneのチェックを書きたくなる場面としては、関数の引数の型をSomeType|Noneでなく、SomeType にしたい場合などでしょうか。

Choose a reason for hiding this comment

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

totally agreeです!

Copy link

Choose a reason for hiding this comment

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

pop 後、append 前の比較ですが、基本的にはどちらでもいいと思います。
ただ、BFS で pop 後にフラグ立てると幾何級数的に増加することがあります。
https://discord.com/channels/1084280443945353267/1336510702742929499/1350740661791625327
同じ追記するべきかの判定が散らかるのに抵抗があるならば、関数化するのも一つでしょう。

node, lower, upper = frontiers.pop()
if node is None:
continue
if lower >= node.val or upper <= node.val:
Copy link

Choose a reason for hiding this comment

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

iterative_fifo.py‎ のように

if not (lower < node.val node.val):

と数直線上に一直線になるように書いたほうが、読み手にとって読みやすくなると思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。その通りですね。

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)
Copy link

Choose a reason for hiding this comment

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

先頭が be 同士だと関数名のように感じる人がいるかもしれません。 left_valid で十分だと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにその通りですね。名詞形の left_validity にまではしなくても良いのでしょうか。

Copy link

Choose a reason for hiding this comment

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

他の平均的なソフトウェアエンジニアが最短で正確に認知負荷少なく、違和感忌避感嫌悪感少なく理解できるのであれば良いと思います。他の方から left_validity のほうが良いというコメントをいただいたら、 left_validity にすれば十分だと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます!

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.

5 participants