-
Notifications
You must be signed in to change notification settings - Fork 0
206. Reverse Linked List #18
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
Changes from all commits
89ee4a3
ac64395
20ec01e
272589a
b69e170
b1cd5ee
bf9db15
0842669
2daa6f8
ef4cd95
a0b6426
723de26
f2a6a87
d1bcd02
023457c
6fdbad7
51276ff
e6d6b0e
5bb3f8c
e74c6a3
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,235 @@ | ||
# Step 1 | ||
|
||
Linked List系の問題は、referenceの操作が正しくできるかどうかが測られているので、(dummy以外)新しいNodeを作らない解法が求められているのだろう、と予想する。 | ||
|
||
# Step 2 | ||
|
||
変数名がなかなか悩みどころ。 | ||
|
||
`prev`, `curr`, `next`だと、reverseしたlistにおける"前"や"次"なのか、元のlistにおける"前"や"次"なのか少し混乱する。 | ||
|
||
また、処理をどうグループ化するかも悩ましい。while文の中に4つの短い処理をスペース無しで並べると、それらに順序的な関係がないような印象を覚えてしまうので (偏った感覚?)、スペースを入れたいのだが | ||
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. 個人的には、全体で数行の規模なので空行はそこまで読みやすさに寄与しないかなと思いました。 |
||
|
||
```python | ||
while node: | ||
# 次に処理するnodeを待避 | ||
next_node_to_reverse = node.next | ||
|
||
# 繋ぎかえて reversed listを伸ばす | ||
node.next = reversed_head | ||
reversed_head = node | ||
|
||
# 処理するnodeを進める | ||
node = next_node_to_reverse | ||
``` | ||
|
||
と見るか | ||
|
||
```python | ||
while node: | ||
# 次に処理するnodeを待避 | ||
next_node_to_reverse = node.next | ||
|
||
# 処理中のnodeのreferenceをひっくり返す | ||
node.next = reversed_head | ||
|
||
# 次に進むためのreferenceの更新 | ||
reversed_head = node | ||
node = next_node_to_reverse | ||
``` | ||
|
||
と見るかで少し悩んだ。 | ||
|
||
## 他の方々のPRを見る | ||
|
||
[h1rosakaさんのPR](https://github.com/h1rosaka/arai60/pull/10) | ||
- helper関数に二つNodeを渡してしまうやり方は考えつかなかったな。この問題に関しては、様々な書き方がありそう。 | ||
[potrueさんのPR](https://github.com/potrue/leetcode/pull/7) | ||
- 残されたコメントを見る限り、再帰的な解法では、せっかく問題を分解しているのだから、reverseしたlistの先頭と末尾を返して1つのステップを簡潔にする方法が自然...なのだろうか。 | ||
[5ky7さんのPR](https://github.com/5ky7/arai60/pull/8) | ||
[irohafternoonさんのPR](https://github.com/irohafternoon/LeetCode/pull/9) | ||
[garunituleさんのPR](https://github.com/garunitule/coding_practice/pull/7) | ||
|
||
## 再帰的な解法 | ||
|
||
どの解法も納得感があるが、現時点で言語化できない個人的な好みにより、step3はstep1と同様にペアを返す方法で実装することにする。 | ||
- 最近、再帰関数でペアを返すことで解ける問題の幅が広いことに気づいた。 | ||
- "If the only tool you have is a hammer, it is tempting to treat everything as if it were a nail." | ||
|
||
### 別解1 | ||
|
||
```python | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
if head is None: | ||
return None | ||
if head.next is None: | ||
return head | ||
|
||
reversed_head = self.reverseList(head.next) | ||
head.next.next = head | ||
head.next = None # for reversed tail | ||
return reversed_head | ||
``` | ||
|
||
### 別解2 | ||
|
||
```python | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
def reverse_list_helper( | ||
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. この関数が何をするつもりなのか、少し日本語で表現してもらえませんか。 さらに curr_node がひっくり返すと後ろになるということを利用しているなどの事情もあるでしょう。 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. ありがとうございます。 ひっくり返す操作によって、元のリストの 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. いや、素直に呼ぶと、「続きの部分」と「ひっくり返す部分」を受け取って、「ひっくり返す部分をひっくり返して、続きの部分をくっつける」ではないですか。これを「前」と「今」とか読んでいるんですよ。で、さらに「ヘルパーする」と名前をつけていますね。 「ヘルパー」するとは、次のことである。 これは正気ではないでしょう。 「続きの部分」と「ひっくり返す部分」を受取り、「ひっくり返す部分」がなければ「続きの部分を」返す。 まだ分かります。 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. 少し混乱していました。元のリストの末尾側からひっくり返していくので、この関数で(X, Y)を受け取ると、Xが未処理・続きの部分、Yがひっくり返す部分になり、そのXとYに「前」、「今」と名付けるのはあまりに不適当ですね。 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. 問題として いいかはともかく逆方向に極端なことをすると def reverse_the_first_and_append_the_second(reversing, appending):
if reversing is None:
return appending
reversing_tails = reversing.next
reversing.next = None
combined = reverse_the_first_and_append_the_second(reversing_tails, reversing)
reversing.next = appending
return combined 読めますか。また、これさらに変形したくなりませんか。 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. 「前」「今」という関数の引数の命名方法、関数の機能とは関係がなくて、自分が典型的にどうやって出会うかですね。そういう名前を引数につけても、あまり読む助けになりません。 料理も手続きなので、どのようにそういった場面でどう説明するかを考えるといいかもしれません。 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. 料理のアナロジー、ありがとうございます。前の場面のコンテクストを引きずった命名をしてしまったことを反省しています。 いただいたコードですが、前の部分がひっくり返す操作、後ろの部分がくっつける操作になっていて、一つの関数内に二つの責務があることが強調されていると感じました。反転・連結の操作によって頭を揺らされる感覚があったので、関数を分けて操作を分離・またはシグネチャを変えて関数内の操作を単純化したいなと思いました。 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. 上のコードはこうしませんか。代入を前に持っていきます。そうするとさらに末尾再帰の形なのでループにできますね。 def reverse_the_first_and_append_the_second(reversing, appending):
if reversing is None:
return appending
reversing_tails = reversing.next
reversing.next = appending
return reverse_the_first_and_append_the_second(reversing_tails, reversing) 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. 1: 末尾再帰の形にできる、2: 末尾再帰はループにできる という思考ステップの回路が繋がっていませんでした。確かに、いただいたコードをループにすることができました。 def reverse_the_first_and_append_the_second(reversing, appending):
while reversing is not None:
reversing_tails = reversing.next
reversing.next = appending
reversing, appending = reversing_tails, reversing
return appending 末尾再帰と末尾呼出し最適化について調べる中で、OCamlのList Reversalの実装を確認したところ、いただいた末尾再帰の形のコードとほぼ一致していることに気がつきました。 let rec rev_append l1 l2 =
match l1 with
[] -> l2
| a :: l -> rev_append l (a :: l2)
let rev l = rev_append l [] この問題の裏テーマとして末尾再帰があるように思いました。再帰 -> 末尾再帰 -> ループを意識していこうと思います。辛抱強く付き合っていただいてありがとうございます! 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. はい。そのコード step1_iterative.py とほぼ同じですね。こういう風に色々な変形が念頭にあって書いてます。 |
||
prev_node: Optional[ListNode], curr_node: Optional[ListNode] | ||
) -> Optional[ListNode]: | ||
if curr_node is None: | ||
return prev_node | ||
|
||
reversed_head = reverse_list_helper(curr_node, curr_node.next) | ||
curr_node.next = prev_node | ||
return reversed_head | ||
|
||
return reverse_list_helper(None, head) | ||
``` | ||
|
||
# Step 3 | ||
|
||
Step 3までやってようやくしっくりきたのだが、私のiterativeな解法は元のリストを先頭→末尾に進みながら繋ぎかえが発生するが、recursiveだと元のリストの末尾→先頭の順で繋ぎかえが発生している(Stackを用いているため逆順になるのは当たり前だが)。 | ||
|
||
# Step 4 | ||
|
||
## Tail Recursion | ||
|
||
末尾再帰 (tail recursion)とは、再帰のパターンの一種で、再帰関数において再起呼び出しが処理の末尾にあり、その戻り値をそのまま関数全体の戻り値として使用しているパターンを指す。 | ||
末尾再帰の利点は、通常の再帰と異なり、再起呼び出しの後に追加の計算がないため、呼び出し側のスタックフレームを保持する必要がなくなる点にある。 | ||
そうすると、ループ構造に機械的に書き換えられる、または末尾呼び出し最適化(tail call optimization)によってスタックフレームを積まずに済む。 | ||
|
||
```python | ||
def reverse_the_first_and_append_the_second(reversing, appending): | ||
if reversing is None: | ||
return appending | ||
reversing_tails = reversing.next | ||
reversing.next = appending | ||
return reverse_the_first_and_append_the_second(reversing_tails, reversing) | ||
``` | ||
|
||
```python | ||
def reverse_the_first_and_append_the_second(reversing, appending): | ||
while reversing is not None: | ||
reversing_tails = reversing.next | ||
reversing.next = appending | ||
reversing, appending = reversing_tails, reversing | ||
return appending | ||
``` | ||
|
||
### Resources | ||
|
||
[Tail recursion - HaskellWiki](https://wiki.haskell.org/index.php?title=Tail_recursion) | ||
|
||
> A recursive function is tail recursive if the final result of the recursive call is the final result of the function itself. If the result of the recursive call must be further processed (say, by adding 1 to it, or consing another element onto the beginning of it), it is not tail recursive. | ||
> In many programming languages, calling a function uses stack space, so a function that is tail recursive can build up a large stack of calls to itself, which wastes memory. Since in a tail call, the containing function is about to return, its environment can actually be discarded and the recursive call can be entered without creating a new stack frame. This trick is called tail call elimination or tail call optimisation and allows tail-recursive functions to recur indefinitely. | ||
|
||
[3.1.1.5. Tail Recursion · Functional Programming in OCaml](https://courses.cs.cornell.edu/cs3110/2021sp/textbook/data/tail_recursion.html?q=) | ||
|
||
[github.com/ocaml - ocaml/stdlib/list.ml](https://github.com/ocaml/ocaml/blob/d325f299896417c5f1d477171135acfdf402e770/stdlib/list.ml#L57) | ||
|
||
```ocaml | ||
let rec rev_append l1 l2 = | ||
match l1 with | ||
[] -> l2 | ||
| a :: l -> rev_append l (a :: l2) | ||
|
||
let rev l = rev_append l [] | ||
``` | ||
|
||
これはOdaさんにいただいた末尾再帰のReverse Linked Listのコードとほぼ一致する。 | ||
|
||
```python | ||
def reverse_the_first_and_append_the_second(reversing, appending): | ||
if reversing is None: | ||
return appending | ||
reversing_tails = reversing.next | ||
reversing.next = appending | ||
return reverse_the_first_and_append_the_second(reversing_tails, reversing) | ||
``` | ||
|
||
### Examples | ||
|
||
階乗を求めるプログラム(末尾再帰ではない形)。この方法では、再起呼び出しの結果にnをかけている。 | ||
|
||
```python | ||
def factorial(n: int) -> int: | ||
if n == 0: | ||
return 1 | ||
return n * factorial(n - 1) | ||
``` | ||
|
||
末尾再帰で書くと | ||
|
||
```python | ||
def factorial(n: int, accumulated: int = 1) -> int: | ||
if n == 0: | ||
return accumulated | ||
return factorial(n - 1, accumulated * n) | ||
``` | ||
|
||
この方法では、関数内で最後にやることが再起呼び出しの結果を返すことになっている。 | ||
|
||
## Feedback | ||
|
||
- **再帰 <-> ループ間変形** | ||
- 末尾再帰 | ||
- 変数の末尾にコメントを書くとフォーマットが大変なので、素直にdocstringに書いた方がいいかも | ||
- 意味ではなく操作の順番からの命名 (previous) に気を付ける、パズルを作らないように | ||
- 四行くらいしかないなら空行を入れても読みやすさは変わらないかも | ||
- どうせ空行を入れるなら、コメントを積極的に入れてもいいかも | ||
- もう一度書いてみると、確かに空行なくてもいいかな、と思った。仕事だと、長いscript的なものを書くと空行でブロックを区切るが、そこまでの量でもない。 | ||
- 私の脳内回路がまだ組み変わっていないのか、書き下すとまだ少し違和感が残るが、少し時間をおいてみよう。 | ||
- `reversing`, `appending`という命名 | ||
- PEP8の (sparingly) というニュアンスに気づかなかった "Extra blank lines may be used (sparingly) to separate groups of related functions." | ||
- `rest*`, `*_tails` | ||
|
||
```python | ||
# Step 4 tail recursion | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
def reverse_append(reversing, appending): | ||
if reversing is None: | ||
return appending | ||
reversing_tails = reversing.next | ||
reversing.next = appending | ||
return reverse_append(reversing_tails, reversing) | ||
|
||
return reverse_append(head, None) | ||
``` | ||
|
||
```python | ||
# Step 4 tail recursion -> iterative | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
appending = None | ||
reversing = head | ||
while reversing: | ||
reversing_tails = reversing.next | ||
reversing.next = appending | ||
reversing, appending = reversing_tails, reversing | ||
return appending | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
prev = None | ||
curr = head | ||
|
||
while curr: | ||
next_ = curr.next | ||
|
||
curr.next = prev | ||
prev = curr | ||
|
||
curr = next_ | ||
|
||
return prev |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
def reverse_list_helper( | ||
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. L12 のコメントのせいで見にくくなっていると思いました。以下のように別の行に書くのもありでしょうか。 def reverse_list_helper(
node: Optional[ListNode],
) -> tuple[Optional[ListNode], Optional[ListNode]]:
# Returns: reversed_tail, reversed_head 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. 確かに、どうせ返り値の説明を書くなら素直にdocstringを書いた方がいい気がしてきました。ありがとうございます! |
||
node: Optional[ListNode], | ||
) -> tuple[ | ||
Optional[ListNode], Optional[ListNode] | ||
]: # reversed_tail, reversed_head | ||
if node is None: | ||
return None, None | ||
|
||
if node.next is None: | ||
return node, node | ||
|
||
reversed_tail, reversed_head = reverse_list_helper(node.next) | ||
reversed_tail.next = node | ||
node.next = None # to avoid cycle at the end of the list | ||
|
||
return node, reversed_head | ||
|
||
return reverse_list_helper(head)[1] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
reversed_head = None | ||
node = head | ||
|
||
while node: | ||
next_node_to_reverse = node.next | ||
|
||
node.next = reversed_head | ||
reversed_head = node | ||
|
||
node = next_node_to_reverse | ||
|
||
return reversed_head |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
def reverse_list_helper( | ||
node: Optional[ListNode], | ||
) -> tuple[ | ||
Optional[ListNode], Optional[ListNode] | ||
]: # (reversed_tail, reversed_head) | ||
if node is None: | ||
return None, None | ||
if node.next is None: | ||
return node, node | ||
|
||
reversed_tail, reversed_head = reverse_list_helper(node.next) | ||
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. (私のPR見てくださったようなのでご存知かもですが、)実はtailを受け取らずに解く方法もあります。 |
||
reversed_tail.next = node | ||
node.next = None | ||
|
||
return node, reversed_head | ||
|
||
return reverse_list_helper(head)[1] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
reversed_head = None | ||
node = head | ||
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. 今注目しているのも、という意味での 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. ありがとうございます!確かに |
||
|
||
while node: | ||
next_node_to_reverse = node.next | ||
|
||
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://pep8-ja.readthedocs.io/ja/latest/#id11
https://google.github.io/styleguide/pyguide.html#35-blank-lines 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. 気になって原文だとどういう表現か見てみたら、確かに
"(sparingly)"という文が入っていました。PEP8の著者たちがどれくらいのニュアンスで入れたのか測りかねますが、意味をあまり持たない空行は無くすべきとのご指摘、その通りだと思います。 Googleのスタイルガイドもありがとうございます!
このあたりの感度を高めていけるといいなと思いました。 |
||
node.next = reversed_head | ||
reversed_head = node | ||
|
||
node = next_node_to_reverse | ||
|
||
return reversed_head | ||
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. 個人的な感覚ですが、最近はせっかく空行を入れるならそこにコメントを積極的に入れてもいいような気がしてきました。何かしらの区切りであることはわかるのですが、空行だけだとどういう区切りかが上から読んでいったときにわからないので。(関数名や構文などから明らかにどういう区切りかわかる場合は別ですが!) 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. 確かに、解いている間はいいのですが、記憶を消してコードを眺めてみるとどういった区切りなのかわかりにくいですね。ハッとさせられる指摘でした、ありがとうございます! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Definition for singly-linked list. | ||
# class ListNode: | ||
# def __init__(self, val=0, next=None): | ||
# self.val = val | ||
# self.next = next | ||
class Solution: | ||
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
def reverse_list_helper( | ||
node: Optional[ListNode], | ||
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. 個人的には、このぐらい複雑な処理になるとnode以上の名前をつけてもいい気がしました。(reversing_nodeなど) 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. reversing_nodeという変数名いいですね!参考になります |
||
) -> tuple[ | ||
Optional[ListNode], Optional[ListNode] | ||
]: # (reversed_tail, reversed_head) | ||
if node is None: | ||
return None, None | ||
if node.next is None: | ||
return node, node | ||
|
||
reversed_tail, reversed_head = reverse_list_helper(node.next) | ||
reversed_tail.next = node | ||
node.next = None | ||
|
||
return node, reversed_head | ||
|
||
return reverse_list_helper(head)[1] |
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.
STEP3では改善されていたと思いますが、意味と形式のどちらから名前を付けるか、混乱する場合は両方試して比較してみると良いと思いました。
t0hsumi/leetcode#7 (comment)
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.
参考リンクありがとうございます!