Skip to content

Add 103. Binary Tree Zigzag Level Order Traversal.md #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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
229 changes: 229 additions & 0 deletions 103. Binary Tree Zigzag Level Order Traversal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# step 1
階層型BFSが穏当そう。

DFSは流石に無理かと感じた。あるlevelで初めに一番左のノードから見て、その次のlevelでは
逆方向というのをうまくやる方法が思いつかなかった。

階層型でないBFSについても、ノードを入れる/取り出す順序を変える必要を感じた。

`nodes_in_level`, `nodes_in_next_level`ではあるレベルのノードが
左から右に並ぶように統一して解いた。

- time complexity: O(n)
- space complexity: O(n)
```python
from collections import deque


class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
zigzag_traversed_values = []
nodes_in_level = deque([root])
Copy link

Choose a reason for hiding this comment

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

先頭からの pop() をしていないため、 deque である必要がないように感じました。より軽い list を使ったほうがよいと思います。

start_from_left = True
while True:

Choose a reason for hiding this comment

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

個人的にループ条件を指定できるのであればしたいなという気持ちになりました。仮にもっと長いソースで何か変更して無限ループになった時、ソースを読む負担が減るんじゃないかなと思いました。

nodes_in_level = deque(filter(None, nodes_in_level))
Copy link

Choose a reason for hiding this comment

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

None を取り除いているということが分かりにくく感じました。
nodes_in_level に入れる前に None であるかをチェックしてから入れたほうが分かりやすいと思います。あるいは、 nodes_in_level に入れ終わった直後にこの行を入れ、かつコメントで処理内容を補足するとよいと思いました。

if not nodes_in_level:
break
values_in_level = []
nodes_in_next_level = deque()
while nodes_in_level:
if start_from_left:
node = nodes_in_level.popleft()
values_in_level.append(node.val)
nodes_in_next_level.append(node.left)
nodes_in_next_level.append(node.right)
else:
node = nodes_in_level.pop()
values_in_level.append(node.val)
nodes_in_next_level.appendleft(node.right)
nodes_in_next_level.appendleft(node.left)
Comment on lines +36 to +39
Copy link

@nittoco nittoco Mar 11, 2025

Choose a reason for hiding this comment

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

個人的には、本当に逆順で入っているかの確認のためにL29から32とL36から39をじっくり見比べる必要があり、あまり好みではないです。(depthが奇数の時reverseする、などの処理だけの方が読む負担が少ない)

zigzag_traversed_values.append(values_in_level)
nodes_in_level = nodes_in_next_level
start_from_left = not start_from_left
return zigzag_traversed_values
```

ループの先頭ではstart_from_leftがTrueの時はnodes_in_levelが右のノードから左のノードに、
Falseの時はnodes_in_levelが左から右に並ぶようにした解法。(dequeを使わない解法)

個人的にはあまり好みではない。
```python
class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
zigzag_traversed_values = []
start_from_left = True
nodes_in_level = [root]
while True:
nodes_in_level = list(filter(None, nodes_in_level))
if not nodes_in_level:
break
nodes_in_level.reverse()
Copy link

Choose a reason for hiding this comment

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

これも結構読む時の認知負荷がありますね。
ここで毎回reverse()した結果、nodes_in_levelの処理とnode_in_next_levelへのappend処理がどの順で行われているのか追うのが大変でした

nodes_in_next_level = []
values_in_level = []
for node in nodes_in_level:
values_in_level.append(node.val)
if start_from_left:
nodes_in_next_level.append(node.left)
nodes_in_next_level.append(node.right)
else:
nodes_in_next_level.append(node.right)
nodes_in_next_level.append(node.left)
nodes_in_level = nodes_in_next_level
start_from_left = not start_from_left
zigzag_traversed_values.append(values_in_level)
return zigzag_traversed_values
```

Noneもとりあえず入れる方法にしたが、弾くようにするとnodes_in_next_level.append()でネストが
深くなる。
# step 2
- https://github.com/olsen-blue/Arai60/pull/27/files
- 階層型BFS
- ノードの探索は通常のBFSと同じにしており、あるレベルでの値のリストを必要に応じて反転している
- https://github.com/colorbox/leetcode/pull/41/files
- ほぼ上と同じ
- https://github.com/goto-untrapped/Arai60/pull/51#discussion_r1749461242
- リストに入れる時にひっくり返すか、最後にまとめてひっくり返すか
- >自分も入れるときひっくり返す方が好きですが、最後に奇数番目の列をひっくり返すのもありかなと思います
それぞれのloopでやることを1つに絞ってるので初見での読みやすさが上がってそうです
- https://github.com/goto-untrapped/Arai60/pull/51/files
- DFSによる解法あり。読みやすいかは別にして面白かった。思いつかなかったのが悔しい。
- traverseの順番に規則があるのなら、それと今の深さの兼ね合いで返り値となるリストの作り方をうまいこと調整できる。
- https://github.com/python/cpython/blob/main/Objects/listobject.c#L1527
- pythonのlistのreverseメソッド
- 中でreverse_slice()を呼んでいてこれ自体はO(n)
- https://docs.python.org/3/library/functions.html#reversed
- あれば`__reversed__()`という特殊メソッドを読んでいるみたい
- https://github.com/python/cpython/blob/main/Objects/listobject.c#L4071
- https://github.com/python/cpython/blob/main/Objects/listobject.c#L4103
- listの`__reversed__()`は末尾のイテレータを返しているだけみたい。
- traverseで必要になったら、後ろから調べるようにしている。

