Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
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
287 changes: 287 additions & 0 deletions 103BinaryTreeZigzagLevelOrderTraversal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
以下のコードは全てGoのデフォルトフォーマッターgofmtにかけてあります。

```Go
// Definition for a binary tree node.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
```

### Step 1
- 方針: BFSで各レベルを左から右へ探索して値をスライスに溜める。
レベル(0-index)が奇数だったらスライスをreverseする。
- 気にしたこと: slices.Reverseの処理が重くならないかどうか確認した。
slices.Reverseは線形時間で実行できる。
入力条件より、木のノード数は高々2000で、最大ノード数を持つレベルのノード数は1000未満。
要素数1000のスライスに対するReverseは、CPUのクロック周波数1e8psを考慮すると、1000 / 1e8 = 1e-5 = 10μs。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

このクロック周波数はどのような環境を想定していますか?現在のCPUは1GHzくらいは普通にあるのかなという感覚値でいます。

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.

ここの記述は、以前nodchipさんにされた指摘 #9 (comment) を参考にして算出していました

CPU の動作周波数は数 GHz 程度です。これは 1 秒間に数十億回機械語が実行されることを表します。 C++ をコンパイルして機械語に変換したときのオーバーヘッドを考慮すると、 1 秒間に 1~10 億ステップ程度という数字が出てきます。

正直ちゃんと環境を想定できていたわけではなかったです。自分の使っているM1チップについて調べたところ、3.2GHzでした

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

環境を想定しましょうということではなく、10^8だと100MHzで結構遅いなと思ったのでどういうマシンを想定しているんだろうと気になったというのが背景になります。ただ自分の使っているPCのメモリやコア数などは知っておくと良いというか、普通は興味をもつはずみたいなことを講師陣の方はいうと思いますし、自分もそう思います。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

i486 の1994年頃のモデルが 100MHz です。
2005年にインテルから出ている初期のマルチコア CPU が Pentium D が 3 GHz 前後です。

周波数は、光の速度 約 3*10^8 m/s との関係で、3 GHz だと1クロックに 10 cm になってきてそろそろ色々と限界といわれていたので、コアを増やす方向に動いていったというのが歴史です。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

クロック周波数は最大でも9GHzくらいなんですね。
https://www.itmedia.co.jp/pcuser/articles/2310/22/news029.html

これだけ短い時間で処理できるなら大丈夫。
- n: ノード数, k: 最大ノード数を持つレベルのノード数
- 時間計算量: O(nk)
- 空間計算量: O(n)

```Go
func zigzagLevelOrder(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
zigzagLevelNodes := [][]int{}
level := 0
currentLevelNodes := []*TreeNode{root}
for len(currentLevelNodes) > 0 {
currentLevelNodeValues := []int{}
nextLevelNodes := []*TreeNode{}
for _, node := range currentLevelNodes {
currentLevelNodeValues = append(currentLevelNodeValues, node.Val)
if node.Left != nil {
nextLevelNodes = append(nextLevelNodes, node.Left)
}
if node.Right != nil {
nextLevelNodes = append(nextLevelNodes, node.Right)
}
}
if level%2 == 1 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

for len(currentLevelNodes) > 0 のループを終えたあとに、まとめて奇数レベルだけ slices.Reverse したほうが、ネストが浅くなり、読みやすくなるかもしれません。

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.

なるほどです。実装してみました

func zigzagLevelOrder(root *TreeNode) [][]int {
	if root == nil {
		return [][]int{}
	}
	zigzagLevelNodes := [][]int{}
	currentLevelNodes := []*TreeNode{root}
	for len(currentLevelNodes) > 0 {
		currentLevelNodeValues := []int{}
		nextLevelNodes := []*TreeNode{}
		for _, node := range currentLevelNodes {
			currentLevelNodeValues = append(currentLevelNodeValues, node.Val)
			if node.Left != nil {
				nextLevelNodes = append(nextLevelNodes, node.Left)
			}
			if node.Right != nil {
				nextLevelNodes = append(nextLevelNodes, node.Right)
			}
		}
		zigzagLevelNodes = append(zigzagLevelNodes, currentLevelNodeValues)
		currentLevelNodes = nextLevelNodes
	}
	for i, _ := range zigzagLevelNodes {
		if i%2 == 1 {
			slices.Reverse(zigzagLevelNodes[i])
		}
	}
	return zigzagLevelNodes
}

slices.Reverse(currentLevelNodeValues)
}
zigzagLevelNodes = append(zigzagLevelNodes, currentLevelNodeValues)
level++
currentLevelNodes = nextLevelNodes
}
return zigzagLevelNodes
}
```

