Skip to content

98. Validate Binary Search Tree#27

Open
hroc135 wants to merge 4 commits intomainfrom
98ValidateBinarySearchTree
Open

98. Validate Binary Search Tree#27
hroc135 wants to merge 4 commits intomainfrom
98ValidateBinarySearchTree

Conversation

@hroc135
Copy link
Copy Markdown
Owner

@hroc135 hroc135 commented Nov 3, 2024

@hroc135 hroc135 changed the title 98. alidate binary search tree 98. Validate Binary Search Tree Nov 3, 2024
return tree.valid
}

func isValidBSTRecursively(root *TreeNode) subtree {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

些末なことかも知れないですが、返り値が boolean 値出ないのに接頭辞が is になっているのが気になりました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

なんと、本当ですね
subtree構造体のvaidフィールドが何かということをメインに調べたいので、checkSubtreeRecursivelyみたいな感じの方がいいですかね

- 標準ライブラリslicesのIsSortedFuncの第二引数の関数の引数が、
a = s[i], b = s[i+1]の順に並んでいると思ったら逆だった。
なぜ逆順なのだろう?
pkg.go.devのドキュメントに特に何も注意書きがなく、内部実装を見て初めて気がついた
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IsSorted の気持ちは分かります。sorted な配列に追加することがあるから後ろから見ていきたくて、less のほうが基本的だからこの構造です。Func も合わせたんでしょう。
https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/slices/sort.go;drc=6d93de2c110f66457f103c33ba496ff2e2bf33af;l=53

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

そういうことだったんですね
そういえばslices.Sortで使われているpattern-defeating quicksortもソートされた配列に最後の要素だけ追加が生じた場合にO(n)でできるアルゴリズムであると以前知りました

https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/slices/sort.go;l=16;drc=6d93de2c110f66457f103c33ba496ff2e2bf33af
https://github.com/orlp/pdqsort?tab=readme-ov-file#the-best-case

@TORUS0818
Copy link
Copy Markdown

拝見しました。
良いと思います。

参考に再帰in-order貼っておきます。

class Solution:
  def isValidBST(self, root: Optional[TreeNode]) -> bool:
      if not root:
          return True

      if root.left:
          if root.left.val >= root.val:
              return False
          if not self.isValidBST(root.left):
              return False

      if root.right:
          if root.right.val <= root.val:
              return False
          if not self.isValidBST(root.right):
              return False

      return True

@oda
Copy link
Copy Markdown

oda commented Nov 5, 2024

参考に再帰in-order貼っておきます。

これ、left の下全体が、root.val よりも小さいという制限をチェックしていないんじゃないですか?

return true
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Go らしさのあるものをと思ったので chan を使ったコードとかを上げてみます。

func traverseInorderHelper(node *TreeNode, c chan *TreeNode) {
    if (node.Left != nil) {
        traverseInorderHelper(node.Left, c)
    }
    c <- node
    if (node.Right != nil) {
        traverseInorderHelper(node.Right, c)
    }
}

func traverseInorder(root *TreeNode, c chan *TreeNode) {
    traverseInorderHelper(root, c)
    close(c)
}

func isValidBST(root *TreeNode) bool {
    c := make(chan *TreeNode)
    go traverseInorder(root, c)
    var previous *TreeNode = nil
    for {
        node, ok := <- c
        if !ok {
            return true
        }
        if previous != nil && node.Val <= previous.Val {
            return false
        }
        previous = node
    }
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@oda
ありがとうございます。goroutineに慣れていないので確認の質問をさせてください。
このコードの利点はinorderなノード探索の結果をチャンネルに押し出していく形にすることによって、スライスに溜める時と比べてメモリ消費量を抑えられることにある、ということですか?
あともうひとつの利点はgoroutineの並列処理を使っているので、ノード同士の関係性チェックとinorder探索が並列で行われているので、実行時間の短縮が期待される、ということも挙げられると思うのですが、合っていますか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

それから別の類の質問なのですが、odaさんのコードを見ていると上から子関数->親関数という順序で書いていることが多いのですが、この順序は単に好みですか?styleguideに特に記載はなかったのですが、個人的には親->子という順序で書いたほうが読みやすいので、子->親としている背景があったら知りたいと思った次第です

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あー、メリットとしては複数のスレッドで並列計算されることくらいでしょうが、どちらかというとこれは「言語の機能を見てみよう」くらいの気持ちで書いているので、そんなに真面目に考えなくていいと思います。メモリー消費量は分かりませんが直感的には増えるんじゃないでしょうか。関数呼び出しのスタックが積まれるので。

子供から書くのは、おそらく C/C++ 由来の癖と思います。先に signature がないといけません。どちらでもいいんじゃないでしょうか。

Copy link
Copy Markdown

@hayashi-ay hayashi-ay left a comment

Choose a reason for hiding this comment

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

2ndのIterative版も書いても良いと思います。

役割分担をどう定義するかなんだな、と頭でわかっているつもりだが、
いまだに実装の苦手意識がある。
今Haskellを使う授業を受けているので再帰のいい訓練になることを願う
- 一般的に、末尾再帰はコンパイラの最適化がかかることが多いので、
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2ndのコードは、末尾再帰のある言語、処理系でも末尾再帰最適化されないと思います。
returnする際に、

  • 左のノードについて評価
  • 結果を保存
  • 右のノードについて評価
  • 最後に&&を評価

という流れで呼び出した結果を呼び出し元で保存する必要があるので、関数呼び出しをジャンプに変換することができないと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。末尾再帰とその最適化についてよく理解できていなさそうだったので、以下の記事を読んでみました。
https://qiita.com/pebblip/items/cf8d3230969b2f6b3132
https://qiita.com/TakekazuKATO/items/fc3fedf0ab36ff039aaa#%E6%9C%AB%E5%B0%BE%E5%86%8D%E5%B8%B0%E3%81%A8%E6%9C%AB%E5%B0%BE%E5%86%8D%E5%B8%B0%E6%9C%80%E9%81%A9%E5%8C%96

<自分用メモ>
末尾呼び出し: ある関数fのリターン前の最後の計算が関数gの呼び出しであるということ
末尾再帰最適化: 末尾再帰になっているコードに対してコンパイラが行う最適化で、コールスタックサイズの縮小が期待される。具体的には、

func f(x1) {
    x2 := x1 * 2
    return f(x2)
}

みたいなコードがあったとして、return f(x2)の時にf(x1)の現状をスタックフレームとしてコールスタックに積む代わりに、func f(x1)の行にジャンプして引数をx2に更新する。といった動作をする

末尾再帰最適化はあくまで言語処理系が行う最適化であるので、最適化してくれない処理系を使っているときに末尾再帰を書いてもパフォーマンス上の利点はない

Goは末尾再帰最適化がないと思っていたが、一部あるらしい
https://groups.google.com/g/golang-nuts/c/0oIZPHhrDzY/m/2nCpUZDKZAAJ

JSはES6でサポートされるようになったらしい

@TORUS0818
Copy link
Copy Markdown

@oda
ほんとですね。。root =[5,4,6,null,null,3,7]で通りませんでした。失礼しました。

@hroc135
Copy link
Copy Markdown
Owner Author

hroc135 commented Nov 7, 2024

@TORUS0818
再帰inorderの他の実装を考えてみましたが、思いつかなかったです

@liquo-rice
Copy link
Copy Markdown

@TORUS0818 再帰inorderの他の実装を考えてみましたが、思いつかなかったです

inorderで一つ前の値をなんらかの方法で再帰関数に渡してあげれば良いです。

@hroc135
Copy link
Copy Markdown
Owner Author

hroc135 commented Nov 8, 2024

@liquo-rice
ヒントありがとうございます。一晩考えてやっとできました

func isValidBST(root *TreeNode) bool {
	isValid, _ := isValidBSTHelper(root, nil)
	return isValid
}

func isValidBSTHelper(node *TreeNode, previousValue *int) (bool, *int) {
	value := previousValue
	if node.Left != nil {
		isValid, v := isValidBSTHelper(node.Left, previousValue)
		if !isValid {
			return false, nil
		}
		value = v
	}
	if value != nil && node.Val <= *value {
		return false, nil
	}
	value = &node.Val
	if node.Right != nil {
		isValid, v := isValidBSTHelper(node.Right, value)
		if !isValid {
			return false, nil
		}
		value = v
	}
	return true, value
}

@liquo-rice
Copy link
Copy Markdown

このようにもできますね。

type optionalInt struct {
    HasValue bool
    Value int
}

func isValidBST(root *TreeNode) bool {
    previousValue := optionalInt{false, 0}
    return isValidBSTHelper(root, &previousValue)
}

func isValidBSTHelper(node *TreeNode, previousValue *optionalInt) bool {
    if node == nil {
        return true
    }
    if (!isValidBSTHelper(node.Left, previousValue)) {
        return false
    }
    if previousValue.HasValue && node.Val <= previousValue.Value {
        return false
    }
    previousValue.HasValue = true
    previousValue.Value = node.Val
    return isValidBSTHelper(node.Right, previousValue)
}

return subtree{valid: true, maxValue: nil, minValue: nil}
}
leftSubtree := isValidBSTRecursively(root.Left)
isLeftSubtreeValid := leftSubtree.valid && (leftSubtree.maxValue == nil || *leftSubtree.maxValue < root.Val)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

leftSubtree.maxValue == nil || *leftSubtree.maxValue < root.Valというのが、ここにもL115にも出ているのはやや気になりました。
ここだけで関数化するかというと、なかなか難しいですね

for node != nil {
stack = append(stack, node)
node = node.Left
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

L135~138と同じ処理ですね(これは関数化しやすいかもです)

@nittoco
Copy link
Copy Markdown

nittoco commented Nov 10, 2024

話が出てたので、末尾再帰で再帰関数が1つのコードを書いてみました(自分の練習がてら)

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        stack = []
        return self.is_valid(root, stack, None)

    def is_valid(self, node, stack, prev_val):
        if not stack and not node: 
            return True
        while node:
            stack.append(node)
            node = node.left
        node = stack.pop()
        if prev_val is not None and prev_val >= node.val:
            return False
        prev_val = node.val
        return self.is_valid(node.right, stack, prev_val)

@hroc135
Copy link
Copy Markdown
Owner Author

hroc135 commented Nov 14, 2024

@liquo-rice
ありがとうございます。自分のやり方でもポインタを使っていたので参照先の値を書き換えてやれば戻り値で値を返す必要がなかったのですね

↑これ違いますね。ポインタを使っていると、n番目の再帰のpreviousValueとしてヌルポインタが渡されていると、それ以降の探索でpreviousValueが任意の値へのポインタとなってもヌルポインタに影響がない
だからoptionalIntという構造体を使うことによってヌルポインタを使わなくてもいいことになっている

@hroc135
Copy link
Copy Markdown
Owner Author

hroc135 commented Nov 14, 2024

@nittoco
末尾再帰コードありがとうございます。Goでも書いてみました。

func isValidBST(root *TreeNode) bool {
	stack := []*TreeNode{}
	return isValidBSTHelper(root, stack, nil)
}

func isValidBSTHelper(node *TreeNode, stack []*TreeNode, previousValue *int) bool {
	if len(stack) == 0 && node == nil {
		return true
	}
	for node != nil {
		stack = append(stack, node)
		node = node.Left
	}
	top := stack[len(stack)-1]
	stack = stack[:len(stack)-1]
	if previousValue != nil && top.Val <= *previousValue {
		return false
	}
	previousValue = &top.Val
	return isValidBSTHelper(top.Right, stack, previousValue)
}

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.

7 participants