step 1の解法ではノードもちゃんとzigzagに進むように作っていたが、必ずしもそうする必要はない。
そうしなかった時の方が複雑さが少ないように感じた。

通常のtraverseをしてから、リストをひっくり返していく
```python
class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
level_ordered_values = []
nodes_in_level = [root]
while True:
nodes_in_level = list(filter(None, nodes_in_level))
if not nodes_in_level:
break
nodes_in_next_level = []
values_in_level = []
for node in nodes_in_level:
values_in_level.append(node.val)
nodes_in_next_level.append(node.left)
nodes_in_next_level.append(node.right)
nodes_in_level = nodes_in_next_level
level_ordered_values.append(values_in_level)
Copy link

Choose a reason for hiding this comment

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

ここで if not start_from_left: values_in_level.reverse() とすると下がいらなくなりますね。


start_from_left = True
zigzag_traversed_values = []
for left_to_right_traversed_values in level_ordered_values:
if start_from_left:
zigzag_traversed_values.append(left_to_right_traversed_values)
else:
right_to_left_traversed_values = list(
reversed(left_to_right_traversed_values)
)
zigzag_traversed_values.append(right_to_left_traversed_values)
start_from_left = not start_from_left
return zigzag_traversed_values
```

stackをつかったDFS
```python
from collections import deque


class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
zigzag_traversed_values = []
nodes_and_levels = [(root, 0)]
while nodes_and_levels:
node, level = nodes_and_levels.pop()
if node is None:
continue
while not len(zigzag_traversed_values) > level:
zigzag_traversed_values.append(deque())
if level % 2:
Copy link

Choose a reason for hiding this comment

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

この書き方少し不安になります。
言語による差もあるので比較演算子を使うほうが無難かなと思います。
https://zenn.dev/bugbearr/articles/69e1101ccc676a4b28b6

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにそうですね。ありがとうございます

zigzag_traversed_values[level].append(node.val)
else:
zigzag_traversed_values[level].appendleft(node.val)
Comment on lines +153 to +156
Copy link

@olsen-blue olsen-blue Mar 8, 2025

Choose a reason for hiding this comment

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

深さごとの部屋 deque() に入れる時の方向をappend, appendleft で切り替えるんですね。なるほどです。

nodes_and_levels.append((node.left, level + 1))
nodes_and_levels.append((node.right, level + 1))
zigzag_traversed_values = [
list(values) for values in zigzag_traversed_values
]
return zigzag_traversed_values
```

recursiveなDFS