### Step 2
#### 2a reverse実装
```Go
func zigzagLevelOrder(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
zigzagLevelNodes := [][]int{}
level := 0
currentLevelNodes := []*TreeNode{root}
for len(currentLevelNodes) > 0 {
currentLevelNodeValues := []int{}
nextLevelNodes := []*TreeNode{}
for _, node := range currentLevelNodes {
currentLevelNodeValues = append(currentLevelNodeValues, node.Val)
if node.Left != nil {
nextLevelNodes = append(nextLevelNodes, node.Left)
}
if node.Right != nil {
nextLevelNodes = append(nextLevelNodes, node.Right)
}
}
if level%2 == 1 {
// slices.Reverse(currentLevelNodeValues)
reverse(&currentLevelNodeValues)
}
zigzagLevelNodes = append(zigzagLevelNodes, currentLevelNodeValues)
level++
currentLevelNodes = nextLevelNodes
}
return zigzagLevelNodes
}

func reverse(s *[]int) {
i := 0
j := len(*s) - 1
for i < j {
(*s)[i], (*s)[j] = (*s)[j], (*s)[i]
i++
j--
}
}
```

#### 2b reverse呼ばない
- 参考: https://github.com/seal-azarashi/leetcode/pull/26/files#diff-39117f806e1c76b27224df2000d2200427b950646feb7d63be476d764d29dfa8R14
- 関数呼び出しのオーバーヘッドがなくなる
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

これ時間にするとどれくらいでしょうか。

Copy link
Copy Markdown
Owner Author

@hroc135 hroc135 Nov 1, 2024

Choose a reason for hiding this comment

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

手元で実験してみたところ、4nsという数字が出てきました
実験方法は、slices.Reverseの実行開始から終了までの時間と、slices.Reverseの中身の開始から終了までの時間をそれぞれ計測し、差分を出しました。後者の場合の方が時間がかかる場合もあり、かなり分散が大きかったので、実質差はないだろうというのが結論になりました

start := time.Now()
slices.Reverse(currentLevelNodeValues)
functionCallTime += time.Since(start)
start := time.Now()
for i, j := 0, len(currentLevelNodeValues)-1; i < j; i, j = i+1, j-1 {
	currentLevelNodeValues[i], currentLevelNodeValues[j] = currentLevelNodeValues[j], currentLevelNodeValues[i]
}
functionCallTime += time.Since(start)

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.

遠い記憶を掘り出してt検定をしてみたらp値0.95 > 0.05で両者に有意差がないという帰無仮説が受諾されました

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

そうですね。
適当に検索して出てきたこれも 2 ns くらいといっていますね。
https://billglover.me/2018/09/17/how-expensive-is-a-go-function-call/

- その代わり読みにくい
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

個人的には気になるほどの読みにくさは感じませんでした。ここをどうするかについてはオーダーに影響するので、上の oda さんのレビューに返答するのと合わせて、読みやすさのために処理効率を犠牲にしていいのか考えてもいいと思います。

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.

step1のコードと2bのコードの実行時間を、深さ10、要素数2^10-1個の木で計測したところ、2bの方が平均6μs短かったです。一応t検定してみたらp値0.395 > 0.05で優位差はなさそうでした
それから2bのコードを読み返してみたのですが、たしかにstep1と読みやすさの観点でも優位差はないのかなと感じました
最終的に一番すっきりかけているのが https://github.com/hroc135/leetcode/pull/26/files#r1825776653 だと思うので、実行時間的にも問題ないと思うので、これが自分は一番好きかなと思いました


```Go
func zigzagLevelOrder(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
result := [][]int{}
level := 0
currentLevelNodes := []*TreeNode{root}
for len(currentLevelNodes) > 0 {
currentLevelNodeCount := len(currentLevelNodes)
currentLevelNodeValues := make([]int, 0, currentLevelNodeCount)
nextLevelNodes := []*TreeNode{}
for i := 0; i < currentLevelNodeCount; i++ {
if level%2 == 0 {
currentLevelNodeValues = append(currentLevelNodeValues, currentLevelNodes[i].Val)
} else {
currentLevelNodeValues = append(currentLevelNodeValues, currentLevelNodes[currentLevelNodeCount-i-1].Val)
}
if currentLevelNodes[i].Left != nil {
nextLevelNodes = append(nextLevelNodes, currentLevelNodes[i].Left)
}
if currentLevelNodes[i].Right != nil {
nextLevelNodes = append(nextLevelNodes, currentLevelNodes[i].Right)
}
}
result = append(result, currentLevelNodeValues)
level++
currentLevelNodes = nextLevelNodes
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

なかなか難しいですが、currentLevelNodesとcurrentLevelNodeValuesがよく似てるので一瞬わからなくなりそうですね。
ValuesCurrentLevelNodesとかだとどうでしょう。

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.

たしかにそうですね、思い切ってcurrentLevelを省いてnodesとvalues(nodeValues)にしてしまうのもありかもしれません

}
return result
}
```

