-
Notifications
You must be signed in to change notification settings - Fork 0
98. Validate Binary Search Tree #27
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,5 @@ | ||
| start_new_problem.sh | ||
| main.go | ||
| go.mod | ||
| go.sum | ||
| *.go |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,356 @@ | ||
| 問題: https://leetcode.com/problems/validate-binary-search-tree/description/ | ||
|
|
||
| 以下のコードは全てGoのデフォルトフォーマッターgofmtにかけてあります。 | ||
|
|
||
| ```Go | ||
| type TreeNode struct { | ||
| Val int | ||
| Left *TreeNode | ||
| Right *TreeNode | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 1 | ||
| - 方針: サブツリーがそれぞれvalidであるかどうかを調べる。 | ||
| 一つでもinvalidであれば全体もinvalid。 | ||
| 任意のノードに対して、左側の子が根となるサブツリーの最大要素がノードの値より大きかったらinvalid。 | ||
| 同様に、右側の子が根となるサブツリーの最小要素がノードの値より小さかったらinvalid。 | ||
| - n: 木のノード数 | ||
| - 時間計算量: O(n) | ||
| - 空間計算量: O(n) | ||
| - 一つのスタックフレームの大きさはO(1)であり、 | ||
| 最悪の場合、木が一直線になっていると、 | ||
| スタックフレームがn個積まれることになるから | ||
| - balancedな木であればO(log n) | ||
| - 上記より、再帰の回数は最大n回。 | ||
| 1スタックフレームの大きさは高々100Bで、 | ||
| Goのスタックサイズは1GBなので1e7回分くらいまで耐えられる | ||
|
|
||
| ```Go | ||
| type subtree struct { | ||
| valid bool | ||
| maxValue int | ||
| minValue int | ||
| } | ||
|
|
||
| func isValidBST(root *TreeNode) bool { | ||
| tree := isValidBSTRecursively(root) | ||
| return tree.valid | ||
| } | ||
|
|
||
| func isValidBSTRecursively(root *TreeNode) subtree { | ||
| if root == nil { | ||
| return subtree{valid: true, maxValue: math.MinInt, minValue: math.MaxInt} | ||
| } | ||
| leftSubtree := isValidBSTRecursively(root.Left) | ||
| isLeftSubtreeValid := leftSubtree.valid && leftSubtree.maxValue < root.Val | ||
| rightSubtree := isValidBSTRecursively(root.Right) | ||
| isRightSubtreeValid := rightSubtree.valid && rightSubtree.minValue > root.Val | ||
| return subtree{ | ||
| valid: isLeftSubtreeValid && isRightSubtreeValid, | ||
| maxValue: max(root.Val, rightSubtree.maxValue), | ||
| minValue: min(root.Val, leftSubtree.minValue), | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 2 | ||
| #### 2a | ||
| - step1の修正 | ||
| - step1のコードはleetcodeのテストは通ってしまったが、 | ||
| 以下のvalidなBSTに対してinvalidと判定してしまう | ||
| ```Go | ||
| root := &TreeNode{2, &TreeNode{Val: math.MinInt}, &TreeNode{Val: 3}} | ||
| ``` | ||
| - 原因は、再帰関数にnilノードが渡された時の返り値が、 | ||
| ```Go | ||
| return subtree{valid: true, maxValue: math.MinInt, minValue: math.MaxInt} | ||
| ``` | ||
| になっており、 | ||
| ```Go | ||
| isLeftSubtreeValid := leftSubtree.valid && leftSubtree.maxValue < root.Val | ||
| ``` | ||
| のときにroot.Valがmath.MinIntと等しい値だとfalseとして判定されてしまうからである | ||
| - 修正方法として考えたのは、再帰関数にnilノードが渡された時にnilノードであった旨が返り値を見てわかるようにすること。 | ||
| そのために、subtree構造体のフィールドであるmaxValue, minValueをそれぞれint型へのポインタとし、nilポインタを返せるようにした。 | ||
| - n: 木のノード数 | ||
| - 時間計算量: O(n) | ||
| - 空間計算量: O(n) | ||
|
|
||
|
|
||
| ```Go | ||
| type subtree struct { | ||
| valid bool | ||
| maxValue *int | ||
| minValue *int | ||
| } | ||
|
|
||
| func isValidBST(root *TreeNode) bool { | ||
| tree := isValidBSTRecursively(root) | ||
| return tree.valid | ||
| } | ||
|
|
||
| func isValidBSTRecursively(root *TreeNode) subtree { | ||
| if root == nil { | ||
| return subtree{valid: true, maxValue: nil, minValue: nil} | ||
| } | ||
| leftSubtree := isValidBSTRecursively(root.Left) | ||
| isLeftSubtreeValid := leftSubtree.valid && (leftSubtree.maxValue == nil || *leftSubtree.maxValue < root.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. leftSubtree.maxValue == nil || *leftSubtree.maxValue < root.Valというのが、ここにもL115にも出ているのはやや気になりました。 |
||
| rightSubtree := isValidBSTRecursively(root.Right) | ||
| isRightSubtreeValid := rightSubtree.valid && (rightSubtree.minValue == nil || *rightSubtree.minValue > root.Val) | ||
| return subtree{ | ||
| valid: isLeftSubtreeValid && isRightSubtreeValid, | ||
| maxValue: pointerToMaxValue(root.Val, rightSubtree.maxValue), | ||
| minValue: pointerToMinValue(root.Val, leftSubtree.minValue), | ||
| } | ||
| } | ||
|
|
||
| func pointerToMaxValue(rootValue int, rightSubtreeMaxValue *int) *int { | ||
| if rightSubtreeMaxValue == nil || rootValue > *rightSubtreeMaxValue { | ||
| return &rootValue | ||
| } else { | ||
| return rightSubtreeMaxValue | ||
| } | ||
| } | ||
|
|
||
| func pointerToMinValue(rootValue int, leftSubtreeMinValue *int) *int { | ||
| if leftSubtreeMinValue == nil || rootValue < *leftSubtreeMinValue { | ||
| return &rootValue | ||
| } else { | ||
| return leftSubtreeMinValue | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| #### 2b | ||
| - in-orderに調べていく方法 | ||
| - 参考: https://github.com/hayashi-ay/leetcode/pull/38/files | ||
| - n: 木のノード数 | ||
| - 時間計算量: O(n) | ||
| - 空間計算量: O(n) | ||
|
|
||
| ```Go | ||
| func isValidBST(root *TreeNode) bool { | ||
| var previousValue *int = nil | ||
| stack := []*TreeNode{} | ||
| node := root | ||
| for node != nil { | ||
| stack = append(stack, node) | ||
| node = node.Left | ||
| } | ||
| for len(stack) > 0 { | ||
| top := stack[len(stack)-1] | ||
| stack = stack[:len(stack)-1] | ||
| if previousValue != nil && *previousValue >= top.Val { | ||
| return false | ||
| } | ||
| previousValue = &top.Val | ||
| node = top.Right | ||
| for node != nil { | ||
| stack = append(stack, node) | ||
| node = node.Left | ||
| } | ||
|
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. L135~138と同じ処理ですね(これは関数化しやすいかもです) |
||
| } | ||
| return true | ||
| } | ||
| ``` | ||
|
|
||
| #### 2c | ||
| - inorderに値を入れたリストを作り、最後にstrictな昇順になっているかどうかを確かめる | ||
| - 標準ライブラリslicesのIsSortedFuncの第二引数の関数の引数が、 | ||
| a = s[i], b = s[i+1]の順に並んでいると思ったら逆だった。 | ||
| なぜ逆順なのだろう? | ||
| pkg.go.devのドキュメントに特に何も注意書きがなく、内部実装を見て初めて気がついた | ||
|
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. IsSorted の気持ちは分かります。sorted な配列に追加することがあるから後ろから見ていきたくて、less のほうが基本的だからこの構造です。Func も合わせたんでしょう。
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. そういうことだったんですね https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/slices/sort.go;l=16;drc=6d93de2c110f66457f103c33ba496ff2e2bf33af |
||
| - n: 木のノード数 | ||
| - 時間計算量: O(n) | ||
| - 他の解法はinvalidな部分を見つけ次第returnできるが、 | ||
| valuesInorderを作りきるまで待って最後に検証するので、 | ||
| 効率が悪い | ||
| - 改善方法として、valuesInorderの要素数が100とか任意の値になった時に逐次IsSortedFuncをかけて確かめ、okだったらclearすればよし、という策が挙げられる | ||
| - 空間計算量: O(n) | ||
| - 要素がO(n)入るスライスが2つあることと、 | ||
| valuesInorderに必ずn個要素が入ることから、 | ||
| 他の解法と比べると同じO(n)でも少し効率が悪い | ||
|
|
||
| ```Go | ||
| func isValidBST(root *TreeNode) bool { | ||
| valuesInorder := []int{} | ||
| stack := []*TreeNode{} | ||
| node := root | ||
| for node != nil { | ||
| stack = append(stack, node) | ||
| node = node.Left | ||
| } | ||
| for len(stack) > 0 { | ||
| top := stack[len(stack)-1] | ||
| stack = stack[:len(stack)-1] | ||
| valuesInorder = append(valuesInorder, top.Val) | ||
| node = top.Right | ||
| for node != nil { | ||
| stack = append(stack, node) | ||
| node = node.Left | ||
| } | ||
| } | ||
| return slices.IsSortedFunc(valuesInorder, func(a, b int) int { | ||
| return a - b - 1 | ||
| }) | ||
| } | ||
| ``` | ||
|
|
||
| #### 2d | ||
| - 末尾再帰 | ||
| - 参考: https://github.com/hayashi-ay/leetcode/pull/38/files | ||
| - step1の再帰より簡潔 | ||
| - odaさんが何度もいろんなところで再帰の考え方を説明していて、 | ||
| 役割分担をどう定義するかなんだな、と頭でわかっているつもりだが、 | ||
| いまだに実装の苦手意識がある。 | ||
| 今Haskellを使う授業を受けているので再帰のいい訓練になることを願う | ||
| - 一般的に、末尾再帰はコンパイラの最適化がかかることが多いので、 | ||
|
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. 2ndのコードは、末尾再帰のある言語、処理系でも末尾再帰最適化されないと思います。
という流れで呼び出した結果を呼び出し元で保存する必要があるので、関数呼び出しをジャンプに変換することができないと思います。
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 f(x2)の時にf(x1)の現状をスタックフレームとしてコールスタックに積む代わりに、func f(x1)の行にジャンプして引数をx2に更新する。といった動作をする 末尾再帰最適化はあくまで言語処理系が行う最適化であるので、最適化してくれない処理系を使っているときに末尾再帰を書いてもパフォーマンス上の利点はない Goは末尾再帰最適化がないと思っていたが、一部あるらしい JSはES6でサポートされるようになったらしい |
||
| そうでない再帰より望ましいが、 | ||
| Goコンパイラは末尾再帰に対する最適化をかけないので、 | ||
| あまりパフォーマンスに影響はない | ||
| - 時間・空間: O(n) | ||
|
|
||
| ```Go | ||
| func isValidBST(root *TreeNode) bool { | ||
| return isValidBSTRecursively(root, nil, nil) | ||
| } | ||
|
|
||
| func isValidBSTRecursively(node *TreeNode, lowerBound, upperBound *int) bool { | ||
| if node == nil { | ||
| return true | ||
| } | ||
| if lowerBound != nil && node.Val <= *lowerBound { | ||
| return false | ||
| } | ||
| if upperBound != nil && node.Val >= *upperBound { | ||
| return false | ||
| } | ||
| return isValidBSTRecursively(node.Left, lowerBound, &node.Val) && isValidBSTRecursively(node.Right, &node.Val, upperBound) | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 3 | ||
| - inorderにチェック | ||
|
|
||
| ```Go | ||
| func isValidBST(root *TreeNode) bool { | ||
| var previousValue *int | ||
| node := root | ||
| stack := []*TreeNode{} | ||
| for node != nil { | ||
| stack = append(stack, node) | ||
| node = node.Left | ||
| } | ||
| for len(stack) > 0 { | ||
| top := stack[len(stack)-1] | ||
| stack = stack[:len(stack)-1] | ||
| if previousValue != nil && top.Val <= *previousValue { | ||
| return false | ||
| } | ||
| previousValue = &top.Val | ||
| node = top.Right | ||
| for node != nil { | ||
| stack = append(stack, node) | ||
| node = node.Left | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| ``` | ||
|
|
||
|
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. 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
}
}
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. @oda
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. それから別の類の質問なのですが、odaさんのコードを見ていると上から子関数->親関数という順序で書いていることが多いのですが、この順序は単に好みですか?styleguideに特に記載はなかったのですが、個人的には親->子という順序で書いたほうが読みやすいので、子->親としている背景があったら知りたいと思った次第です 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. あー、メリットとしては複数のスレッドで並列計算されることくらいでしょうが、どちらかというとこれは「言語の機能を見てみよう」くらいの気持ちで書いているので、そんなに真面目に考えなくていいと思います。メモリー消費量は分かりませんが直感的には増えるんじゃないでしょうか。関数呼び出しのスタックが積まれるので。 子供から書くのは、おそらく C/C++ 由来の癖と思います。先に signature がないといけません。どちらでもいいんじゃないでしょうか。 |
||
| - 末尾再帰 | ||
|
|
||
| ```Go | ||
| func isValidBST(root *TreeNode) bool { | ||
| return isValidBSTRecursive(root, nil, nil) | ||
| } | ||
|
|
||
| func isValidBSTRecursive(node *TreeNode, lowerBound, upperBound *int) bool { | ||
| if node == nil { | ||
| return true | ||
| } | ||
| if lowerBound != nil && node.Val <= *lowerBound { | ||
| return false | ||
| } | ||
| if upperBound != nil && node.Val >= *upperBound { | ||
| return false | ||
| } | ||
| return isValidBSTRecursive(node.Left, lowerBound, &node.Val) && | ||
| isValidBSTRecursive(node.Right, &node.Val, upperBound) | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 4 | ||
| #### 4a | ||
| - 再帰inorderを実装 | ||
| - 経緯 | ||
| - https://github.com/hroc135/leetcode/pull/27#issuecomment-2462253173 | ||
| - https://github.com/hroc135/leetcode/pull/27#issuecomment-2462311329 | ||
| - https://github.com/hroc135/leetcode/pull/27#issuecomment-2465026481 | ||
| - https://github.com/hroc135/leetcode/pull/27#issuecomment-2465128526 | ||
|
|
||
| ```Go | ||
| type optionalInt struct { | ||
| hasValue bool | ||
| value int | ||
| } | ||
|
|
||
| func isValidBST(root *TreeNode) bool { | ||
| previousValue := &optionalInt{hasValue: false, value: 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) | ||
| } | ||
| ``` | ||
|
|
||
| #### 4b | ||
| - goroutine | ||
| - マルチスレッドを使える | ||
| - 参考: https://github.com/hroc135/leetcode/pull/27#discussion_r1828839054 | ||
|
|
||
| ```Go | ||
| func isValidBST(root *TreeNode) bool { | ||
| var previousValue *int | ||
| c := make(chan *TreeNode) | ||
| go traverseInorder(root, c) | ||
| for { | ||
| node, ok := <-c | ||
| if !ok { | ||
| return true | ||
| } | ||
| if previousValue != nil && node.Val <= *previousValue { | ||
| return false | ||
| } | ||
| previousValue = &node.Val | ||
| } | ||
| } | ||
|
|
||
| func traverseInorder(node *TreeNode, c chan *TreeNode) { | ||
| traverseInorderHelper(node, c) | ||
| close(c) | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
| ``` | ||
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.
些末なことかも知れないですが、返り値が boolean 値出ないのに接頭辞が is になっているのが気になりました。
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.
なんと、本当ですね
subtree構造体のvaidフィールドが何かということをメインに調べたいので、checkSubtreeRecursivelyみたいな感じの方がいいですかね