preorder traversalをしながら、けれど値はzigzagに集めていくというののいい関数名が思いつかなかった。
関数名に動作であるpreorderをいれて、中の変数にzigzagを入れた。
```python
from collections import deque


class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
def traverse_tree_in_preorder(
node: Optional[TreeNode],
level: int
) -> None:
if node is None:
return
nonlocal zigzag_traversed_values
Copy link

Choose a reason for hiding this comment

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

確認ですが、これは、なくてもいい、であってますか。

Copy link
Owner Author

@t0hsumi t0hsumi Mar 7, 2025

Choose a reason for hiding this comment

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

Copy link

@olsen-blue olsen-blue Mar 8, 2025

Choose a reason for hiding this comment

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

この問題で、nonlocalを書かないとエラーになる理由が、やっと何となくわかりました。
https://github.com/olsen-blue/Arai60/pull/29/files#:~:text=return%20None-,nonlocal%20preorder_index,-preorder_index%20%2B%3D%201

nonlocalなしでpreorder_index = preorder_index + 1と代入(bound)の形で書くと、preorder_indexがローカル変数扱いになるということだったんですね。そんなローカル変数はないぞ、というエラー(UnboundLocalError)が実際出ていました。
nonlocalを書くと解決できていました。
https://docs.python.org/3/reference/executionmodel.html#naming-and-binding:~:text=If%20a%20name%20is%20bound%20in%20a%20block%2C%20it%20is%20a%20local%20variable%20of%20that%20block%2C%20unless%20declared%20as%20nonlocal%20or%20global.

今回のzigzag_traversed_valuesは、変数自体を更新しているわけではなく、変数自体に要素追加の更新をしているだけなので、nonlocal不要ということですかね。

ざっくりいうと、イミュータブルなものを更新したければnonlocalが必要で、ミュータブルなものの更新(要素追加など)であれば、nonlocal不要ということでしょうか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ちらっと書かれていますが、mutable, immutableというより、より厳密にはinner functionのなかでbind(今回はassignment)があるかどうかな気がします(各種bind)。

関数block内で定義されたオブジェクトのスコープは、(inner functionの定義内を含め)その関数定義内全てとなる。ただ、(inner functionなど)内部に新たなblockができ、そこで同じ名前で新たなbindを導入すると、その名前に紐づいたobjectは内部のblock内に限られる。(原文, blockの定義)

従って整理すると以下のような感じかと思います

  • inner functionの中では、読み込み専用で使う分には(bindがないので)外側の関数の値を見ることができる。
  • 書き込みをしたい場合、immutableだと代入以外に変数名に紐づいた値を更新できないが、代入をするとその変数のスコープはinner function内に限られる。
    • nonlocalをつけることで、外側のスコープのものを引っ張ってこれるので書き込みも可能になる
  • mutableだと代入以外の方法でも中身の書き換えができるので、結果としてnonlocalを使わずに済む場合がある。

実際に言語化するといい生理になりますね。コメントありがとうございます

Choose a reason for hiding this comment

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

補足ありがとうございます。bindとは何かがわかっていなかったです。読んでみます。

Choose a reason for hiding this comment

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

Arai 60のBacktrackingの問題を解いていたのですが、書いていただいたこの辺りの理解がようやく深まった気がします。ありがとうございます。

inner functionの中では、読み込み専用で使う分には(bindがないので)外側の関数の値を見ることができる。
書き込みをしたい場合、immutableだと代入以外に変数名に紐づいた値を更新できないが、代入をするとその変数のスコープはinner function内に限られる。
nonlocalをつけることで、外側のスコープのものを引っ張ってこれるので書き込みも可能になる
mutableだと代入以外の方法でも中身の書き換えができるので、結果としてnonlocalを使わずに済む場合がある。

while not len(zigzag_traversed_values) > level:
zigzag_traversed_values.append(deque())
if level % 2:
zigzag_traversed_values[level].appendleft(node.val)
else:
zigzag_traversed_values[level].append(node.val)
traverse_tree_in_preorder(node.left, level + 1)
traverse_tree_in_preorder(node.right, level + 1)

zigzag_traversed_values = []
traverse_tree_in_preorder(root, 0)
zigzag_traversed_values = [
list(values) for values in zigzag_traversed_values
]
return zigzag_traversed_values
```

# step 3

```python
class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
level_ordered_values = []
nodes_in_level = [root]
while True:
Copy link

@colorbox colorbox Mar 8, 2025

Choose a reason for hiding this comment

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

trueだとループの終了条件を読み取る時にbreakのあるところまで読む必要があり、コードを読む量が増えて読みづらくなってしまいます。
nodes_in_levelがemptyではない、という条件で良さそうに思います

Copy link

Choose a reason for hiding this comment

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

nodes_in_level に None のみが含まれている場合にもループを抜けたいのだと思います。そのため、直後の行で None を取り除き、そのあとで空かどうか判定しているのだと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

補足ありがとうございます。
おっしゃる通り、nodes_in_levelには、Noneも気にせず入れていいという感じでやっています。したがって、この書き方で、L206のTrueをnot nodes_in_levelとしてもその条件が成立することはないので、そこだけを変更してもかえって複雑かなと思ってます。

ただ、ループ始まってすぐのL207実行後にnodes_in_levelにNoneが含まれなくなる(変数の意味が少し変わる)こと、L207自体が少し複雑な書き方をしていること、指摘されている通りbreakかreturnを探さないといけないことなど、そもそもこの構造自体がすこし複雑だったかもなと今は思っています。

(書いた時は、なかなかうまく書けたんじゃないかと思ったんですが見返すと厳しいですね。いつもコメントありがとうございます)

Choose a reason for hiding this comment

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

nodes_in_level に None のみが含まれている場合にもループを抜けたいのだと思います

おっしゃる通り、nodes_in_levelには、Noneも気にせず入れていいという感じでやっています。

コメントありがとうございます、ちゃんと全部読めていなかったです。

nodes_in_level = list(filter(None, nodes_in_level))
if not nodes_in_level:
break
nodes_in_next_level = []
values_in_level = []
for node in nodes_in_level:
values_in_level.append(node.val)
nodes_in_next_level.append(node.left)
nodes_in_next_level.append(node.right)
nodes_in_level = nodes_in_next_level
level_ordered_values.append(values_in_level)

zigzag_traversed_values = []
start_from_left = True
for left_to_right_values in level_ordered_values:
if start_from_left:
zigzag_traversed_values.append(left_to_right_values)
else:
right_to_left_values = list(reversed(left_to_right_values))
zigzag_traversed_values.append(right_to_left_values)
start_from_left = not start_from_left
return zigzag_traversed_values
```