#### 2c 再帰
- 参考: https://github.com/seal-azarashi/leetcode/pull/26/files#diff-39117f806e1c76b27224df2000d2200427b950646feb7d63be476d764d29dfa8R69
- 思いつかなかった
- 再帰よりstep1のようなBFSの方がレベルごとの探索をより直感的にできるのでベスト解答ではないと思う
- 再帰の練習として実装してみる
- 再帰の深さは木の深さとなる

```Go
func zigzagLevelOrder(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
zigzagLevelOrderValues := [][]int{}
zigzagLevelOrderRecursively(root, 0, &zigzagLevelOrderValues)
return zigzagLevelOrderValues
}

func zigzagLevelOrderRecursively(node *TreeNode, level int, zigzagLevelOrderValues *[][]int) {
for len(*zigzagLevelOrderValues) <= level {
*zigzagLevelOrderValues = append(*zigzagLevelOrderValues, []int{})
}
if level%2 == 0 {
(*zigzagLevelOrderValues)[level] = append((*zigzagLevelOrderValues)[level], node.Val)
} else {
(*zigzagLevelOrderValues)[level] = append([]int{node.Val}, (*zigzagLevelOrderValues)[level]...)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この行が全部作り直しているので遅いですかね。

}
if node.Left != nil {
zigzagLevelOrderRecursively(node.Left, level+1, zigzagLevelOrderValues)
}
if node.Right != nil {
zigzagLevelOrderRecursively(node.Right, level+1, zigzagLevelOrderValues)
}
}
```

#### 2d BFS reverse使わない
- levelの偶奇によって、currentLevelNodeValuesに前から加えるか後ろから加えるかを決める

```Go
func zigzagLevelOrder(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
order := [][]int{}
level := 0
currentLevelNodes := []*TreeNode{root}
for len(currentLevelNodes) > 0 {
currentLevelNodeValues := []int{}
nextLevelNodes := []*TreeNode{}
for _, node := range currentLevelNodes {
if level%2 == 0 {
currentLevelNodeValues = append(currentLevelNodeValues, node.Val)
} else {
currentLevelNodeValues = append([]int{node.Val}, currentLevelNodeValues...)
}
if node.Left != nil {
nextLevelNodes = append(nextLevelNodes, node.Left)
}
if node.Right != nil {
nextLevelNodes = append(nextLevelNodes, node.Right)
}
}
order = append(order, currentLevelNodeValues)
level++
currentLevelNodes = nextLevelNodes
}
return order
}
```

#### 2e BFS zigzag探索

```Go
func zigzagLevelOrder(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
order := [][]int{}
level := 0
currentLevelNodes := []*TreeNode{root}
for len(currentLevelNodes) > 0 {
currentLevelNodeValues := []int{}
nextLevelNodes := []*TreeNode{}
for i := 0; i < len(currentLevelNodes); i++ {
currentLevelNodeValues = append(currentLevelNodeValues, currentLevelNodes[i].Val)
if level%2 == 0 {
rightToLeft(currentLevelNodes[len(currentLevelNodes)-i-1], &nextLevelNodes)
} else {
leftToRight(currentLevelNodes[len(currentLevelNodes)-i-1], &nextLevelNodes)
}
}
order = append(order, currentLevelNodeValues)
level++
currentLevelNodes = nextLevelNodes
}
return order
}

func leftToRight(node *TreeNode, nextLevelNodes *[]*TreeNode) {
if node.Left != nil {
*nextLevelNodes = append(*nextLevelNodes, node.Left)
}
if node.Right != nil {
*nextLevelNodes = append(*nextLevelNodes, node.Right)
}
}

func rightToLeft(node *TreeNode, nextLevelNodes *[]*TreeNode) {
if node.Right != nil {
*nextLevelNodes = append(*nextLevelNodes, node.Right)
}
if node.Left != nil {
*nextLevelNodes = append(*nextLevelNodes, node.Left)
}
}
```

### Step 3
- BFSでlevelが奇数だったらreverseしてorderに入れる方法。
このやり方が最も自然だろうと思った

```Go
func zigzagLevelOrder(root *TreeNode) [][]int {
if root == nil {
return [][]int{}
}
order := [][]int{}
level := 0
currentLevelNodes := []*TreeNode{root}
for len(currentLevelNodes) > 0 {
currentLevelNodeValues := []int{}
nextLevelNodes := []*TreeNode{}
for _, node := range currentLevelNodes {
currentLevelNodeValues = append(currentLevelNodeValues, node.Val)
if node.Left != nil {
nextLevelNodes = append(nextLevelNodes, node.Left)
}
if node.Right != nil {
nextLevelNodes = append(nextLevelNodes, node.Right)
}
}
if level%2 == 1 {
slices.Reverse(currentLevelNodeValues)
}
order = append(order, currentLevelNodeValues)
level++
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

好みですが、数値型の level の代わりに、boolean 型の変数を使ってもいいと思いました。

currentLevelNodes = nextLevelNodes
}
return order
}
```